MediaWiki:Gadget-blamefinder.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.
/**
* Gadget: Blame Finder
* Бета-версия 1.0
* Назначение: поиск автора любого текста в статье для удобства администраторов
* Сделан под API для cyclowiki.org, в других вики возможно нужна локализация
* Рабочей базой служил движок Mediawiki 1.39.1
* Автор: Урахара
* Лицензия: CC BY-SA 4.0
* Использование:
1. Нажимаете кнопку «Кто автор?» в правом нижнем углу
2. Выделяете любой текст на странице (минимум 20 символов)
3. Кликайте по появившейся в центре кнопке таймера (20 секунд на всё)
4. Гаджет анализирует последние 500 правок страницы
5. Находит первое появление этого текста в истории
6. Показывает:
· Имя автора
· Дату правки
· Комментарий к правке
· Ссылки на версию и diff
* Что может:
· Искать авторов фрагментов текста в статьях
· Расчитан на работу в пространстве статей и форума
· Поддерживает мышь и сенсорный экран
· Показывает панель логов с отладкой
*/
(function() {
'use strict';
// ===== ЗАЩИТА ОТ CLICKJACKING =====
if (window.self !== window.top) {
console.log('BlameFinder: не запускаемся в iframe по соображениям безопасности');
return;
}
// ===== ЭКРАНИРОВАНИЕ =====
function escapeHtml(text) {
if (text === undefined || text === null) return '';
return String(text)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}
function escapeForLog(text) {
if (text === undefined || text === null) return '';
return String(text).replace(/[<>"']/g, function(match) {
if (match === '<') return '<';
if (match === '>') return '>';
if (match === '"') return '"';
if (match === "'") return ''';
return match;
});
}
// ===== СОЗДАНИЕ ЭЛЕМЕНТОВ =====
function createSafeElement(tag, options) {
var element = document.createElement(tag);
if (!options) return element;
if (options.className) element.className = options.className;
if (options.id) element.id = options.id;
if (options.text) element.textContent = options.text;
// Безопасная установка HTML только для проверенного контента
if (options.html) {
// Создаем временный div, устанавливаем textContent для экранирования
var tempDiv = document.createElement('div');
tempDiv.textContent = options.html;
element.innerHTML = tempDiv.innerHTML;
}
if (options.css) {
Object.keys(options.css).forEach(function(prop) {
element.style[prop] = options.css[prop];
});
}
if (options.attrs) {
Object.keys(options.attrs).forEach(function(key) {
// Валидация URL для href и src
if (key === 'href' || key === 'src') {
var value = options.attrs[key];
if (value && typeof value === 'string') {
// Разрешаем только относительные или безопасные протоколы
if (value.startsWith('/') ||
value.startsWith('https://') ||
value.startsWith('http://') && value.includes('wikimedia.org') ||
value.startsWith('#')) {
element.setAttribute(key, value);
}
}
} else {
element.setAttribute(key, options.attrs[key]);
}
});
}
return element;
}
// ===== СОЗДАНИЕ ССЫЛКИ =====
function createSafeLink(url, text, title) {
// Валидация URL
var safeUrl = '';
if (url && typeof url === 'string') {
// Разрешаем только относительные URL или URL с доверенными доменами
if (url.startsWith('/') || url.startsWith('#') ||
url.includes('cyclowiki.org') || url.includes('wikipedia.org')) {
safeUrl = url;
} else {
// Если URL не прошел валидацию, создаем ссылку на #
safeUrl = '#';
}
}
var link = createSafeElement('a', {
text: text,
attrs: {
href: safeUrl,
target: '_blank',
rel: 'noopener noreferrer',
title: title ? escapeHtml(title) : escapeHtml(text)
}
});
return link;
}
// ===== ВАЛИДАЦИЯ TITLE =====
function validateTitle(title) {
if (!title || typeof title !== 'string') return null;
// Удаляем управляющие символы и потенциально опасные
var cleaned = title.replace(/[\x00-\x1F\x7F<>"']/g, '').trim();
// Проверяем, что после очистки осталось что-то значимое
return cleaned.length > 0 ? cleaned : null;
}
// ===== ПОЛУЧАЕМ ДАННЫЕ О СТРАНИЦЕ =====
function getPageInfo() {
var title = null;
var namespace = 0;
var isSpecial = false;
// Безопасное получение pathname
var pathname = window.location.pathname || '';
var search = window.location.search || '';
if (pathname.includes('Special:') || pathname.includes('Служебная:') ||
pathname.includes('special:') || pathname.includes('служебная:')) {
isSpecial = true;
}
try {
var urlParams = new URLSearchParams(search);
title = urlParams.get('title');
if (title) {
title = validateTitle(title);
if (title && title.includes(':')) {
var parts = title.split(':');
var prefix = parts[0].toLowerCase();
var excludedNamespaces = [
'обсуждение', 'talk', 'участник', 'user', 'user_talk',
'обсуждение_участника', 'файл', 'file', 'file_talk',
'обсуждение_файла', 'mediawiki', 'mediawiki_talk',
'обсуждение_mediawiki', 'шаблон', 'template',
'template_talk', 'обсуждение_шаблона', 'справка',
'help', 'help_talk', 'обсуждение_справки', 'категория',
'category', 'category_talk', 'обсуждение_категории',
'портал', 'portal', 'portal_talk', 'обсуждение_портала',
'special', 'служебная'
];
if (excludedNamespaces.includes(prefix)) {
namespace = 1;
}
}
} else {
if (pathname.includes('/wiki/')) {
var pathTitle = pathname.split('/wiki/')[1];
if (pathTitle) {
try {
pathTitle = decodeURIComponent(pathTitle);
} catch (e) {
pathTitle = pathTitle;
}
if (pathTitle.includes(':')) {
var pathPrefix = pathTitle.split(':')[0].toLowerCase();
var excludedNamespaces = [
'обсуждение', 'talk', 'участник', 'user',
'файл', 'file', 'mediawiki', 'шаблон',
'template', 'справка', 'help', 'категория',
'category', 'портал', 'portal', 'special',
'служебная'
];
if (excludedNamespaces.includes(pathPrefix)) {
namespace = 1;
}
}
title = validateTitle(pathTitle);
}
} else {
var h1 = document.querySelector('h1.firstHeading, .firstHeading, #firstHeading');
if (h1 && h1.textContent) {
title = validateTitle(h1.textContent.trim());
}
}
}
} catch (e) {
console.log('BlameFinder: ошибка получения информации о странице', e);
}
return {
title: title || 'Main_Page',
isArticle: namespace === 0 && !isSpecial
};
}
// ===== РЕАЛИЗАЦИЯ API С ПРОВЕРКОЙ ORIGIN =====
function cycApiRequest(params, callback, errorCallback) {
var apiUrl = '/w/api.php';
params.format = 'json';
params.origin = '*';
// Валидация параметров
var safeParams = {};
Object.keys(params).forEach(function(key) {
var value = params[key];
if (typeof value === 'string') {
// Удаляем потенциально опасные символы из параметров
safeParams[key] = value.replace(/[<>"']/g, '');
} else {
safeParams[key] = value;
}
});
var queryString = Object.keys(safeParams).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(safeParams[key]);
}).join('&');
var url = apiUrl + '?' + queryString;
var xhr = new XMLHttpRequest();
var timeoutId = null;
var completed = false;
timeoutId = setTimeout(function() {
if (!completed) {
completed = true;
xhr.abort();
addLog('Таймаут API: сервер не отвечает', 'error');
if (errorCallback) errorCallback('timeout');
}
}, 30000);
xhr.open('GET', url, true);
xhr.setRequestHeader('Api-User-Agent', 'BlameFinder/1.2');
xhr.onreadystatechange = function() {
if (completed) return;
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Проверяем Content-Type
var contentType = xhr.getResponseHeader('Content-Type') || '';
if (!contentType.includes('application/json') && !contentType.includes('text/javascript')) {
clearTimeout(timeoutId);
completed = true;
addLog('Неверный Content-Type ответа API', 'error');
if (errorCallback) errorCallback('invalid_content_type');
return;
}
try {
var data = JSON.parse(xhr.responseText);
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format');
}
clearTimeout(timeoutId);
completed = true;
callback(data);
} catch (e) {
clearTimeout(timeoutId);
completed = true;
addLog('Ошибка парсинга ответа API', 'error');
if (errorCallback) errorCallback('parse_error');
}
} else {
clearTimeout(timeoutId);
completed = true;
addLog('Ошибка HTTP: ' + xhr.status, 'error');
if (errorCallback) errorCallback('http_' + xhr.status);
}
}
};
xhr.onerror = function() {
if (!completed) {
clearTimeout(timeoutId);
completed = true;
addLog('Ошибка сети при запросе к API', 'error');
if (errorCallback) errorCallback('network_error');
}
};
xhr.send();
}
// ===== ПАНЕЛЬ ЛОГОВ =====
var logPanel = null;
var logEntries = [];
function createLogPanel() {
if (logPanel) return;
logPanel = createSafeElement('div', {
id: 'blame-log-panel',
css: {
position: 'fixed',
bottom: '20px',
left: '20px',
width: '400px',
maxHeight: '300px',
overflowY: 'auto',
background: 'rgba(0,0,0,0.9)',
color: '#0f0',
fontFamily: 'monospace',
fontSize: '12px',
padding: '10px',
borderRadius: '5px',
zIndex: '9999',
border: '2px solid #36c',
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
display: 'none'
}
});
var header = createSafeElement('div', {
css: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '10px',
paddingBottom: '5px',
borderBottom: '2px solid #36c'
}
});
var titleSpan = createSafeElement('span', {
text: 'Логи BlameFinder',
css: {
color: '#36c',
fontWeight: 'bold'
}
});
var buttonContainer = createSafeElement('div');
var clearButton = createSafeElement('button', {
id: 'blame-clear',
text: 'Очистить',
css: {
background: 'none',
border: '1px solid #36c',
color: '#36c',
marginRight: '5px',
cursor: 'pointer',
padding: '5px'
}
});
var toggleButton = createSafeElement('button', {
id: 'blame-toggle',
text: 'Скрыть',
css: {
background: 'none',
border: '1px solid #36c',
color: '#36c',
cursor: 'pointer',
padding: '5px'
}
});
buttonContainer.appendChild(clearButton);
buttonContainer.appendChild(toggleButton);
header.appendChild(titleSpan);
header.appendChild(buttonContainer);
var content = createSafeElement('div', {
id: 'blame-log-content',
css: {
wordBreak: 'break-word'
}
});
logPanel.appendChild(header);
logPanel.appendChild(content);
document.body.appendChild(logPanel);
clearButton.onclick = function(e) {
e.stopPropagation();
logEntries = [];
updateLogDisplay();
};
var logVisible = true;
toggleButton.onclick = function(e) {
e.stopPropagation();
logVisible = !logVisible;
logPanel.style.display = logVisible ? 'block' : 'none';
toggleButton.textContent = logVisible ? 'Скрыть' : 'Показать';
};
logPanel.style.display = 'block';
}
function addLog(message, type) {
var timestamp = new Date().toLocaleTimeString('ru');
var color = '#0f0';
if (type === 'error') color = '#f00';
if (type === 'warning') color = '#ff0';
if (type === 'success') color = '#0f0';
// Экранируем сообщение для логов
var safeMessage = escapeForLog(String(message));
logEntries.unshift({
time: timestamp,
text: safeMessage,
color: color
});
if (logEntries.length > 30) logEntries.pop();
updateLogDisplay();
}
function updateLogDisplay() {
var content = document.getElementById('blame-log-content');
if (!content) return;
// Безопасная очистка
while (content.firstChild) {
content.removeChild(content.firstChild);
}
logEntries.forEach(function(entry) {
var entryDiv = createSafeElement('div', {
css: {
color: entry.color,
marginBottom: '3px'
}
});
// Используем textContent для безопасности
entryDiv.textContent = '[' + entry.time + '] ' + entry.text;
content.appendChild(entryDiv);
});
}
// ===== КНОПКА =====
function addButton() {
addLog('Инициализация BlameFinder для Циклопедии', 'info');
var button = createSafeElement('div', {
id: 'blame-finder-button',
text: 'Кто автор?',
css: {
position: 'fixed',
bottom: '20px',
right: '20px',
background: '#36c',
color: 'white',
padding: '10px 20px',
borderRadius: '5px',
cursor: 'pointer',
zIndex: '9999',
fontFamily: 'sans-serif',
fontSize: '14px',
boxShadow: '0 2px 10px rgba(0,0,0,0.3)'
}
});
button.onmouseover = function() {
this.style.background = '#447ff5';
};
button.onmouseout = function() {
this.style.background = '#36c';
};
button.addEventListener('click', function(e) {
e.preventDefault();
activateSelectionMode();
});
document.body.appendChild(button);
addLog('Кнопка добавлена в правый нижний угол', 'success');
createLogPanel();
}
// ===== ПЕРЕМЕННЫЕ РЕЖИМА ВЫДЕЛЕНИЯ =====
var selectionActive = false;
var selectionTimer = null;
var selectionHandlers = {};
// ===== РЕЖИМ ВЫДЕЛЕНИЯ =====
function activateSelectionMode() {
if (selectionActive) {
addLog('Режим выделения уже активен', 'warning');
return;
}
selectionActive = true;
addLog('Режим выделения активирован', 'info');
addLog('Выделите текст (мышью или касанием)', 'info');
// Создаем тултип безопасно, без innerHTML
var tooltip = createSafeElement('div', {
id: 'blame-tooltip',
css: {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#36c',
color: 'white',
padding: '20px 40px',
borderRadius: '10px',
zIndex: '10000',
fontFamily: 'sans-serif',
fontSize: '18px',
textAlign: 'center',
boxShadow: '0 0 30px rgba(0,0,0,0.5)'
}
});
// Создаём элементы безопасно
var mainText = document.createTextNode('Выделите текст');
tooltip.appendChild(mainText);
var br = document.createElement('br');
tooltip.appendChild(br);
var small = createSafeElement('small', {
css: {
color: '#ff0'
}
});
var smallText = document.createTextNode('осталось ');
small.appendChild(smallText);
var timerSpan = createSafeElement('span', {
id: 'blame-timer',
text: '20',
css: {
color: '#ff0'
}
});
small.appendChild(timerSpan);
var secondsText = document.createTextNode(' секунд');
small.appendChild(secondsText);
tooltip.appendChild(small);
document.body.appendChild(tooltip);
var seconds = 20;
var timer = setInterval(function() {
seconds--;
var timerSpan = document.getElementById('blame-timer');
if (timerSpan) timerSpan.textContent = seconds;
}, 1000);
function mouseHandler() {
if (!selectionActive) return;
handleSelection();
}
function touchHandler() {
if (!selectionActive) return;
setTimeout(handleSelection, 100);
}
function handleSelection() {
if (!selectionActive) return;
var selection = window.getSelection();
var selectedText = selection.toString().trim();
if (selectedText.length >= 20) {
var logText = 'Выделен текст (' + selectedText.length + ' символов): "' +
selectedText.substring(0, 50) +
(selectedText.length > 50 ? '..."' : '"');
addLog(logText, 'success');
deactivateSelectionMode();
findAuthor(selectedText);
} else if (selectedText.length > 0) {
addLog('Слишком короткий текст (' + selectedText.length + ' символов, нужно минимум 20)', 'warning');
}
}
function deactivateSelectionMode() {
if (!selectionActive) return;
selectionActive = false;
document.removeEventListener('mouseup', mouseHandler);
document.removeEventListener('touchend', touchHandler);
if (timer) {
clearInterval(timer);
timer = null;
}
var tip = document.getElementById('blame-tooltip');
if (tip && tip.parentNode) {
tip.parentNode.removeChild(tip);
}
addLog('Режим выделения отключён', 'info');
}
selectionHandlers = {
mouse: mouseHandler,
touch: touchHandler,
deactivate: deactivateSelectionMode
};
document.addEventListener('mouseup', mouseHandler);
document.addEventListener('touchend', touchHandler);
var timeoutId = setTimeout(function() {
if (selectionActive) {
addLog('Время вышло', 'warning');
deactivateSelectionMode();
}
}, 20000);
selectionTimer = timeoutId;
}
// ===== ПОИСК АВТОРА =====
function findAuthor(text) {
var pageInfo = getPageInfo();
var title = pageInfo.title;
addLog('Начинаем поиск для страницы: ' + escapeForLog(title), 'info');
var logText = 'Ищем текст: "' + text.substring(0, 100) +
(text.length > 100 ? '..."' : '"');
addLog(logText, 'info');
var loadingDiv = showLoading('Анализирую историю правок...');
addLog('Запрашиваем историю правок через API', 'info');
var globalTimeout = setTimeout(function() {
hideLoading(loadingDiv);
addLog('Глобальный таймаут: сервер не отвечает', 'error');
}, 30000);
getRevisions(title, function(revisions) {
clearTimeout(globalTimeout);
addLog('Получено ревизий: ' + revisions.length, 'info');
if (revisions.length === 0) {
addLog('Не удалось загрузить историю правок', 'error');
hideLoading(loadingDiv);
return;
}
addLog('Начинаем поиск первого появления текста...', 'info');
findTextAppearance(revisions, text, title, function(result) {
hideLoading(loadingDiv);
if (result) {
addLog('Текст найден! Автор: ' + escapeForLog(result.rev.user), 'success');
showResult(result);
} else {
addLog('Текст не найден в истории', 'error');
}
});
}, function(error) {
clearTimeout(globalTimeout);
hideLoading(loadingDiv);
addLog('Ошибка при запросе к API: ' + error, 'error');
});
}
function getRevisions(title, callback, errorCallback) {
var revisions = [];
var continueParam = {};
var totalFetched = 0;
var maxRevisions = 500;
function fetchRevisions() {
var params = {
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'ids|timestamp|user|comment|content',
rvlimit: 50,
rvdir: 'older',
rvslots: 'main',
formatversion: 2
};
if (continueParam.rvcontinue) {
// Валидация continue параметра
if (typeof continueParam.rvcontinue === 'string') {
params.rvcontinue = continueParam.rvcontinue.replace(/[<>"']/g, '');
}
}
cycApiRequest(params, function(data) {
if (data.error) {
addLog('Ошибка API: ' + escapeForLog(data.error.info), 'error');
if (errorCallback) errorCallback(data.error.info);
return;
}
if (!data.query || !data.query.pages || !data.query.pages[0]) {
addLog('Странный ответ от API', 'error');
if (errorCallback) errorCallback('invalid_response');
return;
}
var page = data.query.pages[0];
if (page.missing) {
addLog('Страница не найдена в API', 'error');
if (errorCallback) errorCallback('page_missing');
return;
}
if (page.revisions && page.revisions.length > 0) {
page.revisions.forEach(function(rev) {
// Валидация данных ревизии
revisions.push({
revid: rev.revid || 0,
timestamp: rev.timestamp || '',
user: (rev.user && typeof rev.user === 'string') ? rev.user : 'Аноним',
comment: (rev.comment && typeof rev.comment === 'string') ? rev.comment : '—',
content: (rev.slots && rev.slots.main && rev.slots.main.content) ?
rev.slots.main.content : ''
});
});
totalFetched += page.revisions.length;
addLog('Загружено ревизий: ' + totalFetched + '/' + maxRevisions, 'info');
} else {
addLog('У страницы нет истории правок', 'warning');
callback(revisions);
return;
}
if (data.continue && totalFetched < maxRevisions) {
continueParam = {
rvcontinue: data.continue.rvcontinue
};
fetchRevisions();
} else {
if (totalFetched >= maxRevisions) {
addLog('Достигнут лимит загрузки (' + maxRevisions + ' ревизий)', 'warning');
}
callback(revisions);
}
}, errorCallback);
}
fetchRevisions();
}
function findTextAppearance(revisions, text, title, callback) {
var oldestFirst = revisions.slice().reverse();
addLog('Анализируем ' + oldestFirst.length + ' ревизий от старых к новым', 'info');
var foundRev = null;
var previousContent = null;
var checkedCount = 0;
for (var i = 0; i < oldestFirst.length; i++) {
var rev = oldestFirst[i];
var content = rev.content || '';
checkedCount++;
if (checkedCount % 50 === 0) {
addLog('Проверено ' + checkedCount + '/' + oldestFirst.length + ' ревизий...', 'info');
}
if (content.indexOf(text) !== -1) {
addLog('Текст найден в ревизии от ' + rev.timestamp + ' (id: ' + rev.revid + ')', 'info');
if (i > 0) {
previousContent = oldestFirst[i - 1].content || '';
if (previousContent.indexOf(text) !== -1) {
addLog('Текст был и в предыдущей ревизии, продолжаем поиск...', 'info');
continue;
} else {
foundRev = rev;
addLog('Найдено первое появление! Ревизия: ' + rev.revid, 'success');
break;
}
} else {
foundRev = rev;
addLog('Текст есть в самой первой ревизии страницы', 'info');
break;
}
}
}
if (!foundRev) {
addLog('Текст не найден ни в одной ревизии', 'error');
callback(null);
return;
}
if (previousContent !== null) {
addLog('Выполняю дополнительную проверку...', 'info');
var appearsInNew = (foundRev.content || '').indexOf(text) !== -1;
var appearsInOld = (previousContent || '').indexOf(text) !== -1;
if (appearsInNew && !appearsInOld) {
addLog('Проверка пройдена: текст появился именно в этой правке', 'success');
callback({ rev: foundRev, title: title });
} else {
addLog('Проверка не пройдена: текст изменялся позже', 'warning');
var index = -1;
for (var j = 0; j < revisions.length; j++) {
if (revisions[j].revid === foundRev.revid) {
index = j;
break;
}
}
if (index !== -1) {
revisions.splice(index, 1);
addLog('Перезапускаю поиск...', 'info');
findTextAppearance(revisions, text, title, callback);
} else {
callback(null);
}
}
} else {
callback({ rev: foundRev, title: title });
}
}
// ===== БЕЗОПАСНОЕ ОТОБРАЖЕНИЕ РЕЗУЛЬТАТА =====
function showResult(result) {
var rev = result.rev;
var title = result.title;
var date = new Date(rev.timestamp);
var formattedDate = date.toLocaleString('ru', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
addLog('Результат: автор ' + escapeForLog(rev.user) + ', дата ' + formattedDate, 'success');
// Экранирование пользовательских данных
var safeUser = escapeHtml(rev.user);
var safeComment = escapeHtml(rev.comment);
// Создаем overlay
var overlay = createSafeElement('div', {
id: 'blame-overlay',
css: {
position: 'fixed',
top: '0',
left: '0',
right: '0',
bottom: '0',
background: 'rgba(0,0,0,0.5)',
zIndex: '9999'
}
});
// Создаём модальное окно
var modal = createSafeElement('div', {
id: 'blame-result-modal',
css: {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'white',
border: '2px solid #a2a9b1',
borderRadius: '8px',
padding: '20px',
zIndex: '10000',
maxWidth: '500px',
boxShadow: '0 5px 20px rgba(0,0,0,0.3)',
fontFamily: 'sans-serif'
}
});
// Создаём заголовок
var titleElement = createSafeElement('h3', {
text: 'Найден автор текста',
css: {
marginTop: '0',
color: '#36c'
}
});
modal.appendChild(titleElement);
// Создаём параграф с участником
var userP = createSafeElement('p');
var userStrong = createSafeElement('strong', { text: 'Участник:' });
userP.appendChild(userStrong);
userP.appendChild(document.createTextNode(' ' + safeUser));
modal.appendChild(userP);
// Создаём параграф с датой
var dateP = createSafeElement('p');
var dateStrong = createSafeElement('strong', { text: 'Дата правки:' });
dateP.appendChild(dateStrong);
dateP.appendChild(document.createTextNode(' ' + formattedDate));
modal.appendChild(dateP);
// Создаём параграф с комментарием
var commentP = createSafeElement('p');
var commentStrong = createSafeElement('strong', { text: 'Комментарий:' });
commentP.appendChild(commentStrong);
commentP.appendChild(document.createTextNode(' "' + safeComment + '"'));
modal.appendChild(commentP);
// Создаём параграф со ссылкой на версию
var versionP = createSafeElement('p');
var versionStrong = createSafeElement('strong', { text: 'Версия:' });
versionP.appendChild(versionStrong);
versionP.appendChild(document.createTextNode(' '));
var versionUrl = '/w/index.php?title=' + encodeURIComponent(title) + '&oldid=' + rev.revid;
var versionLink = createSafeLink(versionUrl, 'посмотреть эту версию');
versionP.appendChild(versionLink);
modal.appendChild(versionP);
// Создаём параграф со ссылкой на дифф
var diffP = createSafeElement('p');
var diffStrong = createSafeElement('strong', { text: 'Дифф:' });
diffP.appendChild(diffStrong);
diffP.appendChild(document.createTextNode(' '));
var diffUrl = '/w/index.php?title=' + encodeURIComponent(title) + '&diff=prev&oldid=' + rev.revid;
var diffLink = createSafeLink(diffUrl, 'посмотреть изменения');
diffP.appendChild(diffLink);
modal.appendChild(diffP);
// Разделитель
var hr = createSafeElement('hr', {
css: {
border: 'none',
borderTop: '1px solid #eaecf0'
}
});
modal.appendChild(hr);
// Кнопка закрытия
var closeButton = createSafeElement('button', {
id: 'blame-close',
text: 'Закрыть',
css: {
background: '#36c',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer'
}
});
modal.appendChild(closeButton);
document.body.appendChild(overlay);
document.body.appendChild(modal);
function closeModal() {
var m = document.getElementById('blame-result-modal');
var o = document.getElementById('blame-overlay');
if (m && m.parentNode) {
m.parentNode.removeChild(m);
}
if (o && o.parentNode) {
o.parentNode.removeChild(o);
}
}
closeButton.onclick = closeModal;
overlay.onclick = closeModal;
}
function showLoading(message) {
var div = createSafeElement('div', {
id: 'blame-loading',
text: message || 'Загрузка...',
css: {
position: 'fixed',
top: '20px',
right: '20px',
background: '#f8f9fa',
border: '1px solid #a2a9b1',
padding: '10px 20px',
borderRadius: '4px',
zIndex: '10000',
fontFamily: 'sans-serif',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}
});
document.body.appendChild(div);
return div;
}
function hideLoading(div) {
if (div && div.parentNode) {
div.parentNode.removeChild(div);
}
}
// ===== ЗАПУСК =====
var pageInfo = getPageInfo();
if (pageInfo.isArticle) {
if (document.readyState === 'complete') {
addButton();
} else {
window.addEventListener('load', addButton);
}
} else {
console.log('BlameFinder: не основное пространство имён, гаджет не активирован');
}
})();