Популярный сайт vs AdBlock

в 17:03, , рубрики: adblock, pikabu

Многие из нас знают сайт 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 на сайте. И администрация ресурса обвиняет в тормозах именно блокировщики рекламы:

Популярный сайт vs AdBlock - 1
Популярный сайт vs AdBlock - 2

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

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

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

Популярный сайт vs AdBlock - 3

Автор: ertaquo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js