Участник:Землеройкин/remove.js

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
/*
	CC-BY-SA 4.0 Основано на u:higimo/remove.js?oldid=107716412 // v5.0.4
	Вынесение на КБУ (+ отсроченное), КУ (+ множественное, + оставлено), КУЛ, КПМ.
	Подключение (на своём /common.js):
	importScript('у:Землеройкин/remove.js');
	
	var g_user_alert = 1 // можно указать, чтоб уведомлять автора по умолчанию
*/
$(document).ready(function() {
	var config = mw.config.get(['skin', 'wgNamespaceIds', 'wgFormattedNamespaces', 'wgNamespaceNumber', 'wgPageName', 'wgPageContentModel', 'wgIsRedirect', 'wgUserName']),
		/*
			Пространства имён, в которых будет включён удалятор
		*/
		rmNamespaces = (typeof g_rm_namespaces == 'undefined') ? [0, 2, 4, 6, 10, 14, 100, 104, 828] : g_rm_namespaces,
		/*
			Все кнопки меню
		*/
		menu_raw = {imp:'КУЛ', rnm:'КПМ', tRm:'КУ', mRm:'Много КУ', fRm:'КБУ', merge:'КОБ', split:'КРАЗД', recov:'ВУС', ret:'Оставить', doneRnm:'Переименовано', noRnm:'Не переименовано'},
		/*
			Доступные кнопки
		*/
		rmActions = (typeof g_rm_actions == 'undefined') ? Object.keys(menu_raw) : g_rm_actions,
		/*
			Переменная, управляющая функцией оповещения создателя статьи
		*/
		setAlert = (typeof g_user_alert == 'undefined') ? 0 : g_user_alert,
		/*
			Индикатор ошибки api
		*/
		isError,
		/*
			Здесь хранятся параметры номинации
		*/
		param,
		rOld,
		/*
			Возможные разделы КБУ в зависимости от пространства имён
		*/
		fastRemovePrefix = (config.wgIsRedirect ? 'ОП' : 'О') + ({0:'Сd', 2:'У', 3:'У', 6:'Ф', 14:'К'}[config.wgNamespaceNumber] || ''),
		/*
			Список всех доступных причин КБУ
			Коды для админов, текст для остальных.
			Используется максимально короткий шаблон.
			Третий параметр — условие для появления <input>
		*/
		fastRemove = [
			['подст:ds',		'ds Отсроченное'				],
			['уд-бессвязно',	'О1 Бессвязный текст'			],
			['уд-тест',			'О2 Тестовая страница'			],
			['уд-ванд',			'О3 Вандальная страница'		],
			['уд-повторно',		'О4 Уже удалялось'				],
			['уд-автор',		'О5 По просьбе автора'			],
			['уд-обс',			'О6 Ненужная подстраница'		],
			['уд-переим',		'О7 Для переименования',		'страницу'],
			['уд-дубль',		'О8 Дубликат',					'страницу'],
			['уд-реклама',		'О9 Реклама или спам'			],
			['db-badtalk',		'О10 Нецелевая СО'				],
			['уд-копивио',		'О11 Нарушение АП',				'ссылку'],
			['уд-пусто',		'С1 Пусто или коротко'			],
			['уд-иностр',		'С2 Не на русском'				],
			['уд-ссылки',		'С3 Лишь ссылки'				],
			['уд-нз',			'С5 Явно незначимо'				],
			['уд-в никуда',		'П1 Перенапр. в никуда'			],
			['db-redirspace',	'П2 Межпростр. перенапр.'		],
			['уд-опечатка',		'П3 Перенапр. с опечаткой'		],
			['уд-падеж',		'П4 Не именительный падеж'		],
			['уд-смысл',		'П5 Неверное перенапр.'			],
			['db-redirtalk',	'П6 Перенапр. на СО'			],
			['db-duplicate',	'Ф1 Копия файла',				'файл'],
			['db-badimage',		'Ф2 Повреждённый файл'			],
			['подст:nld',		'Ф3 Нет данных о лицензии'		],
			['подст:nsd',		'Ф3 Нет данных о источнике'		],
			['подст:nad',		'Ф3 Нет данных о авторе'		],
			['подст:dd',		'Ф3 Сомнительные данные файла'	],
			['подст:ofud',		'Ф4 Неиспользуемый КДИ'			],
			['подст:dfud',		'Ф5 Нет КДИ'					],
			['db-badfairuse',	'Ф6 Неоправданное КДИ'			],
			['NCT',				'Ф8 Есть на Складе',			'файл'],
			['подст:Nothost',	'Ф9 Файл — ВП:НЕХОСТИНГ'		],
			['уд-пусткат',		'К1 Пустая категория'			],
			['уд-перекат',		'К2 Переименованная кат.',		'категорию'],
			['уд-владелец',		'У1 По желанию владельца'		],
			['уд-анон',			'У2 Устаревшая СО анонима'		],
			['уд-несущ',		'У3 Несуществующий участник'	],
			['уд-нецелевое',	'У4 Нецелевое использ. ЛП'		],
			['уд-неактив',		'У5 Подстраница неактивного'	],
			['db',				'Особый случай',				'причину']
		].filter(function(arr) {
			return fastRemovePrefix.indexOf(arr[1].charAt(0)) >= 0
		}),
		/*
			Функция запроса к API.
			Токен и формат каждый раз дополняется перед запросом.
			Аргументы:
				1. Передаваемые параметры
				2. Режим запроса (query, edit или parse)
				3. Колбек
			void apiReq(object, string, function(result, status))
		*/
		apiReq = function(par, mode, clbck) {
			par.format = 'json'
			par.token  = mw.user.tokens.get('csrfToken')
			par.action = mode
			$.post('/ruwiki/w/api.php', par, clbck)
		},
		/*
			Функция получения даты.
			Необходимо, например, для КУ-запросов.
			Есть возможность указать собственную дату для подытоживания номинации.
			['1051-32-33', '20 апреля 2014'] getDate(string)
		*/
		getDate = function(s) {
			var d = (!!s) ? new Date(s) : new Date()
			return [d.toISOString().substr(0, 10), d.getUTCDate() + ' ' +
				'января,февраля,марта,апреля,мая,июня,июля,августа,сентября,октября,ноября,декабря'.split(',')[d.getUTCMonth()] +
				' ' + d.getUTCFullYear()]
		},
		/*
			Функция получения <input>
			* Атрибут «h» используется в КБУ, указывая type
			* Атрибут placeholder используется везде, для улучшения UI
			* Атрибут id используется везде для получения информации из поля
			string getInput(string, string, bool)
		*/
		getInput = function(id, p, h) {
			return '<input id=' + id + ' type=' + (h ? 'hidden' : 'text') + ' placeholder="' + p + '" class=messagebox>'
		},
		/*
			Определяет СО страницы
		*/
		getSO = function(pg) {
			var tmp = /([^:]*:)?(.*)/.exec(pg)
			if (tmp[1]) {
				var ns = config.wgNamespaceIds[tmp[1].slice(0, -1).toLowerCase().replace(/ /g, '_')]
				if (ns != undefined)
					return config.wgFormattedNamespaces[ns | 1] + ':' + tmp[2]
			}
			return 'Обсуждение:' + pg
		},
		/*
			Функция получения текста страницы, null если ошибка
			getText(string, function(string))
		*/
		getText = function(pg, clbck) {
			apiReq({
				prop: 'wikitext',
				page: pg
			}, 'parse', function(txt) {
				clbck(txt.parse ? txt.parse.wikitext['*'] : null)
			})
		},
		/*
			Функция отправки уведомления пользователю
			По выполнению вызывает callback
			userAlert(string, function(error))
		*/
		userAlert = function(pg, clbck) {
			apiReq({
				prop: 'revisions',
				rvprop: 'user',
				rvdir: 'newer',
				titles: pg
			}, 'query', function (t) {
				var i = t.query.pages
				if (!('-1' in i)) {
					var rvdata = i[Object.keys(i)[0]].revisions[0]
					if (!('anon' in rvdata) && !rvdata.userhidden && (rvdata.user && rvdata.user !== config.wgUserName)) {
						apiReq({
							title: 'User talk:' + rvdata.user,
							section: 'new',
							sectiontitle: 'Удалятор: [[:' + pg + ']]',
							summary: param.sum,
							text: 'Страница [[:' + pg + ']], созданная вами, ' + (param[3] ? '' : 'предложена ') + param[1] + '. ' +
								(param.place ? 'Обсуждение — на странице [[' + param.place + '#' + param.sectionNW + ']]. ' : '') +
								'~~\~~<br><small>Это автоматическое уведомление, сгенерированное [[у:Землеройкин/remove.js|скриптом «Удалятор»]].</small>',
							assertuser: mw.config.get( 'wgUserName' ),
						}, 'edit', function(t) {
							clbck(t.error)
						})
					}
					else clbck('не нужно')
				}
				else clbck('страница удалена')
			})
		},
		/*
			Функция работы с текстом статьи.
			Определяет необходимые шаблоны и устанавливает их на СО или статью.
			changeArticle(string, function(object))
		*/
		changeArticle = function(pg, clbck) {
			if (/(noRnm|ret|doneRnm)/g.test(param[0]))
				changeArticleDenom(pg, clbck)
			else
				changeArticleNom(pg, clbck)
		},
		// Закрытие номинации (снять в статье, установить на СО)
		changeArticleDenom = function(pg, clbck) {
			getText(pg, function(article) {
				var tpl = RegExp('{\{(' + param[3] + ')\\|(\\d{4}-\\d\\d-\\d\\d)\\|?(.*?)}}', 'gi').exec(article)
				if (tpl == null) {
					clbck({					
						code: 'ошибка',
						info: 'Невозможно снять шаблон «' + param[3] + '».'
					})
					return
				}
				
				rOld += ' '
				
				param.date = getDate(tpl[2])
				param.place = 'ВП:' + param[3].replace(/\|.*/, '') + '/' + param.date[1]
				if (param[0] == 'doneRnm') { //переименовано
					param.sectionNW = rOld + ' → ' + pg
					param.tplpar = rOld + '|' + pg
				}
				if (param[0] == 'noRnm') { //не переименовано
					param.sectionNW = pg + ' → ' + tpl[3]
					param.tplpar = pg + '|' + tpl[3]
				}
				if (param[0] == 'ret') { //оставлено
					param.sectionNW = tpl[3].length ? tpl[3] : pg
					param.tplpar = 'l1=' + param.sectionNW
				}
				param.sum = '[[у:Землеройкин/remove.js|Удалятор]]: номинация [[' + (param.place ? param.place + '#' : '') + param.sectionNW + ']] — ' + param[2]

				apiReq({
					summary: param.sum,
					title: getSO(pg),
					prependtext: '{{' + param[2] + '|' + param.date[1] + '|' + param.tplpar + '}}\n'
				}, 'edit')

				article = article.replace(RegExp('(<noin.*?>)?{\{(' + param[3] + ')\\|.*?}}\n?(<\/noin.*?>)?\n?', 'gi'), '')
				apiReq({
					title: pg,
					text: article,
					summary: param.sum,
					watchlist: 'nochange',
					assertuser: mw.config.get( 'wgUserName' ),
				}, 'edit', function(t) {
					clbck(t.error)
				})
			})
		},
		// Открытие номинации (установка шаблона в статье)
		changeArticleNom = function(pg, clbck) {
			getText(pg, function(article) {
				if (article == null) {
					clbck({
						code: 'ошибка',
						info: 'Страница «' + pg + '» не существует.'
					})
					return
				}
				var tpl = ''
				if (param[0] == 'fRm') {
					tpl = fastRemove[$('#rmSel').val()][0] + '|1=' + $('#fiRm').val()
				}
				else {
					tpl = param.tplpar
					if (param[0] == 'merge') {
						tpl = ('|' + tpl + '|').replace('|' + pg + '|', '|').slice(1, -1)
					}
					tpl = param[2] + '|' + param.date[0] + (tpl.length ? '|' + tpl : '')
				}
				apiReq({
					title: pg,
					text: (tpl.length ? '<noinclude>{{' + tpl + '}}\n</noinclude>' : '') + article,
					summary: param.sum
				}, 'edit', function(t) {
					clbck(t.error)
				})
			})
		},
		/*
			Функция установки номинации на соответствующую страницу
			void setNominate(callback)
		*/
		setNominate = function(clbck) {
			apiReq({
				title: param.place,
				createonly: '1',
				text: '{{' + param[4] + '-Навигация}}\n',
				summary: '[[у:Землеройкин/remove.js|Удалятор]]: автоматическая шапка',
				assertuser: mw.config.get( 'wgUserName' ),
			}, 'edit', function(t) {
				apiReq({
					title: param.place,
					section: 'new',
					sectiontitle: param.section,
					summary: param.sum,
					text: param.msg + ' ~~\~~',
					assertuser: mw.config.get( 'wgUserName' ),
				}, 'edit', function(t) {
					clbck(t.error)
				})
			})
		},
		/*
			Заполнение параметров номинации (переменная param).
				.date — дата номинации
				.msg — текст номинации
				.place — место обсуждения (ВП:к_чему-то-там... или пусто, если КБУ)
				.section — заголовок раздела номинации
				.sectionNW — то же, без викификации
				.tplpar — параметры шаблона, для установки в статье
				.sum — описание правок скрипта
			Возвращает массив страниц для обработки (до 5 шт. для МногоКУ, 2 для КОБ, 0 для ВУС, иначе 1)
		*/
		setParam = function() {
			var	i,
				pg = config.wgPageName.replace(/_/g, ' '),
				ttl = $('#rmHeader').val(),
				ttl2 = $('#rmHeader2').length ? $('#rmHeader2').val() : '',
				msg = $('#rmMsg').val(),
				pgs = [pg]
				
			rOld = ttl

			if (/(noRnm|ret|doneRnm)/g.test(param[0]))
				return pgs
			

			param.date = getDate()
			param.msg = msg ? msg.trim() : ''
			param.place = param[0] != 'fRm' ? 'ВП:' + param[2] + '/' + param.date[1] : ''

			param.section =	param[0] == 'mRm' ? ttl	: '[[:' + pg + ']]'
			param.tplpar = ''
			
			if (param[0] == 'rnm') {
				param.tplpar = ttl
				param.section += ' → [[:' + ttl + ']]'
			}
			else if (param[0] == 'merge') {
				param.tplpar = pg + '|' + ttl
				param.section += ' и [[:' + ttl + ']]'
			}
			else if (param[0] == 'split') {
				param.tplpar = '[[:' + ttl + ']]' + (ttl2 ? ' и [[:' + ttl2 + ']]' : '')
				param.section += ' → ' + param.tplpar
			}

			param.sectionNW = param.section.replace(/\[\[:/g, '').replace(/]]/g, '')
			param.sum = '[[у:Землеройкин/remove.js|Удалятор]]: номинация [[' + (param.place ? param.place + '#' : '') + param.sectionNW + ']]' + (param[0] == 'fRm' ? ' ' + param[2] : '')

			if (param[0] == 'mRm') {
				pgs = []
				param.msg = '=== По всем ===\n' + param.msg
				for (i = 4; i >= 0; i--) {
					pg = $('#rmArticle' + i).val()
					if (pg.length) {
						pgs.push(pg)
						param.msg = '=== [[:' + pg + ']] ===\n' + param.msg
					}
				}
			}
			else if (param[0] == 'merge') {
				pgs.push(ttl)
			}
			else if (param[0] == 'recov') {
				pgs = []
			}
			return pgs
		},
		/*
			Вывод сообщений об ошибках
		*/
		logError = function(s, err) {
			if (err && err.code) isError = 1
			$('#rmWindow').append(
				'<br>' + s + ' — ' + (
					err ? (err.code ? '<span class="error"><small>' + err.code + ': ' + err.info + '</small></span>' : err)
						: 'OK'
				)
			)
		},
		/*
			Обработка массива страниц
		*/
		processPages = function(pgs) {
			if (pgs.length) {
				var pg = pgs.pop()
				changeArticle(pg, function(err) {
					logError('Правка статьи «' + pg + '»', err)
					if (isError) {
						finalizeWindow()
						return
					}
					if (setAlert) {
						userAlert(pg, function(err) {
							logError('Уведомление создателя', err)
							processPages(pgs)
						})
					}
					else processPages(pgs)
				})
			}
			else {
				if (param[4]) {
					setNominate(function(err) {
						logError('Запись номинации', err)
//						window.open('/wiki/' + param.place + '#' + encodeURI(param.sectionNW.replace(/ /g, '_')))
						finalizeWindow()
					})
				}
				else finalizeWindow()	
			}
		},
		/*
			Вызывается, когда всё сделано
		*/
		finalizeWindow = function() {
			if (isError) {
				$('.mw-small-spinner').remove()
				$('#rmWindow')
					.append('<p class="error">При выполнении скрипта случились ошибки. Поправьте всё, что надо, вручную.')
					.children().prop('disabled', false)
				$('#rmBtn').attr('disabled', 1)
				$('#rmClose').text('Закрыть')
			}
			else location.reload()
		},
		/*
			Функция создания модального окна
		*/
		modalHandler = function() {
			var i,
				content = ''

			if (param[0] == 'mRm') {
				content += getInput('rmHeader', 'Заголовок номинации')
				for (i = 0; i < 5; i++) {
					content += getInput('rmArticle' + i, 'Статья' + (i + 1))
				}		
			}
			if (param[0] == 'fRm') {
				content += '<select id=rmSel class=messagebox>'
				for (i = 0; i < fastRemove.length; i++) {
					content += '<option value=' + i + '>' + fastRemove[i][1] + '</option>'
				}
				content += '</select>' + getInput('fiRm', '', 1)
			}
			if (param[0] == 'rnm') {
				content += getInput('rmHeader', 'Новое название')
			}
			if (param[0] == 'doneRnm') {
				content += getInput('rmHeader', 'Старое название')
			}
			if (param[0] == 'merge') {
				content += getInput('rmHeader', 'Объединить с…')
			}
			if (param[0] == 'split') {
				content += getInput('rmHeader', 'Разделить на эту')
				content += getInput('rmHeader2', 'И на эту')
			}
			if (param[4]) {
				content += '<textarea id=rmMsg placeholder="Текст номинации без «~~\~~»." rows=4></textarea>'
			}
			$('#content').prepend(
				'<div id=rmWindow style="padding:2em;margin:1em;border:1px solid #985; background: #fec;">' +
				'<h1>Удалятор: ' + param[2] + '</h1>' + content +
				'<br><label><input name="rmUAlert" type=checkbox ' + ((setAlert) ? 'checked' : '') + '>Оповестить автора</label><br>' +
				'<button id=rmBtn class=mw-ui-button>Отправить</button><button id=rmClose class=mw-ui-button>Отмена</button>'
			)
			if (param[0] == 'mRm') $('#rmArticle0').val(config.wgPageName.replace(/_/g, ' '))

			$('#rmSel').change(function() {
				var i = fastRemove[this.value][2]
				$('#fiRm').attr({type: (i ? 'text' : 'hidden'), placeholder: 'Укажите ' + i})
			})
			$('#rmClose').click(function() {
				$('#rmWindow').remove()
			})

			$('#rmBtn').click(function() {
				$('#rmWindow')
					.append('<b class=mw-small-spinner></b>')
					.children().attr('disabled', '1')
				setAlert = $('[name="rmUAlert"]').is(':checked')
				var pgs = setParam()
				processPages(pgs)
			})
			/*
				Реализация ctrl+enter события
			*/
			$(window).keydown(function (e) {
				if (e.ctrlKey && e.keyCode == 13)
					$('#rmBtn').click()
			})
		};
	/*
		Добавление выпадающего меню на все страницы
	*/
	if ((rmNamespaces.indexOf(config.wgNamespaceNumber & ~1) >= 0) && (config.wgPageContentModel == 'wikitext')) {
		var menuLocation = 'p-cactions';
		var nextNode = '#ca-move';
		if ( config.skin === 'minerva' ) {
			menuLocation = 'p-tb';
			nextNode = null;
		}
		if ( config.skin === 'vector' || config.skin === 'vector-2022' || config.skin === 'timeless' ) {
			var portlet = mw.util.addPortlet( 'p-remove-js', 'Удалятор', '#p-cactions' );

			menuLocation = 'p-remove-js';
			nextNode = null;
		}
		
		for (var a of rmActions) {
			var portletLink = mw.util.addPortletLink( menuLocation, '#', menu_raw[ a ], 'ca-remove-' + a, null, null, nextNode );
			$( portletLink ).find( 'a' ).click(function( e ) {
				e.preventDefault();
				var i = $( this ).parent().attr( 'id' ).replace( 'ca-remove-', '' );
				param = (
					// [текущее действие, комментарий, шаблон/место обсуждения, поддерживаемые шаблоны
					i == 'imp'	  ? [ i, 'к срочному улучшению',		'к улучшению',		'',	'КУЛ'] :
					i == 'rnm'	  ? [ i, 'к переименованию',			'к переименованию',	'',	'КПМ'] :
					i == 'tRm'	  ? [ i, 'к удалению',				    'к удалению',		'',	'КУ'] :
					i == 'mRm'	  ? [ i, 'к удалению',				    'к удалению',		'',	'КУ'] :
					i == 'merge'  ? [ i, 'к объединению с другой',	    'к объединению',	'',	'КОБ'] :
					i == 'split'  ? [ i, 'к разделению',				'к разделению',		'',	'КР'] :
					i == 'fRm'	  ? [ i, 'к [[ВП:КБУ|быстрому удалению]]',	'к быстрому удалению'] :
					i == 'recov'  ? [ i, '',							'к восстановлению',	'',	'ВУС'] :
					i == 'ret'	  ? [ i, 'оставлена',					'оставлено',		'к удалению|ку'] :
					i == 'doneRnm'? [ i, 'переименована',			    'переименовано',	'к переименованию|кпм|rename'] :
					i == 'noRnm'  ? [ i, 'не переименована',			'не переименовано',	'к переименованию|кпм|rename'] :
					0
				)
				isError = 0
				modalHandler()
			});
		}
	}
})