В четверг, 7 мая, около 16 часов (MSK) регистратор заморозил домен «cyclowiki.org» без уведомления владельцев. Сайт недоступен из большинства стран. Правление изучает возможности решения проблемы.

MediaWiki:Gadget-markblocked.js

Материал из Циклопедии
Перейти к навигации Перейти к поиску

Замечание: Возможно, после публикации вам придётся очистить кэш своего браузера, чтобы увидеть изменения.

  • Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl+F5 или Ctrl+R (⌘+R на Mac)
  • Google Chrome: Нажмите Ctrl+Shift+R (⌘+Shift+R на Mac)
  • Internet Explorer / Edge: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl+F5
  • Opera: Нажмите Ctrl+F5.
'use strict';

(function() {
    // ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========

    // Экранирование HTML-спецсимволов
    function escapeHTML(str) {
        return String(str)
            .replace(/&/g, '&')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    // Безопасная обработка CSS-значений
    function sanitizeCSS(val, fallback) {
        if (typeof val !== 'string') return fallback;
        // Удаляем всё, кроме безопасных CSS-символов
        val = val.replace(/[{};'"\\]|url\(|expression\(|javascript:/gi, '');
        return val || fallback;
    }

    // Преобразует timestamp в объект Date
    function parseTS(ts) {
        var m = ts.replace(/\D/g, '').match(/(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/);
        return new Date(Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5], m[6]));
    }

    // Дополняет число нулём слева
    function zz(v) {
        if (v <= 9) v = '0' + v;
        return v;
    }

    // Преобразует миллисекунды в читаемую строку
    function inHours(ms) {
        var mm = Math.floor(ms / 60000);
        if (!mm) return Math.floor(ms / 1000) + 'с';
        var hh = Math.floor(mm / 60);
        mm = mm % 60;
        var dd = Math.floor(hh / 24);
        hh = hh % 24;
        if (dd) return dd + (dd < 10 ? '.' + zz(hh) : '') + 'д';
        else return hh + ':' + zz(mm);
    }

    // ========== САНИТИЗАЦИЯ НАСТРОЕК ==========

    // Стили для временно заблокированных
    var tempStyle = sanitizeCSS(window.mbTempStyle, 'opacity: 0.7; text-decoration: line-through');
    // Стили для бессрочно заблокированных
    var indefStyle = sanitizeCSS(window.mbIndefStyle, 'opacity: 0.4; font-style: italic; text-decoration: line-through');
    // Стили для всплывающей подсказки
    var tipBoxStyle = sanitizeCSS(window.mbTipBoxStyle, 'font-size:smaller; background:#FFFFF0; border:1px solid #FEA; padding:0 0.3em; color:#AAA');

    // Добавляем стили на страницу
    mw.util.addCSS(
        '.user-blocked-temp{' + tempStyle + '}' +
        '.user-blocked-indef{' + indefStyle + '}' +
        '.user-blocked-tipbox{' + tipBoxStyle + '}'
    );

    // Шаблон подсказки (с вырезанием HTML-тегов и экранированием)
    var rawTip = window.mbTooltip || '; заблокирован ($1) администратором $2: $3 ($4 назад)';
    var mbTooltip = rawTip.replace(/<[^>]*>/g, '');

    // ========== ОСНОВНАЯ ФУНКЦИЯ ==========

    var isMarkingInProgress = false;
    var currentRequest = null;

    function markBlocked(container) {
        // Предотвращаем повторный запуск
        if (isMarkingInProgress) return;
        isMarkingInProgress = true;

        // Получаем переменные MediaWiki
        var wgNamespaceIds = mw.config.get('wgNamespaceIds');
        var wgArticlePath = mw.config.get('wgArticlePath');
        var wgScript = mw.config.get('wgScript');

        // Определяем, где искать ссылки
        var contentLinks = container
            ? $(container).find('a')
            : mw.util.$content.find('a').add('#ca-nstab-user a');

        // Получаем все псевдонимы для пространств "Участник:" и "Обсуждение участника:"
        var userNS = [];
        for (var ns in wgNamespaceIds) {
            if (wgNamespaceIds[ns] == 2 || wgNamespaceIds[ns] == 3) {
                userNS.push(ns.replace(/_/g, ' ') + ':');
            }
        }

        // Регулярка для заголовков
        var userTitleRX = new RegExp(
            '^(' + userNS.join('|') + '|Служебная:Вклад\\/|Special:Contributions\\/)' + '([^\\/#]+)$',
            'i'
        );

        // Регулярки для разбора URL ссылок
        var articleRX = new RegExp('^' + wgArticlePath.replace('$1', '') + '([^#]+)');
        var scriptRX = new RegExp('^' + wgScript + '\\?title=([^#&]+)');

        var userLinks = {};
        var url, ma, pgTitle;

        // Находим все ссылки на участников
        contentLinks.each(function(i, lnk) {
            url = $(lnk).attr('href');
            if (!url || url.charAt(0) != '/') return;
            else if ((ma = articleRX.exec(url))) pgTitle = ma[1];
            else if ((ma = scriptRX.exec(url))) pgTitle = ma[1];
            else return;

            pgTitle = decodeURIComponent(pgTitle).replace(/_/g, ' ');
            var user = userTitleRX.exec(pgTitle);
            if (!user) return;
            user = user[2];

            $(lnk).addClass('userlink');
            if (!userLinks[user]) userLinks[user] = [];
            userLinks[user].push(lnk);
        });

        // Преобразуем объект в массив имён участников
        var users = [];
        for (var u in userLinks) users.push(u);
        if (users.length == 0) {
            isMarkingInProgress = false;
            return;
        }

        // Индикатор загрузки
        var wgServerTime, apiRequests = 0, hasErrors = false;
        var waitingCSS = mw.util.addCSS('a.userlink {opacity:' + (window.mbLoadingOpacity || 0.85) + '}');

        // Отправляем запросы к API (пакетами по 50 имён)
        while (users.length > 0) {
            apiRequests++;
            var currentUsers = users.splice(0, 50);
            currentRequest = $.ajax({
                url: mw.util.wikiScript('api') + '?format=json&action=query',
                type: 'POST',
                data: {
                    list: 'blocks',
                    bklimit: 100,
                    bkusers: currentUsers.join('|'),
                    bkprop: 'user|by|timestamp|expiry|reason'
                },
                dataType: 'json'
            })
            .done(function(resp, textStatus, xhr) {
                markLinks(resp, xhr);
            })
            .fail(function(jqXHR, textStatus, errorThrown) {
                hasErrors = true;
                console.warn('MarkBlocked: API request failed for users:', currentUsers.join(', '), textStatus, errorThrown);
                checkCompletion();
            });
        }

        // ========== ОБРАБОТКА ОТВЕТА API ==========

        function markLinks(resp, xhr) {
            // Получаем серверное время из заголовка ответа
            if (!wgServerTime && xhr) {
                wgServerTime = new Date(xhr.getResponseHeader('Date'));
            }

            var list, blk, tip, links, lnk, clss, blTime;
            if (!resp || !(list = resp.query) || !(list = list.blocks)) {
                checkCompletion();
                return;
            }

            // Обрабатываем каждый блок
            for (var i = 0; i < list.length; i++) {
                blk = list[i];

                // Определяем тип блокировки
                if (/^in/.test(blk.expiry)) {
                    clss = 'user-blocked-indef';
                    blTime = escapeHTML(blk.expiry);
                } else {
                    clss = 'user-blocked-temp';
                    var expiryTime = parseTS(blk.expiry);
                    var timestampTime = parseTS(blk.timestamp);
                    blTime = inHours(expiryTime - timestampTime);
                }

                // Формируем подсказку с экранированием
                tip = mbTooltip
                    .replace('$1', blTime)
                    .replace('$2', escapeHTML(blk.by))
                    .replace('$3', escapeHTML(blk.reason))
                    .replace('$4', wgServerTime ? inHours(wgServerTime - parseTS(blk.timestamp)) : 'неизвестно');

                // Применяем стили и подсказку ко всем ссылкам заблокированного участника
                links = userLinks[blk.user];
                if (!links) continue;

                for (var k = 0; k < links.length; k++) {
                    lnk = $(links[k]);
                    
                    // Предотвращаем дублирование
                    if (lnk.hasClass('user-blocked-processed')) continue;
                    lnk.addClass('user-blocked-processed');
                    
                    lnk.addClass(clss);
                    
                    if (window.mbTipBox) {
                        $('<span class="user-blocked-tipbox">#</span>')
                            .attr('title', tip)
                            .insertBefore(lnk);
                    } else {
                        var currentTitle = lnk.attr('title') || '';
                        lnk.attr('title', currentTitle + (currentTitle ? ' ' : '') + tip);
                    }
                }
            }

            checkCompletion();
        }

        // Проверка завершения всех запросов
        function checkCompletion() {
            apiRequests--;
            if (apiRequests <= 0) {
                // Убираем индикатор загрузки
                if (waitingCSS) {
                    waitingCSS.disabled = true;
                }
                
                // Удаляем кнопку ручного запуска, если есть
                var showBlocksLink = document.getElementById('ca-showblocks');
                if (showBlocksLink && showBlocksLink.parentNode) {
                    showBlocksLink.parentNode.removeChild(showBlocksLink);
                }
                
                isMarkingInProgress = false;
                currentRequest = null;
                
                if (hasErrors) {
                    console.warn('MarkBlocked: Some API requests failed. Results may be incomplete.');
                }
            }
        }
    }

    // ========== ЗАПУСК ==========

    var wgAction = mw.config.get('wgAction');
    var wgNamespaceNumber = mw.config.get('wgNamespaceNumber');

    switch (wgAction) {
        case 'edit':
        case 'submit':
            // Не запускаем в режиме редактирования
            break;
        case 'view':
            // Не запускаем в основном пространстве при просмотре
            if (wgNamespaceNumber == 0) break;
            // В остальных случаях — продолжаем
        default:
            // 'history', 'purge' и всё остальное
            $(function() {
                if (window.mbNoAutoStart) {
                    // Добавляем ссылку в меню для ручного запуска
                    mw.util.addPortletLink('p-cactions', '#', 'Показать блокировки', 'ca-showblocks');
                    $('#ca-showblocks').on('click', function(e) {
                        e.preventDefault();
                        markBlocked();
                    });
                } else {
                    // Автоматический запуск
                    markBlocked();
                }
            });
    }

})();