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

削除された内容 追加された内容
「コメントアウトされたテンプレートは抽出時に無視」にバグがあったため修正
とりあえずWikipedia名前空間内に位置するページのサブページに保護タグを添付する場合は <noinclude> で囲むように変更 (プロジェクト‐ノート:ウィキ技術部#参照読み込み対策について)
20行目: 20行目:
(function(mw, $) { // 関数スコープを作成
(function(mw, $) { // 関数スコープを作成


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


// 関数間で共有する変数を格納するためのオブジェクト
// 全般設定および関数間で共有する変数を格納するためのオブジェクト (null 部はスクリプト内で動的に使用するため設定禁止)
var MassProtect = {};
var MassProtect = {
vip: null,
lta: null,
// 以下のテンプレートは、保護タグの自動添付設定がオンの場合除去・置換する
template: {
names: ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // テンプレート名称完全一致
prefixes: ['pp-'] // テンプレート名称前方一致
}
};

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


// ページ名が「特別:一括保護」、「特別:MassProtect」または「特別:MP」の場合、一括保護フォーム生成
// ページ名が「特別:一括保護」、「特別:MassProtect」または「特別:MP」の場合、一括保護フォーム生成
912行目: 922行目:


// 保護を実行
// 保護を実行
MassProtect.template = { // ウィキテキストからテンプレートを抽出する際に使用
'names': ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // この名前のテンプレートを抽出
'prefixes': ['pp-'] // この文字から始まるテンプレートを抽出
};
var protectCnt = 0;
var protectCnt = 0;
protectPages();
protectPages();
1,014行目: 1,020行目:
if (!res || !res.query || !(resPages = res.query.pages)) {
if (!res || !res.query || !(resPages = res.query.pages)) {
// 進捗を「タグ: 失敗 (通信エラー)」に更新し、次の保護処理に移行
// 進捗を「タグ: 失敗 (通信エラー)」に更新し、次の保護処理に移行
updateProgress(pagetitle, 'tag', '通信エラー', true);
updateProgress(pagetitle, 'tag', '通信エラー');
if (ep.pages[protectCnt]) protectPages();
if (ep.pages[protectCnt]) protectPages();
return;
return;
1,041行目: 1,047行目:
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, '');
content = content.replace(regex, '');

// テンプレートを除去後、文字列を含まない noinclude タグなどがある場合は除去
content = content.replace(/<noinclude>(?:\s)*?<\/noinclude>[^\S\n\r]*\n?/gm, '').replace(/\/\*(?:\s)*?\*\/[^\S\n\r]*\n?/gm, '');


}
}
1,061行目: 1,070行目:


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

var linebreak;


// Template名前空間のcssページなら
// Template名前空間のcssページなら
if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) {
if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) {
// コメントアウトしたタグを挿入
// コメントアウトしたタグを挿入
content = '/*' + ep.tag + '*/' + '\n' + content;
content = '/* ' + ep.tag + ' */' + '\n' + content;
// Cssページでなければ
// それ以外のTemplate名前空間のページ、またはWikipedia名前空間内に位置するページのサブページの場合
} else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/)) {
} else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/) || (linebreak = needNoinclude(pagetitle))) {
// トランスクルードさせずにタグを挿入
// トランスクルードさせずにタグを挿入
content = '<noinclude>' + ep.tag + '</noinclude>' + content;
content = '<noinclude>' + ep.tag + '</noinclude>' + (linebreak ? '\n' : '') + content;
// Template名前空間以外のページなら
// Template名前空間以外のページなら
} else {
} else {
1,131行目: 1,142行目:
}
}


/**
});
* あるページに保護タグを添付する際に <noinclude> で囲むべきかを判別する関数
* @param {string} pagetitle
* @returns {boolean} 対象ページがWikipedia名前空間にあり、サブページの場合のみtrue
*/
function needNoinclude(pagetitle) {
return pagetitle.match(/^Wikipedia:[^#\/]+?\/[^#]+/i) ? true : false;
}


});
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> タグは意図的に閉じない
};


/**
/**
1,164行目: 1,178行目:
return $(this).text() === page; // 特定のページ名を text として持つものに絞り込み
return $(this).text() === page; // 特定のページ名を text として持つものに絞り込み
}).siblings('.mp-progress-' + type); // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ
}).siblings('.mp-progress-' + type); // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ

// セレクタの置換用メッセージ
var progressMsg = {
done: '<span class="mp-resolved" style="color: MediumSeaGreen;">完了</span>',
failed: '<span class="mp-resolved" style="color: MediumVioletRed;">失敗 (' + result + ')</span>',
cancelled: '<span class="mp-resolved">中止 (' + result + ')</span>'
};


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

}
}



2022年7月8日 (金) 23:59時点における版

/*********************************************************************************************\
    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, $) { // 関数スコープを作成

// ************************************** 設定 **************************************

// 全般設定および関数間で共有する変数を格納するためのオブジェクト (null 部はスクリプト内で動的に使用するため設定禁止)
var MassProtect = {
    vip: null,
    lta: null,
    // 以下のテンプレートは、保護タグの自動添付設定がオンの場合除去・置換する
    template: {
        names: ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // テンプレート名称完全一致
        prefixes: ['pp-'] // テンプレート名称前方一致
    }
};

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

// ページ名が「特別:一括保護」、「特別: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>' +
                            '<label for="mp-copy-note" style="display: inline-block; width: 4ch;"></label>' +
                            '<p id="mp-copy-note" style="display: inline-block;">(選択のみで自動的にコピーされます)</label>' +
                        '</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;" placeholder="非定型の保護理由 (自由記述)"><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.filter(function(obj) {
            return $.inArray(obj.line, excludeList) === -1 && obj.level == 3; // セクション名が除外リストに含まれず、レベルが3のものに絞り込み
        }).map(function(obj) {
            return '[[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)); // あえて resolve
        });

        return deferred.promise();

    };

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

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

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

        // フォームのドロップダウンへ反映
        ltalist = ltalist.map(function(lta) {
            return '<option>' + lta + '</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();
        }).filter(function(item, index) { // 空の文字列と重複要素を配列から除去
            return item !== '' && inputVal.indexOf(item) === index;
        });

        // 整形したSplit配列を使ってインプットボックスの値を更新
        $input.val(inputVal.join('\n'));

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

    // 保護対象のページ数表示を更新
    $('#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'));

    // 保護を実行
    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 succeeded;

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

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

            // タグ添付をする場合は
            if (ep.tag) {
                // 保護に成功した場合のみ
                if (succeeded) {
                    // タグ添付プロセスに移行
                    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', '通信エラー');
                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, '');

                    // テンプレートを除去後、文字列を含まない noinclude タグなどがある場合は除去
                    content = content.replace(/<noinclude>(?:\s)*?<\/noinclude>[^\S\n\r]*\n?/gm, '').replace(/\/\*(?:\s)*?\*\/[^\S\n\r]*\n?/gm, '');

                }

                // 冒頭に保護タグを挿入 (リダイレクトとテンプレート名前空間のページは別処理が必要)
                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 { // リダイレクトでなければ

                    var linebreak;

                    // Template名前空間のcssページなら
                    if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) {
                        // コメントアウトしたタグを挿入
                        content = '/* ' + ep.tag + ' */' + '\n' + content;
                    // それ以外のTemplate名前空間のページ、またはWikipedia名前空間内に位置するページのサブページの場合
                    } else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/) || (linebreak = needNoinclude(pagetitle))) {
                        // トランスクルードさせずにタグを挿入
                        content = '<noinclude>' + ep.tag + '</noinclude>' + (linebreak ? '\n' : '') +  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();

            });

        });
    }

    /**
     * あるページに保護タグを添付する際に <noinclude> で囲むべきかを判別する関数
     * @param {string} pagetitle 
     * @returns {boolean} 対象ページがWikipedia名前空間にあり、サブページの場合のみtrue
     */
    function needNoinclude(pagetitle) {
        return pagetitle.match(/^Wikipedia:[^#\/]+?\/[^#]+/i) ? true : false;
    }

});

/**
 * 一括保護の進捗状態を更新する関数
 * @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> が更新対象のセレクタ

    // セレクタの置換用メッセージ
    var progressMsg = {
        done: '<span class="mp-resolved" style="color: MediumSeaGreen;">完了</span>',
        failed: '<span class="mp-resolved" style="color: MediumVioletRed;">失敗 (' + result + ')</span>',
        cancelled: '<span class="mp-resolved">中止 (' + result + ')</span>'
    };

    // 更新
    switch (result) {
        case true:
            $progress.prop('outerHTML', progressMsg.done);
            break;
        case false:
            $progress.prop('outerHTML', progressMsg.failed);
            break;
        default:
            if (cancelled) {
                $progress.prop('outerHTML', progressMsg.cancelled);
            } else {
                $progress.prop('outerHTML', progressMsg.failed);
            }
    }

}

/** 
 * ウィキテキストからテンプレートを抽出する関数
 * @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 内の全てのテンプレートが格納されている

    // コメントアウトされたテンプレートなど、トランスクルード状態にないテンプレートを配列から除去
    var co = extractCommentOuts(wikitext);
    if (co) {
        co.forEach(function(item) {
            templates = templates.filter(function(template) {
                return item.indexOf(template) === -1; // コメントアウト部に含まれないテンプレートのみを残す
            });
        });
    }

    // テンプレートの絞り込みが不要、またはウィキテキスト内にテンプレートがない場合はここで終了
    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;

}

/**
 * <!-- -->, <nowiki />, <pre />, <syntaxhighlight />, and <source /> に囲まれた文字列をウィキテキストから抽出
 * @param {string} wikitext 
 * @returns {Array|null} 
 */
function extractCommentOuts(wikitext) {
    return wikitext.match(/(<!--[\s\S]*?-->|<nowiki>[\s\S]*?<\/nowiki>|<pre[\s\S]*?<\/pre>|<syntaxhighlight[\s\S]*?<\/syntaxhighlight>|<source[\s\S]*?<\/source>)/gm);
};

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

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