MediaWiki:Gadget-twinkleprotect.js

//

(function() {

/*

****************************************

*** twinkleprotect.js: Protect/RPP module

****************************************

* Mode of invocation: Tab ("PP"/"RPP")

* Active on: Non-special, non-MediaWiki pages

*/

// Note: a lot of code in this module is re-used/called by batchprotect.

Twinkle.protect = function twinkleprotect() {

if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgNamespaceNumber') === 8) {

return;

}

Twinkle.addPortletLink(Twinkle.protect.callback, Morebits.userIsSysop ? 'PP' : 'RPP', 'tw-rpp',

Morebits.userIsSysop ? 'Protect page' : 'Request page protection');

};

Twinkle.protect.callback = function twinkleprotectCallback() {

const Window = new Morebits.SimpleWindow(620, 530);

Window.setTitle(Morebits.userIsSysop ? 'Apply, request or tag page protection' : 'Request or tag page protection');

Window.setScriptName('Twinkle');

Window.addFooterLink('Protection templates', 'Template:Protection templates');

Window.addFooterLink('Protection policy', 'WP:PROT');

Window.addFooterLink('Twinkle help', 'WP:TW/DOC#protect');

Window.addFooterLink('Give feedback', 'WT:TW');

const form = new Morebits.QuickForm(Twinkle.protect.callback.evaluate);

const actionfield = form.append({

type: 'field',

label: 'Type of action'

});

if (Morebits.userIsSysop) {

actionfield.append({

type: 'radio',

name: 'actiontype',

event: Twinkle.protect.callback.changeAction,

list: [

{

label: 'Protect page',

value: 'protect',

tooltip: 'Apply actual protection to the page.',

checked: true

}

]

});

}

actionfield.append({

type: 'radio',

name: 'actiontype',

event: Twinkle.protect.callback.changeAction,

list: [

{

label: 'Request page protection',

value: 'request',

tooltip: 'If you want to request protection via WP:RPP' + (Morebits.userIsSysop ? ' instead of doing the protection by yourself.' : '.'),

checked: !Morebits.userIsSysop

},

{

label: 'Tag page with protection template',

value: 'tag',

tooltip: 'If the protecting admin forgot to apply a protection template, or you have just protected the page without tagging, you can use this to apply the appropriate protection tag.',

disabled: mw.config.get('wgArticleId') === 0 || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710 // TimedText

}

]

});

form.append({ type: 'field', label: 'Preset', name: 'field_preset' });

form.append({ type: 'field', label: '1', name: 'field1' });

form.append({ type: 'field', label: '2', name: 'field2' });

form.append({ type: 'submit' });

const result = form.render();

Window.setContent(result);

Window.display();

// We must init the controls

const evt = document.createEvent('Event');

evt.initEvent('change', true, true);

result.actiontype[0].dispatchEvent(evt);

// get current protection level asynchronously

Twinkle.protect.fetchProtectionLevel();

};

// A list of bots who may be the protecting sysop, for whom we shouldn't

// remind the user contact before requesting unprotection (evaluate)

Twinkle.protect.trustedBots = ['MusikBot II', 'TFA Protector Bot'];

// Customizable namespace and FlaggedRevs settings

// In theory it'd be nice to have restrictionlevels defined here,

// but those are only available via a siteinfo query

// mw.loader.getState('ext.flaggedRevs.review') returns null if the

// FlaggedRevs extension is not registered. Previously, this was done with

// wgFlaggedRevsParams, but after 1.34-wmf4 it is no longer exported if empty

// (https://gerrit.wikimedia.org/r/c/mediawiki/extensions/FlaggedRevs/+/508427)

const hasFlaggedRevs = mw.loader.getState('ext.flaggedRevs.review') &&

// FlaggedRevs only valid in some namespaces, hardcoded until phab:T218479

(mw.config.get('wgNamespaceNumber') === 0 || mw.config.get('wgNamespaceNumber') === 4);

// Limit template editor; a Twinkle restriction, not a site setting

const isTemplate = mw.config.get('wgNamespaceNumber') === 10 || mw.config.get('wgNamespaceNumber') === 828;

// Contains the current protection level in an object

// Once filled, it will look something like:

// { edit: { level: "sysop", expiry: , cascade: true }, ... }

Twinkle.protect.currentProtectionLevels = {};

// returns a jQuery Deferred object, usage:

// Twinkle.protect.fetchProtectingAdmin(apiObject, pageName, protect/stable).done(function(admin_username) { ...code... });

Twinkle.protect.fetchProtectingAdmin = function twinkleprotectFetchProtectingAdmin(api, pageName, protType, logIds) {

logIds = logIds || [];

return api.get({

format: 'json',

action: 'query',

list: 'logevents',

letitle: pageName,

letype: protType

}).then((data) => {

// don't check log entries that have already been checked (e.g. don't go into an infinite loop!)

const event = data.query ? $.grep(data.query.logevents, (le) => $.inArray(le.logid, logIds))[0] : null;

if (!event) {

// fail gracefully

return null;

} else if (event.action === 'move_prot' || event.action === 'move_stable') {

return twinkleprotectFetchProtectingAdmin(api, protType === 'protect' ? event.params.oldtitle_title : event.params.oldtitle, protType, logIds.concat(event.logid));

}

return event.user;

});

};

Twinkle.protect.fetchProtectionLevel = function twinkleprotectFetchProtectionLevel() {

const api = new mw.Api();

const protectDeferred = api.get({

format: 'json',

indexpageids: true,

action: 'query',

list: 'logevents',

letype: 'protect',

letitle: mw.config.get('wgPageName'),

prop: hasFlaggedRevs ? 'info|flagged' : 'info',

inprop: 'protection|watched',

titles: mw.config.get('wgPageName')

});

const stableDeferred = api.get({

format: 'json',

action: 'query',

list: 'logevents',

letype: 'stable',

letitle: mw.config.get('wgPageName')

});

const earlyDecision = [protectDeferred];

if (hasFlaggedRevs) {

earlyDecision.push(stableDeferred);

}

$.when.apply($, earlyDecision).done((protectData, stableData) => {

// $.when.apply is supposed to take an unknown number of promises

// via an array, which it does, but the type of data returned varies.

// If there are two or more deferreds, it returns an array (of objects),

// but if there's just one deferred, it retuns a simple object.

// This is annoying.

protectData = $(protectData).toArray();

const pageid = protectData[0].query.pageids[0];

const page = protectData[0].query.pages[pageid];

const current = {};

let adminEditDeferred;

// Save requested page's watched status for later in case needed when filing request

Twinkle.protect.watched = page.watchlistexpiry || page.watched === '';

$.each(page.protection, (index, protection) => {

// Don't overwrite actual page protection with cascading protection

if (!protection.source) {

current[protection.type] = {

level: protection.level,

expiry: protection.expiry,

cascade: protection.cascade === ''

};

// logs report last admin who made changes to either edit/move/create protection, regardless if they only modified one of them

if (!adminEditDeferred) {

adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'protect');

}

} else {

// Account for the page being covered by cascading protection

current.cascading = {

expiry: protection.expiry,

source: protection.source,

level: protection.level // should always be sysop, unused

};

}

});

if (page.flagged) {

current.stabilize = {

level: page.flagged.protection_level,

expiry: page.flagged.protection_expiry

};

adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'stable');

}

// show the protection level and log info

Twinkle.protect.hasProtectLog = !!protectData[0].query.logevents.length;

Twinkle.protect.protectLog = Twinkle.protect.hasProtectLog && protectData[0].query.logevents;

Twinkle.protect.hasStableLog = hasFlaggedRevs ? !!stableData[0].query.logevents.length : false;

Twinkle.protect.stableLog = Twinkle.protect.hasStableLog && stableData[0].query.logevents;

Twinkle.protect.currentProtectionLevels = current;

if (adminEditDeferred) {

adminEditDeferred.done((admin) => {

if (admin) {

$.each(['edit', 'move', 'create', 'stabilize', 'cascading'], (i, type) => {

if (Twinkle.protect.currentProtectionLevels[type]) {

Twinkle.protect.currentProtectionLevels[type].admin = admin;

}

});

}

Twinkle.protect.callback.showLogAndCurrentProtectInfo();

});

} else {

Twinkle.protect.callback.showLogAndCurrentProtectInfo();

}

});

};

Twinkle.protect.callback.showLogAndCurrentProtectInfo = function twinkleprotectCallbackShowLogAndCurrentProtectInfo() {

const currentlyProtected = !$.isEmptyObject(Twinkle.protect.currentProtectionLevels);

if (Twinkle.protect.hasProtectLog || Twinkle.protect.hasStableLog) {

const $linkMarkup = $('');

if (Twinkle.protect.hasProtectLog) {

$linkMarkup.append(

$('protection log'));

if (!currentlyProtected || (!Twinkle.protect.currentProtectionLevels.edit && !Twinkle.protect.currentProtectionLevels.move)) {

const lastProtectAction = Twinkle.protect.protectLog[0];

if (lastProtectAction.action === 'unprotect') {

$linkMarkup.append(' (unprotected ' + new Morebits.Date(lastProtectAction.timestamp).calendar('utc') + ')');

} else { // protect or modify

$linkMarkup.append(' (expired ' + new Morebits.Date(lastProtectAction.params.details[0].expiry).calendar('utc') + ')');

}

}

$linkMarkup.append(Twinkle.protect.hasStableLog ? $('') : null);

}

if (Twinkle.protect.hasStableLog) {

$linkMarkup.append($('pending changes log)'));

if (!currentlyProtected || !Twinkle.protect.currentProtectionLevels.stabilize) {

const lastStabilizeAction = Twinkle.protect.stableLog[0];

if (lastStabilizeAction.action === 'reset') {

$linkMarkup.append(' (reset ' + new Morebits.Date(lastStabilizeAction.timestamp).calendar('utc') + ')');

} else { // config or modify

$linkMarkup.append(' (expired ' + new Morebits.Date(lastStabilizeAction.params.expiry).calendar('utc') + ')');

}

}

}

Morebits.Status.init($('div[name="hasprotectlog"] span')[0]);

Morebits.Status.warn(

currentlyProtected ? 'Previous protections' : 'This page has been protected in the past',

$linkMarkup[0]

);

}

Morebits.Status.init($('div[name="currentprot"] span')[0]);

let protectionNode = [], statusLevel = 'info';

if (currentlyProtected) {

$.each(Twinkle.protect.currentProtectionLevels, (type, settings) => {

let label = type === 'stabilize' ? 'Pending Changes' : Morebits.string.toUpperCaseFirstChar(type);

if (type === 'cascading') { // Covered by another page

label = 'Cascading protection ';

protectionNode.push($('' + label + '')[0]);

if (settings.source) { // Should by definition exist

const sourceLink = '' + settings.source + '';

protectionNode.push($('from ' + sourceLink + '')[0]);

}

} else {

let level = settings.level;

// Make cascading protection more prominent

if (settings.cascade) {

level += ' (cascading)';

}

protectionNode.push($('' + label + ': ' + level + '')[0]);

}

if (settings.expiry === 'infinity') {

protectionNode.push(' (indefinite) ');

} else {

protectionNode.push(' (expires ' + new Morebits.Date(settings.expiry).calendar('utc') + ') ');

}

if (settings.admin) {

const adminLink = '' + settings.admin + '';

protectionNode.push($('by ' + adminLink + '')[0]);

}

protectionNode.push($(' \u2022 ')[0]);

});

protectionNode = protectionNode.slice(0, -1); // remove the trailing bullet

statusLevel = 'warn';

} else {

protectionNode.push($('no protection')[0]);

}

Morebits.Status[statusLevel]('Current protection level', protectionNode);

};

Twinkle.protect.callback.changeAction = function twinkleprotectCallbackChangeAction(e) {

let field_preset;

let field1;

let field2;

switch (e.target.values) {

case 'protect':

field_preset = new Morebits.QuickForm.Element({ type: 'field', label: 'Preset', name: 'field_preset' });

field_preset.append({

type: 'select',

name: 'category',

label: 'Choose a preset:',

event: Twinkle.protect.callback.changePreset,

list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate

});

field2 = new Morebits.QuickForm.Element({ type: 'field', label: 'Protection options', name: 'field2' });

field2.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback

field2.append({ type: 'div', name: 'hasprotectlog', label: ' ' });

// for existing pages

if (mw.config.get('wgArticleId')) {

field2.append({

type: 'checkbox',

event: Twinkle.protect.formevents.editmodify,

list: [

{

label: 'Modify edit protection',

name: 'editmodify',

tooltip: 'If this is turned off, the edit protection level, and expiry time, will be left as is.',

checked: true

}

]

});

field2.append({

type: 'select',

name: 'editlevel',

label: 'Who can edit:',

event: Twinkle.protect.formevents.editlevel,

// Filter TE outside of templates and modules

list: Twinkle.protect.protectionLevels.filter((level) => isTemplate || level.value !== 'templateeditor')

});

field2.append({

type: 'select',

name: 'editexpiry',

label: 'Expires:',

event: function(e) {

if (e.target.value === 'custom') {

Twinkle.protect.doCustomExpiry(e.target);

}

},

// default expiry selection (2 days) is conditionally set in Twinkle.protect.callback.changePreset

list: Twinkle.protect.protectionLengths

});

field2.append({

type: 'checkbox',

event: Twinkle.protect.formevents.movemodify,

list: [

{

label: 'Modify move protection',

name: 'movemodify',

tooltip: 'If this is turned off, the move protection level, and expiry time, will be left as is.',

checked: true

}

]

});

field2.append({

type: 'select',

name: 'movelevel',

label: 'Who can move:',

event: Twinkle.protect.formevents.movelevel,

// Autoconfirmed is required for a move, redundant

list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'autoconfirmed' && (isTemplate || level.value !== 'templateeditor'))

});

field2.append({

type: 'select',

name: 'moveexpiry',

label: 'Expires:',

event: function(e) {

if (e.target.value === 'custom') {

Twinkle.protect.doCustomExpiry(e.target);

}

},

// default expiry selection (2 days) is conditionally set in Twinkle.protect.callback.changePreset

list: Twinkle.protect.protectionLengths

});

if (hasFlaggedRevs) {

field2.append({

type: 'checkbox',

event: Twinkle.protect.formevents.pcmodify,

list: [

{

label: 'Modify pending changes protection',

name: 'pcmodify',

tooltip: 'If this is turned off, the pending changes level, and expiry time, will be left as is.',

checked: true

}

]

});

field2.append({

type: 'select',

name: 'pclevel',

label: 'Pending changes:',

event: Twinkle.protect.formevents.pclevel,

list: [

{ label: 'None', value: 'none' },

{ label: 'Pending change', value: 'autoconfirmed', selected: true }

]

});

field2.append({

type: 'select',

name: 'pcexpiry',

label: 'Expires:',

event: function(e) {

if (e.target.value === 'custom') {

Twinkle.protect.doCustomExpiry(e.target);

}

},

// default expiry selection (1 month) is conditionally set in Twinkle.protect.callback.changePreset

list: Twinkle.protect.protectionLengths

});

}

} else { // for non-existing pages

field2.append({

type: 'select',

name: 'createlevel',

label: 'Create protection:',

event: Twinkle.protect.formevents.createlevel,

// Filter TE always, and autoconfirmed in mainspace, redundant since WP:ACPERM

list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'templateeditor' && (mw.config.get('wgNamespaceNumber') !== 0 || level.value !== 'autoconfirmed'))

});

field2.append({

type: 'select',

name: 'createexpiry',

label: 'Expires:',

event: function(e) {

if (e.target.value === 'custom') {

Twinkle.protect.doCustomExpiry(e.target);

}

},

// default expiry selection (indefinite) is conditionally set in Twinkle.protect.callback.changePreset

list: Twinkle.protect.protectionLengths

});

}

field2.append({

type: 'textarea',

name: 'protectReason',

label: 'Reason (for protection log):'

});

field2.append({

type: 'div',

name: 'protectReason_notes',

label: 'Notes:',

style: 'display:inline-block; margin-top:4px;',

tooltip: 'Add a note to the protection log that this was requested at RfPP.'

});

field2.append({

type: 'checkbox',

event: Twinkle.protect.callback.annotateProtectReason,

style: 'display:inline-block; margin-top:4px;',

list: [

{

label: 'RfPP request',

name: 'protectReason_notes_rfpp',

checked: false,

value: 'requested at WP:RfPP'

}

]

});

field2.append({

type: 'input',

event: Twinkle.protect.callback.annotateProtectReason,

label: 'RfPP revision ID',

name: 'protectReason_notes_rfppRevid',

value: '',

tooltip: 'Optional revision ID of the RfPP page where protection was requested.'

});

if (!mw.config.get('wgArticleId') || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710) { // tagging isn't relevant for non-existing, module, or TimedText pages

break;

}

/* falls through */

case 'tag':

field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'Tagging options', name: 'field1' });

field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback

field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' });

field1.append({

type: 'select',

name: 'tagtype',

label: 'Choose protection template:',

list: Twinkle.protect.protectionTags,

event: Twinkle.protect.formevents.tagtype

});

var isTemplateNamespace = mw.config.get('wgNamespaceNumber') === 10;

var isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:Articles for deletion/');

var isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'));

field1.append({

type: 'checkbox',

list: [

{

name: 'small',

label: 'Iconify (small=yes)',

tooltip: 'Will use the |small=yes feature of the template, and only render it as a keylock',

checked: true

},

{

name: 'noinclude',

label: 'Wrap protection template with <noinclude>',

tooltip: 'Will wrap the protection template in <noinclude> tags, so that it won\'t transclude',

checked: (isTemplateNamespace || isAFD) && !isCode

}

]

});

break;

case 'request':

field_preset = new Morebits.QuickForm.Element({ type: 'field', label: 'Type of protection', name: 'field_preset' });

field_preset.append({

type: 'select',

name: 'category',

label: 'Type and reason:',

event: Twinkle.protect.callback.changePreset,

list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate

});

field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'Options', name: 'field1' });

field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback

field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' });

field1.append({

type: 'select',

name: 'expiry',

label: 'Duration:',

list: [

{ label: , selected: true, value: },

{ label: 'Temporary', value: 'temporary' },

{ label: 'Indefinite', value: 'infinity' }

]

});

field1.append({

type: 'textarea',

name: 'reason',

label: 'Reason:'

});

break;

default:

alert("Something's afoot in twinkleprotect");

break;

}

let oldfield;

if (field_preset) {

oldfield = $(e.target.form).find('fieldset[name="field_preset"]')[0];

oldfield.parentNode.replaceChild(field_preset.render(), oldfield);

} else {

$(e.target.form).find('fieldset[name="field_preset"]').css('display', 'none');

}

if (field1) {

oldfield = $(e.target.form).find('fieldset[name="field1"]')[0];

oldfield.parentNode.replaceChild(field1.render(), oldfield);

} else {

$(e.target.form).find('fieldset[name="field1"]').css('display', 'none');

}

if (field2) {

oldfield = $(e.target.form).find('fieldset[name="field2"]')[0];

oldfield.parentNode.replaceChild(field2.render(), oldfield);

} else {

$(e.target.form).find('fieldset[name="field2"]').css('display', 'none');

}

if (e.target.values === 'protect') {

// fake a change event on the preset dropdown

const evt = document.createEvent('Event');

evt.initEvent('change', true, true);

e.target.form.category.dispatchEvent(evt);

// reduce vertical height of dialog

$(e.target.form).find('fieldset[name="field2"] select').parent().css({ display: 'inline-block', marginRight: '0.5em' });

$(e.target.form).find('fieldset[name="field2"] input[name="protectReason_notes_rfppRevid"]').parent().css({display: 'inline-block', marginLeft: '15px'}).hide();

}

// re-add protection level and log info, if it's available

Twinkle.protect.callback.showLogAndCurrentProtectInfo();

};

// NOTE: This function is used by batchprotect as well

Twinkle.protect.formevents = {

editmodify: function twinkleprotectFormEditmodifyEvent(e) {

e.target.form.editlevel.disabled = !e.target.checked;

e.target.form.editexpiry.disabled = !e.target.checked || (e.target.form.editlevel.value === 'all');

e.target.form.editlevel.style.color = e.target.form.editexpiry.style.color = e.target.checked ? '' : 'transparent';

},

editlevel: function twinkleprotectFormEditlevelEvent(e) {

e.target.form.editexpiry.disabled = e.target.value === 'all';

},

movemodify: function twinkleprotectFormMovemodifyEvent(e) {

// sync move settings with edit settings if applicable

if (e.target.form.movelevel.disabled && !e.target.form.editlevel.disabled) {

e.target.form.movelevel.value = e.target.form.editlevel.value;

e.target.form.moveexpiry.value = e.target.form.editexpiry.value;

} else if (e.target.form.editlevel.disabled) {

e.target.form.movelevel.value = 'sysop';

e.target.form.moveexpiry.value = 'infinity';

}

e.target.form.movelevel.disabled = !e.target.checked;

e.target.form.moveexpiry.disabled = !e.target.checked || (e.target.form.movelevel.value === 'all');

e.target.form.movelevel.style.color = e.target.form.moveexpiry.style.color = e.target.checked ? '' : 'transparent';

},

movelevel: function twinkleprotectFormMovelevelEvent(e) {

e.target.form.moveexpiry.disabled = e.target.value === 'all';

},

pcmodify: function twinkleprotectFormPcmodifyEvent(e) {

e.target.form.pclevel.disabled = !e.target.checked;

e.target.form.pcexpiry.disabled = !e.target.checked || (e.target.form.pclevel.value === 'none');

e.target.form.pclevel.style.color = e.target.form.pcexpiry.style.color = e.target.checked ? '' : 'transparent';

},

pclevel: function twinkleprotectFormPclevelEvent(e) {

e.target.form.pcexpiry.disabled = e.target.value === 'none';

},

createlevel: function twinkleprotectFormCreatelevelEvent(e) {

e.target.form.createexpiry.disabled = e.target.value === 'all';

},

tagtype: function twinkleprotectFormTagtypeEvent(e) {

e.target.form.small.disabled = e.target.form.noinclude.disabled = (e.target.value === 'none') || (e.target.value === 'noop');

}

};

Twinkle.protect.doCustomExpiry = function twinkleprotectDoCustomExpiry(target) {

const custom = prompt('Enter a custom expiry time. \nYou can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).', '');

if (custom) {

const option = document.createElement('option');

option.setAttribute('value', custom);

option.textContent = custom;

target.appendChild(option);

target.value = custom;

} else {

target.selectedIndex = 0;

}

};

// NOTE: This list is used by batchprotect as well

Twinkle.protect.protectionLevels = [

{ label: 'All', value: 'all' },

{ label: 'Autoconfirmed', value: 'autoconfirmed' },

{ label: 'Extended confirmed', value: 'extendedconfirmed' },

{ label: 'Template editor', value: 'templateeditor' },

{ label: 'Sysop', value: 'sysop', selected: true }

];

// default expiry selection is conditionally set in Twinkle.protect.callback.changePreset

// NOTE: This list is used by batchprotect as well

Twinkle.protect.protectionLengths = [

{ label: '1 hour', value: '1 hour' },

{ label: '2 hours', value: '2 hours' },

{ label: '3 hours', value: '3 hours' },

{ label: '6 hours', value: '6 hours' },

{ label: '12 hours', value: '12 hours' },

{ label: '1 day', value: '1 day' },

{ label: '2 days', value: '2 days' },

{ label: '3 days', value: '3 days' },

{ label: '4 days', value: '4 days' },

{ label: '10 days', value: '10 days' },

{ label: '1 week', value: '1 week' },

{ label: '2 weeks', value: '2 weeks' },

{ label: '1 month', value: '1 month' },

{ label: '2 months', value: '2 months' },

{ label: '3 months', value: '3 months' },

{ label: '6 months', value: '6 months' },

{ label: '1 year', value: '1 year' },

{ label: '2 years', value: '2 years' },

{ label: 'indefinite', value: 'infinity' },

{ label: 'Custom...', value: 'custom' }

];

Twinkle.protect.protectionTypes = [

{ label: 'Unprotection', value: 'unprotect' },

{

label: 'Full protection',

list: [

{ label: 'Generic (full)', value: 'pp-protected' },

{ label: 'Content dispute/edit warring (full)', value: 'pp-dispute' },

{ label: 'Persistent vandalism (full)', value: 'pp-vandalism' },

{ label: 'User talk of blocked user (full)', value: 'pp-usertalk' }

]

},

{

label: 'Template protection',

list: [

{ label: 'Highly visible template (TE)', value: 'pp-template' }

]

},

{

label: 'Extended confirmed protection',

list: [

{ label: 'Generic (ECP)', value: 'pp-30-500' },

{ label: 'Arbitration enforcement (ECP)', selected: true, value: 'pp-30-500-arb' },

{ label: 'Persistent vandalism (ECP)', value: 'pp-30-500-vandalism' },

{ label: 'Disruptive editing (ECP)', value: 'pp-30-500-disruptive' },

{ label: 'BLP policy violations (ECP)', value: 'pp-30-500-blp' },

{ label: 'Sockpuppetry (ECP)', value: 'pp-30-500-sock' }

]

},

{

label: 'Semi-protection',

list: [

{ label: 'Generic (semi)', value: 'pp-semi-protected' },

{ label: 'Persistent vandalism (semi)', selected: true, value: 'pp-semi-vandalism' },

{ label: 'Disruptive editing (semi)', value: 'pp-semi-disruptive' },

{ label: 'Adding unsourced content (semi)', value: 'pp-semi-unsourced' },

{ label: 'BLP policy violations (semi)', value: 'pp-semi-blp' },

{ label: 'Sockpuppetry (semi)', value: 'pp-semi-sock' },

{ label: 'User talk of blocked user (semi)', value: 'pp-semi-usertalk' }

]

},

{

label: 'Pending changes',

list: [

{ label: 'Generic (PC)', value: 'pp-pc-protected' },

{ label: 'Persistent vandalism (PC)', value: 'pp-pc-vandalism' },

{ label: 'Disruptive editing (PC)', value: 'pp-pc-disruptive' },

{ label: 'Adding unsourced content (PC)', value: 'pp-pc-unsourced' },

{ label: 'BLP policy violations (PC)', value: 'pp-pc-blp' }

]

},

{

label: 'Move protection',

list: [

{ label: 'Generic (move)', value: 'pp-move' },

{ label: 'Dispute/move warring (move)', value: 'pp-move-dispute' },

{ label: 'Page-move vandalism (move)', value: 'pp-move-vandalism' },

{ label: 'Highly visible page (move)', value: 'pp-move-indef' }

]

}

]

// Filter for templates and flaggedrevs

.filter((type) => (isTemplate || type.label !== 'Template protection') && (hasFlaggedRevs || type.label !== 'Pending changes'));

Twinkle.protect.protectionTypesCreate = [

{ label: 'Unprotection', value: 'unprotect' },

{

label: 'Create protection',

list: [

{ label: 'Offensive name', value: 'pp-create-offensive' },

{ label: 'Repeatedly recreated', selected: true, value: 'pp-create-salt' },

{ label: 'Recently deleted BLP', value: 'pp-create-blp' }

]

}

];

// A page with both regular and PC protection will be assigned its regular

// protection weight plus 2

Twinkle.protect.protectionWeight = {

sysop: 40,

templateeditor: 30,

extendedconfirmed: 20,

autoconfirmed: 10,

flaggedrevs_autoconfirmed: 5, // Pending Changes protection alone

all: 0,

flaggedrevs_none: 0 // just in case

};

// NOTICE: keep this synched with MediaWiki:Protect-dropdown

// Also note: stabilize = Pending Changes level

// expiry will override any defaults

Twinkle.protect.protectionPresetsInfo = {

'pp-protected': {

edit: 'sysop',

move: 'sysop',

reason: null

},

'pp-dispute': {

edit: 'sysop',

move: 'sysop',

reason: 'Edit warring / content dispute'

},

'pp-vandalism': {

edit: 'sysop',

move: 'sysop',

reason: 'Persistent vandalism'

},

'pp-usertalk': {

edit: 'sysop',

move: 'sysop',

expiry: 'infinity',

reason: 'Inappropriate use of user talk page while blocked'

},

'pp-template': {

edit: 'templateeditor',

move: 'templateeditor',

expiry: 'infinity',

reason: 'Highly visible template'

},

'pp-30-500-arb': {

edit: 'extendedconfirmed',

move: 'extendedconfirmed',

expiry: 'infinity',

reason: 'Arbitration enforcement',

template: 'pp-extended'

},

'pp-30-500-vandalism': {

edit: 'extendedconfirmed',

move: 'extendedconfirmed',

reason: 'Persistent vandalism from (auto)confirmed accounts',

template: 'pp-extended'

},

'pp-30-500-disruptive': {

edit: 'extendedconfirmed',

move: 'extendedconfirmed',

reason: 'Persistent disruptive editing from (auto)confirmed accounts',

template: 'pp-extended'

},

'pp-30-500-blp': {

edit: 'extendedconfirmed',

move: 'extendedconfirmed',

reason: 'Persistent violations of the biographies of living persons policy from (auto)confirmed accounts',

template: 'pp-extended'

},

'pp-30-500-sock': {

edit: 'extendedconfirmed',

move: 'extendedconfirmed',

reason: 'Persistent sock puppetry',

template: 'pp-extended'

},

'pp-30-500': {

edit: 'extendedconfirmed',

move: 'extendedconfirmed',

reason: null,

template: 'pp-extended'

},

'pp-semi-vandalism': {

edit: 'autoconfirmed',

reason: 'Persistent vandalism',

template: 'pp-vandalism'

},

'pp-semi-disruptive': {

edit: 'autoconfirmed',

reason: 'Persistent disruptive editing',

template: 'pp-protected'

},

'pp-semi-unsourced': {

edit: 'autoconfirmed',

reason: 'Persistent addition of unsourced or poorly sourced content',

template: 'pp-protected'

},

'pp-semi-blp': {

edit: 'autoconfirmed',

reason: 'Violations of the biographies of living persons policy',

template: 'pp-blp'

},

'pp-semi-usertalk': {

edit: 'autoconfirmed',

move: 'autoconfirmed',

expiry: 'infinity',

reason: 'Inappropriate use of user talk page while blocked',

template: 'pp-usertalk'

},

'pp-semi-template': { // removed for now

edit: 'autoconfirmed',

move: 'autoconfirmed',

expiry: 'infinity',

reason: 'Highly visible template',

template: 'pp-template'

},

'pp-semi-sock': {

edit: 'autoconfirmed',

reason: 'Persistent sock puppetry',

template: 'pp-sock'

},

'pp-semi-protected': {

edit: 'autoconfirmed',

reason: null,

template: 'pp-protected'

},

'pp-pc-vandalism': {

stabilize: 'autoconfirmed', // stabilize = Pending Changes

reason: 'Persistent vandalism',

template: 'pp-pc'

},

'pp-pc-disruptive': {

stabilize: 'autoconfirmed',

reason: 'Persistent disruptive editing',

template: 'pp-pc'

},

'pp-pc-unsourced': {

stabilize: 'autoconfirmed',

reason: 'Persistent addition of unsourced or poorly sourced content',

template: 'pp-pc'

},

'pp-pc-blp': {

stabilize: 'autoconfirmed',

reason: 'Violations of the biographies of living persons policy',

template: 'pp-pc'

},

'pp-pc-protected': {

stabilize: 'autoconfirmed',

reason: null,

template: 'pp-pc'

},

'pp-move': {

move: 'sysop',

reason: null

},

'pp-move-dispute': {

move: 'sysop',

reason: 'Move warring'

},

'pp-move-vandalism': {

move: 'sysop',

reason: 'Page-move vandalism'

},

'pp-move-indef': {

move: 'sysop',

expiry: 'infinity',

reason: 'Highly visible page'

},

unprotect: {

edit: 'all',

move: 'all',

stabilize: 'none',

create: 'all',

reason: null,

template: 'none'

},

'pp-create-offensive': {

create: 'sysop',

reason: 'Offensive name'

},

'pp-create-salt': {

create: 'extendedconfirmed',

reason: 'Repeatedly recreated'

},

'pp-create-blp': {

create: 'extendedconfirmed',

reason: 'Recently deleted BLP'

}

};

Twinkle.protect.protectionTags = [

{

label: 'None (remove existing protection templates)',

value: 'none'

},

{

label: 'None (do not remove existing protection templates)',

value: 'noop'

},

{

label: 'Edit protection templates',

list: [

{ label: '{{pp-vandalism}}: vandalism', value: 'pp-vandalism' },

{ label: '{{pp-dispute}}: dispute/edit war', value: 'pp-dispute' },

{ label: '{{pp-blp}}: BLP violations', value: 'pp-blp' },

{ label: '{{pp-sock}}: sockpuppetry', value: 'pp-sock' },

{ label: '{{pp-template}}: high-risk template', value: 'pp-template' },

{ label: '{{pp-usertalk}}: blocked user talk', value: 'pp-usertalk' },

{ label: '{{pp-protected}}: general protection', value: 'pp-protected' },

{ label: '{{pp-semi-indef}}: general long-term semi-protection', value: 'pp-semi-indef' },

{ label: '{{pp-extended}}: extended confirmed protection', value: 'pp-extended' }

]

},

{

label: 'Pending changes templates',

list: [

{ label: '{{pp-pc}}: pending changes', value: 'pp-pc' }

]

},

{

label: 'Move protection templates',

list: [

{ label: '{{pp-move-dispute}}: dispute/move war', value: 'pp-move-dispute' },

{ label: '{{pp-move-vandalism}}: page-move vandalism', value: 'pp-move-vandalism' },

{ label: '{{pp-move-indef}}: general long-term', value: 'pp-move-indef' },

{ label: '{{pp-move}}: other', value: 'pp-move' }

]

}

]

// Filter FlaggedRevs

.filter((type) => hasFlaggedRevs || type.label !== 'Pending changes templates');

Twinkle.protect.callback.changePreset = function twinkleprotectCallbackChangePreset(e) {

const form = e.target.form;

const actiontypes = form.actiontype;

let actiontype;

for (let i = 0; i < actiontypes.length; i++) {

if (!actiontypes[i].checked) {

continue;

}

actiontype = actiontypes[i].values;

break;

}

if (actiontype === 'protect') { // actually protecting the page

const item = Twinkle.protect.protectionPresetsInfo[form.category.value];

if (mw.config.get('wgArticleId')) {

if (item.edit) {

form.editmodify.checked = true;

Twinkle.protect.formevents.editmodify({ target: form.editmodify });

form.editlevel.value = item.edit;

Twinkle.protect.formevents.editlevel({ target: form.editlevel });

} else {

form.editmodify.checked = false;

Twinkle.protect.formevents.editmodify({ target: form.editmodify });

}

if (item.move) {

form.movemodify.checked = true;

Twinkle.protect.formevents.movemodify({ target: form.movemodify });

form.movelevel.value = item.move;

Twinkle.protect.formevents.movelevel({ target: form.movelevel });

} else {

form.movemodify.checked = false;

Twinkle.protect.formevents.movemodify({ target: form.movemodify });

}

form.editexpiry.value = form.moveexpiry.value = item.expiry || '2 days';

if (form.pcmodify) {

if (item.stabilize) {

form.pcmodify.checked = true;

Twinkle.protect.formevents.pcmodify({ target: form.pcmodify });

form.pclevel.value = item.stabilize;

Twinkle.protect.formevents.pclevel({ target: form.pclevel });

} else {

form.pcmodify.checked = false;

Twinkle.protect.formevents.pcmodify({ target: form.pcmodify });

}

form.pcexpiry.value = item.expiry || '1 month';

}

} else {

if (item.create) {

form.createlevel.value = item.create;

Twinkle.protect.formevents.createlevel({ target: form.createlevel });

}

form.createexpiry.value = item.expiry || 'infinity';

}

const reasonField = actiontype === 'protect' ? form.protectReason : form.reason;

if (item.reason) {

reasonField.value = item.reason;

} else {

reasonField.value = '';

}

// Add any annotations

Twinkle.protect.callback.annotateProtectReason(e);

// sort out tagging options, disabled if nonexistent, lua, or TimedText

if (mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710) {

if (form.category.value === 'unprotect') {

form.tagtype.value = 'none';

} else {

form.tagtype.value = item.template ? item.template : form.category.value;

}

Twinkle.protect.formevents.tagtype({ target: form.tagtype });

// Default settings for adding tags to protection templates

const isTemplateEditorProtection = form.category.value === 'pp-template';

const isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:Articles for deletion/');

const isNotTemplateNamespace = mw.config.get('wgNamespaceNumber') !== 10;

const isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'));

if ((isTemplateEditorProtection || isAFD) && !isCode) {

form.noinclude.checked = true;

} else if (isCode || isNotTemplateNamespace) {

form.noinclude.checked = false;

}

}

} else { // RPP request

if (form.category.value === 'unprotect') {

form.expiry.value = '';

form.expiry.disabled = true;

} else {

form.expiry.value = '';

form.expiry.disabled = false;

}

}

};

Twinkle.protect.callback.evaluate = function twinkleprotectCallbackEvaluate(e) {

const form = e.target;

const input = Morebits.QuickForm.getInputData(form);

let tagparams;

if (input.actiontype === 'tag' || (input.actiontype === 'protect' && mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710 /* TimedText */)) {

tagparams = {

tag: input.tagtype,

reason: false,

small: input.small,

noinclude: input.noinclude

};

}

switch (input.actiontype) {

case 'protect':

// protect the page

Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');

Morebits.wiki.actionCompleted.notice = 'Protection complete';

var statusInited = false;

var thispage;

var allDone = function twinkleprotectCallbackAllDone() {

if (thispage) {

thispage.getStatusElement().info('done');

}

if (tagparams) {

Twinkle.protect.callbacks.taggingPageInitial(tagparams);

}

};

var protectIt = function twinkleprotectCallbackProtectIt(next) {

thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Protecting page');

if (mw.config.get('wgArticleId')) {

if (input.editmodify) {

thispage.setEditProtection(input.editlevel, input.editexpiry);

}

if (input.movemodify) {

// Ensure a level has actually been chosen

if (input.movelevel) {

thispage.setMoveProtection(input.movelevel, input.moveexpiry);

} else {

alert('You must chose a move protection level!');

return;

}

}

thispage.setWatchlist(Twinkle.getPref('watchProtectedPages'));

} else {

thispage.setCreateProtection(input.createlevel, input.createexpiry);

thispage.setWatchlist(false);

}

if (input.protectReason) {

thispage.setEditSummary(input.protectReason);

} else {

alert('You must enter a protect reason, which will be inscribed into the protection log.');

return;

}

if (input.protectReason_notes_rfppRevid && !/^\d+$/.test(input.protectReason_notes_rfppRevid)) {

alert('The provided revision ID is malformed. Please see https://en.wikipedia.org/wiki/Help:Permanent_link for information on how to find the correct ID (also called "oldid").');

return;

}

if (!statusInited) {

Morebits.SimpleWindow.setButtonsEnabled(false);

Morebits.Status.init(form);

statusInited = true;

}

thispage.setChangeTags(Twinkle.changeTags);

thispage.protect(next);

};

var stabilizeIt = function twinkleprotectCallbackStabilizeIt() {

if (thispage) {

thispage.getStatusElement().info('done');

}

thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Applying pending changes protection');

thispage.setFlaggedRevs(input.pclevel, input.pcexpiry);

if (input.protectReason) {

thispage.setEditSummary(input.protectReason + Twinkle.summaryAd); // flaggedrevs tag support: phab:T247721

} else {

alert('You must enter a protect reason, which will be inscribed into the protection log.');

return;

}

if (!statusInited) {

Morebits.SimpleWindow.setButtonsEnabled(false);

Morebits.Status.init(form);

statusInited = true;

}

thispage.setWatchlist(Twinkle.getPref('watchProtectedPages'));

thispage.stabilize(allDone, (error) => {

if (error.errorCode === 'stabilize_denied') { // phab:T234743

thispage.getStatusElement().error('Failed trying to modify pending changes settings, likely due to a mediawiki bug. Other actions (tagging or regular protection) may have taken place. Please reload the page and try again.');

}

});

};

if (input.editmodify || input.movemodify || !mw.config.get('wgArticleId')) {

if (input.pcmodify) {

protectIt(stabilizeIt);

} else {

protectIt(allDone);

}

} else if (input.pcmodify) {

stabilizeIt();

} else {

alert("Please give Twinkle something to do! \nIf you just want to tag the page, you can choose the 'Tag page with protection template' option at the top.");

}

break;

case 'tag':

// apply a protection template

Morebits.SimpleWindow.setButtonsEnabled(false);

Morebits.Status.init(form);

Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');

Morebits.wiki.actionCompleted.followRedirect = false;

Morebits.wiki.actionCompleted.notice = 'Tagging complete';

Twinkle.protect.callbacks.taggingPageInitial(tagparams);

break;

case 'request':

// file request at RFPP

var typename, typereason;

switch (input.category) {

case 'pp-dispute':

case 'pp-vandalism':

case 'pp-usertalk':

case 'pp-protected':

typename = 'full protection';

break;

case 'pp-template':

typename = 'template protection';

break;

case 'pp-30-500-arb':

case 'pp-30-500-vandalism':

case 'pp-30-500-disruptive':

case 'pp-30-500-blp':

case 'pp-30-500-sock':

case 'pp-30-500':

typename = 'extended confirmed protection';

break;

case 'pp-semi-vandalism':

case 'pp-semi-disruptive':

case 'pp-semi-unsourced':

case 'pp-semi-usertalk':

case 'pp-semi-sock':

case 'pp-semi-blp':

case 'pp-semi-protected':

typename = 'semi-protection';

break;

case 'pp-pc-vandalism':

case 'pp-pc-blp':

case 'pp-pc-protected':

case 'pp-pc-unsourced':

case 'pp-pc-disruptive':

typename = 'pending changes';

break;

case 'pp-move':

case 'pp-move-dispute':

case 'pp-move-indef':

case 'pp-move-vandalism':

typename = 'move protection';

break;

case 'pp-create-offensive':

case 'pp-create-blp':

case 'pp-create-salt':

typename = 'create protection';

break;

case 'unprotect':

var admins = $.map(Twinkle.protect.currentProtectionLevels, (pl) => {

if (!pl.admin || Twinkle.protect.trustedBots.includes(pl.admin)) {

return null;

}

return 'User:' + pl.admin;

});

if (admins.length && !confirm('Have you attempted to contact the protecting admins (' + Morebits.array.uniq(admins).join(', ') + ') first?')) {

return false;

}

// otherwise falls through

default:

typename = 'unprotection';

break;

}

switch (input.category) {

case 'pp-dispute':

typereason = 'Content dispute/edit warring';

break;

case 'pp-vandalism':

case 'pp-semi-vandalism':

case 'pp-pc-vandalism':

case 'pp-30-500-vandalism':

typereason = 'Persistent vandalism';

break;

case 'pp-semi-disruptive':

case 'pp-pc-disruptive':

case 'pp-30-500-disruptive':

typereason = 'Persistent disruptive editing';

break;

case 'pp-semi-unsourced':

case 'pp-pc-unsourced':

typereason = 'Persistent addition of unsourced or poorly sourced content';

break;

case 'pp-template':

typereason = 'High-risk template';

break;

case 'pp-30-500-arb':

typereason = 'Arbitration enforcement';

break;

case 'pp-usertalk':

case 'pp-semi-usertalk':

typereason = 'Inappropriate use of user talk page while blocked';

break;

case 'pp-semi-sock':

case 'pp-30-500-sock':

typereason = 'Persistent sockpuppetry';

break;

case 'pp-semi-blp':

case 'pp-pc-blp':

case 'pp-30-500-blp':

typereason = 'BLP policy violations';

break;

case 'pp-move-dispute':

typereason = 'Page title dispute/move warring';

break;

case 'pp-move-vandalism':

typereason = 'Page-move vandalism';

break;

case 'pp-move-indef':

typereason = 'Highly visible page';

break;

case 'pp-create-offensive':

typereason = 'Offensive name';

break;

case 'pp-create-blp':

typereason = 'Recently deleted BLP';

break;

case 'pp-create-salt':

typereason = 'Repeatedly recreated';

break;

default:

typereason = '';

break;

}

var reason = typereason;

if (input.reason !== '') {

if (typereason !== '') {

reason += '\u00A0\u2013 '; // U+00A0 NO-BREAK SPACE; U+2013 EN RULE

}

reason += input.reason;

}

if (reason !== '' && reason.charAt(reason.length - 1) !== '.') {

reason += '.';

}

var rppparams = {

reason: reason,

typename: typename,

category: input.category,

expiry: input.expiry

};

Morebits.SimpleWindow.setButtonsEnabled(false);

Morebits.Status.init(form);

var rppName = 'Wikipedia:Requests for page protection/Increase';

// Updating data for the action completed event

Morebits.wiki.actionCompleted.redirect = 'Wikipedia: Requests for page protection';

Morebits.wiki.actionCompleted.notice = 'Nomination completed, redirecting now to the discussion page';

var rppPage = new Morebits.wiki.Page(rppName, 'Requesting protection of page');

rppPage.setFollowRedirect(true);

rppPage.setCallbackParameters(rppparams);

rppPage.load(Twinkle.protect.callbacks.fileRequest);

break;

default:

alert('twinkleprotect: unknown kind of action');

break;

}

};

Twinkle.protect.protectReasonAnnotations = [];

Twinkle.protect.callback.annotateProtectReason = function twinkleprotectCallbackAnnotateProtectReason(e) {

const form = e.target.form;

const protectReason = form.protectReason.value.replace(new RegExp('(?:; )?' + mw.util.escapeRegExp(Twinkle.protect.protectReasonAnnotations.join(': '))), '');

if (this.name === 'protectReason_notes_rfpp') {

if (this.checked) {

Twinkle.protect.protectReasonAnnotations.push(this.value);

$(form.protectReason_notes_rfppRevid).parent().show();

} else {

Twinkle.protect.protectReasonAnnotations = [];

form.protectReason_notes_rfppRevid.value = '';

$(form.protectReason_notes_rfppRevid).parent().hide();

}

} else if (this.name === 'protectReason_notes_rfppRevid') {

Twinkle.protect.protectReasonAnnotations = Twinkle.protect.protectReasonAnnotations.filter((el) => !el.includes('[[Special:Permalink'));

if (e.target.value.length) {

const permalink = 'Special:Permalink/' + e.target.value + '#' + Morebits.pageNameNorm + '';

Twinkle.protect.protectReasonAnnotations.push(permalink);

}

}

if (!Twinkle.protect.protectReasonAnnotations.length) {

form.protectReason.value = protectReason;

} else {

form.protectReason.value = (protectReason ? protectReason + '; ' : '') + Twinkle.protect.protectReasonAnnotations.join(': ');

}

};

Twinkle.protect.callbacks = {

taggingPageInitial: function(tagparams) {

if (tagparams.tag === 'noop') {

Morebits.Status.info('Applying protection template', 'nothing to do');

return;

}

const protectedPage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging page');

protectedPage.setCallbackParameters(tagparams);

protectedPage.load(Twinkle.protect.callbacks.taggingPage);

},

taggingPage: function(protectedPage) {

const params = protectedPage.getCallbackParameters();

let text = protectedPage.getPageText();

let tag, summary;

const oldtag_re = /(?:\/\*)?\s*(?:)?\s*\{\{\s*(pp-[^{}]*?|protected|(?:t|v|s|p-|usertalk-v|usertalk-s|sb|move)protected(?:2)?|protected template|privacy protection)\s*?\}\}\s*(?:<\/noinclude>)?\s*(?:\*\/)?\s*/gi;

const re_result = oldtag_re.exec(text);

if (re_result) {

if (params.tag === 'none' || confirm('{{' + re_result[1] + '}} was found on the page. \nClick OK to remove it, or click Cancel to leave it there.')) {

text = text.replace(oldtag_re, '');

}

}

if (params.tag === 'none') {

summary = 'Removing protection template';

} else {

tag = params.tag;

if (params.reason) {

tag += '|reason=' + params.reason;

}

if (params.small) {

tag += '|small=yes';

}

if (/^\s*#redirect/i.test(text)) { // redirect page

// Only tag if no {{rcat shell}} is found

if (!text.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh)/i)) {

text = text.replace(/#REDIRECT ?(\[\[.*?\]\])(.*)/i, '#REDIRECT $1$2\n\n{{' + tag + '}}');

} else {

Morebits.Status.info('Redirect category shell present', 'nothing to do');

return;

}

} else {

const needsTagToBeCommentedOut = ['javascript', 'css', 'sanitized-css'].includes(protectedPage.getContentModel());

if (needsTagToBeCommentedOut) {

if (params.noinclude) {

tag = '/* {{' + tag + '}} */';

} else {

tag = '/* {{' + tag + '}} */\n';

}

// Prepend tag at very top

text = tag + text;

} else {

if (params.noinclude) {

tag = '{{' + tag + '}}';

if (text.startsWith('==')) {

tag += '\n'; // a newline is needed to prevent section headings at the very beginning of the page from breaking

}

} else {

tag = '{{' + tag + '}}\n';

}

// Insert tag after short description or any hatnotes

const wikipage = new Morebits.wikitext.Page(text);

text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText();

}

}

summary = 'Adding {{' + params.tag + '}}';

}

protectedPage.setEditSummary(summary);

protectedPage.setChangeTags(Twinkle.changeTags);

protectedPage.setWatchlist(Twinkle.getPref('watchPPTaggedPages'));

protectedPage.setPageText(text);

protectedPage.setCreateOption('nocreate');

protectedPage.suppressProtectWarning(); // no need to let admins know they are editing through protection

protectedPage.save();

},

fileRequest: function(rppPage) {

const rppPage2 = new Morebits.wiki.Page('Wikipedia:Requests for page protection/Decrease', 'Loading requests pages');

rppPage2.load(() => {

const params = rppPage.getCallbackParameters();

let text = rppPage.getPageText();

const statusElement = rppPage.getStatusElement();

let text2 = rppPage2.getPageText();

const rppRe = new RegExp('===\\s*(\\[\\[)?\\s*:?\\s*' + Morebits.string.escapeRegExp(Morebits.pageNameNorm) + '\\s*(\\]\\])?\\s*===', 'm');

const tag = rppRe.exec(text) || rppRe.exec(text2);

const rppLink = document.createElement('a');

rppLink.setAttribute('href', mw.util.getUrl('Wikipedia:Requests for page protection'));

rppLink.appendChild(document.createTextNode('Wikipedia:Requests for page protection'));

if (tag) {

statusElement.error([ 'There is already a protection request for this page at ', rppLink, ', aborting.' ]);

return;

}

let newtag = '=== :' + Morebits.pageNameNorm + ' ===\n';

if (new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text) || new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text2)) {

statusElement.error([ 'There is already a protection request for this page at ', rppLink, ', aborting.' ]);

return;

}

newtag += '* {{pagelinks|1=' + Morebits.pageNameNorm + '}}\n\n';

let words;

switch (params.expiry) {

case 'temporary':

words = 'Temporary ';

break;

case 'infinity':

words = 'Indefinite ';

break;

default:

words = '';

break;

}

words += params.typename;

newtag += "" + Morebits.string.toUpperCaseFirstChar(words) + (params.reason !== '' ? ": " +

Morebits.string.formatReasonText(params.reason) : ".'''") + ' ~~~~';

// If either protection type results in a increased status, then post it under increase

// else we post it under decrease

let increase = false;

const protInfo = Twinkle.protect.protectionPresetsInfo[params.category];

// function to compute protection weights (see comment at Twinkle.protect.protectionWeight)

const computeWeight = function(mainLevel, stabilizeLevel) {

let result = Twinkle.protect.protectionWeight[mainLevel || 'all'];

if (stabilizeLevel) {

if (result) {

if (stabilizeLevel.level === 'autoconfirmed') {

result += 2;

}

} else {

result = Twinkle.protect.protectionWeight['flaggedrevs_' + stabilizeLevel];

}

}

return result;

};

// compare the page's current protection weights with the protection we are requesting

const editWeight = computeWeight(Twinkle.protect.currentProtectionLevels.edit &&

Twinkle.protect.currentProtectionLevels.edit.level,

Twinkle.protect.currentProtectionLevels.stabilize &&

Twinkle.protect.currentProtectionLevels.stabilize.level);

if (computeWeight(protInfo.edit, protInfo.stabilize) > editWeight ||

computeWeight(protInfo.move) > computeWeight(Twinkle.protect.currentProtectionLevels.move &&

Twinkle.protect.currentProtectionLevels.move.level) ||

computeWeight(protInfo.create) > computeWeight(Twinkle.protect.currentProtectionLevels.create &&

Twinkle.protect.currentProtectionLevels.create.level)) {

increase = true;

}

if (increase) {

const originalTextLength = text.length;

text += '\n' + newtag;

if (text.length === originalTextLength) {

const linknode = document.createElement('a');

linknode.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing RPP'));

linknode.appendChild(document.createTextNode('How to fix RPP'));

statusElement.error([ 'Could not find relevant heading on WP:RPP. To fix this problem, please see ', linknode, '.' ]);

return;

}

statusElement.status('Adding new request...');

rppPage.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Requesting ' + params.typename + (params.typename === 'pending changes' ? ' on [[:' : ' of [[:') +

Morebits.pageNameNorm + ']].');

rppPage.setChangeTags(Twinkle.changeTags);

rppPage.setPageText(text);

rppPage.setCreateOption('recreate');

rppPage.save(() => {

// Watch the page being requested

const watchPref = Twinkle.getPref('watchRequestedPages');

// action=watch has no way to rely on user preferences (T262912), so we do it manually.

// The watchdefault pref appears to reliably return '1' (string),

// but that's not consistent among prefs so might as well be "correct"

const watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));

if (watch) {

const watch_query = {

action: 'watch',

titles: mw.config.get('wgPageName'),

token: mw.user.tokens.get('watchToken')

};

// Only add the expiry if page is unwatched or already temporarily watched

if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') {

watch_query.expiry = watchPref;

}

new Morebits.wiki.Api('Adding requested page to watchlist', watch_query).post();

}

});

} else {

const originalTextLength2 = text2.length;

text2 += '\n' + newtag;

if (text2.length === originalTextLength2) {

const linknode2 = document.createElement('a');

linknode2.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing RPP'));

linknode2.appendChild(document.createTextNode('How to fix RPP'));

statusElement.error([ 'Could not find relevant heading on WP:RPP. To fix this problem, please see ', linknode2, '.' ]);

return;

}

statusElement.status('Adding new request...');

rppPage2.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Requesting ' + params.typename + (params.typename === 'pending changes' ? ' on [[:' : ' of [[:') +

Morebits.pageNameNorm + ']].');

rppPage2.setChangeTags(Twinkle.changeTags);

rppPage2.setPageText(text2);

rppPage2.setCreateOption('recreate');

rppPage2.save(() => {

// Watch the page being requested

const watchPref = Twinkle.getPref('watchRequestedPages');

// action=watch has no way to rely on user preferences (T262912), so we do it manually.

// The watchdefault pref appears to reliably return '1' (string),

// but that's not consistent among prefs so might as well be "correct"

const watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));

if (watch) {

const watch_query = {

action: 'watch',

titles: mw.config.get('wgPageName'),

token: mw.user.tokens.get('watchToken')

};

// Only add the expiry if page is unwatched or already temporarily watched

if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') {

watch_query.expiry = watchPref;

}

new Morebits.wiki.Api('Adding requested page to watchlist', watch_query).post();

}

});

}

});

}

};

Twinkle.addInitCallback(Twinkle.protect, 'protect');

}());

//