「プロジェクト:ウィキ技術部/スクリプト開発/trunk/MassProtect.js」の版間の差分

削除された内容 追加された内容
テンプレートリテラルを除去 (ES6の機能)
機能の修正と追加 (詳細は特別:差分/90331208を参照)
タグ: サイズの大幅な増減
18行目: 18行目:
//<nowiki>
//<nowiki>


(function(mw, $) {
(function(mw, $) { // 関数スコープを作成


// ******************* ページの初期化 ***********************
// ************************************** ページの初期化 **************************************


// 関数間で共有する変数を格納するためのオブジェクト
// 関数間で共有する変数を格納するためのオブジェクト
var MassProtect = {};
var MassProtect = {};


// ページ名が「特別:一括保護」、「特別:MassProtect」または「特別:MP」の場合、一括保護フォーム生成
// フォーム生成 (該当特別ページ以外の場合はポートレットリンクを生成)
// if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm]ass[Pp]rotect|[Mm][Pp])$/)) {
if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm][Pp])$/)) {
if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm][Pp])$/)) {
// if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm]ass[Pp]rotect|[Mm][Pp])$/)) {

// ページタイトルを変更 (「そのような特別ページはありません」⇒「一括保護 - Wikipedia」)
$(document).prop('title', '一括保護 - Wikipedia');
$(document).prop('title', '一括保護 - Wikipedia');

// 使用者が管理者なら
if (/sysop/.test(mw.config.get('wgUserGroups'))) {
if (/sysop/.test(mw.config.get('wgUserGroups'))) {
// 一括保護のフォームを生成
createForm();
createForm();
// 管理者でなければ
} else {
} else {
showUserRightError();
// 権限エラーを表示
showUserRightError();
return;
return;
}
}

} else {
} else { // 該当特別ページ以外では

// 「特別:一括保護」へのリンクを生成
mw.util.addPortletLink('p-tb', '/wiki/特別:一括保護', '一括保護 (改)' , 't-mp', 'ページを一括で保護する');
mw.util.addPortletLink('p-tb', '/wiki/特別:一括保護', '一括保護 (改)' , 't-mp', 'ページを一括で保護する');
return;
return;

}
}




// ******************* 主要関数 ***********************
// ************************************** 主要関数 **************************************


/**
* 一括保護のフォームを生成する関数
*/
function createForm() {
function createForm() {


// 複数回使用するHTMLを変数に格納
var getUrl = function(pagename, params) {
var protectLevels = // 保護レベルオプション
return mw.util.getUrl(pagename, params ? params : {});
'<option selected class="mp-protectionlevel-none" value="">変更なし</option>' +
};
'<option class="mp-protectionlevel-all" value="all">すべての利用者に許可</option>' +
'<option class="mp-protectionlevel-autoconfirmed" value="autoconfirmed">自動承認された利用者のみ許可</option>' +
'<option class="mp-protectionlevel-extendedconfirmed" value="extendedconfirmed">拡張承認された利用者と管理者に許可</option>' +
'<option class="mp-protectionlevel-sysop" value="sysop">管理者のみ許可</option>';


var protectReasons = // 保護理由オプション
var protectionLevels =
'<option selected value="">変更なし</option>' +
'<optgroup label="保護理由">' +
'<option value="all">すべての利用者に許可</option>' +
'<option>度重なる荒らし</option>' +
'<option value="autoconfirmed">自動承認された利用者のみ許可</option>' +
'<option>度重なる宣伝</option>' +
'<option value="extendedconfirmed">拡張承認された利用者と管理者に許可</option>' +
'<option>編集合戦</option>' +
'<option value="sysop">管理者のみ許可</option>';
'<option>移動合戦</option>' +
'<option>プライバシー侵害の記述の繰り返し</option>' +
'<option>IP利用者による問題投稿の繰り返し</option>' +
'<option>IP・新規利用者による問題投稿の繰り返し</option>' +
'<option>[[WP:SOCK|sockpuppet]]による問題投稿の繰り返し</option>' +
'<option>[[Wikipedia:保護依頼]]による</option>' +
'<option>削除タグ剥離</option>' +
'<option>移動不要ページ</option>' +
'<option>利用者ページ 本人希望</option>' +
'<option>削除されたページの改善なき再作成の繰り返し</option>' +
'<option>更新の必要性が低い重要ページ</option>' +
'<option>[[WP:HRT|影響が特に大きいテンプレート]]</option>' +
'<option>議論が終了済みの過去ログ</option>' +
'<option>履歴保存</option>' +
'<option>特定版削除に伴う保護のかけなおし</option>' +
'<option>特定版削除に伴う半保護のかけなおし</option>' +
'</optgroup>' +
'<optgroup label="解除理由">' +
'<option>[[Wikipedia:保護解除依頼]]による</option>' +
'<option>保護理由消滅のため</option>' +
'<option>編集内容の合意の成立</option>' +
'</optgroup>';


// ページコンテンツの内部HTMLを一括保護のフォームに置換
$(mw.util.$content).prop('innerHTML',
$(mw.util.$content).prop('innerHTML',
'<div id="mp-container" style="font-size: 95%;">' +
'<div id="mp-container" style="font-size: 95%;">' +
62行目: 102行目:
'<p>' +
'<p>' +
'指定されたページ群の保護レベルを一括変更できます。変更する場合は、' +
'指定されたページ群の保護レベルを一括変更できます。変更する場合は、' +
'<a href="' + getUrl('Wikipedia:保護の方針') + '" title="Wikipedia:保護の方針">保護の方針</a>、' +
'<a href="' + mw.util.getUrl('Wikipedia:保護の方針') + '" title="Wikipedia:保護の方針">保護の方針</a>、' +
'<a href="' + getUrl('Wikipedia:拡張半保護の方針') + '" title="Wikipedia:拡張半保護の方針">保護の方針</a>、' +
'<a href="' + mw.util.getUrl('Wikipedia:拡張半保護の方針') + '" title="Wikipedia:拡張半保護の方針">拡張半保護の方針</a>、' +
'<a href="' + getUrl('Wikipedia:半保護の方針') + '" title="Wikipedia:半保護の方針">保護の方針</a>、' +
'<a href="' + mw.util.getUrl('Wikipedia:半保護の方針') + '" title="Wikipedia:半保護の方針">保護の方針</a>、' +
'に基づいているか確認して下さい。' +
'に基づいているか確認して下さい。' +
'</p>' +
'</p>' +
74行目: 114行目:
'<a href="http://www.gnu.org/software/tar/manual/html_node/Date-input-formats.html">GNU標準フォーマット</a>' +
'<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"' +
'): "12 hours"、"5 days"、"3 weeks"、"2012-09-25 20:00"' +
' (日時は<a href="' + getUrl('協定世界時') + '" title="協定世界時">UTC</a>)' +
' (日時は<a href="' + mw.util.getUrl('協定世界時') + '" title="協定世界時">UTC</a>)' +
'</li>' +
'</li>' +
'</ul>' +
'</ul>' +
'<li>' +
'<li>' +
'保護レベルを変更した場合、ページ上で保護テンプレート (' +
'保護レベルを変更した場合、ページ上で保護テンプレート (' +
'<a href="' + getUrl('Template:Pp') + '">Template:Pp</a>' +
'<a href="' + mw.util.getUrl('Template:Pp') + '">Template:Pp</a>' +
') を更新してください。' +
') を更新してください。' +
'</li>' +
'</li>' +
116行目: 156行目:
'<label for="mp-protectionlevel-create" style="width: 8ch; display: inline-block;">作成保護</label>' +
'<label for="mp-protectionlevel-create" style="width: 8ch; display: inline-block;">作成保護</label>' +
'<select class="mp-protectionlevel" id="mp-protectionlevel-create" style="margin-bottom: 0.3em;">' +
'<select class="mp-protectionlevel" id="mp-protectionlevel-create" style="margin-bottom: 0.3em;">' +
protectionLevels +
protectLevels +
'</select><br>' +
'</select><br>' +
'<label for="mp-protectionlevel-edit" style="width: 8ch; display: inline-block;">編集保護</label>' +
'<label for="mp-protectionlevel-edit" style="width: 8ch; display: inline-block;">編集保護</label>' +
'<select class="mp-protectionlevel" id="mp-protectionlevel-edit" style="margin-bottom: 0.3em;">' +
'<select class="mp-protectionlevel" id="mp-protectionlevel-edit" style="margin-bottom: 0.3em;">' +
protectionLevels +
protectLevels +
'</select><br>' +
'</select><br>' +
'<label for="mp-protectionlevel-move" style="width: 8ch; display: inline-block;">移動保護</label>' +
'<label for="mp-protectionlevel-move" style="width: 8ch; display: inline-block;">移動保護</label>' +
'<select class="mp-protectionlevel" id="mp-protectionlevel-move" style="margin-bottom: 0.3em;">' +
'<select class="mp-protectionlevel" id="mp-protectionlevel-move" style="margin-bottom: 0.3em;">' +
protectionLevels +
protectLevels +
'</select><br>' +
'</select><br>' +
'<input id="mp-watchlist" type="checkbox" style="margin-right: 0.3em;">' +
'<input id="mp-watchlist" type="checkbox" style="margin-right: 0.3em;">' +
133行目: 173行目:
'<label for="mp-tag">' +
'<label for="mp-tag">' +
'保護タグを自動添付' +
'保護タグを自動添付' +
' (<a href="' + getUrl('Template:Pp#関連項目') + '" title="Template:Pp#関連項目">一覧</a>)' +
' (<a href="' + mw.util.getUrl('Template:Pp#関連項目') + '" title="Template:Pp#関連項目">一覧</a>)' +
'</label>' +
'</label>' +
'<ul id="mp-tag-list" style="display: none;">' +
'<ul id="mp-tag-list" style="display: none;">' +
165行目: 205行目:
'<select id="mp-tag-small" style="width: 20ch; margin-bottom: 0.3em;">' +
'<select id="mp-tag-small" style="width: 20ch; margin-bottom: 0.3em;">' +
'<option id="mp-tag-small-blank" value=""></option>' +
'<option id="mp-tag-small-blank" value=""></option>' +
'<option id="mp-tag-small-yes" value="|small=yes">small=yes</option>' +
'<option selected id="mp-tag-small-yes" value="|small=yes">small=yes</option>' +
'<option hidden id="mp-tag-small-no" value="|small=no">small=no</option>' +
'<option hidden id="mp-tag-small-no" value="|small=no">small=no</option>' +
'</select>' +
'</select>' +
'</li>' +
'</li>' +
'(既存の保護タグ・保護依頼タグは自動除去され、リダイレクトページには添付されません)' +
'(既存の保護タグ・保護依頼タグは自動で置換されま)' +
'</ul>' +
'</ul>' +
'</fieldset>' +
'</fieldset>' +
'<label for="mp-reason-dropdown" style="width: 6ch; display: inline-block;">理由</label>' +
'<fieldset id="mp-copy" style="display: none;">' +
'<legend>リンクのコピー</legend>' +
'<div id="mp-copy-vip" style="display: none;">' +
'<label for="mp-copy-vip-input" style="display: inline-block; width: 4ch;">VIP</label>' +
'<input id="mp-copy-vip-input" list="mp-copy-vip-list" placeholder="コピーする場合は検索・選択してください" style="width: 50ch;">' +
'<datalist id="mp-copy-vip-list"></datalist>' +
'</div>' +
'<div id="mp-copy-lta" style="display: none; margin-top: 0.3em;">' +
'<label for="mp-copy-lta-input" style="display: inline-block; width: 4ch;">LTA</label>' +
'<input id="mp-copy-lta-input" list="mp-copy-lta-list" placeholder="コピーする場合は検索・選択してください" style="width: 50ch;">' +
'<datalist id="mp-copy-lta-list"></datalist>' +
'</div>' +
'</fieldset>' +
'<label for="mp-reason-dropdown" style="width: 10ch; display: inline-block;">定型理由1</label>' +
'<select id="mp-reason-dropdown" style="width: auto; margin-bottom: 0.3em;">' +
'<select id="mp-reason-dropdown" style="width: auto; margin-bottom: 0.3em;">' +
'<optgroup label="その他の理由">' +
'<optgroup label="指定なし">' +
'<option value="other">その他の理由</option>' +
'<option value="">なし</option>' +
'</optgroup>' +
'</optgroup>' +
'<optgroup label="保護理由">' +
protectReasons +
'<option>度重なる荒らし</option>' +
'</select><br>' +
'<option>度重なる宣伝</option>' +
'<label for="mp-reason-dropdown2" style="width: 10ch; display: inline-block;">定型理由2</label>' +
'<option>編集合戦</option>' +
'<select id="mp-reason-dropdown2" style="width: auto; margin-bottom: 0.3em;">' +
'<option>移動合戦</option>' +
'<optgroup label="指定なし">' +
'<option>プライバシー侵害の記述の繰り返し</option>' +
'<option value="">し</option>' +
'<option>IP利用者による問題投稿の繰り返し</option>' +
'<option>IP・新規利用者による問題投稿の繰り返し</option>' +
'<option>[[WP:SOCK|sockpuppet]]による問題投稿の繰り返し</option>' +
'<option>[[Wikipedia:保護依頼]]による</option>' +
'<option>削除タグ剥離</option>' +
'<option>移動不要ページ</option>' +
'<option>利用者ページ 本人希望</option>' +
'<option>削除されたページの改善なき再作成の繰り返し</option>' +
'<option>更新の必要性が低い重要ページ</option>' +
'<option>[[WP:HRT|影響が特に大きいテンプレート]]</option>' +
'<option>議論が終了済みの過去ログ</option>' +
'<option>履歴保存</option>' +
'<option>特定版削除に伴う保護のかけなおし</option>' +
'<option>特定版削除に伴う半保護のかけなおし</option>' +
'</optgroup>' +
'<optgroup label="解除理由">' +
'<option>[[Wikipedia:保護解除依頼]]による</option>' +
'<option>保護理由消滅のため</option>' +
'<option>編集内容の合意の成立</option>' +
'</optgroup>' +
'</optgroup>' +
protectReasons +
'</select><br>' +
'</select><br>' +
'<label for="mp-reason-custom" style="width: 6ch; display: inline-block;"></label>' +
'<label for="mp-reason-custom" style="width: 10ch; display: inline-block;">非定型理由</label>' +
'<input id="mp-reason-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
'<input id="mp-reason-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
'<label for="mp-expiry-dropdown" style="width: 6ch; display: inline-block;">期間</label>' +
'<label for="mp-expiry-dropdown" style="width: 10ch; display: inline-block;">期間</label>' +
'<select id="mp-expiry-dropdown" style="width: 20ch; margin-bottom: 0.3em;">' +
'<select id="mp-expiry-dropdown" style="width: 20ch; margin-bottom: 0.3em;">' +
'<optgroup label="デフォルトタイム">' +
'<optgroup label="デフォルトタイム">' +
229行目: 264行目:
'</optgroup>' +
'</optgroup>' +
'</select><br>' +
'</select><br>' +
'<label for="mp-expiry-custom" style="width: 6ch; display: inline-block;"></label>' +
'<label for="mp-expiry-custom" style="width: 10ch; display: inline-block;"></label>' +
'<input disabled id="mp-expiry-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
'<input disabled id="mp-expiry-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
'<input type="button" id="mp-submit" value="保護を実行" style="margin-top: 0.5em;">' +
'<input type="button" id="mp-submit" value="保護を実行" style="margin-top: 0.5em;">' +
237行目: 272行目:
'</div>'
'</div>'
);
);

// VIPリストとLTAリストを取得
getVipList();
getLtaList();


}
}


/**
* 使用者が管理者権限を持たない場合に権限エラーを表示する関数
*/
function showUserRightError() {
function showUserRightError() {
$(mw.util.$content).prop('innerHTML',
$(mw.util.$content).prop('innerHTML',
254行目: 296行目:


/**
/**
* コピー用のVIP名称リストを取得する関数 (「Wikipedia:進行中の荒らし行為」の全セクション名を取得し絞りこむことで取得)
* 保護対象ページインプットボックス内の重複分、赤点スペース、余分な改行を除去・整形 + ページ一覧を取得
* @returns {Array}
* @returns {jQuery.Promise}
*/
function getVipList() {

// 関数を非同期化
var def = new $.Deferred();

// APIリクエスト
new mw.Api().get({
'action': 'parse',
'page': 'Wikipedia:進行中の荒らし行為',
'prop': 'sections',
'formatversion': '2'
}).then(function(res) {

var resSect;
if (!res || !res.parse || !(resSect = res.parse.sections)) return def.reject();
if (resSect.length === 0) return def.resolve();

// VIP名称とは無関係のセクションタイトルを定義
var excludeList = [
'記述について',
'急を要する二段階',
'配列',
'ブロック等の手段',
'このページに利用者名を加える',
'注意と選択',
'警告の方法',
'未登録(匿名・IP)ユーザーの場合',
'登録済み(ログイン)ユーザーの場合',
'警告中',
'関連項目'
];

// 配列内のオブジェクトをループしVIP名称を取得
var viplist = [];
resSect.forEach(function(obj) {
// セクション名が除外リストに含まれず、レベルが3の場合
if ($.inArray(obj.line, excludeList) === -1 && obj.level == 3) {
// VIP名称のため配列にリンク形式で保存
viplist.push('[[WP:VIP#' + obj.line + ']]');
}
});
if (viplist.length === 0) return def.resolve();

// 参照用にオブジェクトに配列を保存
MassProtect.vip = viplist;

// フォームのドロップダウンへ反映
viplist = viplist.map(function(vip) {
return '<option>' + vip + '</option>'; // リンクを option タグ化
});
$('#mp-copy, #mp-copy-vip').css('display', 'block'); // 「リンクをコピー」の fieldset とVIPリスト用の div を表示
$('#mp-copy-vip-list').append(viplist.join('')); // VIPリストを datalist に追加
def.resolve();

}).catch(function(code, err) {
def.reject(console.log(err.error.info));
});

return def.promise();

}

/**
* コピー用のLTA名称リストを取得する関数 (「LTA:」でページ名称を前方一致検索することで取得)
* @returns {jQuery.Promise}
*/
function getLtaList() {

// 関数を非同期化
var def = new $.Deferred();

// 500件を超えてマッチする場合ループが必要なためAPIリクエストを独立関数化
var ltalist = [];
var query = function(apcontinue) {
// この内部関数自体も非同期化
var deferred = new $.Deferred();

// APIリクエスト
new mw.Api().get({
'action': 'query',
'list': 'allpages',
'apprefix': 'LTA:',
'apnamespace': '0',
'apfilterredir': 'redirects',
'aplimit': 'max',
'apcontinue': apcontinue,
'formatversion': '2'
}).then(function(res) {

var resPages;
if (!res || !res.query || !(resPages = res.query.allpages)) return deferred.resolve(); // ループ中に reject が起きると困るためあえて resolve

// ページ名が「LTA:」単体でなく、「LTA:」から始まり、「/」を含まない (サブページへのSCではない) 場合に配列にページ名を保存
resPages.forEach(function(obj) {
if (obj.title !== 'LTA:' && obj.title.match(/^LTA:/) && obj.title.indexOf('/') === -1) ltalist.push('[[' + obj.title + ']]');
});

// まだ条件に一致するページがある場合は
if (res.continue) {
// もう一度同様のAPIリクエストを飛ばし、値が返ってきてから終了
query(res.continue.apcontinue).then(function() {
deferred.resolve();
});
// 条件に一致するページがこれ以上ない場合はここで終了
} else {
deferred.resolve();
}

}).catch(function(code, err) {
deferred.resolve(console.error(err.error.info)); // ループ中に reject が起きると困るためあえて resolve
});

return deferred.promise();

};

// 実際にAPIリクエストを送信し、値が返って来たら
query().then(function() {

// 配列が空の場合は何もせずに終了
if (ltalist.length === 0) return def.resolve();

// 参照用にオブジェクトに配列を保存
MassProtect.lta = ltalist;

// フォームのドロップダウンへ反映
ltalist = ltalist.map(function(ltaName) {
return '<option>' + ltaName + '</option>'; // リンクを option タグ化
}).sort(); // 昇順に並び替え
$('#mp-copy, #mp-copy-lta').css('display', 'block'); // 「リンクをコピー」の fieldset とLTAリスト用の div を表示
$('#mp-copy-lta-list').append(ltalist.join('')); // LTAリストを datalist に追加
def.resolve();

});

return def.promise();

}

// VIP/LTAリストからオプションが選択されたらクリップボードへコピー
$(document).off('input', '#mp-copy-vip-input, #mp-copy-lta-input').on('input', '#mp-copy-vip-input, #mp-copy-lta-input', function() {

var inputVal = $(this).val(),
list = MassProtect[$(this).prop('id').substring(8, 11)]; // MassProtect.vip または MassProtect.lta

if ($.inArray(inputVal, list) === -1) return; // インプットボックスに値が入力され、リスト内のリンクと一致しない場合はコピーせずに終了
copyToClipboard(inputVal);

});

/**
* 文字列をクリップボードにコピーする関数
* @param {string} str
*/
function copyToClipboard(str) {
var $temp = $('<textarea></textarea>');
$('body').append($temp); // 仮のテキストフィールドをDOM上に作成
$temp.val(str).select(); // 引数の文字列をテキストフィールドの値に設定しそれを選択
document.execCommand('copy'); // クリップボードにコピー
$temp.remove(); // テキストフィールドを除去
}

/**
* 保護対象ページインプットボックス内の重複分、赤点スペース、余分な改行を除去・整形 + ページ一覧を取得する関数
* @returns {Array} 保護対象ページタイトルの配列
*/
*/
function getTargetPages() {
function getTargetPages() {

// インプットボックスの値を取得し、赤点スペースおよび余分な改行を除去
var $input = $('#mp-targetpages');
var $input = $('#mp-targetpages');
var inputVal = $input.val().replace(/\u200e/g, '').trim();
var inputVal = $input.val().replace(/\u200e/g, '').trim();

if (inputVal) {
// 整形されたインプットボックスの値が空の文字列でなければ
if (inputVal) {

// 値を改行で分割し配列を作成
inputVal = inputVal.split('\n');
inputVal = inputVal.split('\n');
// 配列要素のそれぞれから赤点スペースを除去の上アンダーバーを半角スペースに置換、前後の余分なスペースを除去
inputVal = inputVal.map(function(item) {
inputVal = inputVal.map(function(item) {
return item.replace(/\u200e/g, '').replace(/_/g, ' ').trim();
return item.replace(/\u200e/g, '').replace(/_/g, ' ').trim();
});
});
// 空の文字列と重複要素をsplit配列から除去
inputVal = inputVal.filter(function(item, index) {
inputVal = inputVal.filter(function(item, index) {
return item !== '' && inputVal.indexOf(item) === index; // 空の文字列と重複要素をsplit配列から除去
return item !== '' && inputVal.indexOf(item) === index;
});
});
// 整形したSplit配列を使ってインプットボックスの値を更新
$input.val(inputVal.join('\n'));
$input.val(inputVal.join('\n'));

// 値が空の文字列なら
} else {
} else {
// Return用に空の配列を代入
inputVal = [];
inputVal = [];
}
}

// 保護対象のページ数表示を更新
$('#mp-targetpages-count').text(inputVal.length);
$('#mp-targetpages-count').text(inputVal.length);

// 保護対象ページ群の配列をreturn
return inputVal;
return inputVal;

}
}


283行目: 509行目:
$(document).off('input', '#mp-targetpages-search-input').on('input', '#mp-targetpages-search-input', function() {
$(document).off('input', '#mp-targetpages-search-input').on('input', '#mp-targetpages-search-input', function() {


/*******************************************************************************************************\
ページ情報検索タブのHTML構造
'<div id="mp-targetpages-search">' +
'<div id="mp-targetpages-search-input-div">' +
'ページ情報検索<br>' +
'<input id="mp-targetpages-search-input">' +
'<p id="mp-targetpages-search-links" style="display: none;">' +
' (<a id="mp-targetpages-search-history" href="">履歴</a> /' +
' <a id="mp-targetpages-search-log" href="">記録</a> /' +
' <a id="mp-targetpages-search-log" href="">保護記録</a>)' +
'</p>' +
'</div>' +
'<div id="mp-targetpages-search-status-div" style="display: none;">' +
'<p id="mp-targetpages-search-status" style="display: inline-block;"></p>' +
'</div>' +
'<div id="mp-targetpages-search-missing-div" style="display: none;">' +
'<p id="mp-targetpages-search-missing" style="display: inline-block;">未作成のページ</p>' +
'</div>' +
'</div>'
\*******************************************************************************************************/

// jQueryオブジェクトを変数に格納
var $input = $(this),
var $input = $(this),
$links = $(this).siblings('p'),
$links = $(this).siblings('p'),
289行目: 537行目:
$missingDiv = $('#mp-targetpages-search-missing-div');
$missingDiv = $('#mp-targetpages-search-missing-div');


// 既にinputイベントがトリガーされている際に、秒数カウントをリセットしタイピング終了後3.5秒で1度だけイベントをトリガー
clearTimeout(inputboxUpdateTimeout);
clearTimeout(inputboxUpdateTimeout);
inputboxUpdateTimeout = setTimeout(function() { // タイピング終了後3.5秒で駆動
inputboxUpdateTimeout = setTimeout(function() {


// ページ名を取得しリンク群をアップデート
// 入力されたページ名を取得
var pagename = $input.val().replace(/\u200e/g, '').trim();
var pagename = $input.val().replace(/\u200e/g, '').trim();

if (!pagename) { // インプットボックスが空白化されたら
// インプットボックスが空白化されたら
if (!pagename) {
// 全てのリンクを非表示に
$links.css('display', 'none');
$links.css('display', 'none');
$statusDiv.css('display', 'none');
$statusDiv.css('display', 'none');
300行目: 552行目:
return;
return;
}
}

$links.css('display', 'inline-block');
// インプットボックスに有効な値が入力されたら
$links.children('a').eq(0).attr('href', mw.util.getUrl('特別:pagehistory/' + pagename));
$links.css('display', 'inline-block'); // リンクを表示し
$links.children('a').eq(0).attr('href', mw.util.getUrl('特別:pagehistory/' + pagename)); // リンク先を更新
$links.children('a').eq(1).attr('href', mw.util.getUrl('特別:ログ', {'page': pagename}));
$links.children('a').eq(1).attr('href', mw.util.getUrl('特別:ログ', {'page': pagename}));
$links.children('a').eq(2).attr('href', mw.util.getUrl('特別:ログ', {'type': 'protect', 'page': pagename}));
$links.children('a').eq(2).attr('href', mw.util.getUrl('特別:ログ', {'type': 'protect', 'page': pagename}));


/**
/**
* クエの返り値を日本語に変換 (e.g. '編集半保護 (YYYY-MM-DDThh:mmまで)')
* 保護状態を確認するAPIリクエストの返り値を日本語に変換する関数 (e.g. '編集半保護 (YYYY-MM-DDThh:mmまで)')
* @param {Array} arr res.query.pages.key.protection: [{type: __, level: __, expiry: __}]
* @param {Array} arr res.query.pages.key.protection: [{type: __, level: __, expiry: __}]
* @returns {string}
* @returns {string}
*/
*/
var getProtectInfo = function(arr) {
var getProtectInfo = function(arr) {

var protectInfo = '';
var protectInfo = '';

// 返り値内の配列をループ
for (var i = 0; i < arr.length; i++) {
for (var i = 0; i < arr.length; i++) {

// 保護種別を日本語に変換
switch (arr[i].type) {
switch (arr[i].type) {
case 'create':
case 'create':
325行目: 584行目:
case 'upload':
case 'upload':
protectInfo += 'アップロード';
protectInfo += 'アップロード';
default:
protectInfo += '??';
}
}

// 保護レベルを日本語に変換
switch (arr[i].level) {
switch (arr[i].level) {
case 'autoconfirmed':
case 'autoconfirmed':
336行目: 599行目:
protectInfo += '全保護';
protectInfo += '全保護';
}
}

protectInfo += ' (' + (arr[i].expiry === 'infinity' ? '無期限' : arr[i].expiry.substring(0, arr[i].expiry.length - 4) + 'まで') + '), ';
// 保護期間を括弧書きで追加 (保護期間は無期限の場合 'infinity'、有期の場合JSON形式のタイムスタンプ: e.g. 2022-01-01T00:00:00Z)
}
protectInfo += ' (' + (arr[i].expiry === 'infinity' ? '無期限' : arr[i].expiry.replace(/Z$/, '') + 'まで') + ')<br>';

} // ループ後のprotectInfoの例: 「作成半保護 (無期限)」、「移動全保護 (2022-01-01T00:00:00まで)」など

// この段階でprotectInfoが空の文字列なら、該当ページは保護されていない
if (!protectInfo) protectInfo = '保護設定なし';
if (!protectInfo) protectInfo = '保護設定なし';

return protectInfo.replace(/, $/, '');
// 語末に改行がある場合は除去し、文字列をreturn
return protectInfo.replace(/<br>$/, '');

};
};


/**
* フォーム上に検索対象ページの保護状態を表示する関数
* @param {string} protectInfo
* @param {boolean} pageMissing
*/
var updateForm = function(protectInfo, pageMissing) {
var updateForm = function(protectInfo, pageMissing) {


// 保護状態表示用 div を表示しメッセージを更新
$status.text(protectInfo);
$statusDiv.css('display', 'block');
$statusDiv.css('display', 'block');
$status.prop('innerHTML', protectInfo);


// ページが未作成の場合のみ「未作成のページ」と表示
if (pageMissing) {
if (pageMissing) {
$missingDiv.css('display', 'block');
$missingDiv.css('display', 'block');
355行目: 633行目:
};
};


// ページの保護情報を取得
// ページの保護状態を取得
$.get(mw.util.wikiScript('api'), {
new mw.Api().get({
'action': 'query',
'action': 'query',
'titles': pagename,
'titles': pagename,
'prop': 'info',
'prop': 'info',
'inprop': 'protection',
'inprop': 'protection',
'format': 'json'
'formatversion': '2'
}, function(res) {
}).then(function(res) {
var resPages, pageMissing, protectInfo;
var resPages, pageMissing, protectInfo;
if (!res || !res.query || !(resPages = res.query.pages)) return;
if (!res || !res.query || !(resPages = res.query.pages)) return;
for (var page in resPages) { // 1ページずつのためループは1度のみ
for (var page in resPages) { // 1ページずつのためループは1度のみ
if (resPages[page].missing === '') pageMissing = true;
pageMissing = resPages[page].missing;
protectInfo = getProtectInfo(resPages[page].protection); // 加算不要
protectInfo = getProtectInfo(resPages[page].protection); // 加算不要
}
}
373行目: 651行目:


}, 350);
}, 350);

});

// 保護レベルドロップダウンの値が変わった際にトリガー (作成保護と編集保護・移動保護は同時指定不可)
$(document).off('change', '.mp-protectionlevel').on('change', '.mp-protectionlevel', function() {

var create = '#mp-protectionlevel-create',
edit = '#mp-protectionlevel-edit',
move = '#mp-protectionlevel-move',
level = $(this).children('option').filter(':selected').val(), // '', 'all', 'autoconfirmed', 'extendedconfirmed', 'sysop'
id = '#' + $(this).prop('id');

switch (id) {
case create:

// 選択されたのが「変更なし」以外なら
if (level) {
// 編集保護と移動保護のドロップダウンを無効化し、値を「変更なし」にリセット
$('.mp-protectionlevel').not(create).prop('disabled', true).children('.mp-protectionlevel-none').prop('selected', true);
// 選択されたのが「変更なし」なら
} else {
// 編集保護と移動保護のドロップダウンを有効化
$('.mp-protectionlevel').not(create).prop('disabled', false);
}
break;

case edit:
case move:

// editが変更された場合はmoveの選択値を、moveが変更された場合はeditの選択値を取得
var levelInTheOther = $(id === edit ? move : edit).children('option').filter(':selected').val();

// 選択されたのが「変更なし」以外なら
if (level) {
// 作成保護ドロップダウンを無効化し、値を「変更なし」にリセット
$('#mp-protectionlevel-create').prop('disabled', true).children('.mp-protectionlevel-none').prop('selected', true);
// 選択されたのが「変更なし」なら
} else {
// もう一方も「変更なし」の場合のみ作成保護ドロップダウンを有効化
if (!levelInTheOther) $('#mp-protectionlevel-create').prop('disabled', false);
}
}

});
});


// 「保護タグを自動添付」がチェックされたらドロップダウンを表示
// 「保護タグを自動添付」がチェックされたらドロップダウンを表示
$(document).off('change', '#mp-tag').on('change', '#mp-tag', function() {
$(document).off('change', '#mp-tag').on('change', '#mp-tag', function() {

// チェックされたら
if ($(this).is(':checked')) {
if ($(this).is(':checked')) {
// ドロップダウンを表示
$('#mp-tag-list').css('display', 'block');
$('#mp-tag-list').css('display', 'block');
// チェックが外されたら
} else {
} else {
// ドロップダウンを隠す
$('#mp-tag-list').css('display', 'none');
$('#mp-tag-list').css('display', 'none');
}
}

});
});


388行目: 715行目:


/********************************************************\
/********************************************************\
[[Template:Pp]]の引数一覧
* template action small *
* pp-(meta) edit, move, upload def=no *
template action small
* dispute edit, move, upload X *
pp-(meta) edit, move, upload def=no
* vandalism edit, move def=no *
dispute edit, move, upload X
* (semi-indef) ---------- --------- *
vandalism edit, move def=no
* template X def=yes *
(semi-indef) ---------- ---------
* permanent X def=yes *
template X def=yes
* move X def=no *
permanent X def=yes
* move-vandalism X def=no *
move X def=no
* move-dispute X X *
move-vandalism X def=no
* office X X *
move-dispute X X
* reset X X *
office X X
* office-dmca X X *
reset X X
office-dmca X X
\********************************************************/
\********************************************************/


433行目: 761行目:
$actionUpload.prop('hidden', true);
$actionUpload.prop('hidden', true);
$smallNo.prop('hidden', true);
$smallNo.prop('hidden', true);
$smallYes.prop('selected', true); // {{pp-vamdalism}} は一番よく使うため {{pp-vamdalism|small=yes}} をデフォルトに
break;
break;
case 'mp-tag-template-template':
case 'mp-tag-template-template':
456行目: 785行目:
// カスタム期間指定インプットボックスをドロップダウンの値によって有効化・無効化
// カスタム期間指定インプットボックスをドロップダウンの値によって有効化・無効化
$(document).off('change', '#mp-expiry-dropdown').on('change', '#mp-expiry-dropdown', function() {
$(document).off('change', '#mp-expiry-dropdown').on('change', '#mp-expiry-dropdown', function() {

if ($(this).val() === '') { // カスタム期間
// 「その他の期間」が選択されたら
if ($(this).val() === '') {
// インプットボックスを有効化
$('#mp-expiry-custom').prop('disabled', false);
$('#mp-expiry-custom').prop('disabled', false);
} else { // プリセット期間
// プリセット期間が選択されたら
} else {
// インプットボックスを無効化
$('#mp-expiry-custom').val('').prop('disabled', true);
$('#mp-expiry-custom').val('').prop('disabled', true);
}
}

});
});


/**
/**
* action=protectの 'protections' パラメータを取得 (書式: action=level (e.g. edit=sysop))
* action=protectの 'protections' パラメータを取得する関数 (書式: action=level (e.g. edit=sysop))
* @returns {string}
* @returns {string|boolean} 適切なパラメータならそのパラメータ、パラメータが指定されていなければfalse、不正なパラメータなら空の文字列
*/
*/
function getProtectionLevelParam() {
function getProtectionLevelParam() {

var param = [];
var param = [],
actions = ['create=', 'edit=', 'move='];

// mp-protectionlevelクラスを持つ <select> タグをループ
$('.mp-protectionlevel').each(function(i) {
$('.mp-protectionlevel').each(function(i) {
var type = (i === 0 ? 'create=' : (i === 1 ? 'edit=' : 'move='));
var level = $(this).children('option').filter(':selected').val(); // '', 'all', 'autoconfirmed', 'extendedconfirmed', 'sysop'
var level = $(this).children('option').filter(':selected').val();
if (level) param.push(actions[i] + level);
if (level) param.push(type + level);
});
});

// param配列に要素がなければ
if (param.length === 0) {
if (param.length === 0) {
// undefined を return (保護レベルドロップダウンは全て「変更なし」)
return false;
return;
// param配列に要素があれば
} else {
} else {
// 配列を文字列に変換し return (例: ['edit=sysop', 'move=sysop'] => 'edit=sysop|move=sysop')
param = param.join('|');
return param.join('|');
if (param.indexOf('create=') !== -1 && (param.indexOf('edit=') !== -1 || param.indexOf('move=') !== -1)) {
return '';
} else {
return param;
}
}
}

}
}


/**
/**
* 保護実行前の最終確認 + 必要情報取得
* 保護実行前の最終確認 + 必要情報取得する関数
* @returns {{pages: Array, levels: string, expiry: string, reason: string, cascade: boolean, watchlist: string, tag: string}}
* @returns {{pages: Array, levels: string, expiry: string, reason: string, cascade: boolean, watchlist: string, tag: string}}
*/
*/
496行目: 835行目:
if (targetPages.length === 0) return alert('保護対象ページが入力されていません');
if (targetPages.length === 0) return alert('保護対象ページが入力されていません');
var protectionLevel = getProtectionLevelParam();
var protectionLevel = getProtectionLevelParam();
switch (protectionLevel) {
if (!protectionLevel) return alert('保護レベルが設定されていません');
case false:
return alert('保護レベルが設定されていません');
case '':
return alert('未作成のページに適用される作成保護と作成済みのページに適用される編集保護・移動保護は同時指定できません');
default:
}


// 保護理由を取得 (「その他」かつ空白の場合警告)
// 保護理由を取得 (空白の場合警告)
var reasonOption = $('#mp-reason-dropdown').find('option').filter(':selected').val();
var reasonOption = $('#mp-reason-dropdown').find('option').filter(':selected').val(),
var reasonCustom = $('#mp-reason-custom').val().replace(/\u200e/g, '').trim();
reasonOption2 = $('#mp-reason-dropdown2').find('option').filter(':selected').val(),
reasonCustom = $('#mp-reason-custom').val().replace(/\u200e/g, '').trim();
if (reasonOption === 'other' && !reasonCustom) {
if (!(reasonOption + reasonOption2 + reasonCustom)) {
if (!confirm('「その他の理由」が選択されていますが理由が入力されていません。このまま実行しますか?')) return;
if (!confirm('保護理由が入力されていません。このまま実行しますか?')) return;
}
}
var reason = reasonOption === 'other' ? reasonCustom : reasonOption + (reasonCustom ? ': ' + reasonCustom : '');
reasonOption = reasonOption ? reasonOption + ': ' : ''; // 保護理由ドロップダウンの値が空でなければ語末に ': ' を追加
reasonOption2 = reasonOption2 ? reasonOption2 + ': ' : '';
var reason = (reasonOption + reasonOption2 + reasonCustom).replace(/: $/, ''); // 全ての理由を組み合わせ、語末に ': ' がある場合は除去


// 保護タグを取得
// 保護タグを取得
var tag = '';
var tag = '';
if ($('#mp-tag').is(':checked')) {
if ($('#mp-tag').is(':checked') && protectionLevel.indexOf('create=') === -1) { // 「保護タグを自動添付」がチェックされており、作成保護でなければ
var tagname = $('#mp-tag-template').children('option').filter(':selected').val(),
var tagName = $('#mp-tag-template').children('option').filter(':selected').val(),
tagtype = $('#mp-tag-action').children('option').filter(':selected').val(),
tagAction = $('#mp-tag-action').children('option').filter(':selected').val(),
tagdisplay = $('#mp-tag-small').children('option').filter(':selected').val();
tagSmall = $('#mp-tag-small').children('option').filter(':selected').val();
tag = (tagname + tagtype + tagdisplay) ? '{{' + tagname + tagtype + tagdisplay + '}}' : '';
tag = (tagName + tagAction + tagSmall) ? '{{' + tagName + tagAction + tagSmall + '}}' : '';
}
}


545行目: 881行目:
if (!ep) return;
if (!ep) return;


// ボタンを無効化
// 「保護を実行」ボタンを無効化
$(this).prop('disabled', true);
$(this).prop('disabled', true);


// 進捗状態を表示
// 進捗状態を表示
$('#mp-protection-settings').append(
$('#mp-protection-settings').append( // 「保護を実行」ボタンの下にフィールドを作成
'<fieldset id="mp-progress">' +
'<fieldset id="mp-progress">' +
'<legend>進捗</legend>' +
'<legend>進捗</legend>' +
555行目: 891行目:
);
);
var spinner = '<img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; max-height: 100%; border: 0;">';
var spinner = '<img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; max-height: 100%; border: 0;">';
ep.pages.forEach(function(page) {
ep.pages.forEach(function(page) { // 保護対象ページの全てをループ
$('#mp-progress').append(
$('#mp-progress').append(
'<p>' +
'<p>' +
564行目: 900行目:
);
);
});
});

/**
* 画面の最下部に追加される進捗状態が見える位置までスクロールダウンする関数
* @param {number} pos スクロール位置のY座標
*/
var scrollDown = function(pos) {
var scrollDown = function(pos) {
var i = window.scrollY;
var i = window.scrollY;
586行目: 927行目:
*/
*/
function protectPages() {
function protectPages() {

// 保護対象ページのタイトルを取得
var pagetitle = ep.pages[protectCnt];
var pagetitle = ep.pages[protectCnt];

// APIリクエストのパラメータを設定
var params = {
var params = {
'action': 'protect',
'action': 'protect',
597行目: 942行目:
'format': 'json'
'format': 'json'
};
};
if (ep.cascade) params.cascade = true;
if (ep.cascade) params.cascade = true; // なぜか false で保護リクエストを投げてもカスケード保護されるため true の場合のみパラメータを追加

// 保護リクエストを送信
$.post(mw.util.wikiScript('api'), params, function(res) {
$.post(mw.util.wikiScript('api'), params, function(res) {


var protected;
if (res.error) { // 何らかの理由で保護に失敗

// 何らかの理由で保護に失敗
if (res.error) {
// 進捗を「保護: 失敗 (エラーの詳細)」に更新
updateProgress(pagetitle, 'protect', res.error.info);
updateProgress(pagetitle, 'protect', res.error.info);
} else if (!res || !res.protect) { // 不明なエラー
// 不明なエラー
} else if (!res || !res.protect) {
// 進捗を「保護: 失敗 (不明なエラー)」に更新
updateProgress(pagetitle, 'protect', false);
updateProgress(pagetitle, 'protect', false);
} else { // 成功
// 成功
} else {
// 進捗を「保護: 成功」に更新
updateProgress(pagetitle, 'protect', true);
updateProgress(pagetitle, 'protect', true);
protected = true;
}
}


// 次の保護に移行するために保護対象ページ配列のインデックスを加算 (pasteTagの前に更新する必要があるためこの位置での更新必須)
protectCnt++;
protectCnt++;

// タグ添付をする場合は
if (ep.tag) {
if (ep.tag) {
pasteTag(pagetitle);
// 保護に成功した場合のみ
} else {
if (protected) {
if (ep.pages[protectCnt]) protectPages();
// タグ添付プロセスに移行
return pasteTag(pagetitle);
// 保護に失敗している場合は
} else {
// 進捗を「タグ: 中止 (保護失敗)」に更新
updateProgress(pagetitle, 'tag', '保護失敗', true);
}
}
}

// タグ添付をしない、もしくは保護に失敗した場合は
if (ep.pages[protectCnt]) protectPages(); // 次の保護プロセスに移行


});
});

}
}


/**
* タグ添付を実行する関数
* @param {string} pagetitle
*/
function pasteTag(pagetitle) {
function pasteTag(pagetitle) {


if (pagetitle.match(/^(?:モジュール|[Mm]odule):/)) { // タグ添付対象がモジュール名前空間のページなら
// タグ添付対象がモジュール名前空間のページなら
updateProgress(pagetitle, 'tag', 'モジュール', true); // 進捗を「中止 (モジュール)」にして
if (pagetitle.match(/^(?:モジュール|[Mm]odule):/)) {
// 進捗を「中止 (モジュール)」にして
if (ep.pages[protectCnt]) protectPages(); // タグ添付をせずに次の保護処理に移行
updateProgress(pagetitle, 'tag', 'モジュール', true);
// タグ添付をせずに次の保護処理に移行
if (ep.pages[protectCnt]) protectPages();
return;
return;
}
}
638行目: 1,014行目:


var resPages;
var resPages;

// APIリクエストの返り値にページの編集に必要な情報がなければ
if (!res || !res.query || !(resPages = res.query.pages)) {
if (!res || !res.query || !(resPages = res.query.pages)) {
// 進捗を「タグ: 失敗 (通信エラー)」に更新し、次の保護処理に移行
updateProgress(pagetitle, 'tag', '通信エラー', true);
updateProgress(pagetitle, 'tag', '通信エラー', true);
if (ep.pages[protectCnt]) protectPages();
if (ep.pages[protectCnt]) protectPages();
644行目: 1,023行目:
}
}


var curtimestamp = res.curtimestamp;
// ページの編集に必要な情報を取得
var curtimestamp = res.curtimestamp; // 最新版を取りに行った時間のタイムスタンプ (編集競合対策)
for (var key in resPages) { // キーはランダムナンバー (ページID)
for (var key in resPages) { // キーはランダムナンバー (ページID)


var resObj = resPages[key];
var resObj = resPages[key];
var pageMissing = resObj.missing === '';
var isRedirect = resObj.redirect === ''; // リダイレクトか否か
var isRedirect = resObj.redirect === '';
var basetimestamp = resObj.touched; // 最新版のタイムスタンプ (編集競合対策)
var content = resObj.revisions[0].slots.main['*']; // ページ内容

// ページが未作成の場合はタグ添付をしない
if (pageMissing) {
updateProgress(pagetitle, 'tag', '未作成', true);
if (ep.pages[protectCnt]) protectPages();
return;
}

// タグ添付するページの編集に必要な情報を取得
var basetimestamp = resObj.touched;
var content = resObj.revisions[0].slots.main['*']; // ページ内容を取得
var originalContent = JSON.parse(JSON.stringify(content)); // 後で比較する用の魚拓
var originalContent = JSON.parse(JSON.stringify(content)); // 後で比較する用の魚拓

// ページ内に除去が必要なテンプレートがあれば除去
var templates = findTemplates(content, MassProtect.template.names, MassProtect.template.prefixes); // 除去対象のテンプレートを取得
var templates = findTemplates(content, MassProtect.template.names, MassProtect.template.prefixes); // 除去対象のテンプレートを取得
if (templates.length !== 0) {
if (templates.length !== 0) { // 除去対象テンプレートがあれば

// 正規表現内で使用するため特殊文字をエスケープ
templates = templates.map(function(item) {
templates = templates.map(function(item) {
return mw.util.escapeRegExp(item); // 正規表現内で使用するため特殊文字をエスケープ
return mw.util.escapeRegExp(item);
});
});

// ページ内の「{{TEMPLATE}}\n」を空の文字列に置換
var regex = new RegExp('(?:' + templates.join('|') + ')[^\\S\\n\\r]*\\n?', 'g');
var regex = new RegExp('(?:' + templates.join('|') + ')[^\\S\\n\\r]*\\n?', 'g');
content = content.replace(regex, ''); // 「{{TEMPLATE}}\n」を空の文字列に置換
content = content.replace(regex, '');

}
}


// 冒頭に保護タグを挿入 (リダイレクトとテンプレート名前空間のページは別処理が必要)
// 冒頭に保護タグを挿入 (リダイレクトとテンプレート名前空間のページは別処理が必要)
if (isRedirect) { // リダイレクトなら
if (isRedirect) { // リダイレクトなら

content = content.trim(); // リダイレクトのリンクの前に改行があることがあるのでそれを除去
if (content.indexOf('\n') === -1) { // ページ内容が転送リンクの行のみなら
// リダイレクトのリンクの前に改があることがあるでそれを除去
content += '\n' + ep.tag; // 2行目にタグを挿入
content = content.trim();

} else { // 複数行あれば
content = content.replace('\n', '\n' + ep.tag + '\n'); // 1つ目行の後にタグを挿入
// ページ内容が転送リンク行のみなら
if (content.indexOf('\n') === -1) {
// 2行目にタグを挿入
content += '\n' + ep.tag;
// 複数行あれば
} else {
// 1つ目の改行の後にタグを挿入
content = content.replace('\n', '\n' + ep.tag + '\n');
}
}

} else {
if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) { // .cssページ
} else { // リダイレクトでければ

content = '/*' + ep.tag + '*/' + '\n' + content; // コメントアウト
} else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/)) { // .cssページければ
// Template名前空間のcssページな
if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) {
content = '<noinclude>' + ep.tag + '</noinclude>' + content; // トランスクルードさせない
// コメントアウトしたタグを挿入
content = '/*' + ep.tag + '*/' + '\n' + content;
// Cssページでなければ
} else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/)) {
// トランスクルードさせずにタグを挿入
content = '<noinclude>' + ep.tag + '</noinclude>' + content;
// Template名前空間以外のページなら
} else {
} else {
// 単純に1行目にタグを挿入
content = ep.tag + '\n' + content;
content = ep.tag + '\n' + content;
}
}

}
}


713行目: 1,106行目:
}, function(res2) {
}, function(res2) {


if (res2.error) { // 何らかの理由で編集に失敗
// 何らかの理由で編集に失敗
if (res2.error) {
// 進捗を「タグ: 失敗 (エラーの詳細)」に更新
updateProgress(pagetitle, 'tag', res2.error.info);
updateProgress(pagetitle, 'tag', res2.error.info);
} else if (!res2 || !res2.edit) { // 不明なエラー
// 不明なエラー
} else if (!res2 || !res2.edit) {
// 進捗を「タグ: 失敗 (不明なエラー)」に更新
updateProgress(pagetitle, 'tag', false);
updateProgress(pagetitle, 'tag', false);
// それ以外
} else {
} else {
if (res2.edit.result === 'Success') { // 成功
// 成功
if (res2.edit.result === 'Success') {
// 進捗を「タグ: 成功」に更新
updateProgress(pagetitle, 'tag', true);
updateProgress(pagetitle, 'tag', true);
// 失敗 (res2.edit.result === 'Failure')、キャプチャの手動入力が求められた時などにこの値が返ってくるが、管理者は無関係のため通常はこのコードには辿り着かない
} else {
} else {
// 進捗を「タグ: 失敗 (Failure)」に更新
updateProgress(pagetitle, 'tag', res2.edit.result);
updateProgress(pagetitle, 'tag', res2.edit.result);
}
}
}
}


// まだ保護対象ページが残っている場合は次の保護プロセスに移行
if (ep.pages[protectCnt]) protectPages();
if (ep.pages[protectCnt]) protectPages();


741行目: 1,144行目:


/**
/**
* 一括保護の進捗状態を更新する関数
* @param {number} page 保護対象ページ名
* @param {string} page 保護対象ページ名
* @param {string} type 'protect' または 'tag'
* @param {string} type 'protect' または 'tag'
* @param {boolean|string} result 成功したらtrue、不明なエラーが発生したらfalse、理由の分かるエラーが発生したらその詳細
* @param {boolean|string} result 成功したらtrue、不明なエラーが発生したらfalse、理由の分かるエラーが発生したらその詳細 (または「中止 (XXX)」のXXXにあたる文字列)
* (cancelledtrueの場合「中止 (XXX)」のXXXにあたる文字列)
* @param {boolean} [cancelled] trueの場合「中止 (XXX)」を表示
* @param {boolean} [cancelled]
*/
*/
function updateProgress(page, type, result, cancelled) {
function updateProgress(page, type, result, cancelled) {
761行目: 1,164行目:
\********************************************************************************************/
\********************************************************************************************/


// 更新対象のセレクタ
var $progress = $('.mp-progress-page').filter(function() { // mp-progress-page のクラス属性を持っている <span> タグを
var $progress = $('.mp-progress-page').filter(function() { // mp-progress-page のクラス属性を持っている <span> タグを
return $(this).text() === page; // 特定のページ名を text として持つものに絞り込み
return $(this).text() === page; // 特定のページ名を text として持つものに絞り込み
}).siblings('.mp-progress-' + type); // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ
}).siblings('.mp-progress-' + type); // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ


// 更新
switch (result) {
switch (result) {
case true:
case true:
782行目: 1,187行目:


/**
/**
* ウィキテキストからテンプレートを抽出
* ウィキテキストからテンプレートを抽出する関数
* @param {string} wikitext
* @param {string} wikitext テンプレートを検索する対象のウィキテキスト
* @param {string|Array} [templateName] テンプレート名で絞り込み
* @param {string|Array} [templateName] テンプレート名で絞り込み
* @param {string|Array} [templatePrefix] テンプレートの接頭辞で絞り込み
* @param {string|Array} [templatePrefix] テンプレートの接頭辞で絞り込み
792行目: 1,197行目:
// 原文を '{{' で split
// 原文を '{{' で split
var tempInnerContent = wikitext.split('{{'); // tempInnerContent[0] は必ずテンプレートとは無関係の文字列
var tempInnerContent = wikitext.split('{{'); // tempInnerContent[0] は必ずテンプレートとは無関係の文字列
if (tempInnerContent.length === 0) return [];
if (tempInnerContent.length === 0) return []; // テンプレートがない場合は空の配列を return
var templates = [], nest = [];


// テンプレートを抽出
// テンプレートを抽出
var templates = [], nest = [];
for (var i = 1; i < tempInnerContent.length; i++) { // split配列をループ
for (var i = 1; i < tempInnerContent.length; i++) { // split配列をループ

(function() { // for のブロックスコープを作成
// 即時関数で for のブロックスコープを作成
(function() {


var tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // 配列要素内の '}}' の数
var tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // 配列要素内の '}}' の数
839行目: 1,246行目:


})();
})();

} // ループ終了時には templates 配列に wikitext 内の全てのテンプレートが格納されている
} // ループ終了時には templates 配列に wikitext 内の全てのテンプレートが格納されている


844行目: 1,252行目:
if ((!templateName && !templatePrefix) || templates.length === 0) return templates;
if ((!templateName && !templatePrefix) || templates.length === 0) return templates;


// 関数が受け取った引数が string の場合 Array に変換 (このスクリプト内では配列しか渡されないがこの関数がコピペ使用された時用)
// テンプレートの絞り込み
if (templateName && typeof templateName === 'string') templateName = [templateName];
if (templateName && typeof templateName === 'string') templateName = [templateName];
if (templatePrefix && typeof templatePrefix === 'string') templatePrefix = [templatePrefix];
if (templatePrefix && typeof templatePrefix === 'string') templatePrefix = [templatePrefix];


/**
var caseInsensitiveFirstLetter = function(str) { // テンプレートの1文字目は大文字小文字の区別がないためその正規表現を作る関数
* テンプレートの1文字目は大文字小文字の区別がないためその正規表現を作る関数
* @param {string} str
* @returns {string} [Xx]
*/
var caseInsensitiveFirstLetter = function(str) {
return '[' + str.substring(0, 1).toUpperCase() + str.substring(0, 1).toLowerCase() + ']';
return '[' + str.substring(0, 1).toUpperCase() + str.substring(0, 1).toLowerCase() + ']';
};
};


// テンプレートの絞り込み用の正規表現を作成
var names = [], prefixes = [];
var names = [], prefixes = [];
if (templateName) {
if (templateName) {
866行目: 1,280行目:
}
}


// テンプレートの絞り込み
templates = templates.filter(function(item) {
templates = templates.filter(function(item) {
var name = item.match(/^\{{2}\s*([^\|\{\}\n]+)/)[1].trim(); // {{ TEMPLATENAME | ... }} の TEMPLATENAME を抽出
var name = item.match(/^\{{2}\s*([^\|\{\}\n]+)/)[1].trim(); // {{ TEMPLATENAME | ... }} の TEMPLATENAME を抽出
881行目: 1,296行目:
}
}


// ******************************************
// ****************************************************************************


})(mediaWiki, jQuery);
})(mediaWiki, jQuery);

2022年7月4日 (月) 06:55時点における版

/*********************************************************************************************\
    MassProtect (一括保護スクリプト)
    管理者権限を所有する利用者が以下のいずれかの特別ページにアクセスすることで使用できます。
    * [[特別:一括保護]]
    * [[特別:MassProtect]] (※ オリジナル版と競合しないように暫定的に無効化中)
    * [[特別:MP]]
      (※ 'Special:' でも可)
    作者: (オリジナル) [[en:User:Timotheus Canens]]
                      => [[en:User:Timotheus Canens/massprotect.js]]
         (移入)      [[User:Infinite0694]]
                      => [[User:Infinite0694/Mass protecting tool ja.js]]
          (大幅改変)  [[User:Dragoniez]] 
                      => このスクリプト
                      (編集者: )
    このスクリプトを使用すると大量のページを一瞬で保護することが可能になりますが、基本的に使用者の
    責任で使用してください。
\*********************************************************************************************/
//<nowiki>

(function(mw, $) { // 関数スコープを作成

// ************************************** ページの初期化 **************************************

// 関数間で共有する変数を格納するためのオブジェクト
var MassProtect = {};

// ページ名が「特別:一括保護」、「特別:MassProtect」または「特別:MP」の場合、一括保護フォーム生成
if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm][Pp])$/)) {
// if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm]ass[Pp]rotect|[Mm][Pp])$/)) {

    // ページタイトルを変更 (「そのような特別ページはありません」⇒「一括保護 - Wikipedia」)
    $(document).prop('title', '一括保護 - Wikipedia');

    // 使用者が管理者なら
    if (/sysop/.test(mw.config.get('wgUserGroups'))) {
        // 一括保護のフォームを生成
        createForm();
    // 管理者でなければ
    } else {
        // 権限エラーを表示
        showUserRightError(); 
        return;
    }

} else { // 該当特別ページ以外では

    // 「特別:一括保護」へのリンクを生成
    mw.util.addPortletLink('p-tb', '/wiki/特別:一括保護', '一括保護 (改)' , 't-mp', 'ページを一括で保護する');
    return;

}


// ************************************** 主要関数 **************************************

/**
 * 一括保護のフォームを生成する関数
 */
function createForm() {

    // 複数回使用するHTMLを変数に格納
    var protectLevels = // 保護レベルオプション
        '<option selected class="mp-protectionlevel-none" value="">変更なし</option>' +
        '<option class="mp-protectionlevel-all" value="all">すべての利用者に許可</option>' +
        '<option class="mp-protectionlevel-autoconfirmed" value="autoconfirmed">自動承認された利用者のみ許可</option>' +
        '<option class="mp-protectionlevel-extendedconfirmed" value="extendedconfirmed">拡張承認された利用者と管理者に許可</option>' +
        '<option class="mp-protectionlevel-sysop" value="sysop">管理者のみ許可</option>';

    var protectReasons = // 保護理由オプション
        '<optgroup label="保護理由">' +
            '<option>度重なる荒らし</option>' +
            '<option>度重なる宣伝</option>' +
            '<option>編集合戦</option>' +
            '<option>移動合戦</option>' +
            '<option>プライバシー侵害の記述の繰り返し</option>' +
            '<option>IP利用者による問題投稿の繰り返し</option>' +
            '<option>IP・新規利用者による問題投稿の繰り返し</option>' +
            '<option>[[WP:SOCK|sockpuppet]]による問題投稿の繰り返し</option>' +
            '<option>[[Wikipedia:保護依頼]]による</option>' +
            '<option>削除タグ剥離</option>' +
            '<option>移動不要ページ</option>' +
            '<option>利用者ページ 本人希望</option>' +
            '<option>削除されたページの改善なき再作成の繰り返し</option>' +
            '<option>更新の必要性が低い重要ページ</option>' +
            '<option>[[WP:HRT|影響が特に大きいテンプレート]]</option>' +
            '<option>議論が終了済みの過去ログ</option>' +
            '<option>履歴保存</option>' +
            '<option>特定版削除に伴う保護のかけなおし</option>' +
            '<option>特定版削除に伴う半保護のかけなおし</option>' +
        '</optgroup>' +
        '<optgroup label="解除理由">' +
            '<option>[[Wikipedia:保護解除依頼]]による</option>' +
            '<option>保護理由消滅のため</option>' +
            '<option>編集内容の合意の成立</option>' +
        '</optgroup>';

    // ページコンテンツの内部HTMLを一括保護のフォームに置換
    $(mw.util.$content).prop('innerHTML',
        '<div id="mp-container" style="font-size: 95%;">' +
            '<div id="mp-header">' +
                '<h1>一括保護</h1>' +
                '<p>' + 
                    '指定されたページ群の保護レベルを一括変更できます。変更する場合は、' + 
                    '<a href="' + mw.util.getUrl('Wikipedia:保護の方針') + '" title="Wikipedia:保護の方針">保護の方針</a>、' +
                    '<a href="' + mw.util.getUrl('Wikipedia:拡張半保護の方針') + '" title="Wikipedia:拡張半保護の方針">拡張半保護の方針</a>、' +
                    '<a href="' + mw.util.getUrl('Wikipedia:半保護の方針') + '" title="Wikipedia:半保護の方針">半保護の方針</a>、' +
                    'に基づいているか確認して下さい。' +
                '</p>' +
                '<ul>' +
                    '<li>有効期限のデフォルトは無期限です。適切な期間・期限を指定してください。</li>' +
                    '<ul>' +
                        '<li>' +
                            '「その他の期間」の記入例 (' +
                            '<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 href="' + mw.util.getUrl('協定世界時') + '" title="協定世界時">UTC</a>)' +
                        '</li>' +
                    '</ul>' +
                    '<li>' +
                        '保護レベルを変更した場合、ページ上で保護テンプレート (' +
                        '<a href="' + mw.util.getUrl('Template:Pp') + '">Template:Pp</a>' +
                        ') を更新してください。' +
                    '</li>' +
                '</ul>' +
            '</div>' +
            '<div id="mp-body">' +
                '<form>' +
                    '<fieldset id="mp-protection-settings">' +
                        '<legend>一括保護</legend>' +
                        '<fieldset>' +
                            '<legend>保護対象ページ</legend>' +
                            '<textarea id="mp-targetpages" placeholder="ページごとに改行して記入" rows="15" style="font-family: inherit;"></textarea>' +
                            '<input id="mp-targetpages-cleanup" type="button" value="整形" style="margin: 0.5em 0.5em 0 0;">' +
                            '<label for="mp-targetpages-cleanup">(重複分および余分な改行を除去)</label><br>' +
                            '<p>ページ数: <span id="mp-targetpages-count">0</span></p>' +
                            '<div id="mp-targetpages-search" style="margin-top: 1em;">' +
                                '<div id="mp-targetpages-search-input-div" style="margin: 0;">' +
                                    'ページ情報検索<br>' +
                                    '<input id="mp-targetpages-search-input" placeholder="ページ名" style="width: 30ch; margin-right: 0.5em;">' +
                                    '<p id="mp-targetpages-search-links" style="display: none; margin: 0;">' +
                                        ' (<a id="mp-targetpages-search-history" href="">履歴</a> /' +
                                        ' <a id="mp-targetpages-search-log" href="">記録</a> /' +
                                        ' <a id="mp-targetpages-search-log" href="">保護記録</a>)' +
                                    '</p>' +
                                '</div>' +
                                '<div id="mp-targetpages-search-status-div" style="display: none; margin: 0;">' +
                                    '<p id="mp-targetpages-search-status" style="display: inline-block; margin: 0.2em 0 0 0;"></p>' +
                                '</div>' +
                                '<div id="mp-targetpages-search-missing-div" style="display: none; color: MediumVioletRed; margin: 0;">' +
                                    '<p id="mp-targetpages-search-missing" style="display: inline-block; margin: 0;">未作成のページ</p>' +
                                '</div>' +
                            '</div>' +
                        '</fieldset>' +
                        '<fieldset>' +
                            '<legend>保護レベル</legend>' +
                            '<label for="mp-protectionlevel-create" style="width: 8ch; display: inline-block;">作成保護</label>' +
                            '<select class="mp-protectionlevel" id="mp-protectionlevel-create" style="margin-bottom: 0.3em;">' +
                                protectLevels +
                            '</select><br>' +
                            '<label for="mp-protectionlevel-edit" style="width: 8ch; display: inline-block;">編集保護</label>' +
                            '<select class="mp-protectionlevel" id="mp-protectionlevel-edit" style="margin-bottom: 0.3em;">' +
                                protectLevels +
                            '</select><br>' +
                            '<label for="mp-protectionlevel-move" style="width: 8ch; display: inline-block;">移動保護</label>' +
                            '<select class="mp-protectionlevel" id="mp-protectionlevel-move" style="margin-bottom: 0.3em;">' +
                                protectLevels +
                            '</select><br>' +
                            '<input id="mp-watchlist" type="checkbox" style="margin-right: 0.3em;">' +
                            '<label for="mp-watchlist">保護対象ページをウォッチリストに追加</label><br>' +
                            '<input id="mp-cascade" type="checkbox" style="margin-right: 0.3em;">' +
                            '<label for="mp-cascade">カスケード保護 (保護対象ページで読み込まれているテンプレートと画像の自動保護)</label><br>' +
                            '<input id="mp-tag" type="checkbox" style="margin-right: 0.3em; margin-bottom: 0.3em;">' +
                            '<label for="mp-tag">' +
                                '保護タグを自動添付' +
                                ' (<a href="' + mw.util.getUrl('Template:Pp#関連項目') + '" title="Template:Pp#関連項目">一覧</a>)' +
                            '</label>' +
                            '<ul id="mp-tag-list" style="display: none;">' +
                                '<li>' +
                                    '<label for="mp-tag-template" style="width: 12ch; display: inline-block;">テンプレート</label>' +
                                    '<select id="mp-tag-template" style="width: 20ch; margin-bottom: 0.3em;">' +
                                        '<option id="mp-tag-template-meta" value="pp">{{pp}}</option>' +
                                        '<option id="mp-tag-template-dispute" value="pp-dispute">{{pp-dispute}}</option>' +
                                        '<option selected id="mp-tag-template-vandalism" value="pp-vandalism">{{pp-vandalism}}</option>' +
                                        '<option id="mp-tag-template-template" value="pp-template">{{pp-template}}</option>' +
                                        '<option id="mp-tag-template-permanent" value="pp-permanent">{{pp-permanent}}</option>' +
                                        '<option id="mp-tag-template-move" value="pp-move">{{pp-move}}</option>' +
                                        '<option id="mp-tag-template-move-vandalism" value="pp-move-vandalism">{{pp-move-vandalism}}</option>' +
                                        '<option id="mp-tag-template-move-dispute" value="pp-move-dispute">{{pp-move-dispute}}</option>' +
                                        '<option id="mp-tag-template-office" value="pp-office">{{pp-office}}</option>' +
                                        '<option id="mp-tag-template-reset" value="pp-reset">{{pp-reset}}</option>' +
                                        '<option id="mp-tag-template-office-dmca" value="pp-office-dmca">{{pp-office-dmca}}</option>' +
                                    '</select>' +
                                '</li>' +
                                '<li>' +
                                    '<label for="mp-tag-action" style="width: 12ch; display: inline-block;">種別</label>' +
                                    '<select id="mp-tag-action" style="width: 20ch; margin-bottom: 0.3em;">' +
                                        '<option selected id="mp-tag-action-blank" value=""></option>' +
                                        '<option id="mp-tag-action-edit" value="">edit</option>' +
                                        '<option id="mp-tag-action-move" value="|action=move">move</option>' +
                                        '<option id="mp-tag-action-upload" value="|action=upload">upload</option>' +
                                    '</select>' +
                                '</li>' +
                                '<li>' +
                                    '<label for="mp-tag-small" style="width: 12ch; display: inline-block;">縮小表示</label>' +
                                    '<select id="mp-tag-small" style="width: 20ch; margin-bottom: 0.3em;">' +
                                        '<option id="mp-tag-small-blank" value=""></option>' +
                                        '<option selected id="mp-tag-small-yes" value="|small=yes">small=yes</option>' +
                                        '<option hidden id="mp-tag-small-no" value="|small=no">small=no</option>' +
                                    '</select>' +
                                '</li>' +
                                '(既存の保護タグ・保護依頼タグは自動で置換されます)' +
                            '</ul>' +
                        '</fieldset>' +
                        '<fieldset id="mp-copy" style="display: none;">' +
                            '<legend>リンクのコピー</legend>' +
                            '<div id="mp-copy-vip" style="display: none;">' +
                                '<label for="mp-copy-vip-input" style="display: inline-block; width: 4ch;">VIP</label>' +
                                '<input id="mp-copy-vip-input" list="mp-copy-vip-list" placeholder="コピーする場合は検索・選択してください" style="width: 50ch;">' +
                                '<datalist id="mp-copy-vip-list"></datalist>' +
                            '</div>' +
                            '<div id="mp-copy-lta" style="display: none; margin-top: 0.3em;">' +
                                '<label for="mp-copy-lta-input" style="display: inline-block; width: 4ch;">LTA</label>' +
                                '<input id="mp-copy-lta-input" list="mp-copy-lta-list" placeholder="コピーする場合は検索・選択してください" style="width: 50ch;">' +
                                '<datalist id="mp-copy-lta-list"></datalist>' +
                            '</div>' +
                        '</fieldset>' +
                        '<label for="mp-reason-dropdown" style="width: 10ch; display: inline-block;">定型理由1</label>' +
                        '<select id="mp-reason-dropdown" style="width: auto; margin-bottom: 0.3em;">' +
                            '<optgroup label="指定なし">' +
                                '<option value="">なし</option>' +
                            '</optgroup>' +
                            protectReasons +
                        '</select><br>' +
                        '<label for="mp-reason-dropdown2" style="width: 10ch; display: inline-block;">定型理由2</label>' +
                        '<select id="mp-reason-dropdown2" style="width: auto; margin-bottom: 0.3em;">' +
                            '<optgroup label="指定なし">' +
                                '<option value="">なし</option>' +
                            '</optgroup>' +
                            protectReasons +
                        '</select><br>' +
                        '<label for="mp-reason-custom" style="width: 10ch; display: inline-block;">非定型理由</label>' +
                        '<input id="mp-reason-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
                        '<label for="mp-expiry-dropdown" style="width: 10ch; display: inline-block;">期間</label>' +
                        '<select id="mp-expiry-dropdown" style="width: 20ch; margin-bottom: 0.3em;">' +
                            '<optgroup label="デフォルトタイム">' +
                                '<option value="indefinite">無期限</option>' +
                            '</optgroup>' +
                            '<optgroup label="プリセットタイム">' +
                                '<option value="1 hour">1時間</option>' +
                                '<option value="2 hours">2時間</option>' +
                                '<option value="1 day">1日</option>' +
                                '<option value="31 hours">31時間</option>' +
                                '<option value="2 days">2日</option>' +
                                '<option value="3 days">3日</option>' +
                                '<option value="1 week">1週間</option>' +
                                '<option value="2 weeks">2週間</option>' +
                                '<option value="1 month">1ヵ月</option>' +
                                '<option value="3 months">3ヵ月</option>' +
                                '<option value="6 months">6ヵ月</option>' +
                                '<option value="1 year">1年</option>' +
                                '<option value="2 years">2年</option>' +
                                '<option value="3 years">3年</option>' +
                                '<option value="">その他の期間</option>' +
                            '</optgroup>' +
                        '</select><br>' +
                        '<label for="mp-expiry-custom" style="width: 10ch; display: inline-block;"></label>' +
                        '<input disabled id="mp-expiry-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
                        '<input type="button" id="mp-submit" value="保護を実行" style="margin-top: 0.5em;">' +
                    '</fieldset>' +
                '</form>' +
            '</div>' +
        '</div>'
    );

    // VIPリストとLTAリストを取得
    getVipList();
    getLtaList();

}

/**
 * 使用者が管理者権限を持たない場合に権限エラーを表示する関数
 */
function showUserRightError() {
    $(mw.util.$content).prop('innerHTML',
        '<div id="mp-container" style="font-size: 95%;">' +
            '<h1>権限エラー</h1>' +
            '<p>あなたには「保護設定の変更」を行う権限がありません。理由は以下の通りです:</p>' +
            '<p>' +
                'この操作は、以下のグループに属する利用者のみが実行できます: ' +
                '<a href="' + mw.util.getUrl('Wikipedia:管理者') + '">管理者</a>。' +
            '</p>' +
        '</div>'
    );
}

/**
 * コピー用のVIP名称リストを取得する関数 (「Wikipedia:進行中の荒らし行為」の全セクション名を取得し絞りこむことで取得)
 * @returns {jQuery.Promise}
 */
function getVipList() {

    // 関数を非同期化
    var def = new $.Deferred();

    // APIリクエスト
    new mw.Api().get({
        'action': 'parse',
        'page': 'Wikipedia:進行中の荒らし行為',
        'prop': 'sections',
        'formatversion': '2'
    }).then(function(res) {

        var resSect;
        if (!res || !res.parse || !(resSect = res.parse.sections)) return def.reject();
        if (resSect.length === 0) return def.resolve();

        // VIP名称とは無関係のセクションタイトルを定義
        var excludeList = [
            '記述について',
            '急を要する二段階',
            '配列',
            'ブロック等の手段',
            'このページに利用者名を加える',
            '注意と選択',
            '警告の方法',
            '未登録(匿名・IP)ユーザーの場合',
            '登録済み(ログイン)ユーザーの場合',
            '警告中',
            '関連項目'
        ];

        // 配列内のオブジェクトをループしVIP名称を取得
        var viplist = [];
        resSect.forEach(function(obj) {
            // セクション名が除外リストに含まれず、レベルが3の場合
            if ($.inArray(obj.line, excludeList) === -1 && obj.level == 3) {
                // VIP名称のため配列にリンク形式で保存
                viplist.push('[[WP:VIP#' + obj.line + ']]');
            }
        });
        if (viplist.length === 0) return def.resolve();

        // 参照用にオブジェクトに配列を保存
        MassProtect.vip = viplist;

        // フォームのドロップダウンへ反映
        viplist = viplist.map(function(vip) {
            return '<option>' + vip + '</option>'; // リンクを option タグ化
        });
        $('#mp-copy, #mp-copy-vip').css('display', 'block'); // 「リンクをコピー」の fieldset とVIPリスト用の div を表示
        $('#mp-copy-vip-list').append(viplist.join('')); // VIPリストを datalist に追加
        def.resolve();

    }).catch(function(code, err) {
        def.reject(console.log(err.error.info));
    });

    return def.promise();

}

/**
 * コピー用のLTA名称リストを取得する関数 (「LTA:」でページ名称を前方一致検索することで取得)
 * @returns {jQuery.Promise}
 */
function getLtaList() {

    // 関数を非同期化
    var def = new $.Deferred();

    // 500件を超えてマッチする場合ループが必要なためAPIリクエストを独立関数化
    var ltalist = [];
    var query = function(apcontinue) { 
        
        // この内部関数自体も非同期化
        var deferred = new $.Deferred();

        // APIリクエスト
        new mw.Api().get({
            'action': 'query',
            'list': 'allpages',
            'apprefix': 'LTA:',
            'apnamespace': '0',
            'apfilterredir': 'redirects',
            'aplimit': 'max',
            'apcontinue': apcontinue,
            'formatversion': '2'
        }).then(function(res) {

            var resPages;
            if (!res || !res.query || !(resPages = res.query.allpages)) return deferred.resolve(); // ループ中に reject が起きると困るためあえて resolve

            // ページ名が「LTA:」単体でなく、「LTA:」から始まり、「/」を含まない (サブページへのSCではない) 場合に配列にページ名を保存
            resPages.forEach(function(obj) {
                if (obj.title !== 'LTA:' && obj.title.match(/^LTA:/) && obj.title.indexOf('/') === -1) ltalist.push('[[' + obj.title + ']]');
            });

            // まだ条件に一致するページがある場合は
            if (res.continue) {
                // もう一度同様のAPIリクエストを飛ばし、値が返ってきてから終了
                query(res.continue.apcontinue).then(function() {
                    deferred.resolve();
                });
            // 条件に一致するページがこれ以上ない場合はここで終了
            } else {
                deferred.resolve();
            }

        }).catch(function(code, err) {
            deferred.resolve(console.error(err.error.info)); // ループ中に reject が起きると困るためあえて resolve
        });

        return deferred.promise();

    };

    // 実際にAPIリクエストを送信し、値が返って来たら
    query().then(function() {

        // 配列が空の場合は何もせずに終了
        if (ltalist.length === 0) return def.resolve();

        // 参照用にオブジェクトに配列を保存
        MassProtect.lta = ltalist;

        // フォームのドロップダウンへ反映
        ltalist = ltalist.map(function(ltaName) {
            return '<option>' + ltaName + '</option>'; // リンクを option タグ化
        }).sort(); // 昇順に並び替え
        $('#mp-copy, #mp-copy-lta').css('display', 'block'); // 「リンクをコピー」の fieldset とLTAリスト用の div を表示
        $('#mp-copy-lta-list').append(ltalist.join('')); // LTAリストを datalist に追加
        def.resolve();

    });

    return def.promise();

}

// VIP/LTAリストからオプションが選択されたらクリップボードへコピー
$(document).off('input', '#mp-copy-vip-input, #mp-copy-lta-input').on('input', '#mp-copy-vip-input, #mp-copy-lta-input', function() {

    var inputVal = $(this).val(),
        list = MassProtect[$(this).prop('id').substring(8, 11)]; // MassProtect.vip または MassProtect.lta

    if ($.inArray(inputVal, list) === -1) return; // インプットボックスに値が入力され、リスト内のリンクと一致しない場合はコピーせずに終了
    copyToClipboard(inputVal);

});

/**
 * 文字列をクリップボードにコピーする関数
 * @param {string} str
 */
function copyToClipboard(str) {
    var $temp = $('<textarea></textarea>');
    $('body').append($temp); // 仮のテキストフィールドをDOM上に作成
    $temp.val(str).select(); // 引数の文字列をテキストフィールドの値に設定しそれを選択
    document.execCommand('copy'); // クリップボードにコピー
    $temp.remove(); // テキストフィールドを除去
}

/**
 * 保護対象ページインプットボックス内の重複分、赤点スペース、余分な改行を除去・整形 + ページ一覧を取得する関数
 * @returns {Array} 保護対象ページタイトルの配列
 */
function getTargetPages() {

    // インプットボックスの値を取得し、赤点スペースおよび余分な改行を除去
    var $input = $('#mp-targetpages');
    var inputVal = $input.val().replace(/\u200e/g, '').trim();

    // 整形されたインプットボックスの値が空の文字列でなければ
    if (inputVal) { 

        // 値を改行で分割し配列を作成
        inputVal = inputVal.split('\n');
        // 配列要素のそれぞれから赤点スペースを除去の上アンダーバーを半角スペースに置換、前後の余分なスペースを除去
        inputVal = inputVal.map(function(item) {
            return item.replace(/\u200e/g, '').replace(/_/g, ' ').trim();
        });
        // 空の文字列と重複要素をsplit配列から除去
        inputVal = inputVal.filter(function(item, index) {
            return item !== '' && inputVal.indexOf(item) === index;
        });
        // 整形したSplit配列を使ってインプットボックスの値を更新
        $input.val(inputVal.join('\n'));

    // 値が空の文字列なら
    } else {
        // Return用に空の配列を代入
        inputVal = [];
    }

    // 保護対象のページ数表示を更新
    $('#mp-targetpages-count').text(inputVal.length);

    // 保護対象ページ群の配列をreturn
    return inputVal;

}

// 「整形」ボタンを押した際に保護対象ページインプットボックス内を整理
$(document).off('click', '#mp-targetpages-cleanup').on('click', '#mp-targetpages-cleanup', getTargetPages);

// 「ページ情報検索」の値が更新されたらページ情報を表示
var inputboxUpdateTimeout;
$(document).off('input', '#mp-targetpages-search-input').on('input', '#mp-targetpages-search-input', function() {

    /*******************************************************************************************************\
        ページ情報検索タブのHTML構造
        '<div id="mp-targetpages-search">' +
            '<div id="mp-targetpages-search-input-div">' +
                'ページ情報検索<br>' +
                '<input id="mp-targetpages-search-input">' +
                '<p id="mp-targetpages-search-links" style="display: none;">' +
                    ' (<a id="mp-targetpages-search-history" href="">履歴</a> /' +
                    ' <a id="mp-targetpages-search-log" href="">記録</a> /' +
                    ' <a id="mp-targetpages-search-log" href="">保護記録</a>)' +
                '</p>' +
            '</div>' +
            '<div id="mp-targetpages-search-status-div" style="display: none;">' +
                '<p id="mp-targetpages-search-status" style="display: inline-block;"></p>' +
            '</div>' +
            '<div id="mp-targetpages-search-missing-div" style="display: none;">' +
                '<p id="mp-targetpages-search-missing" style="display: inline-block;">未作成のページ</p>' +
            '</div>' +
        '</div>'
    \*******************************************************************************************************/

    // jQueryオブジェクトを変数に格納
    var $input = $(this),
        $links = $(this).siblings('p'),
        $statusDiv = $('#mp-targetpages-search-status-div'),
        $status = $statusDiv.children('p'),
        $missingDiv = $('#mp-targetpages-search-missing-div');

    // 既にinputイベントがトリガーされている際に、秒数カウントをリセットしタイピング終了後3.5秒で1度だけイベントをトリガー
    clearTimeout(inputboxUpdateTimeout);
    inputboxUpdateTimeout = setTimeout(function() {

        // 入力されたページ名を取得
        var pagename = $input.val().replace(/\u200e/g, '').trim();

        // インプットボックスが空白化されたら
        if (!pagename) {
            // 全てのリンクを非表示に
            $links.css('display', 'none');
            $statusDiv.css('display', 'none');
            $missingDiv.css('display', 'none');
            return;
        }

        // インプットボックスに有効な値が入力されたら
        $links.css('display', 'inline-block'); // リンクを表示し
        $links.children('a').eq(0).attr('href', mw.util.getUrl('特別:pagehistory/' + pagename)); // リンク先を更新
        $links.children('a').eq(1).attr('href', mw.util.getUrl('特別:ログ', {'page': pagename}));
        $links.children('a').eq(2).attr('href', mw.util.getUrl('特別:ログ', {'type': 'protect', 'page': pagename}));

        /**
         * 保護状態を確認するAPIリクエストの返り値を日本語に変換する関数 (e.g. '編集半保護 (YYYY-MM-DDThh:mmまで)')
         * @param {Array} arr res.query.pages.key.protection: [{type: __, level: __, expiry: __}]
         * @returns {string}
         */
        var getProtectInfo = function(arr) {

            var protectInfo = '';

            // 返り値内の配列をループ
            for (var i = 0; i < arr.length; i++) {

                // 保護種別を日本語に変換
                switch (arr[i].type) {
                    case 'create':
                        protectInfo += '作成';
                        break;
                    case 'edit':
                        protectInfo += '編集';
                        break;
                    case 'move':
                        protectInfo += '移動';
                        break;
                    case 'upload':
                        protectInfo += 'アップロード';
                    default:
                        protectInfo += '??';
                }

                // 保護レベルを日本語に変換
                switch (arr[i].level) {
                    case 'autoconfirmed':
                        protectInfo += '半保護';
                        break;
                    case 'extendedconfirmed':
                        protectInfo += '拡張半保護';
                        break;
                    case 'sysop':
                        protectInfo += '全保護';
                }

                // 保護期間を括弧書きで追加 (保護期間は無期限の場合 'infinity'、有期の場合JSON形式のタイムスタンプ: e.g. 2022-01-01T00:00:00Z)
                protectInfo += ' (' + (arr[i].expiry === 'infinity' ? '無期限' : arr[i].expiry.replace(/Z$/, '') + 'まで') + ')<br>';

            } // ループ後のprotectInfoの例: 「作成半保護 (無期限)」、「移動全保護 (2022-01-01T00:00:00まで)」など

            // この段階でprotectInfoが空の文字列なら、該当ページは保護されていない
            if (!protectInfo) protectInfo = '保護設定なし';

            // 語末に改行がある場合は除去し、文字列をreturn
            return protectInfo.replace(/<br>$/, '');

        };

        /**
         * フォーム上に検索対象ページの保護状態を表示する関数
         * @param {string} protectInfo 
         * @param {boolean} pageMissing 
         */
        var updateForm = function(protectInfo, pageMissing) {

            // 保護状態表示用 div を表示しメッセージを更新
            $statusDiv.css('display', 'block');
            $status.prop('innerHTML', protectInfo);

            // ページが未作成の場合のみ「未作成のページ」と表示
            if (pageMissing) {
                $missingDiv.css('display', 'block');
            } else {
                $missingDiv.css('display', 'none');
            }

        };

        // ページの保護状態を取得
        new mw.Api().get({
            'action': 'query',
            'titles': pagename,
            'prop': 'info',
            'inprop': 'protection',
            'formatversion': '2'
        }).then(function(res) {
            var resPages, pageMissing, protectInfo;
            if (!res || !res.query || !(resPages = res.query.pages)) return;
            for (var page in resPages) { // 1ページずつのためループは1度のみ
                pageMissing = resPages[page].missing;
                protectInfo = getProtectInfo(resPages[page].protection); // 加算不要
            }
            updateForm(protectInfo, pageMissing);
        });

    }, 350);

});

// 保護レベルドロップダウンの値が変わった際にトリガー (作成保護と編集保護・移動保護は同時指定不可)
$(document).off('change', '.mp-protectionlevel').on('change', '.mp-protectionlevel', function() {

    var create = '#mp-protectionlevel-create',
        edit = '#mp-protectionlevel-edit',
        move = '#mp-protectionlevel-move',
        level = $(this).children('option').filter(':selected').val(), // '', 'all', 'autoconfirmed', 'extendedconfirmed', 'sysop'
        id = '#' + $(this).prop('id');

    switch (id) {
        case create:

            // 選択されたのが「変更なし」以外なら
            if (level) {
                // 編集保護と移動保護のドロップダウンを無効化し、値を「変更なし」にリセット
                $('.mp-protectionlevel').not(create).prop('disabled', true).children('.mp-protectionlevel-none').prop('selected', true);
            // 選択されたのが「変更なし」なら
            } else {
                // 編集保護と移動保護のドロップダウンを有効化
                $('.mp-protectionlevel').not(create).prop('disabled', false);
            }
            break;

        case edit:
        case move:

            // editが変更された場合はmoveの選択値を、moveが変更された場合はeditの選択値を取得
            var levelInTheOther = $(id === edit ? move : edit).children('option').filter(':selected').val();

            // 選択されたのが「変更なし」以外なら
            if (level) {
                // 作成保護ドロップダウンを無効化し、値を「変更なし」にリセット
                $('#mp-protectionlevel-create').prop('disabled', true).children('.mp-protectionlevel-none').prop('selected', true);
            // 選択されたのが「変更なし」なら
            } else {
                // もう一方も「変更なし」の場合のみ作成保護ドロップダウンを有効化
                if (!levelInTheOther) $('#mp-protectionlevel-create').prop('disabled', false);
            }
    }

});

// 「保護タグを自動添付」がチェックされたらドロップダウンを表示
$(document).off('change', '#mp-tag').on('change', '#mp-tag', function() {

    // チェックされたら
    if ($(this).is(':checked')) {
        // ドロップダウンを表示
        $('#mp-tag-list').css('display', 'block');
    // チェックが外されたら
    } else {
        // ドロップダウンを隠す
        $('#mp-tag-list').css('display', 'none');
    }

});

// 保護タグドロップダウンのオプションコントロール
$(document).off('change', '#mp-tag-template').on('change', '#mp-tag-template', function() {

    /********************************************************\
        [[Template:Pp]]の引数一覧
        template         action                  small
        pp-(meta)        edit, move, upload      def=no
        dispute          edit, move, upload      X
        vandalism        edit, move              def=no
        (semi-indef)     ----------              ---------
        template         X                       def=yes
        permanent        X                       def=yes
        move             X                       def=no
        move-vandalism   X                       def=no
        move-dispute     X                       X
        office           X                       X
        reset            X                       X
        office-dmca      X                       X
    \********************************************************/

    // セレクタ
    var $action = $('#mp-tag-action'),
        $actionBlank = $('#mp-tag-action-blank'),
        $actionEdit = $('#mp-tag-action-edit'), // 未使用
        $actionMove = $('#mp-tag-action-move'), // 未使用
        $actionUpload = $('#mp-tag-action-upload'),
        $small = $('#mp-tag-small'),
        $smallBlank = $('#mp-tag-small-blank'),
        $smallYes = $('#mp-tag-small-yes'),
        $smallNo = $('#mp-tag-small-no');

    // リセット (全て表示 + actionとsmallは空オプションを選択)
    $action.prop('disabled', false);
    $action.children('option').prop('hidden', false);
    $small.prop('disabled', false);
    $small.children('option').prop('hidden', false);
    $actionBlank.prop('selected', true);
    $smallBlank.prop('selected', true);

    // オプションコントロール
    switch ($(this).children('option').filter(':selected').prop('id')) {
        case 'mp-tag-template-meta':
            $smallNo.prop('hidden', true);
            break;
        case 'mp-tag-template-dispute':
            $small.prop('disabled', true);
            break;
        case 'mp-tag-template-vandalism':
            $actionUpload.prop('hidden', true);
            $smallNo.prop('hidden', true);
            $smallYes.prop('selected', true); // {{pp-vamdalism}} は一番よく使うため {{pp-vamdalism|small=yes}} をデフォルトに
            break;
        case 'mp-tag-template-template':
        case 'mp-tag-template-permanent':
            $action.prop('disabled', true);
            $smallYes.prop('hidden', true);
            break;
        case 'mp-tag-template-move':
        case 'mp-tag-template-move-vandalism':
            $action.prop('disabled', true);
            $smallNo.prop('hidden', true);
            break;
        case 'mp-tag-template-move-dispute':
        case 'mp-tag-template-office':
        case 'mp-tag-template-reset':
        case 'mp-tag-template-office-dmca':
            $action.prop('disabled', true);
            $small.prop('disabled', true);
    }

});

// カスタム期間指定インプットボックスをドロップダウンの値によって有効化・無効化
$(document).off('change', '#mp-expiry-dropdown').on('change', '#mp-expiry-dropdown', function() {

    // 「その他の期間」が選択されたら
    if ($(this).val() === '') {
        // インプットボックスを有効化
        $('#mp-expiry-custom').prop('disabled', false);
    // プリセット期間が選択されたら
    } else {
        // インプットボックスを無効化
        $('#mp-expiry-custom').val('').prop('disabled', true);
    }

});

/**
 * action=protectの 'protections' パラメータを取得する関数 (書式: action=level (e.g. edit=sysop))
 * @returns {string}
 */
function getProtectionLevelParam() {

    var param = [],
        actions = ['create=', 'edit=', 'move='];

    // mp-protectionlevelクラスを持つ <select> タグをループ
    $('.mp-protectionlevel').each(function(i) {
        var level = $(this).children('option').filter(':selected').val(); // '', 'all', 'autoconfirmed', 'extendedconfirmed', 'sysop'
        if (level) param.push(actions[i] + level);
    });

    // param配列に要素がなければ
    if (param.length === 0) {
        // undefined を return (保護レベルドロップダウンは全て「変更なし」)
        return;
    // param配列に要素があれば
    } else {
        // 配列を文字列に変換し return (例: ['edit=sysop', 'move=sysop'] => 'edit=sysop|move=sysop') 
        return param.join('|');
    }

}

/**
 * 保護実行前の最終確認 + 必要情報を取得する関数
 * @returns {{pages: Array, levels: string, expiry: string, reason: string, cascade: boolean, watchlist: string, tag: string}}
 */
function editPrep() {

    // 必須フィールドの入力チェック
    var targetPages = getTargetPages();
    if (targetPages.length === 0) return alert('保護対象ページが入力されていません');
    var protectionLevel = getProtectionLevelParam();
    if (!protectionLevel) return alert('保護レベルが設定されていません');

    // 保護理由を取得 (空白の場合警告)
    var reasonOption = $('#mp-reason-dropdown').find('option').filter(':selected').val(),
        reasonOption2 = $('#mp-reason-dropdown2').find('option').filter(':selected').val(),
        reasonCustom = $('#mp-reason-custom').val().replace(/\u200e/g, '').trim();
    if (!(reasonOption + reasonOption2 + reasonCustom)) {
        if (!confirm('保護理由が入力されていません。このまま実行しますか?')) return;
    }
    reasonOption = reasonOption ? reasonOption + ': ' : ''; // 保護理由ドロップダウンの値が空でなければ語末に ': ' を追加
    reasonOption2 = reasonOption2 ? reasonOption2 + ': ' : '';
    var reason = (reasonOption + reasonOption2 + reasonCustom).replace(/: $/, ''); // 全ての理由を組み合わせ、語末に ': ' がある場合は除去

    // 保護タグを取得
    var tag = '';
    if ($('#mp-tag').is(':checked') && protectionLevel.indexOf('create=') === -1) { // 「保護タグを自動添付」がチェックされており、作成保護でなければ
        var tagName = $('#mp-tag-template').children('option').filter(':selected').val(),
            tagAction = $('#mp-tag-action').children('option').filter(':selected').val(),
            tagSmall = $('#mp-tag-small').children('option').filter(':selected').val();
        tag = (tagName + tagAction + tagSmall) ? '{{' + tagName + tagAction + tagSmall + '}}' : '';
    }

    // 保護期間を取得
    var expiryOption = $('#mp-expiry-dropdown').find('option').filter(':selected').val();
    var expiry = expiryOption ? expiryOption : $('#mp-expiry-custom').val().replace(/\u200e/g, '').trim();

    // オブジェクトを return
    return {
        'pages': targetPages,
        'levels': protectionLevel,
        'expiry': expiry,
        'reason': reason,
        'cascade': $('#mp-cascade').prop('checked'),
        'watchlist': $('#mp-watchlist').is(':checked') ? 'watch' : 'nochange',
        'tag': tag
    };

}

// ボタンクリック時に一括保護を実行
$(document).off('click', '#mp-submit').on('click', '#mp-submit', function() {

    // 保護設定を取得
    var ep = editPrep();
    if (!ep) return;

    // 「保護を実行」ボタンを無効化
    $(this).prop('disabled', true);

    // 進捗状態を表示
    $('#mp-protection-settings').append( // 「保護を実行」ボタンの下にフィールドを作成
        '<fieldset id="mp-progress">' +
            '<legend>進捗</legend>' +
        '</fieldset>'
    );
    var spinner = '<img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; max-height: 100%; border: 0;">';
    ep.pages.forEach(function(page) { // 保護対象ページの全てをループ
        $('#mp-progress').append(
            '<p>' +
                '<span class="mp-progress-page" style="font-weight: bold;">' + page + '</span> ' +
                '保護: <span class="mp-progress-protect">' + spinner + '</span> ' +
                (ep.tag ? 'タグ: <span class="mp-progress-tag">' + spinner + '</span>' : '') +
            '</p>'
        );
    });

    /**
     * 画面の最下部に追加される進捗状態が見える位置までスクロールダウンする関数
     * @param {number} pos スクロール位置のY座標
     */
    var scrollDown = function(pos) {
        var i = window.scrollY;
        var int = setInterval(function() {
            window.scrollTo(0, i);
            i += 20;
            if (i >= pos) clearInterval(int);
        }, 10);
    };
    scrollDown($('#mp-progress').prop('offsetTop'));

    // 保護を実行
    MassProtect.template = { // ウィキテキストからテンプレートを抽出する際に使用
        'names': ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // この名前のテンプレートを抽出
        'prefixes': ['pp-'] // この文字から始まるテンプレートを抽出
    };
    var protectCnt = 0;
    protectPages();

    /**
     * ページの保護を実行する関数 (意図的にコールバックで順番に実行)
     */
    function protectPages() {

        // 保護対象ページのタイトルを取得
        var pagetitle = ep.pages[protectCnt];

        // APIリクエストのパラメータを設定
        var params = {
            'action': 'protect',
            'title': pagetitle,
            'protections': ep.levels,
            'expiry': ep.expiry,
            'reason': ep.reason,
            'watchlist': ep.watchlist,
            'token': mw.user.tokens.get('csrfToken'),
            'format': 'json'
        };
        if (ep.cascade) params.cascade = true; // なぜか false で保護リクエストを投げてもカスケード保護されるため true の場合のみパラメータを追加

        // 保護リクエストを送信
        $.post(mw.util.wikiScript('api'), params, function(res) {

            var protected;

            // 何らかの理由で保護に失敗
            if (res.error) {
                // 進捗を「保護: 失敗 (エラーの詳細)」に更新
                updateProgress(pagetitle, 'protect', res.error.info);
            // 不明なエラー
            } else if (!res || !res.protect) {
                // 進捗を「保護: 失敗 (不明なエラー)」に更新
                updateProgress(pagetitle, 'protect', false);
            // 成功
            } else {
                // 進捗を「保護: 成功」に更新
                updateProgress(pagetitle, 'protect', true);
                protected = true;
            }

            // 次の保護に移行するために保護対象ページ配列のインデックスを加算 (pasteTagの前に更新する必要があるためこの位置での更新必須)
            protectCnt++;

            // タグ添付をする場合は
            if (ep.tag) {
                // 保護に成功した場合のみ
                if (protected) {
                    // タグ添付プロセスに移行
                    return pasteTag(pagetitle);
                // 保護に失敗している場合は
                } else {
                    // 進捗を「タグ: 中止 (保護失敗)」に更新
                    updateProgress(pagetitle, 'tag', '保護失敗', true);
                }
            }

            // タグ添付をしない、もしくは保護に失敗した場合は
            if (ep.pages[protectCnt]) protectPages(); // 次の保護プロセスに移行

        });

    }

    /**
     * タグ添付を実行する関数
     * @param {string} pagetitle 
     */
    function pasteTag(pagetitle) {

        // タグ添付対象がモジュール名前空間のページなら
        if (pagetitle.match(/^(?:モジュール|[Mm]odule):/)) {
            // 進捗を「中止 (モジュール)」にして
            updateProgress(pagetitle, 'tag', 'モジュール', true);
            // タグ添付をせずに次の保護処理に移行
            if (ep.pages[protectCnt]) protectPages();
            return;
        }

        // タグ添付対象ページの最新版を取得
        $.get(mw.util.wikiScript('api'), {
            'action': 'query',
            'titles': pagetitle,
            'prop': 'info|revisions',
            'rvprop': 'content',
            'rvslots': 'main',
            'curtimestamp': 1,
            'format': 'json'
        }, function(res) {

            var resPages;

            // APIリクエストの返り値にページの編集に必要な情報がなければ
            if (!res || !res.query || !(resPages = res.query.pages)) {
                // 進捗を「タグ: 失敗 (通信エラー)」に更新し、次の保護処理に移行
                updateProgress(pagetitle, 'tag', '通信エラー', true);
                if (ep.pages[protectCnt]) protectPages();
                return;
            }

            // ページの編集に必要な情報を取得
            var curtimestamp = res.curtimestamp; // 最新版を取りに行った時間のタイムスタンプ (編集競合対策)
            for (var key in resPages) { // キーはランダムナンバー (ページID)

                var resObj = resPages[key];
                var isRedirect = resObj.redirect === ''; // リダイレクトか否か
                var basetimestamp = resObj.touched; // 最新版のタイムスタンプ (編集競合対策)
                var content = resObj.revisions[0].slots.main['*']; // ページ内容
                var originalContent = JSON.parse(JSON.stringify(content)); // 後で比較する用の魚拓

                // ページ内に除去が必要なテンプレートがあれば除去
                var templates = findTemplates(content, MassProtect.template.names, MassProtect.template.prefixes); // 除去対象のテンプレートを取得
                if (templates.length !== 0) { // 除去対象テンプレートがあれば

                    // 正規表現内で使用するため特殊文字をエスケープ
                    templates = templates.map(function(item) {
                        return mw.util.escapeRegExp(item);
                    });

                    // ページ内の「{{TEMPLATE}}\n」を空の文字列に置換
                    var regex = new RegExp('(?:' + templates.join('|') + ')[^\\S\\n\\r]*\\n?', 'g');
                    content = content.replace(regex, '');

                }

                // 冒頭に保護タグを挿入 (リダイレクトとテンプレート名前空間のページは別処理が必要)
                if (isRedirect) { // リダイレクトなら

                    // リダイレクトのリンクの前に改行があることがあるのでそれを除去
                    content = content.trim();

                    // ページ内容が転送リンクの一行のみなら
                    if (content.indexOf('\n') === -1) {
                        // 2行目にタグを挿入
                        content += '\n' + ep.tag; 
                    // 複数行あれば
                    } else {
                        // 1つ目の改行の後にタグを挿入
                        content = content.replace('\n', '\n' + ep.tag + '\n'); 
                    }

                } else { // リダイレクトでなければ

                    // Template名前空間のcssページなら
                    if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) {
                        // コメントアウトしたタグを挿入
                        content = '/*' + ep.tag + '*/' + '\n' + content;
                    // Cssページでなければ
                    } else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/)) {
                        // トランスクルードさせずにタグを挿入
                        content = '<noinclude>' + ep.tag + '</noinclude>' + content;
                    // Template名前空間以外のページなら
                    } else {
                        // 単純に1行目にタグを挿入
                        content = ep.tag + '\n' + content;
                    }

                }

            }

            // 編集前と編集後で同じ内容になる場合は編集しない
            if (content === originalContent) {
                updateProgress(pagetitle, 'tag', '同じ内容', true);
                if (ep.pages[protectCnt]) protectPages();
                return;
            }

            // ページの編集
            $.post(mw.util.wikiScript('api'), {
                'action': 'edit',
                'title': pagetitle,
                'text': content,
                'summary': ep.tag + ' (MassProtect)',
                'minor': 1,
                'basetimestamp': basetimestamp,
                'starttimestamp': curtimestamp,
                'nocreate': 1,
                'watchlist': 'nochange',
                'token': mw.user.tokens.get('csrfToken'),
                'format': 'json'
            }, function(res2) {

                // 何らかの理由で編集に失敗
                if (res2.error) {
                    // 進捗を「タグ: 失敗 (エラーの詳細)」に更新
                    updateProgress(pagetitle, 'tag', res2.error.info);
                // 不明なエラー
                } else if (!res2 || !res2.edit) {
                    // 進捗を「タグ: 失敗 (不明なエラー)」に更新
                    updateProgress(pagetitle, 'tag', false);
                // それ以外
                } else {
                    // 成功
                    if (res2.edit.result === 'Success') {
                        // 進捗を「タグ: 成功」に更新
                        updateProgress(pagetitle, 'tag', true);
                    // 失敗 (res2.edit.result === 'Failure')、キャプチャの手動入力が求められた時などにこの値が返ってくるが、管理者は無関係のため通常はこのコードには辿り着かない
                    } else {
                        // 進捗を「タグ: 失敗 (Failure)」に更新
                        updateProgress(pagetitle, 'tag', res2.edit.result);
                    }
                }

                // まだ保護対象ページが残っている場合は次の保護プロセスに移行
                if (ep.pages[protectCnt]) protectPages();

            });

        });
    }

});

MassProtect.protect = { // 一括保護の進捗表示に使用
    'done': '<span class="mp-resolved" style="color: MediumSeaGreen;">完了</span>',
    'failed': '<span class="mp-resolved" style="color: MediumVioletRed;">失敗', // <span> タグは意図的に閉じない
    'cancelled': '<span class="mp-resolved">中止' // <span> タグは意図的に閉じない
};

/**
 * 一括保護の進捗状態を更新する関数
 * @param {string} page 保護対象ページ名
 * @param {string} type 'protect' または 'tag'
 * @param {boolean|string} result 成功したらtrue、不明なエラーが発生したらfalse、理由の分かるエラーが発生したらその詳細 (または「中止 (XXX)」のXXXにあたる文字列)
 * @param {boolean} [cancelled] trueの場合「中止 (XXX)」を表示
 */
function updateProgress(page, type, result, cancelled) {

    /********************************************************************************************\
        進捗状態表示タブのHTML構造
        '<fieldset id="mp-progress">' +
            '<legend>進捗</legend>' +
            '<p>' +
                `<span class="mp-progress-page">${page}</span> ` +
                `保護: <span class="mp-progress-protect">${spinner}</span> ` +
                `タグ: <span class="mp-progress-tag">${spinner}</span>` +
            '</p>' +
        '</fieldset>'
    \********************************************************************************************/

    // 更新対象のセレクタ
    var $progress = $('.mp-progress-page').filter(function() {  // mp-progress-page のクラス属性を持っている <span> タグを
                        return $(this).text() === page;         // 特定のページ名を text として持つものに絞り込み
                    }).siblings('.mp-progress-' + type);        // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ

    // 更新
    switch (result) {
        case true:
            $progress.prop('outerHTML', MassProtect.protect.done);
            break;
        case false:
            $progress.prop('outerHTML', MassProtect.protect.failed + ' (不明なエラー)</span>');
            break;
        default:
            if (cancelled) {
                $progress.prop('outerHTML', MassProtect.protect.cancelled + ' (' + result + ')</span>');
            } else {
                $progress.prop('outerHTML', MassProtect.protect.failed + ' (' + result + ')</span>');
            }
    }
}

/** 
 * ウィキテキストからテンプレートを抽出する関数
 * @param {string} wikitext テンプレートを検索する対象のウィキテキスト
 * @param {string|Array} [templateName] テンプレート名で絞り込み
 * @param {string|Array} [templatePrefix] テンプレートの接頭辞で絞り込み
 * @returns {Array}
 */
function findTemplates(wikitext, templateName, templatePrefix) {

    // 原文を '{{' で split
    var tempInnerContent = wikitext.split('{{'); // tempInnerContent[0] は必ずテンプレートとは無関係の文字列
    if (tempInnerContent.length === 0) return []; // テンプレートがない場合は空の配列を return

    // テンプレートを抽出
    var templates = [], nest = [];
    for (var i = 1; i < tempInnerContent.length; i++) { // split配列をループ

        // 即時関数で for のブロックスコープを作成
        (function() {

            var tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // 配列要素内の '}}' の数
            var temp = ''; // 値をよけておく用

            // 配列要素内に '}}' がない (= 他のテンプレートをネストしている)
            if (tempTailCnt === 0) {

                nest.push(i); // 配列要素のインデックスを記録

            // 配列要素内に '}}' が1つある (= '}}' の左側の文字列がテンプレートの引数群)
            } else if (tempTailCnt === 1) {

                temp = '{{' + tempInnerContent[i].split('}}')[0] + '}}';
                if ($.inArray(temp, templates) === -1) templates.push(temp);

            // 配列要素内に '}}' が2つ以上ある (例: 'TL2|...}}...}}'; = 他のテンプレートがネストされている)
            } else {

                for (var j = 0; j < tempTailCnt; j++) {

                    if (j === 0) { // 一番内側のテンプレート

                        temp = '{{' + tempInnerContent[i].split('}}')[j] + '}}'; // 配列要素内に '}}' が1つある時と同じ
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    } else { // 多重にネストされたテンプレート

                        var elNum = nest[nest.length -1]; // ネストの始まり部分の配列要素インデックス
                        nest.pop(); // ネストインデックスは1度参照したら用済み
                        var nestedTempInnerContent = tempInnerContent[i].split('}}'); // '}}' でsplitした新しい配列を作成

                        temp = '{{' + tempInnerContent.slice(elNum, i).join('{{') + '{{' + nestedTempInnerContent.slice(0, j + 1).join('}}') + '}}';
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    }

                }

            }

        })();

    } // ループ終了時には templates 配列に wikitext 内の全てのテンプレートが格納されている

    // テンプレートの絞り込みが不要、またはウィキテキスト内にテンプレートがない場合はここで終了
    if ((!templateName && !templatePrefix) || templates.length === 0) return templates;

    // 関数が受け取った引数が string の場合 Array に変換 (このスクリプト内では配列しか渡されないがこの関数がコピペ使用された時用)
    if (templateName && typeof templateName === 'string') templateName = [templateName];
    if (templatePrefix && typeof templatePrefix === 'string') templatePrefix = [templatePrefix];

    /**
     * テンプレートの1文字目は大文字小文字の区別がないためその正規表現を作る関数
     * @param {string} str 
     * @returns {string} [Xx]
     */
    var caseInsensitiveFirstLetter = function(str) {
        return '[' + str.substring(0, 1).toUpperCase() + str.substring(0, 1).toLowerCase() + ']';
    };

    // テンプレートの絞り込み用の正規表現を作成
    var names = [], prefixes = [];
    if (templateName) {
        for (var i = 0; i < templateName.length; i++) {
            names.push(caseInsensitiveFirstLetter(templateName[i]) + mw.util.escapeRegExp(templateName[i].substring(1)));
        }
        var templateNameRegExp = new RegExp('^(' + names.join('|') + ')$');
    }
    if (templatePrefix) {
        for (var i = 0; i < templatePrefix.length; i++) {
            prefixes.push(caseInsensitiveFirstLetter(templatePrefix[i]) + mw.util.escapeRegExp(templatePrefix[i].substring(1)));
        }
        var templatePrefixRegExp = new RegExp('^(' + prefixes.join('|') + ')');
    }

    // テンプレートの絞り込み
    templates = templates.filter(function(item) {
        var name = item.match(/^\{{2}\s*([^\|\{\}\n]+)/)[1].trim(); // {{ TEMPLATENAME | ... }} の TEMPLATENAME を抽出
        if (templateName && templatePrefix) {
            return name.match(templateNameRegExp) || name.match(templatePrefixRegExp);
        } else if (templateName) {
            return name.match(templateNameRegExp);
        } else if (templatePrefix) {
            return name.match(templatePrefixRegExp);
        }
    });

    return templates;

}

// ****************************************************************************

})(mediaWiki, jQuery);
//</nowiki>