Многие из нас знают сайт Pikabu.ru. Это довольно популярный российский медиа-ресурс с огромной посещаемостью. Не так давно у сайта сменился владелец, началась волна довольно спорных нововведений... об одном из которых я и хотел бы написать здесь.

Практически все популярные ресурсы монетизируются тем или иным образом. Пикабу - не исключение. Тут и рекламные баннеры, и спонсорские посты, а последнее время ещё и вакансии. Вполне естественно, что пользователи не хотят видеть эту рекламу и пользуются различными плагинами типа AdBlock для её блокировки.

Да, есть сайты, которые показывают баннер на весь экран, когда определяют использование AdBlock. У многих этот баннер можно просто скрыть, нажав на соответствующую кнопку... Но это ведь не наш метод, правда? Лучше сделать так, чтобы сайт тормозил, если включен AdBlock, и свалить вину на него!

Итак, откроем код актуальных скриптов для десктопной версии сайта: https://cs.pikabu.ru/apps/ub/5.3.0/desktop/app.efa0bb5da0cd.mo.js. Где-то в нём есть вот такой кусок (слегка деобфусцировано):

10958: (t,e,s)=>{
        "use strict";
        s.d(e, {
            P: ()=>d
        });
        s(88674), (66992), s(33948);
        var i = s(33877), o = s(26639);
        const n = ["pub300x250", "pub300x250m", "pub728x90", "text-ad", "textAd", "textad", "textads", "text-ads", "text-ad-links", "sidebar-block_placeholder", "BrokenAd"]
          , r = "width:1px!important;height:1px!important;position:absolute!important;left:-10000px!important;top:-1000px!important;";
        let a = false, l = 0;
        const c = 60000;
        async function d(t=3) { // функция, определяющая наличие AdBlock
            const e = Date.now();
            if (e - l < c)
                return a;
            const s = document.createElement("div"); // создаём div
            // устанавливаем ему классы, соответствующие классам рекламных блоков
            s.classList.add(...n),
            s.setAttribute("style", r), // устанавливаем стиль, скрывая его
            document.body.append(s), // добавляем на страницу
            await (0, o.W)(), // ожидаем (o.W выполняет requestAnimationFrame)
            t = Math.max(Math.floor(t) || 0, 0); // t - количество попыток, не меньше 0
            do { // делаем несколько попыток в цикле
                if (a = h(s), a || !t) break; // проверяем, есть ли AdBlock
                await (0, i.g)(1000) // ждём (i.g выполняет setTimeout)
            } while (t--);
            return l = e, s.remove(), a // возвращаем флаг наличия AdBlock в переменной a
        }
        function h(t) { // непосредственно детектор AdBlock
            // если у тела страницы есть атрибут abp - детектим AdBlock Plus
            // если размеры элемента стали нулевыми (хотя в стилях ширина и высота 1),
            // то значит, что-то (AdBlock) скрыло этот элемент
            if (document.body.getAttribute("abp") || null === t.offsetParent || 0 === t.offsetHeight || 0 === t.offsetLeft || 0 === t.offsetTop || 0 === t.offsetWidth || 0 === t.clientHeight || 0 === t.clientWidth)
                return true;
            const e = window.getComputedStyle(t, null); // берём текущие стили элемента
            // если элемент скрыт через display: none или visibility: hidden,
            // то это тоже AdBlock
            return "none" === e.display || "hidden" === e.visibility
        }
    }

В целом этот код очень похож на скрипт detect-adblock.js, с некоторыми изменениями. Возможно, используется какое-то схожее готовое решение - я не знаю.

Эта функция для определения AdBlock используется здесь:

(0, K.P)().then((t=>{ // K.P - это приведённая выше функция
    // проверяется имя хоста - видимо, чтобы исключить localhost при разработке
    const e = String(window.location.hostname).match(/pikabu\.[a-z]+/i)[0];
    // генерируем букву латинского алфавита
    const s = String.fromCharCode(65 + ~~((new Date).getMinutes() / 5));
    // удаляем cookie с названием bs и устанавливаем новое значение:
    // если AdBlock обнаружен, то буква + "1"; если нет - буква + "0"
    0 === e.indexOf("pikabu") && (R.CookieStorage.remove("bs"),
    R.CookieStorage.set("bs", s + (t ? "1" : "0"), {
        expires: 1,
        domain: "." + e
    }))
}
)),

Cookie - это единственный способ незаметно отправить данные на сервер при открытии страницы в браузере, до её непосредственной загрузки и независимо от пути открываемой страницы.

Для чего же нужна эта кука? Если в ней записан "0" ("A0", "E0" и т. п.) - то буквально ничего не происходит. Если же в её значении присутствует "1" ("C1", "K1" и т. п.), то в HTML-код загружаемой страницы будет встроен следующий блок:

		<script type="text/javascript">(() => {
	let xhr = new XMLHttpRequest();
	let header = 'llasalbbubrrl';
	let nT4QtqOeG = 'https://pikabu.ru/zk8XGt3f/vnrQV4589/36d29eT5k9v/PHSIcV-PRTr7OMRGowfmn-ShbH4uV6l8mYEoNuGlnBi5osxOCjpUTnVGHBb1NIcZzXJujrhvoXwLalc1niy';
	let url = '/ajax/?key=' + (() => {
        let x = '';
        for (let key = 0; key < 32; key++) {
            x += ((Math.random() * 16) | 0).toString(16);
        }
        x += '-' + (Math.random().toString(10)).slice(2, 10);
        return x;
    })();

	xhr['op'+'en']('GET',url+(Math.floor(Math.random()*8)+1)+'&vn_buff=%5B14373139%2C13566390%5D&page=4&_=77161239542451');
	xhr.setRequestHeader(header, nT4QtqOeG);
	xhr.onreadystatechange = () => {
		if (xhr.readyState !== 4 || xhr.status !== 200) {
            return;
        }
		eval(xhr["resp"+"onseText"]);
	};
	xhr['se'+'nd']();
})();
(() => {
	let cookieName = 'crookie';
	let userMatchedName = 'cmtchd';
	let ttl = 14 * 24 * 3600 * 1000;

	if (document.cookie.indexOf(cookieName) !== -1 && document.cookie.indexOf(userMatchedName) !== -1) {
		return;
	}

	let xhr = new XMLHttpRequest();
	xhr['op'+'en']('GET', 'https://http-check-headers.yandex.ru');
	xhr.withCredentials = true;
	xhr.onreadystatechange = () => {
		if (xhr.readyState !== 4 || xhr.status !== 200) {
			return;
		}

		if (xhr.response) {
			let expires = new Date(Date.now() + ttl).toUTCString();
			document.cookie = cookieName + '=' + xhr.response
				+ ';path=/;domain=.pikabu.ru;Secure;SameSite=None;expires=' + expires;

			expires = new Date(Date.now() + ttl / 2).toUTCString();
			document.cookie = userMatchedName + '=' + btoa(String(Number(new Date()))).replace(/=/gi, '')
				+ ';path=/;domain=.pikabu.ru;Secure;SameSite=None;expires=' + expires;
		}
	};

	xhr['se'+'nd']();
})();
// здесь была длинная строчка миницифированного скрипта, которую я вырезал
</script>

Сам код выглядит слегка подозрительно, как будто какая-то малварь... но дело даже не в этом. Здесь присутствуют два XHR-запроса, выполняемых в синхронном режиме. То есть во время выполнения этих запросов браузер "подвисает". Для того, чтобы сделать их асинхронными, достаточно передать true третьим параметром в методе XMLHttpRequest.open. Мне кажется, про это знает буквально каждый веб-разработчик из тех, кто ещё пользуется XMLHttpRequest. Более того, про недостаток синхронных запросов говорится во многих местах:

  • в документации к методу XMLHttpRequest.open от Mozilla:

    Note: Synchronous requests on the main thread can be easily disruptive to the user experience and should be avoided; in fact, many browsers have deprecated synchronous XHR support on the main thread entirely. Synchronous requests are permitted in Workers.

  • на отдельной странице у Mozilla, посвящённой синхронным и асинхронным запросам:

    Warning: Synchronous XHR requests often cause hangs on the web, especially with poor network conditions or when the remote server is slow to respond. Synchronous XHR is now deprecated and should be avoided in favor of asynchronous requests.

  • в спецификации WhatWG:

    Synchronous XMLHttpRequest outside of workers is in the process of being removed from the web platform as it has detrimental effects to the end user’s experience. (This is a long process that takes many years.) Developers must not pass false for the async argument when the current global object is a Window object. User agents are strongly encouraged to warn about such usage in developer tools and may experiment with throwing an "InvalidAccessError" DOMException when it occurs.

  • в популярном российском учебнике по Javascript:

    Выглядит, может быть, и неплохо, но синхронные запросы используются редко, так как они блокируют выполнение JavaScript до тех пор, пока загрузка не завершена. В некоторых браузерах нельзя прокручивать страницу, пока идёт синхронный запрос. Ну а если же синхронный запрос по какой-то причине выполняется слишком долго, браузер предложит закрыть «зависшую» страницу.

Да и буквально везде. Единственное, чем можно объяснить наличие синхронных запросов - это желание насолить пользователям и заставить их отключить AdBlock на сайте. И администрация ресурса обвиняет в тормозах именно блокировщики рекламы:

Выводы? А их нет. Просто вот такая монетизация. "Виноваты не мы, виноват AdBlock, не пользуйтесь им, смотрите больше нашей рекламы".

UPD 1: Скрипт, загружаемый таким странным способом, оказался безвредным и содержащим код рекламной системы AdFox от Яндекса. В этом легко убедиться, скопировав значение переменной nT4QtqOeG в строку браузера и перейдя по ней. Короче говоря, если одна реклама режется - затормозим браузер и будем показывать другую рекламу.

Также появился официальный ответ от администрации. Верить им или нет - решайте сами.

Комментарии (24)


  1. click0
    18.09.2023 17:27
    +13

    И какие правила для Adblock нужно добавить, чтоб "тормоза" прекратились?


    1. ertaquo Автор
      18.09.2023 17:27
      +3

      В одном из списков заметил такую строку фильтра для AdBlock Plus:

      pikabu.ru##+js(abort-current-inline-script, XMLHttpRequest, llasalbbubrrl)

      Возможно, поможет, не проверял.


      1. Kvazzzar
        18.09.2023 17:27

        Проверил, не работает :(


    1. bogolt
      18.09.2023 17:27
      +66

      открыть файл
      /etc/hosts


      и отредактировать его, чтобы первая строка выглядела так:
      127.0.0.1 localhost pikabu.ru


      теперь каждый раз когда вы его случайно откроете чтобы смыть в унитаз пару часов своей жизни, ваш браузер радостно напомнит вам что вы его заблокировали, а дальше вы и сами вспомните зачем это сделали.


      1. Boilerplate
        18.09.2023 17:27
        +20

        Я примерно так и сделал пол года назад. Несколько дней назад с мобилки зашел на него, в топе горячего было тема "Их герой", что-то про Бандеру и мужские члены. 3к плюсов, 8 часов висело к тому времени на сайте. Это даже не пропаганда, а какое-то днищенское дно уровня надписей на заборе. Я тогда знатно прифигел.


      1. pantsarny
        18.09.2023 17:27
        +5

        Лучше 0.0.0.0, не будет ожидания и таймаута


      1. Voiddancer
        18.09.2023 17:27
        +2

        Я так и сделал пару лет назад, потом ещё на роутере для телефона заблочил. Существенно помогло.


        1. Akr0n
          18.09.2023 17:27

          Неужели такая зависимость развивается, что надо идти на такие меры?


          1. Voiddancer
            18.09.2023 17:27

            Я так понимаю, у меня что-то вроде СДВГ, это один из симптомов.


          1. vvbob
            18.09.2023 17:27

            Как и большинство других подобных лент. Надо работать, но при любом небольшом перерыве от скуки заходишь полистать и залипаешь на часы. Тоже пришел к выводу что надо как минимум не держать открытой вкладки с такими сайтами. Так что оно может быть и хорошо, что все там всё всрато работает, меньше соблазна туда ходить.


      1. shasoftX
        18.09.2023 17:27
        +2

        Хабр с удовольствием бы хотел стать таким же наркосайтом, чтобы пользователи часами на нем сидели. Но не срастается.


        1. vvbob
          18.09.2023 17:27

          А в чем проблема? Выкинуть всю эту идиотскую систему кармы, разрешить постить в ленту все что угодно, от сисек до "горячих новостей", модерирование свести к минимуму, и вуаля, готова очередная инфопомойка для любителей скроллинга ленты.


      1. yarkov
        18.09.2023 17:27
        +1

        Вот именно так я и поступил полгода назад. Ни капли не жалею ))


    1. alek0585
      18.09.2023 17:27
      +2

      Можно попробовать пропатчить XMLHttpRequest чтобы он всегда отсылал асинхронные запросы. По идее будет работать пока программисты из пикабу не добавят скрипт, где начнут фибоначи вычислять при детекте адблока.


      1. denchik09
        18.09.2023 17:27
        +1

        что-то вроде:

        //Получаем копию оригинального XHR
        let xhrObj = window.XMLHttpRequest;
        
        //Функция-заменитель XHR
        window.XMLHttpRequest = function(){
            //Создаем объект оригинального XHR
            let xhr = new xhrObj();
        
            //Сохраняем оригинальный метод open под случайным названием
            let randomName = '_'+Date.now();
            xhr[randomName] = xhr.open;
        
            //Заменяем метод open
            xhr.open = function(method, url, sync){
                if(sync){
                    //Если пойман синхронный запрос, гордо сообщаем об этом в консоли
                    console.log(`Обнаружен синхронный запрос на ${url} методом ${method}`);
                }
        
                //Вызываем оригинальный open
                return xhr[randomName](method, url, true);
            }
        
            //Возвращаем модифицированный экземпляр XHR
            return xhr;
        }

        встроить можно, например, в userscript и запускать через tampermonkey/greasemonkey


    1. MatVL
      18.09.2023 17:27
      +5

      Перестать заходить на пикабу


  1. Javian
    18.09.2023 17:27
    +3

    Наверное это для авторизованных пользователей? Без авторизации у меня не наблюдаются "тормоза" интерфейса.


    1. Stam_emg
      18.09.2023 17:27

      как вариант - это может быть волнами и не у всех. я вот "накопил" за 10 лет чтобы в настройках профиля выключить рекламу. с ublock тормозов не наблюдаю. но может и совпадение, не разбирался, тк лень.


      1. kalmarius
        18.09.2023 17:27
        +1

        А я наблюдаю - причем даже в мобильной Опере с включенным встроенным блокировщиком (на планшете, версия сайта не мобильная). Реклама отключена в настройках.


    1. ertaquo Автор
      18.09.2023 17:27
      +5

      Да, для неавторизованных пользователей подобной вставки нет. Страдают только авторизованные, либо (как заметил @Stam_emg) проводится какое-то скрытое AB-тестирование функционала.


  1. sasmoney
    18.09.2023 17:27
    +2

    Не вижу смысла в adblock на норм сайтах, на которых действительно думают головой и вставляют блоки так чтобы они не мешали просмотру страницы


  1. EireenK
    18.09.2023 17:27
    +4

    На MDN к XMLHttpRequest.open написано, что параметр async по умолчанию true. Т.е. чтобы отправить синхронный запрос, надо явно передать false. В ваших примерах третий параметр отсутствует, тогда где выполняются синхронные запросы?


  1. pantsarny
    18.09.2023 17:27
    +3

    Но ведь async = true по дефолту, где автор увидел тут иное?


    1. SergeyMax
      18.09.2023 17:27

      Да кого это вообще волнует))