MediaWiki:Gadget-MassProtect.js
/*********************************************************************************************\
MassProtect (一括保護スクリプト)
管理者権限を所有する利用者が以下のいずれかの特別ページにアクセスすることで使用できます。
* 特別:一括保護
* 特別:MassProtect (大文字・小文字の区別なし)
* 特別:MP (大文字・小文字の区別なし)
(※ 'Special:' でも可)
解説: H:MP
作者: (オリジナル) en:User:Timotheus Canens
=> en:User:Timotheus Canens/massprotect.js
(移入)      User:Infinite0694
=> User:Infinite0694/Mass protecting tool ja.js
(大幅改変)  User:Dragoniez
=> このスクリプト
(編集者: )
このスクリプトを使用すると大量のページを一瞬で保護することが可能になりますが、基本的に使用者の
責任で使用してください。
\*********************************************************************************************/
//
(function(mw, $) { // 関数スコープを作成
// ************************************** 設定 **************************************
// 全般設定および関数間で共有する変数を格納するためのオブジェクト
var commons = 'https://upload.wikimedia.org/wikipedia/commons';
var imgStyle = ' style="vertical-align: middle; height: 1em; border: 0;"';
var MassProtect = {
// スクリプトで使用する画像
image: {
doing: '<img src="' + commons + '/4/42/Loading.gif"' + imgStyle + '>',
done: '<img src="' + commons + '/f/fb/Yes_check.svg"' + imgStyle + '>',
failed: '<img src="' + commons + '/a/a2/X_mark.svg"' + imgStyle + '>'
},
// 保護設定の日本語変換用
translate : {
type: {
create: '作成',
edit: '編集',
move: '移動',
upload: 'アップロード'
},
level: {
autoconfirmed: '半保護',
extendedconfirmed: '拡張半保護',
sysop: '全保護'
}
},
// 以下のテンプレートは、保護タグの自動添付設定がオンの場合除去・置換する
template: {
names: ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // テンプレート名称完全一致
prefixes: ['pp-'] // テンプレート名称前方一致
}
};
// ************************************** ページの初期化 **************************************
var api, apiEndpoint;
// 依存モジュールとDOM読み込みが完了したら駆動
$.when(
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.user', 'jquery.ui']),
$.ready
).then(function() {
api = new mw.Api();
apiEndpoint = mw.util.wikiScript('api');
// ページ名が「特別:一括保護」、「特別:MassProtect」または「特別:MP」の場合、一括保護フォーム生成
if (mw.config.get('wgNamespaceNumber') === -1 && /^(一括保護|massprotect|mp)$/i.test(mw.config.get('wgTitle'))) {
// ページタイトルを変更 (「そのような特別ページはありません」⇒「一括保護 - Wikipedia」)
document.title = '一括保護 - Wikipedia';
// 使用者が管理者なら
if (/sysop/.test(mw.config.get('wgUserGroups'))) {
// 一括保護のフォームを生成
createForm();
// 管理者でなければ
} else {
// 権限エラーを表示
showUserRightError();
}
} else { // 該当特別ページ以外では
// 「特別:一括保護」へのリンクを生成
mw.util.addPortletLink('p-tb', mw.config.get('wgArticlePath').replace('$1', 'Special:MassProtect'), '一括保護' , 't-mp', 'ページを一括で保護する');
}
});
// ************************************** 主要関数 **************************************
/**
* 一括保護のフォームを生成する関数
*/
function createForm() {
$('head').append(
+
'#mp-container legend {' +
'font-weight: bold;' +
'}' +
'#mp-body input[type=checkbox] {' +
'margin-right: 0.5em;' +
'}' +
'#mp-level fieldset {' +
'border: 1px solid gray;' +
'padding: 0.5em;' +
'margin: 0.5em 0;' +
'}' +
'#mp-level legend {' +
'padding-bottom: 0;' +
'}' +
'#mp-level label:not(#mp-level-enableadditionaloptions-label),' +
'#mp-contribs-search label:not(#mp-contribs-search-deleted-label) {' +
'width: 8ch;' +
'display: inline-block;' +
'}' +
'#mp-contribs-search input[type="text"] {' +
'margin-bottom: 0.3em;' +
'}' +
'#mp-level select,' +
'#mp-level input[type="text"] {' +
'width: 40ch;' +
'box-sizing: border-box;' +
'}' +
'#mp-level div {' +
'margin-bottom: 0.3em;' +
'}' +
);
/**
* 保護レベル設定フィールドの共通HTMLを生成する関数
* @param {string} protectionType 'create', 'edit', 'move', 'upload'
* @returns {string}
*/
var createLevelSelector = function(protectionType) {
var levelSelector =
'<fieldset>' +
'<legend>' + MassProtect.translate.type[protectionType] + '保護</legend>' +
+
'<label>レベル</label>' +
+
'<option>すべての利用者に許可</option>' +
'<option>自動承認された利用者のみ許可</option>' +
'<option>拡張承認された利用者と管理者に許可</option>' +
'<option>管理者のみ許可</option>' +
+
+
+
'<label>期間</label>' +
+
'<optgroup>' +
'<option>無期限</option>' +
'</optgroup>' +
'<optgroup>' +
'<option>1週間</option>' +
'<option>2週間</option>' +
'<option>1ヵ月</option>' +
'<option>3ヵ月</option>' +
'<option>6ヵ月</option>' +
'<option>1年</option>' +
'<option>2年</option>' +
'<option>3年</option>' +
'</optgroup>' +
'<optgroup>' +
'<option>その他の期間</option>' +
'</optgroup>' +
+
+
+
'<label></label>' +
'<input>' +
+
'</fieldset>';
return levelSelector;
};
// ページコンテンツの内部HTMLを一括保護のフォームに置換
$(mw.util.$content).prop('innerHTML',
+
+
'<h1>一括保護</h1>' +
'<p>' +
'指定されたページ群の保護レベルを一括変更できます。変更する場合は、' +
'<a>保護の方針</a>、' +
'<a>拡張半保護の方針</a>、' +
'<a>半保護の方針</a>、' +
'に基づいているか確認して下さい。' +
'</p>' +
+
'有効期限のデフォルトは無期限です。適切な期間・期限を指定してください。' +
+
+
'「その他の期間」の記入例 (' +
'<a href="http://www.gnu.org/software/tar/manual/html_node/Date-input-formats.html">GNU標準フォーマット</a>' +
'): "12 hours"、"5 days"、"3 weeks"、"2012-09-25 20:00"' +
' (日時は<a>UTC</a>)' +
+
+
+
'保護レベルを変更した場合、ページ上で保護テンプレート (' +
'<a>Template:Pp</a>' +
') を更新してください。' +
+
+
+
+
+
'<fieldset>' +
'<legend>保護対象ページ</legend>' +
'<textarea></textarea>' +
'<input>' +
'<p>ページ数: 0</p> ' +
'<input>' +
'<input>' +
'<p></p>' +
'<fieldset>' +
'<legend>投稿記録からページ名を取得</legend>' +
'<label>利用者名</label>' +
'<input></input> ' +
'<label>期間</label>' +
'<input></input> ~ ' +
'<input></input> ' +
'<input></input>' +
'<label>削除された投稿記録</label> ' +
'<input></input>' +
'<p></p>' +
'</fieldset>' +
'<fieldset>' +
'<legend>ページ情報検索</legend>' +
+
'<input>' +
'<p>' +
' (<a>履歴</a> /' +
' <a>記録</a> /' +
' <a>保護記録</a>)' +
'</p>' +
+
+
'<p></p>' +
+
+
'<p>未作成のページ</p>' +
+
'</fieldset>' +
'</fieldset>' +
'<fieldset>' +
'<legend>保護レベル</legend>' +
+
'<label>区分</label>' +
+
'<option>未作成ページ</option>' +
'<option>既存ページ</option>' +
+
+
+
createLevelSelector('create') +
+
+
createLevelSelector('edit') +
'<input></input>' +
'<label>追加保護オプションのロックを解除</label>' +
createLevelSelector('move') +
createLevelSelector('upload') +
+
'</fieldset>' +
'<fieldset>' +
'<legend>保護理由</legend>' +
'<label>保護理由1</label>' +
+
'<optgroup>' +
'<option>なし</option>' +
'</optgroup>' +
' ' +
'<label>保護理由2</label>' +
+
'<optgroup>' +
'<option>なし</option>' +
'</optgroup>' +
' ' +
'<label></label>' +
'<input>' +
'</fieldset>' +
'<input>' +
'<label>保護対象ページをウォッチリストに追加</label> ' +
+
+
'<label>ウォッチ期間</label>' +
+
'<option>無期限</option>' +
'<option>1週間</option>' +
'<option>1か月</option>' +
'<option>3か月</option>' +
'<option>6か月</option>' +
'<option>1年</option>' +
+
+
+
'<input>' +
'<label>このページに読み込まれているページを保護する (カスケード保護)</label> ' +
'<input>' +
'<label>保護依頼タグを自動除去</label> ' +
'<input>' +
'<label>' +
'保護タグを自動添付' +
' (<a>一覧</a>)' +
'</label> ' +
+
+
'<label>テンプレート</label>' +
+
'<option>pp</option>' +
'<option>pp-dispute</option>' +
'<option>pp-vandalism</option>' +
'<option>pp-template</option>' +
'<option>pp-permanent</option>' +
'<option>pp-move</option>' +
'<option>pp-move-vandalism</option>' +
'<option>pp-move-dispute</option>' +
'<option>pp-office</option>' +
'<option>pp-reset</option>' +
'<option>pp-office-dmca</option>' +
+
+
+
'<label>種別</label>' +
+
'<option></option>' +
'<option>edit</option>' +
'<option>move</option>' +
'<option>upload</option>' +
+
+
+
'<label>縮小表示</label>' +
+
'<option></option>' +
'<option>small=yes</option>' +
'<option>small=no</option>' +
+
+
'(既存の保護タグ・保護依頼タグは自動で置換されます)' +
+
'<input>' +
'<label>既存の保護設定の上書きが必要な場合確認メッセージを表示</label> ' +
'<input>' +
+
+
);
// 「投稿記録からページ名を取得」のテキストボックスにdatepickerを追加
$('#mp-contribs-search-ucstart, #mp-contribs-search-ucend').datepicker({
dateFormat: 'yy-mm-dd'
});
// 保護レベルfieldsetの区分の初期値が「既存ページ」のため、作成保護用のフィールドを初期状態では隠す
$('.mp-level-settings').filter(function() {
return $(this).children('fieldset').prop('id') === 'mp-level-create';
}).css('display', 'none');
// 移動保護とアップロード保護フィールド内のselectとinputを初期では無効化
$('#mp-level-move, #mp-level-upload').find('select, input').prop('disabled', true);
// 保護理由ドロップダウン、VIPリスト、LTAリストを取得
getProtectDropdown();
getSavedPagetitles();
getAutocompleteSource();
}
/**
* 使用者が管理者権限を持たない場合に権限エラーを表示する関数
*/
function showUserRightError() {
$(mw.util.$content).prop('innerHTML',
+
'<h1>権限エラー</h1>' +
'<p>あなたには「保護設定の変更」を行う権限がありません。理由は以下の通りです:</p>' +
'<p>' +
'この操作は、以下のグループに属する利用者のみが実行できます: ' +
'<a>管理者</a>。' +
'</p>' +
);
}
/**
* 保護理由ドロップダウンオプションを取得する関数
* @returns {JQueryPromise<void>}
*/
function getProtectDropdown() {
var msgName = 'protect-dropdown';
/**
* @returns {JQueryPromise}
*/
var query = function() {
return api.getMessages([msgName]).then(function(res) {
return res && res[msgName] ? res[msgName] : undefined;
}).catch(function(code, err) {
console.log(err);
return undefined;
});
};
return query().then(function(msg) {
if (!msg) return;
var wrapper = document.createElement('select');
var optgroup = document.createElement('optgroup');
optgroup.label = '指定なし';
wrapper.appendChild(optgroup);
var opt = document.createElement('option');
opt.textContent = 'なし';
opt.value = ;
optgroup.appendChild(opt);
if (msg) {
var regex = /(\*+)([^*]+)/g;
var m;
while ((m = regex.exec(msg))) {
if (m[1].length === 1) {
optgroup = document.createElement('optgroup');
optgroup.label = m[2].trim();
wrapper.appendChild(optgroup);
} else {
opt = document.createElement('option');
opt.textContent = m[2].trim();
if (optgroup) {
optgroup.appendChild(opt);
} else {
wrapper.appendChild(opt);
}
}
}
} else {
alert('保護理由の取得に失敗しました。ページをリロードすると改善する場合があります。');
}
Array.prototype.forEach.call(document.querySelectorAll('.mp-reason-dropdown'), function(el) {
el.innerHTML = wrapper.innerHTML;
});
});
}
/**
* 保存済みのページ名がある場合「保存済みページを取得」ボタンを表示する関数 (第1引数がtrueの場合はテキストボックスに保存済みページを入力)
* @param {boolean} [input] テキストボックスに保存済みページ名を入力するか否か
* @returns
*/
function getSavedPagetitles(input) {
var savedPages = mw.user.options.get('userjs-mp-targetpages');
if (savedPages) $('#mp-targetpages-restore').css('display', 'inline-block');
if (input) {
savedPages = JSON.parse(savedPages);
$('#mp-targetpages').val(savedPages.join('\n'));
}
}
// 保存ボタンが押された際、入力済みの保護対象ページを保存
$(document).off('click', '#mp-targetpages-save').on('click', '#mp-targetpages-save', function() {
// テキストボックスの内容を取得し、空白の場合はメッセージを表示
var targetPages = getTargetPages();
if (targetPages.length === 0) {
if (confirm('保存済みページを初期化します。よろしいですか?')) {
targetPages = null;
} else {
return;
}
}
// 保存の準備
var $this = $(this);
$this.prop('disabled', true); // 「保存」ボタンを無効化
$('#mp-targetpages-saving').prop('innerHTML', '保存しています' + MassProtect.image.doing); // メッセージを表示
if (targetPages) targetPages = JSON.stringify(targetPages); // APIに渡す値はString型である必要あり
// APIリクエストを送信しページ名を保存
api.saveOption('userjs-mp-targetpages', targetPages)
.then(function() { // 成功
$('#mp-targetpages-saving').prop('innerHTML', '保存しました' + MassProtect.image.done);
var display = targetPages ? 'inline-block' : 'none';
$('#mp-targetpages-restore').css('display', display);
mw.user.options.set('userjs-mp-targetpages', targetPages);
}).catch(function(code, err) { // 失敗
console.log(err.error.info);
$('#mp-targetpages-saving').prop('innerHTML', '保存に失敗しました' + MassProtect.image.failed);
}).then(function() {
setTimeout(function() { // 処理終了の2秒後に進捗メッセージを隠し「保存」ボタンを再度有効化
$('#mp-targetpages-saving').empty();
$this.prop('disabled', false);
}, 2000);
});
});
// 「保存済みページを取得」ボタンが押された際に保護対象ページテキストボックスにページ名称群を入力
$(document).off('click', '#mp-targetpages-restore').on('click', '#mp-targetpages-restore', function() {
var inputVal = $('#mp-targetpages').val();
if (inputVal) {
if(!confirm('入力済みの内容は失われます。よろしいですか?')) return;
}
getSavedPagetitles(true);
});
/**
* 非定形理由のインプットボックスにVIP・LTA名称のautocomplete機能を追加する関数
* @returns {JQueryPromise<void>}
*/
function getAutocompleteSource() {
/**
* @returns {JQueryPromise<Array>}
*/
var getVipList = function() {
// VIPのリストがあるページのセクションタイトルをパーズ
return api.get({
action: 'parse',
page: 'Wikipedia:進行中の荒らし行為',
prop: 'sections',
format: 'json'
}).then(function(res) {
var resSect;
if (!res || !res.parse || !(resSect = res.parse.sections) || !resSect.length) return [];
// VIP名称とは無関係のセクションタイトルを定義
var excludeList = [
'記述について',
'急を要する二段階',
'配列',
'ブロック等の手段',
'このページに利用者名を加える',
'注意と選択',
'警告の方法',
'未登録(匿名・IP)ユーザーの場合',
'登録済み(ログイン)ユーザーの場合',
'警告中',
'関連項目'
];
// 'WP:VIP#NAME' の形の配列を返す
return resSect.reduce(function(acc, obj) {
if (excludeList.indexOf(obj.line) === -1 && obj.level == 3) {
acc.push('WP:VIP#' + obj.line + );
}