RU AdList JS Fixes

try to take over the world!

2017-09-23 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

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.

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

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      20170923.1
// @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(log) {
        // jshint validthis:true
        let nt = this;
        function trace() { if (log) console.trace.apply(this, arguments); }

        nt.define = function(obj, prop, val)
        {
            Object.defineProperty(
                obj, prop, {
                    get: ()  => (trace('get', prop, 'of', obj, 'which is', val), val),
                    set: (v) => (trace('set', prop, 'of', obj, 'to', v), v),
                    enumerable: true
                }
            );
        };
        nt.proxy = function(obj)
        {
            return new Proxy(
                obj, {
                    get: (t, p) => (trace('get', p, 'of', t, 'which is', t[p]), t[p]),
                    set: (t, p, v) => (trace('set', p, 'of', t, 'to', v), v)
                }
            );
        };
        nt.func = (val) => () => (trace('call func, return', val), val);
    }

    // Fake objects of advertisement networks to break their workflow
    scriptLander(
        function()
        {
            let nt = new nullTools();
            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
                }));
            }
            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|video)/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;

            // Yandex API (ADBTools, Metrika)
            let Ya = new Proxy({}, {
                set: function(tgt, prop, val)
                {
                    if (typeof val === 'object' && 'AdvManager' in val)
                        val = tgt.Context;
                    tgt[prop] = val;
                    return tgt[prop];
                }
            });
            nt.define(Ya, '__isSent', true);
            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: { push: (f) => f() },
                _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: () => null,
                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.log('[WSI] Invoked function "'+name+'"', '| Tracing', (new Error()));
                                    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: (val) => val
                            });
                        }
                        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);
                                }
                                return val;
                            },
                            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('Worker _get_', prop);
                                return target[prop];
                            },
                            set: function(target, prop, val) {
                                console.log('Worker _set_', prop, '_to_', val);
                                target[prop] = val;
                                return val;
                            }
                        });
                    }
                }.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);
                                else
                                    scriptMap.get(this).org.apply(this, arguments);
                            }
                        });
                        this.addEventListener('error', scriptMap.get(this).wrp, false);

                        return val;
                    },
                    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: (x) => x
                        };
                    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;
        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.';
                    // build list of ids related to natural results
                    let ids = [];
                    for (let style of _querySelectorAll('style[data-as^="serp-label"]'))
                        ids.push(style.textContent.replace(/\{.*\}/,'').trim());
                    // hide result nodes which doesn't contains any ids from the list (ads)
                    let nodes = _querySelectorAll('.serp-item[data-cid] > .organic');
                    if (ids.length > 0)
                    {
                        for (let node of nodes)
                            if (!node.querySelector(ids.join(',')))
                                res = res|hide(node.parentNode);
                    } else {
                        for (let node of nodes)
                        {
                            let label = (node.querySelector('.label')||{}).textContent;
                            if (adWords[1].test(label) || adWords[2].test(label))
                                res = res|hide(node.parentNode);
                        }
                    }
                    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 (e) {
                console.log('Player FIX: Detected', e, 'player in', win.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)
    {
        if (params.log)
            console.log('[s] starting with', selector, words, scope, JSON.stringify(params));
        let remFunc = (params.hide ? scHide : scRemove),
            iterFunc = (params.siblings > 0 ? 'nextSibling' : 'previousSibling'),
            toRemove = [],
            siblings;
        for (let node of scope.querySelectorAll(selector))
        {
            if (params.log)
                console.log('[s] found node', node);
            if (params.parent)
            {
                while(node !== scope && !(node.matches(params.parent)))
                    node = node.parentNode;
                if (params.log)
                    console.log('[s] moving to parent node', node);
                if (node === scope)
                {
                    if (params.log)
                        console.log('[s] reached scope node, nothing to remove here.');
                    break;
                }
            }
            if (words.test(node.innerHTML) || !node.childNodes.length)
            {
                // drill up to the specified parent node if required
                if (toRemove.indexOf(node) === -1)
                {
                    if (params.log)
                        console.log('[s] adding node into list 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)
                        {
                            if (params.log)
                                console.log('[s] adding sibling node', node);
                            toRemove.push(node);
                            siblings -= 1; //count only element nodes
                        }
                        else if (!params.hide)
                        {
                            if (params.log)
                                console.log('[s] adding sibling node', node);
                            toRemove.push(node);
                        }
                    }
                } else {
                    if (params.log)
                        console.log('[s] node already marked for removal');
                }
            } else {
                if (params.log)
                    console.log('[s] word test failed, proceed to the next node');
            }
        }
        if (params.log)
            console.log('[s] proceeding with', (params.hide?'hide':'removal'), 'of', toRemove);
        for (let node of toRemove)
            remFunc(node);

        return toRemove.length;
    }

    // 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)
    {
        params = params || {};
        if (params.log)
            console.log('[g] starting with', selector, words, JSON.stringify(params));
        let scope = document,
            nonstop = false;
        // narrow down scope to a specific element
        if (params.root)
        {
            scope = scope.querySelector(params.root);
            if (!scope) // exit if the root element is not present on the page
                return 0;
            if (params.log)
                console.log('[g] scope', scope);
        }
        // add observe mode if required
        if (params.observe)
        {
            if (typeof MutationObserver === 'function')
            {
                (new MutationObserver(
                    function(ms)
                    {
                        for (let m of ms) if (m.addedNodes.length)
                            scissors(selector, words, scope, params);
                    }
                )).observe(scope, { childList:true, subtree: true });
                if (params.log)
                    console.log('[g] observer enabled');
            } else {
                nonstop = true;
                if (params.log)
                    console.log('[g] nonstop mode enabled');
            }
        }
        // wait for a full page load to do one extra cut
        win.addEventListener(
            'load', function()
            {
                if (params.log)
                    console.log('[g] onload cleanup');
                scissors(selector, words, scope, params);
            }
        );
        // do multiple cuts during page load until ads removed
        function cut(sci, s, w, sc, p, i)
        {
            if (i > 0)
                i -= 1;
            if (i && !sci(s, w, sc, p))
                setTimeout(cut, 100, sci, s, w, sc, p, i);
        }
        cut(scissors, selector, words, scope, params, (nonstop ? -1 : 30));
    }

    // 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'],
        now: () => {
            if (location.pathname.startsWith('/embed/'))
            {
                let nt = new nullTools();
                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);
                nt.define(win, 'CNight', nt.proxy({
                    Anonymous: function(){return {
                        setThrottle: nt.func(null),
                        start: nt.func(null)
                    }},
                    JobThread: nt.func(null),
                    Token: nt.func(null),
                    User: nt.func(null)
                }));
            } else
                preventPopMix();
        }
    };
    scripts['turbobit.net'] = {
        now: preventPopMix
    };

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

    scripts['4pda.ru'] = {
        now: function()
        {
            // 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 value;
                            }
                        };
                    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'] = {
        now: () => 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'],
        dom: () => gardener('div > a[target^="_"]', /Яндекс\.Директ/i, { root: 'body', observe: true, parent: 'div[class*="frame"]'})
    };

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

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

    scripts['mail.ru'] = {
        now: function()
        {
            // Trick to prevent mail.ru from removing 3rd-party styles
            scriptLander(
                () => Object.defineProperty(Object.prototype, 'restoreVisibility', {
                    get: () => (() => null),
                    set: () => null
                })
            );
            /* Experimental code, disabled for end users for now
            // Ads removal on e.mail.ru
            if (window.location.hostname === 'e.mail.ru')
            {
                let selector = (
                    '.b-datalist div[class]:not([id]) > div[class]:not([class*="js-"]),'+
                    '.b-letter div[class]:not([id]) > div[class]:not([class*="js-"]):not([class*="drop"]):not([class*="letter"]):not([style]):not([id]),'+
                    'div[id]:not([class]) > div[id][class]:not([class*="js-"]):not([class*="drop"]):not([style])'
                );
                let janitor = function(nodes)
                {
                    let color;
                    for (let node of nodes)
                    {
                        if (node.nodeType !== Node.ELEMENT_NODE)
                            continue;
                        color = window.getComputedStyle(node).backgroundColor;
                        if (/^rgb\(/.test(color) && color !== 'rgb(255, 255, 255)')
                        {
                            node.style.display = 'none';
                            console.log('Hide node:', node);
                        }
                    }
                };
                janitor(document.querySelectorAll(selector));
                (new MutationObserver(
                    function(ms)
                    {
                        for (let m of ms)
                            janitor(m.addedNodes);
                    }
                )).observe(
                    document.documentElement, {
                        childList: true,
                        subtree: true
                    }
                );
            }
            /**/
        }
    };

    scripts['megogo.net'] = {
        now: function()
        {
            Object.defineProperty(win, "adBlock", {
                get: () => false,
                set: () => null,
                enumerable : true
            });
            Object.defineProperty(win, "showAdBlockMessage", {
                get: () => (() => null),
                set: () => null,
                enumerable: true
            });
        }
    };

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

    scripts['overclockers.ru'] = {
        now: function()
        {
            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: function()
        {
            createStyle('.needblock {position: fixed; left: -10000px}');
            Object.defineProperty(win, 'adblck', {
                get: () => 'no',
                set: () => null,
                enumerable: true
            });
        }
    };

    scripts['pb.wtf'] = {
        other: ['piratbit.org', 'piratbit.ru'],
        dom: function()
        {
            createStyle('.reques,#result,tbody.row1:not([id]) {display: none !important}');
            // image in the slider in the header
            gardener('a[href^="/ex"],a[href$="=="]', /img/i, {root:'.release-navbar', observe:true, parent:'div'});
            // ads in blocks on the page
            gardener('a[href^="/topic/234257"]', /Как\sразместить/i, {siblings:-1, root:'#main_content', observe:true, parent:'span[style]'});
            // line above topic content
            gardener('.re_top1', /./, {root:'#main_content', parent:'.hidden-sm'});
        }
    };

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

    scripts['qrz.ru'] = {
        now: function()
        {
            Object.defineProperty(win, 'ab', {
                get:()=>false,
                set:()=>null
            });
            Object.defineProperty(win, 'tryMessage', {
                get:()=>(()=>null),
                set:()=>null
            });
        }
    };

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

    scripts['rbc.ru'] = {
        dom: function()
        {
            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: function()
        {
            createStyle('#bannerBottom {display: none!important}');
            let co = document.querySelector('#content');
            if (!co)
                return;
            let nodes = co.parentNode.childNodes,
                i = nodes.length;
            while (i--)
                if (nodes[i] !== co)
                    nodes[i].parentNode.removeChild(nodes[i]);
        }
    };

    scripts['rustorka.com'] = {
        other: ['rumedia.ws'],
        now: function()
        {
            createStyle('.header > div:not(.head-block) a, #sidebar1 img, #logo img {opacity:0!important}', {
                id: 'tempHidingStyles'
            }, true);
            preventPopups();
        },
        dom: function()
        {
            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'] = function()
    {
        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);
        (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'],
        dom: function()
        {
            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(
            function()
            {
                if (location.hostname.endsWith('.media.eagleplatform.com'))
                    return;
                let getDomain = (name) => name.replace(/[^:]+:\/\/([^:/]+)[:/].*/, '$1').replace(/[^.]+\./,'');
                let _onload = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'onload');
                let _set = _onload.set;
                _onload.configurable = false;
                _onload.set = function(func)
                {
                    _set.call(
                        this, function(e)
                        {
                            let d = e.target.href ? getDomain(e.target.href) : null,
                                h = location.hostname;
                            if (d && e.target instanceof HTMLLinkElement &&
                                (d === 'rambler.ru' || d === h || h.indexOf('.'+d) > -1))
                            {
                                console.log('Blocked "onload" for', e.target.href);
                                return false;
                            }
                            return func.apply(this, arguments);
                        }
                    );
                };
                Object.defineProperty(HTMLElement.prototype, 'onload', _onload);
                // 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: function()
        {
            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'] = function()
    {
        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: function()
        {
            let dis = document.querySelector('label[class*="cb-disable"]');
            if (dis)
                dis.click();
        },
        click: function(e)
        {
            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] = { dom: 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);
    }
})();