MediaWiki:Gadget-common-special-search.js: различия между версиями

исправление позиционирования кнопки
 
повышение безопасности
 
(не показаны 2 промежуточные версии этого же участника)
Строка 1: Строка 1:
// Не удаляйте проверку на название спецстраницы
$(function() {
$( function () {
    if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') return;
if ( mw.config.get( 'wgCanonicalSpecialPageName' ) !== 'Search' ) {
   
return;
    var searchInput = document.querySelector('#searchText input, #searchInput');
}
    if (!searchInput) return;
 
   
// External search engines
    // Безопасное получение значения поиска с ограничением длины
var searchInput = document.querySelector( '#searchText input' );
    var searchQuery = (searchInput.value || '').trim();
var list = {
    if (searchQuery.length > 500) {
'Google': 'https://google.com/search?q=%s+site:ru.wikipedia.org&hl=ru',
        searchQuery = searchQuery.slice(0, 500);
'Яндексе': 'https://yandex.ru/yandsearch?text=%s&site=ru.wikipedia.org',
    }
'Bing': 'https://www.bing.com/search?q=%s+site:ru.wikipedia.org',
   
};
    // Валидация и санитизация через encodeURIComponent
var listKeys = Object.keys( list );
    var safeQuery = encodeURIComponent(searchQuery);
 
   
var searchEngines = document.createElement( 'p' );
    // Замораживаем объект с движками для защиты от перезаписи
searchEngines.id = 'searchEngines';
    var engines = Object.freeze({
searchEngines.innerHTML = 'Искать в (';
        'Bing': 'https://www.bing.com/search?q=%s+site:absurdopedia.wiki',
if (!searchInput) return;
        'DuckDuckGo': 'https://duckduckgo.com/?q=%s+site:absurdopedia.wiki',
 
        'Google': 'https://google.com/search?q=%s+site:absurdopedia.wiki&hl=ru',
for ( var i in list ) {
        'Yandex': 'https://yandex.ru/yandsearch?text=%s&site=absurdopedia.wiki'
var link = document.createElement( 'a' );
    });
link.href = list[ i ].replace( '%s', encodeURIComponent( searchInput.value ) );
   
link.textContent = i;
    // Функция безопасной валидации URL
searchEngines.appendChild( link );
    function isValidUrl(url) {
 
        try {
if ( listKeys.indexOf( i ) < listKeys.length - 1 ) {
            var parsedUrl = new URL(url);
searchEngines.appendChild( document.createTextNode( ' | ' ) );
            return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
}
        } catch(e) {
}
            return false;
searchEngines.appendChild(document.createTextNode( ')' ));
        }
 
    }
$( '.searchresults > .mw-search-visualclear' ).last().after( searchEngines );
   
    var $enginesContainer = $('<p>').attr('id', 'searchEngines');
// Добавить ссылку «Искать на подстраницах» в поиск с префиксом
    var $textNode = $('<span>').text('Искать в (');
var urlParams = new URLSearchParams( location.search );
    $enginesContainer.append($textNode);
var prefix = urlParams.get( 'prefix' );
   
if ( prefix && prefix.includes( '/' ) ) {
    // Безопасное получение ключей с проверкой собственных свойств
var basePage = prefix.split( '/' )[ 0 ];
    var engineNames = Object.keys(engines).filter(function(name) {
var $searchAllLink = $( '#mw-content-subtitle a' );
        return engines.hasOwnProperty(name) && typeof engines[name] === 'string';
    });
if ( $searchAllLink.length ) {
   
var $searchPrefix = $searchAllLink.clone();
    engineNames.forEach(function(name, index) {
$searchPrefix.text( 'Искать на подстраницах «' + basePage + '»' );
        var urlTemplate = engines[name];
$searchPrefix.attr( 'href', $searchPrefix.attr( 'href' ) + '&prefix=' + encodeURIComponent( basePage ) );
        // Проверяем, что URL начинается с безопасного протокола
        if (!/^https?:\/\//i.test(urlTemplate)) {
$searchAllLink.after( $searchPrefix );
            return;
$searchAllLink.after( '&nbsp;| ' );
        }
}
        var url = urlTemplate.replace('%s', safeQuery);
}
       
        // Дополнительная проверка финального URL
function clickOnKeydown( e ) {
        if (!isValidUrl(url)) {
if ( [ 'Space', 'Enter' ].includes( e.code ) ) {
            return;
e.preventDefault();
        }
this.click();
       
}
        var $link = $('<a>')
}
            .attr('href', url)
            .attr('target', '_blank')
// Таблица ключевых слов для поиска
            .attr('rel', 'noopener noreferrer')
mw.loader.using( [
            .text(name);
'mediawiki.util',
        $enginesContainer.append($link);
'oojs-ui-core',
        if (index < engineNames.length - 1) {
'oojs-ui-widgets'
            $enginesContainer.append(' | ');
], () => {
        }
var $pseudolinkWrapper = $( '#keywords-popup-pseudolink-wrapper' );
    });
if ( !$pseudolinkWrapper.length ) return;
   
 
    $enginesContainer.append(')');
var $helpLink = $( '#mw-indicator-mw-helplink a' );
    $('.searchresults > .mw-search-visualclear').last().after($enginesContainer);
$helpLink.html( $helpLink.html().replace( 'Справка', 'Полная справка' ) );
   
    var urlParams = new URLSearchParams(location.search);
mw.util.addCSS( '.mw-indicators { display: flex; align-items: center; }' );
    var prefix = urlParams.get('prefix');
var keywordsButton = new OO.ui.PopupButtonWidget( {
    if (prefix && typeof prefix === 'string' && prefix.includes('/')) {
id: 'keywords-button',
        var basePage = prefix.split('/')[0];
label: 'Ключевые слова',
        // Строгая валидация basePage: только безопасные символы
indicator: 'down',
        if (basePage && /^[a-zA-Z0-9\s\u0400-\u04FF\-_]+$/.test(basePage)) {
flags: [
            var $searchAllLink = $('#mw-content-subtitle a');
'progressive'
            if ($searchAllLink.length) {
],
                var $searchPrefix = $searchAllLink.clone();
icon: 'keywords',
                // Используем text() вместо HTML-конкатенации
framed: false,
                var linkText = 'Искать на подстраницах «' + basePage + '»';
popup: {
                $searchPrefix
$content: $( '<div>' ).html( $( '#keywords-popup' ).html() ),
                    .text(linkText)
padded: true,
                    .attr('href', $searchAllLink.attr('href') + '&prefix=' + encodeURIComponent(basePage));
align: 'down',
                $searchAllLink.after(
width: 420
                    $('<span>').text(' | '),
}
                    $searchPrefix
} );
                );
keywordsButton.$element.appendTo( $( '#mw-indicator-0-keywords-popup .mw-parser-output' ) );
            }
        }
var $searchBox = $( '#searchText input' );
    }
var pseudolinkHref = $pseudolinkWrapper.find( 'a' ).attr( 'href' );
   
$( '.keywords-popup-keyword' ).wrap( $( '<a>' )
    mw.loader.using(['mediawiki.util', 'oojs-ui-core', 'oojs-ui-widgets'], function() {
.attr( 'href', pseudolinkHref )
        var $keywordsWrapper = $('#keywords-popup-pseudolink-wrapper');
.attr( 'role', 'button' )
        if (!$keywordsWrapper.length) return;
.attr( 'title', 'Вставить ключевое слово в поле поиска' )
       
.on( 'click', function( e ) {
        $('#mw-indicator-mw-helplink a').text(function(i, text) {
e.preventDefault();
            return text.replace('Справка', 'Полная справка');
$searchBox.val( $searchBox.val() + $( this ).find( '.keywords-popup-keyword' )
        });
.data( 'keyword' ).replace( / /g, ' ' ) ).focus();
       
} )
        mw.util.addCSS('.mw-indicators { display: flex; align-items: center; }');
.on( 'keydown', clickOnKeydown )
       
);
        var keywordsButton = new OO.ui.PopupButtonWidget({
} );
            label: 'Ключевые слова',
} );
            indicator: 'down',
            flags: ['progressive'],
            icon: 'keywords',
            framed: false,
            popup: {
                $content: $('<div>').append($('#keywords-popup').children().clone()),
                padded: true,
                align: 'down',
                width: 420
            }
        });
       
        keywordsButton.$element.appendTo('#mw-indicator-0-keywords-popup .mw-parser-output');
       
        var $searchBox = $('#searchText input');
        $('.keywords-popup-keyword').each(function() {
            var $keyword = $(this);
            var rawKeyword = $keyword.data('keyword');
           
            // Проверяем, что данные существуют и являются строкой
            if (typeof rawKeyword !== 'string') return;
           
            // Усиленная санитизация ключевого слова
            var keywordText = rawKeyword
                .replace(/[<>]/g, '') // Удаляем угловые скобки
                .replace(/['"]/g, '') // Удаляем кавычки
                .replace(/javascript:/gi, '') // Защита от псевдо-протокола
                .replace(/data:/gi, '') // Защита от data: URI
                .replace(/vbscript:/gi, '') // Защита от VBScript
                .replace(/on\w+=/gi, '') // Удаляем обработчики событий
                .slice(0, 500); // Ограничиваем длину
           
            // Дополнительная проверка: не должно быть опасных паттернов
            var hasDangerousPattern = /[<>'"]|javascript:|data:|vbscript:|on\w+=/i.test(keywordText);
            if (hasDangerousPattern) return;
           
            $keyword
                .attr('role', 'button')
                .attr('tabindex', '0')
                .attr('title', 'Вставить ключевое слово в поле поиска')
                .css('cursor', 'pointer')
                .on('click keydown', function(e) {
                    // Исправлено: добавлена поддержка Spacebar для старых браузеров
                    if (e.type === 'click' || e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
                        e.preventDefault();
                        var currentValue = $searchBox.val() || '';
                        var newValue = currentValue + keywordText;
                        // Проверяем длину результата
                        if (newValue.length < 10000) {
                            $searchBox.val(newValue).trigger('focus');
                        }
                    }
                });
        });
    });
});