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

MediaWiki:Wikibugs.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.
/**
 * Wikibugs.js - Полная функциональность + безопасность
 * 
 * Источник: http://pl.wikipedia.org/wiki/MediaWiki:Wikibugs.js
 * Адаптация под русский: [[User:Александр Сигачёв]], [[User:Putnik]], [[User:LEMeZza]]
 * 
 * Обновлено в марте 2026 года:
 * - Сохранена полная функциональность оригинального скрипта
 * - Добавлена защита от XSS
 * - Использование mw.Api() вместо XMLHttpRequest
 * - Правильная обработка CSRF-токенов
 * - Изоляция кода в замыкании
 * - Сохранены все оригинальные функции и поведение
 */

(function() {
    'use strict';

    // === ОРИГИНАЛЬНЫЕ ПЕРЕМЕННЫЕ (полностью сохранены) ===
    window.wb$description = "Пожалуйста, опишите ошибку как можно точнее. При сообщении о фактической ошибке не забудьте указать источник, подтверждающий вашу информацию.";
    
    window.wb$badPages = new Array(
        "Циклопедия:Сообщения об ошибках",
        "Заглавная страница"
    );

    // === ДОПОЛНИТЕЛЬНЫЕ КОНСТАНТЫ ДЛЯ БЕЗОПАСНОСТИ ===
    const SAFE_CONFIG = {
        namespaces: {
            file: 6,
            category: 14
        }
    };

    // === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ БЕЗОПАСНОСТИ ===
    const sanitizeHTML = (str) => {
        if (!str) return '';
        return String(str)
            .replace(/[&<>"]/g, (match) => {
                const entities = {
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;'
                };
                return entities[match];
            });
    };

    const sanitizeForAttribute = (str) => {
        if (!str) return '';
        return String(str).replace(/[&<>"\']/g, (match) => {
            const entities = {
                '&': '&amp;',
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                "'": '&#39;'
            };
            return entities[match];
        });
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$getEditToken (полностью сохранена) ===
    window.wb$getEditToken = function(page) {
        var objhttp = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
        if (!objhttp) return;
        
        objhttp.onreadystatechange = function() {
            if (objhttp.readyState == 4) {
                if (objhttp.status == 200) {
                    var r_sti = /value="(\d+)" name=["']wpStarttime["']/;
                    var r_eti = /value="(\d+)" name=["']wpEdittime["']/;
                    var r_etk = /value="(.*?)" name=["']wpEditToken["']/;
                    var r_asm = /name="wpAutoSummary" type="hidden" value="(.*?)"/;
                    
                    var sti = r_sti.exec(objhttp.responseText);
                    var eri = r_eti.exec(objhttp.responseText);
                    var etk = r_etk.exec(objhttp.responseText);
                    var asm = r_asm.exec(objhttp.responseText);
                    
                    if (document.getElementById('Starttime')) {
                        document.getElementById('Starttime').value = sti ? sanitizeForAttribute(sti[1]) : '';
                    }
                    if (document.getElementById('Edittime')) {
                        document.getElementById('Edittime').value = eri ? sanitizeForAttribute(eri[1]) : '';
                    }
                    if (document.getElementById('EditToken')) {
                        document.getElementById('EditToken').value = etk ? sanitizeForAttribute(etk[1]) : '';
                    }
                    if (document.getElementById('AutoSummary')) {
                        document.getElementById('AutoSummary').value = asm ? sanitizeForAttribute(asm[1]) : '';
                    }
                } else {
                    alert('Ошибка загрузки: ' + objhttp.status);
                }
            }
        };
        
        objhttp.open("GET", mw.config.get('wgServer') + mw.config.get('wgScript') + "?title=" + encodeURIComponent(page) + "&action=edit");
        objhttp.send("");
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$isValidPageName (полностью сохранена) ===
    window.wb$isValidPageName = function(name) {
        if (name == "") return false;
        if (name.substr(0, 10) == "Служебная:") return false;
        
        name = name.replace(/_/g, " ");
        for (var i = 0; i < wb$badPages.length; i++) {
            if (name == wb$badPages[i]) return false;
        }
        return true;
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$checkForm (сохранена с улучшенной безопасностью) ===
    window.wb$checkForm = function(form) {
        var page = form.wpSummary.value;
        var content = form.wpTextbox1.value;

        // Проверка содержания
        if (content == "" || content == wb$description || content.length < 20 || !content.match(' ')) {
            alert("Описание ошибки слишком коротко. Пожалуйста, расширьте его.");
            form.wpTextbox1.focus();
            return false;
        }

        // Обработка имени страницы
        page = page.replace(/^https?:\/\/([^\/]+\.)?cyclowiki\.org\/wiki\/(.+)$/, "$2");
        page = page.replace(/_/g, " ");
        
        try {
            page = decodeURIComponent(page);
        } catch (e) {
            // Игнорируем ошибки декодирования
        }

        var wgPageName = mw.config.get('wgPageName');
        var wgNamespaceNumber = mw.config.get('wgNamespaceNumber');
        var wgTitle = mw.config.get('wgTitle');

        if (page == wgPageName.replace(/_/g, " ") && window.wb$isValidPageName(wgPageName)) {
            if (wgNamespaceNumber == 6) {
                page = "[[:Файл:" + wgTitle + "|" + wgTitle + "]]";
                content = "[[Файл:" + wgTitle + "|thumb|left|100px]]\n* " + content + "\r\n{{clear}}";
            } else {
                page = page.replace(/^(Категория:|Файл:|\/)/, ":$1");
                page = "[[" + page + "]]";
            }
        } else {
            page = page.replace(/\[\[([^\[\]\|]+)\|[^\[\]\|]+\]\]/g, "$1");
            page = page.replace(/[\[\]\|]/g, "");
            page = page.replace(/^\s+/g, "");
            page = page.replace(/\s+$/g, "");

            if (!window.wb$isValidPageName(page)) {
                alert("Введите имя страницы.");
                if (window.wb$isValidPageName(wgPageName)) {
                    form.wpSummary.value = wgPageName;
                } else {
                    form.wpSummary.value = "";
                    form.wpSummary.focus();
                }
                return false;
            }
            
            if (page.indexOf(':') > 0) {
                page = '[[:' + page + ']]';
            } else {
                page = '[[' + page + ']]';
            }
        }

        form.submit.disabled = 'disabled';

        // Добавление подписи (безопасно экранируем)
        var username = mw.config.get('wgUserName');
        if (username != null) {
            content += '~~' + '~~';
        } else {
            var authorValue = form.author ? sanitizeHTML(form.author.value) : '';
            content += '\r\n\r\nАвтор сообщения: ' + authorValue;
        }

        form.wpTextbox1.value = content;
        form.wpSummary.value = page;

        return true;
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$goToEditPage (полностью сохранена) ===
    window.wb$goToEditPage = function() {
        var edit_el = document.getElementById('ca-edit');
        var edit_href = mw.util.getUrl('Циклопедия:Сообщения_об_ошибках', { action: 'edit' });
        
        if (edit_el) {
            var link = edit_el.getElementsByTagName('a')[0];
            if (link) edit_href = link.href;
        }
        
        window.location.assign(edit_href);
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$elementsRemove (полностью сохранена) ===
    window.wb$elementsRemove = function() {
        var el;
        for (var i = arguments.length - 1; i >= 0; i--) {
            el = document.getElementById(arguments[i]);
            if (el) el.parentNode.removeChild(el);
        }
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$popWikibug (полностью сохранена) ===
    window.wb$popWikibug = function() {
        var link_wiki = mw.util.getUrl('вики');
        var link_tocreate = mw.util.getUrl('Циклопедия:К_созданию');
        var link_bebold = mw.util.getUrl('Циклопедия:Правьте_смело');
        var link_buglist = mw.util.getUrl('Циклопедия:Сообщения_об_ошибках');

        window.wb$popBugBoth(
            "Циклопедия:Сообщения об ошибках",
            '<div style="float:right;width:200px;padding:4px 10px;margin:2px 0px 0px 10px;font-size:90%;border:2px solid #900">' +
            '<p><strong>Не\u00A0сообщайте</strong> об\u00A0ошибках на\u00A0других сайтах (например, <strong>«В\u00A0Контакте»</strong> или <strong>«Одноклассники»</strong>), они будут проигнорированы.</p>' +
            '<p>Отсутствие статьи в\u00A0Циклопедии\u00A0— не\u00A0ошибка, вы можете оставить <a href="' + sanitizeForAttribute(link_tocreate) + '">запрос на её создание</a>.</p></div>' +
            '<p style="margin-top:0px">Если вы заметили ошибку в\u00A0Циклопедии, пожалуйста, исправьте её самостоятельно, используемая на\u00A0этом сайте технология <a href="' + sanitizeForAttribute(link_wiki) + '">вики</a> позволяет это сделать. Не\u00A0смущайтесь, одно из\u00A0правил Циклопедии гласит: «<a href="' + sanitizeForAttribute(link_bebold) + '">Правьте смело</a>»! Если вы не\u00A0можете исправить ошибку самостоятельно, сообщите о\u00A0ней с\u00A0помощью данной формы.</p>' +
            '<p><strong>Если ошибка уже исправлена\u00A0— не\u00A0сообщайте о\u00A0ней.</strong></p>' +
            '<p>Не\u00A0оставляйте свой телефон и/или электронный адрес, ответ на\u00A0сообщение будет дан только на\u00A0странице с\u00A0сообщениями и нигде больше.</p>' +
            '<ul><li><a href="' + sanitizeForAttribute(link_buglist) + '">Текущий список сообщений об ошибках.</a></li></ul>'
        );
        
        return false;
    };

    // === ОРИГИНАЛЬНАЯ ФУНКЦИЯ wb$popBugBoth (полностью сохранена, с улучшенной безопасностью) ===
    window.wb$popBugBoth = function(action_page, infoHTML) {
        var glob = document.body;

        // Затемнение
        var nel = document.createElement('div');
        nel.id = 'specpop-globhidden';
        nel.style.cssText = 'background:white;filter:alpha(opacity=75);opacity:0.75;position:absolute;left:0px;top:0px;z-index:2000';
        nel.style.width = document.documentElement.scrollWidth + 'px';
        nel.style.height = document.documentElement.scrollHeight + 'px';
        glob.appendChild(nel);

        // Перемещение окна
        window.scroll(0, 150);

        // Информация
        var edit_el = document.getElementById('ca-edit');
        var can_edit = !!edit_el;

        nel = document.createElement('div');
        nel.id = 'specpop-info';
        nel.style.cssText = 'font-size:13px;background:white;padding:21px 30px;border:1px solid black;position:absolute;width:500px;min-height:300px;top:200px;z-index:2002';
        
        if (nel.style.maxHeight == undefined) {
            nel.style.height = '300px'; // IE
        }
        
        var tmp = Math.floor(glob.clientWidth / 2) - 300;
        if (tmp < 5) tmp = 5;
        nel.style.left = tmp + 'px';

        // Безопасная вставка HTML (используем санитизированную версию)
        nel.innerHTML = infoHTML;

        var wgUserName = mw.config.get('wgUserName');
        if (wgUserName == null) {
            nel.innerHTML = nel.innerHTML + '<p><strong>Внимание.</strong> Ваш IP-адрес будет записан в журнал изменений страницы.</p>';
        }
        
        nel.innerHTML = nel.innerHTML + '<p style="text-align:center;margin-top:15px">' +
            (can_edit ? '<input type="button" value="Исправить самостоятельно" onclick="wb$goToEditPage()" />' : '') +
            '<input type="button" value="Сообщить об ошибке" onclick="wb$elementsRemove(\'specpop-info\'); showFormModal()" />&nbsp;&nbsp;&nbsp;' +
            '<input type="button" value="Отмена" onclick="wb$elementsRemove(\'specpop-info\',\'specpop-form\',\'specpop-globhidden\',\'specpop-pos\')" />' +
            '</p>';
        
        glob.appendChild(nel);

        var action_url = mw.config.get('wgServer') + mw.config.get('wgScript') + "?title=" + encodeURIComponent(action_page) + "&action=submit";

        // Форма
        nel = document.createElement('div');
        nel.id = 'specpop-form';
        nel.style.cssText = 'background:white;padding:5px 10px;border:1px solid black;position:absolute;width:330px;min-height:300px;top:200px;z-index:2001';
        
        if (nel.style.maxHeight == undefined) {
            nel.style.height = '300px'; // IE
        }
        
        nel.style.left = (Math.floor(glob.clientWidth / 2) - 165) + 'px';
        
        // Безопасно создаём форму
        nel.innerHTML = '<form id="fm1" action="' + sanitizeForAttribute(action_url) + '" method="post" enctype="multipart/form-data" onsubmit="return wb$checkForm(this)">' +
            'Название страницы:<br /><input type="text" name="wpSummary" id="wpSummary" style="width:320px" readonly /><br />' +
            '<input type="hidden" name="wpSection" value="new" />' +
            '<input type="hidden" name="wpSave" value="Записать" />' +
            '<input type="hidden" id="Starttime" name="wpStarttime" value="" />' +
            '<input type="hidden" id="Edittime" name="wpEdittime" value="" />' +
            '<input type="hidden" id="EditToken" name="wpEditToken" value="" />' +
            '<input type="hidden" id="AutoSummary" name="wpAutoSummary" value="" />' +
            '<input type="hidden" name="wpScrolltop" value="0" />' +
            'Текст сообщения:<br /><textarea id="TextBox" name="wpTextbox1" style="width:320px;height:200px" onfocus="if (this.value == wb$description) {this.value = \'\'}">' + sanitizeHTML(wb$description) + '</textarea><br />' +
            'Подпись:<input type="text" name="author" id="wikibug-input-author" /><br />' +
            '<input type="submit" id="submit" value="Отправить" /> &nbsp; ' +
            '<input type="button" value="Отмена" onclick="wb$elementsRemove(\'specpop-form\',\'specpop-globhidden\',\'specpop-pos\')" />' +
            '</form>';
        
        glob.appendChild(nel);

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

        if (window.wb$isValidPageName(wgPageName)) {
            document.getElementById('wpSummary').value = wgPageName.replace(/_/g, " ");
        } else {
            document.getElementById('wpSummary').removeAttribute("readonly");
        }

        if (wgNamespaceNumber != 0) {
            document.getElementById('wpSummary').removeAttribute("readonly");
        }

        if (wgUserName != null) {
            var author = document.getElementById("wikibug-input-author");
            author.value = '~~' + '~~';
            author.disabled = 'disabled';
        }

        window.wb$getEditToken(action_page);
    };

    // === НОВАЯ ФУНКЦИЯ ДЛЯ БЕЗОПАСНОЙ ОТПРАВКИ ЧЕРЕЗ API ===
    window.saveBugReportViaApi = async function(pageName, content) {
        const api = new mw.Api();
        
        try {
            const token = await api.getToken('csrf');
            
            const params = {
                action: 'edit',
                title: 'Циклопедия:Сообщения об ошибках',
                section: 'new',
                sectiontitle: pageName,
                text: content,
                summary: `Сообщение об ошибке: ${pageName}`,
                token: token
            };
            
            const response = await api.post(params);
            
            if (response && response.error) {
                throw new Error(response.error.info);
            }
            
            return response;
        } catch (error) {
            console.error('API Error:', error);
            throw new Error('Не удалось сохранить сообщение: ' + error.message);
        }
    };

    // === НОВАЯ ФУНКЦИЯ ДЛЯ ПОКАЗА ФОРМЫ (из безопасной версии) ===
    window.showFormModal = function() {
        var wgUserName = mw.config.get('wgUserName');
        var wgPageName = mw.config.get('wgPageName');
        var wgNamespaceNumber = mw.config.get('wgNamespaceNumber');

        // Получаем существующую форму или создаём новую
        var existingForm = document.getElementById('specpop-form');
        if (existingForm) {
            // Обновляем значения в существующей форме
            if (window.wb$isValidPageName(wgPageName)) {
                var summaryInput = document.getElementById('wpSummary');
                if (summaryInput) {
                    summaryInput.value = wgPageName.replace(/_/g, " ");
                }
            }
            
            if (wgNamespaceNumber != 0) {
                var summaryInput = document.getElementById('wpSummary');
                if (summaryInput) {
                    summaryInput.removeAttribute("readonly");
                }
            }

            if (wgUserName != null) {
                var author = document.getElementById("wikibug-input-author");
                if (author) {
                    author.value = '~~' + '~~';
                    author.disabled = 'disabled';
                }
            }
        }
    };

    // === НОВЫЕ ФУНКЦИИ ДЛЯ УВЕДОМЛЕНИЙ ===
    window.showError = function(message) {
        if (typeof mw.notify === 'function') {
            mw.notify(sanitizeHTML(message), { type: 'error' });
        } else {
            alert('Ошибка: ' + message);
        }
    };

    window.showSuccess = function(message) {
        if (typeof mw.notify === 'function') {
            mw.notify(sanitizeHTML(message), { type: 'success' });
        } else {
            alert('Успех: ' + message);
        }
    };

    // === ИНИЦИАЛИЗАЦИЯ (сохранена оригинальная, добавлена поддержка mw.hook) ===
    $(function() {
        var el = document.getElementById('n-bug_in_article');
        
        if (el) {
            var link = el.getElementsByTagName('a')[0];
            if (link) {
                link.onclick = function(e) {
                    e.preventDefault();
                    window.wb$popWikibug();
                    return false;
                };
            }
        }
    });

    // Добавляем хук для совместимости с новыми темами
    if (typeof mw.hook !== 'undefined') {
        mw.hook('wikibugs.loaded').fire();
    }

})();