User:Guycn2/sandbox.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*
== Vandal Cleaner ==
This tool allows you to handle vandalism more easily.
Use it to quickly clean all actions made by a given vandal:
Block the vandal, rollback all edits, delete all pages, hide all edits –
all at the touch of a button.
See full documentation at:
[[:en:User:Guycn2/VandalCleaner]]
See also:
* [[MediaWiki:סקריפטים/107.css]] – for the corresponding style sheet
* [[MediaWiki:סקריפטים/107.js/config.js]] – for i18n and configuration
Skins supported:
Vector (both 2022 and 2010), Monobook, Timeless, and Minerva.
Also fully supported on the mobile interface.
Dependencies:
* mediawiki.api
* mediawiki.util
* user.options
* oojs-ui-core
* oojs-ui-windows
* oojs-ui.styles.icons-accessibility
* oojs-ui.styles.icons-alerts
* oojs-ui.styles.icons-editing-core
* oojs-ui.styles.icons-interactions
* oojs-ui.styles.icons-media
* oojs-ui.styles.icons-moderation
Written by: [[User:Guycn2]]
__________________________________________________
== סקריפט לטיפול מהיר בהשחתות ==
כלי זה מקל על הטיפול בטרולים ובמשחיתים.
ניתן להשתמש בו כדי לנקות מיידית את כל הפעולות שנעשו ע"י משחית מסוים:
חסימת המשחית, שחזור כל העריכות, מחיקת כל הדפים, הסתרת כל העריכות –
כל זאת בלחיצת כפתור.
ראו תיעוד מלא בדף:
[[עזרה:טיפול מהיר בהשחתות]]
ראו גם:
* [[מדיה ויקי:סקריפטים/107.css]] – לגיליון הסגנונות המשויך
* [[מדיה ויקי:סקריפטים/107.js/config.js]] – להודעות מערכת והגדרות
עיצובים נתמכים:
וקטור (2022 ו־2010), מונובוק, מחוץ לזמן, מינרווה נויה.
הסקריפט נתמך במלואו גם בממשק למכשירים ניידים.
נכתב ע"י: [[משתמש:Guycn2]]
*/
( async () => {
'use strict';
const vandal = mw.config.get( 'wgRelevantUserName' );
const editor = mw.config.get( 'wgUserName' );
if (
mw.config.get( 'vandalCleanerLoaded' ) ||
!vandal ||
mw.config.get( 'wgCanonicalSpecialPageName' ) !== 'Contributions' ||
vandal === editor
) {
return;
}
mw.config.set( 'vandalCleanerLoaded', true );
let timer_start;
await mw.loader.using('mediawiki.storage');
let timer_values = mw.storage.getObject('vc_timer');
if (timer_values && Array.isArray(timer_values.values)) timer_values = timer_values.values;
else timer_values = [];
await mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] );
const isAnon = mw.util.isIPAddress( vandal );
const api = new mw.Api();
const vcData = {};
async function checkInitData() {
const params = {
list: 'users',
usprop: isAnon ? 'rights' : 'rights|gender',
ususers: isAnon ? editor : `${ editor }|${ vandal }`
};
const data = await api.get( params );
const res = data.query.users;
const editorRights = res[ 0 ].rights;
const vandalRights = isAnon ? [] : res[ 1 ].rights;
const vandalGender = isAnon ? 'unknown' : res[ 1 ].gender;
if (
vandalRights.includes( 'autopatrol' ) ||
vandalRights.includes( 'patrol' )
) {
return false;
}
vcData.editorRights = editorRights;
vcData.vandalGender = vandalGender;
return true;
}
function i18n( key, args = [] ) {
const messages = mw.config.get( 'vandalCleanerConfig' ).messages;
const lang = mw.config.get( 'wgUserLanguage' );
let output = '';
if ( messages[ lang ] && messages[ lang ][ key ] ) {
output = messages[ lang ][ key ];
} else {
output = messages.en[ key ];
}
if ( typeof output !== 'string' ) {
return output;
}
const isAnonPattern = /{ISANON\|yes=(.*?)\|no=(.*?)\|ISANON-END}/g;
const genderPattern = /{GENDER\|m=(.*?)\|f=(.*?)\|GENDER-END}/g;
output = output
.replace( isAnonPattern, isAnon ? '$1' : '$2' )
.replace( genderPattern, vcData.vandalGender === 'female' ? '$2' : '$1' );
output = convertPlural( output, args );
args.forEach( ( arg, index ) =>
output = output.replaceAll( `$${ index + 1 }`, arg )
);
return output;
}
function convertPlural( str, args ) {
let output = str;
const pattern = /{PLURAL:\$+[0-9]+\|one=(.*?)\|more=(.*?)\|PLURAL-END}/g;
if ( output.match( pattern ) ) {
output.match( pattern ).forEach( match => {
const count = args[ Number( match.match( /[0-9]/ )[ 0 ] ) - 1 ];
let singular = match.split( '|one=' )[ 1 ];
let dual = match.split( '|two=' )[ 1 ];
if ( typeof dual === 'string' ) {
singular = singular.split( '|two=' )[ 0 ];
dual = dual.split( '|more=' )[ 0 ];
} else {
singular = singular.split( '|more=' )[ 0 ];
}
const plural = match.split( '|more=' )[ 1 ].split( '|PLURAL-END}' )[ 0 ];
if ( count === 1 ) {
output = output.replace( match, singular );
} else if ( count === 2 && typeof dual === 'string' ) {
output = output.replace( match, dual );
} else {
output = output.replace( match, plural );
}
} );
}
return output;
}
if ( !( await checkInitData() ) ) {
return;
}
mw.loader.load(
'https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/107.css&action=raw&ctype=text/css',
'text/css'
);
await $.when(
mw.loader.using( [ 'oojs-ui-core', 'oojs-ui.styles.icons-editing-core' ] ),
mw.loader.getScript( 'https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/107.js/config.js&action=raw&ctype=text/javascript' ),
$.ready
);
const contribsBtn = new OO.ui.ButtonWidget( {
flags: 'destructive',
icon: 'editUndo',
id: 'vandal-cleaner-contribs-btn',
label: i18n( 'contribsBtnLabel' ),
title: i18n( 'contribsBtnTooltip' )
} );
contribsBtn.on( 'click', init ).$element.insertBefore( '#mw-content-text' );
async function init() {
function ProcessDialog( config ) {
ProcessDialog.super.call( this, config );
}
await mw.loader.using( [
'user.options',
'oojs-ui-windows',
'oojs-ui.styles.icons-interactions'
] );
OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
ProcessDialog.static.name = 'vandalCleanerDialog';
ProcessDialog.static.title = i18n( 'dialogTitle' );
ProcessDialog.static.size = 'large';
ProcessDialog.static.actions = [
{
action: 'help',
icon: 'help',
label: i18n( 'helpBtnLabel' ),
modes: [ 'firstConfig', 'secondConfig', 'working', 'final' ]
},
{
action: 'cancel',
flags: [ 'close', 'safe' ],
label: i18n( 'cancelBtnTooltip' ),
modes: 'firstConfig'
},
{
action: 'continue',
flags: 'primary',
label: i18n( 'continueBtnLabel' ),
modes: 'firstConfig'
},
{
action: 'back',
flags: [ 'back', 'safe' ],
label: i18n( 'backBtnTooltip' ),
modes: 'secondConfig'
},
{
action: 'run',
flags: [ 'destructive', 'primary' ],
label: i18n( 'runBtnLabel' ),
modes: 'secondConfig'
},
{
action: 'stop',
flags: [ 'destructive', 'safe' ],
icon: 'stop',
label: i18n( 'stopBtnLabel' ),
modes: 'working'
},
{
action: 'cancel',
flags: 'safe',
label: i18n( 'reloadBtnLabel' ),
modes: 'final'
}
];
ProcessDialog.prototype.initialize = function () {
ProcessDialog.super.prototype.initialize.apply( this, arguments );
createStackLayout.call( this );
};
ProcessDialog.prototype.getBodyHeight = () => {};
ProcessDialog.prototype.getSetupProcess = function ( data = {} ) {
return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
.next( function () {
this.actions.setMode( 'firstConfig' );
createFeedbackArea();
createFirstConfigPanel.call( this );
refineErrorDialog( ProcessDialog );
$( document ).on( 'keydown', e => {
if (
e.key === 'Escape' &&
this.stackLayout.currentItem.elementId ===
'vandal-cleaner-working-panel'
) {
this.executeAction( 'stop' );
}
} );
this.actions.on( 'change', () =>
$( '#vandal-cleaner-dialog .oo-ui-window-body' ).scrollTop( 0 )
);
vcData.prettifiedVandal =
`<bdi class="vandal-cleaner-prettified-vandal">
${ mw.util.prettifyIP( vandal ) }
</bdi>`;
}, this );
};
ProcessDialog.prototype.getActionProcess = function ( action ) {
switch ( action ) {
case 'help':
return new OO.ui.Process( () => window.open( i18n( 'helpPageUrl' ) ) );
case 'cancel':
return new OO.ui.Process( function () {
this.close();
}, this );
case 'continue':
return new OO.ui.Process( processFirstConfigInput, this )
.next( getEdits )
.next( createSecondConfigPanel, this )
.next( function () {
this.stackLayout.setItem( this.secondConfigPanel );
this.actions.setMode( 'secondConfig' );
}, this );
case 'back':
return new OO.ui.Process( function () {
this.stackLayout.setItem( this.firstConfigPanel );
this.actions.setMode( 'firstConfig' );
}, this );
case 'run':
return new OO.ui.Process( processSecondConfigInput, this )
.next( createWorkingPanel, this )
.next( function () {
this.stackLayout.setItem( this.workingPanel );
this.actions.setMode( 'working' );
ProcessDialog.static.escapable = false;
runCleaner.call( this, ProcessDialog );
}, this );
case 'stop':
return new OO.ui.Process( function () {
const confirmStopText = new OO.ui.HtmlSnippet( `
<p>${ i18n( 'confirmStopAreYouSure' ) }</p>
<p>${ i18n( 'confirmStopPleaseNote' ) }</p>
` );
OO.ui.confirm( confirmStopText ).then( confirmed => {
if ( confirmed ) {
this.executeAction( 'cancel' );
}
} );
}, this );
default:
return ProcessDialog.super.prototype.getActionProcess.call( this, action );
}
};
ProcessDialog.prototype.getTeardownProcess = function ( data = {} ) {
let isReloadNeeded;
return ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
.first( function () {
api.abort();
isReloadNeeded =
[ 'vandal-cleaner-working-panel', 'vandal-cleaner-final-panel' ]
.includes( this.stackLayout.currentItem.elementId );
}, this )
.next( () => {
vcData.windowManager.destroy();
if ( isReloadNeeded ) {
mw.util.$content.css( 'pointer-events', 'none' ).fadeTo( '_default', 0.3 );
mw.notify( i18n( 'reloadingPage' ), { autoHide: false } );
window.location.reload( true );
}
} );
};
vcData.windowManager = new OO.ui.WindowManager();
$( document.body ).append( vcData.windowManager.$element );
const dialog = new ProcessDialog( { id: 'vandal-cleaner-dialog' } );
// posX and posY are used to prevent the browser from jumping
// to the top of the page when closing the dialog window.
// See the windowManager's "closing" event listener below.
const posX = window.scrollX;
const posY = window.scrollY;
vcData.windowManager.addWindows( [ dialog ] );
vcData.windowManager.openWindow( dialog );
vcData.windowManager.on( 'closing', ( win, closed ) =>
closed.then( () => window.scrollTo( posX, posY ) )
);
}
function createStackLayout() {
this.firstConfigPanel = new OO.ui.PanelLayout( {
classes: [ 'vandal-cleaner-panel' ],
expanded: false,
id: 'vandal-cleaner-first-config-panel',
padded: true
} );
this.secondConfigPanel = new OO.ui.PanelLayout( {
classes: [ 'vandal-cleaner-panel' ],
expanded: false,
id: 'vandal-cleaner-second-config-panel',
padded: true
} );
this.workingPanel = new OO.ui.PanelLayout( {
classes: [ 'vandal-cleaner-panel' ],
expanded: false,
id: 'vandal-cleaner-working-panel',
padded: true
} );
this.finalPanel = new OO.ui.PanelLayout( {
classes: [ 'vandal-cleaner-panel' ],
expanded: false,
id: 'vandal-cleaner-final-panel',
padded: true
} );
this.stackLayout = new OO.ui.StackLayout( {
items: [
this.firstConfigPanel,
this.secondConfigPanel,
this.workingPanel,
this.finalPanel
]
} );
this.$body.append( this.stackLayout.$element );
}
function createFeedbackArea() {
const feedbackIcon = new OO.ui.IconWidget( {
icon: 'feedback',
id: 'vandal-cleaner-feedback-icon'
} );
const feedbackLabel = new OO.ui.LabelWidget( {
id: 'vandal-cleaner-feedback-label',
label: new OO.ui.HtmlSnippet(
i18n( 'feedbackLabel', [ i18n( 'feedbackPageUrl' ) ] )
)
} );
const feedbackLayout = new OO.ui.HorizontalLayout( {
id: 'vandal-cleaner-feedback-layout',
items: [ feedbackIcon, feedbackLabel ]
} );
feedbackLayout.$element
.appendTo( '#vandal-cleaner-dialog .oo-ui-window-foot' );
}
function createFirstConfigPanel() {
const $broomImg = $( '<img>' ).attr( {
alt: i18n( 'introWelcome' ),
id: 'vandal-cleaner-broom-img',
src: 'https://upload.wikimedia.org/wikipedia/commons/f/f5/Broom_Icon_(template-icon).svg'
} );
const $welcomeText = $( '<div>' )
.attr( 'id', 'vandal-cleaner-welcome-text' )
.append(
$( '<p>' ).text( i18n( 'introWelcome' ) ),
$( '<p>' ).text( i18n( 'introToolPurpose' ) ),
$( '<p>' ).html( i18n( 'introReadHelp', [ i18n( 'helpPageUrl' ) ] ) ),
$( '<p>' ).text( i18n( 'introSetOptions' ) )
);
const $welcomeContainer = $( '<div>' )
.attr( 'id', 'vandal-cleaner-welcome-container' )
.addClass( 'vandal-cleaner-fancy-border-bottom' )
.append( $broomImg, $welcomeText );
vcData.inputs = {};
let prefilledSummary = getPref( 'summary' );
if ( !prefilledSummary && !getPref( 'useEmptySummary' ) ) {
prefilledSummary = i18n( 'defaultSummary' );
}
vcData.inputs.summaryInput = new OO.ui.TextInputWidget( {
id: 'vandal-cleaner-summary-input',
maxLength: 500,
validate: value => value.length <= 500,
value: prefilledSummary
} );
const summaryField = new OO.ui.FieldLayout(
vcData.inputs.summaryInput,
{
align: 'top',
help: i18n( 'summaryHelp' ),
helpInline: true,
label: $( '<span>' )
.addClass( 'vandal-cleaner-prominent-label' )
.text( i18n( 'summaryLabel' ) )
}
);
vcData.inputs.rememberSummaryCbx = new OO.ui.CheckboxInputWidget( {
classes: [ 'vandal-cleaner-checkbox' ]
} );
const rememberSummaryField = new OO.ui.FieldLayout(
vcData.inputs.rememberSummaryCbx,
{
align: 'inline',
id: 'vandal-cleaner-remember-summary-field',
label: i18n( 'rememberSummaryLabel' )
}
);
const summaryFieldset = new OO.ui.FieldsetLayout( {
items: [ summaryField, rememberSummaryField ]
} );
const advancedOptionsBtn = new OO.ui.ButtonWidget( {
flags: 'progressive',
framed: false,
id: 'vandal-cleaner-advanced-options-btn',
indicator: 'down',
label: i18n( 'advancedOptionsLabel' )
} );
vcData.inputs.numOfDaysInput = new OO.ui.NumberInputWidget( {
classes: [ 'vandal-cleaner-number-input' ],
max: 60,
min: 1,
step: 1,
value: getPref( 'numOfDays' ) || '30'
} );
vcData.inputs.numOfDaysInput.$element
.find( 'input' ).attr( 'required', true );
const numOfDaysHelp = new OO.ui.PopupButtonWidget( {
classes: [ 'oo-ui-fieldLayout-help' ],
framed: false,
icon: 'info',
invisibleLabel: true,
label: i18n( 'numOfDaysHelpTooltip' ),
popup: {
$content: $( '<p>' )
.addClass( 'vandal-cleaner-field-help-popup-text' )
.text( i18n( 'numOfDaysHelpText' ) ),
align: 'backwards',
padded: true
}
} );
const numOfDaysField = new OO.ui.FieldLayout(
vcData.inputs.numOfDaysInput,
{
align: 'top',
label: $( '<span>' )
.addClass( 'vandal-cleaner-prominent-label' )
.text( i18n( 'numOfDaysLabel' ) )
}
);
numOfDaysField.$element.find( '.oo-ui-fieldLayout-header' )
.prepend( numOfDaysHelp.$element );
vcData.inputs.rememberNumOfDaysCbx = new OO.ui.CheckboxInputWidget( {
classes: [ 'vandal-cleaner-checkbox' ]
} );
const rememberNumOfDaysField = new OO.ui.FieldLayout(
vcData.inputs.rememberNumOfDaysCbx,
{
align: 'inline',
label: i18n( 'rememberNumOfDaysLabel' )
}
);
const numOfDaysFieldset = new OO.ui.FieldsetLayout( {
items: [ numOfDaysField, rememberNumOfDaysField ]
} );
vcData.inputs.numOfActionsInput = new OO.ui.NumberInputWidget( {
classes: [ 'vandal-cleaner-number-input' ],
max: 300,
min: 1,
step: 1,
value: getPref( 'numOfActions' ) || '100'
} );
vcData.inputs.numOfActionsInput.$element
.find( 'input' ).attr( 'required', true );
const numOfActionsHelp = new OO.ui.PopupButtonWidget( {
classes: [ 'oo-ui-fieldLayout-help' ],
framed: false,
icon: 'info',
invisibleLabel: true,
label: i18n( 'numOfActionsHelpTooltip' ),
popup: {
$content: $( '<p>' )
.addClass( 'vandal-cleaner-field-help-popup-text' )
.text( i18n( 'numOfActionsHelpText' ) ),
align: 'backwards',
padded: true
}
} );
const numOfActionsField = new OO.ui.FieldLayout(
vcData.inputs.numOfActionsInput,
{
align: 'top',
label: $( '<span>' )
.addClass( 'vandal-cleaner-prominent-label' )
.text( i18n( 'numOfActionsLabel' ) )
}
);
numOfActionsField.$element.find( '.oo-ui-fieldLayout-header' )
.prepend( numOfActionsHelp.$element );
vcData.inputs.rememberNumOfActionsCbx = new OO.ui.CheckboxInputWidget( {
classes: [ 'vandal-cleaner-checkbox' ]
} );
const rememberNumOfActionsField = new OO.ui.FieldLayout(
vcData.inputs.rememberNumOfActionsCbx,
{
align: 'inline',
label: i18n( 'rememberNumOfActionsLabel' )
}
);
const numOfActionsFieldset = new OO.ui.FieldsetLayout( {
items: [ numOfActionsField, rememberNumOfActionsField ]
} );
vcData.inputs.skipInitialConfigCbx = new OO.ui.CheckboxInputWidget( {
classes: [ 'vandal-cleaner-checkbox' ],
id: 'vandal-cleaner-skip-initial-config-checkbox',
selected: Boolean( getPref( 'skipInitialConfig' ) )
} );
const skipInitialConfigField = new OO.ui.FieldLayout(
vcData.inputs.skipInitialConfigCbx,
{
align: 'inline',
help: i18n( 'skipInitialConfigHelp' ),
helpInline: true,
id: 'vandal-cleaner-skip-initial-config-field',
label: i18n( 'skipInitialConfigLabel' )
}
);
const additionalOptionsFieldset = new OO.ui.FieldsetLayout( {
items: [ skipInitialConfigField ],
label: $( '<span>' )
.attr( 'id', 'vandal-cleaner-additional-options-label' )
.text( i18n( 'additionalOptionsLabel' ) )
} );
const $advancedOptionsContainer = $( '<div>' )
.attr( 'id', 'vandal-cleaner-advanced-options-container' )
.append(
numOfDaysFieldset.$element,
numOfActionsFieldset.$element,
additionalOptionsFieldset.$element
);
this.firstConfigPanel.$element.append(
$welcomeContainer,
summaryFieldset.$element,
advancedOptionsBtn.$element,
$advancedOptionsContainer
);
[
vcData.inputs.summaryInput,
vcData.inputs.numOfDaysInput,
vcData.inputs.numOfActionsInput
].forEach( widget =>
widget.on( 'enter', () => this.executeAction( 'continue' ) )
);
advancedOptionsBtn.on( 'click', () =>
toggleAdvancedOptions( $advancedOptionsContainer, advancedOptionsBtn )
);
if ( getPref( 'skipInitialConfig' ) ) {
this.executeAction( 'continue' );
}
}
function toggleAdvancedOptions( $advancedOptionsContainer, advancedOptionsBtn ) {
$advancedOptionsContainer.slideToggle( 'fast', () => {
if ( advancedOptionsBtn.getIndicator() === 'down' ) {
scrollIntoViewIfNeeded(
$advancedOptionsContainer,
-225,
advancedOptionsBtn.$element,
'start'
);
advancedOptionsBtn.setIndicator( 'up' );
} else {
advancedOptionsBtn.setIndicator( 'down' );
}
} );
}
async function processFirstConfigInput() {
const defer = $.Deferred();
try {
await $.when(
vcData.inputs.summaryInput.getValidity(),
vcData.inputs.numOfDaysInput.getValidity(),
vcData.inputs.numOfActionsInput.getValidity()
);
} catch ( e ) {
document.activeElement.blur();
return defer.reject( new OO.ui.Error( i18n( 'invalidInput' ) ) );
}
vcData.summary = vcData.inputs.summaryInput.getValue().trim();
vcData.numOfDays = vcData.inputs.numOfDaysInput.getNumericValue();
vcData.numOfActions = vcData.inputs.numOfActionsInput.getNumericValue();
vcData.earliestEditTimestamp =
subtractDaysFromTimestamp( mw.now(), vcData.numOfDays );
if ( vcData.inputs.rememberSummaryCbx.isSelected() ) {
setPref( 'summary', vcData.summary );
setPref( 'useEmptySummary', vcData.summary === '' ? 1 : 0 );
}
if ( vcData.inputs.rememberNumOfDaysCbx.isSelected() ) {
setPref( 'numOfDays', vcData.numOfDays );
}
if ( vcData.inputs.rememberNumOfActionsCbx.isSelected() ) {
setPref( 'numOfActions', vcData.numOfActions );
}
const isSkipInitialConfigSelected =
vcData.inputs.skipInitialConfigCbx.isSelected();
if ( isSkipInitialConfigSelected && !getPref( 'skipInitialConfig' ) ) {
setPref( 'skipInitialConfig', 1 );
}
if ( !isSkipInitialConfigSelected && getPref( 'skipInitialConfig' ) ) {
setPref( 'skipInitialConfig', 0 );
}
return defer.resolve();
}
function subtractDaysFromTimestamp( timestamp, days ) {
const result = new Date( timestamp );
result.setDate( result.getDate() - days );
return `${ result.toISOString().split( '.' )[ 0 ] }Z`;
}
async function getEdits() {
await $.when(
getRevertibleEdits(),
getDeletablePages(),
getHideableRevs(),
getParsedSummary()
);
}
async function getRevertibleEdits() {
if ( !vcData.editorRights.includes( 'rollback' ) ) {
vcData.revertibleEditsCount = 0;
return;
}
const params = {
list: 'usercontribs',
uclimit: vcData.numOfActions + 1,
ucend: vcData.earliestEditTimestamp,
ucuser: vandal,
ucprop: 'title|timestamp',
ucshow: '!new|top'
};
const data = await api.get( params );
vcData.revertibleEdits = data.query.usercontribs;
if ( vcData.revertibleEdits.length === vcData.numOfActions + 1 ) {
vcData.tooManyRevertibleEdits = true;
vcData.revertibleEdits.pop();
} else {
vcData.tooManyRevertibleEdits = false;
}
vcData.revertibleEditsCount = vcData.revertibleEdits.length;
}
async function getDeletablePages() {
if ( !vcData.editorRights.includes( 'delete' ) ) {
vcData.deletablePagesCount = 0;
return;
}
const params = {
list: 'usercontribs',
uclimit: vcData.numOfActions + 1,
ucend: vcData.earliestEditTimestamp,
ucuser: vandal,
ucprop: 'title',
ucshow: 'new'
};
const data = await api.get( params );
vcData.deletablePages = data.query.usercontribs;
if ( vcData.deletablePages.length === vcData.numOfActions + 1 ) {
vcData.tooManyDeletablePages = true;
vcData.deletablePages.pop();
} else {
vcData.tooManyDeletablePages = false;
}
vcData.deletablePagesCount = vcData.deletablePages.length;
}
async function getHideableRevs() {
if ( !vcData.editorRights.includes( 'deleterevision' ) ) {
vcData.hideableRevsCount = 0;
return;
}
const params = {
list: 'usercontribs',
uclimit: vcData.numOfActions + 1,
ucend: vcData.earliestEditTimestamp,
ucuser: vandal,
ucprop: 'ids|title'
};
const data = await api.get( params );
vcData.hideableRevs = data.query.usercontribs;
if ( vcData.hideableRevs.length === vcData.numOfActions + 1 ) {
vcData.tooManyHideableRevs = true;
vcData.hideableRevs.pop();
} else {
vcData.tooManyHideableRevs = false;
}
vcData.hideableRevsCount = vcData.hideableRevs.length;
}
async function getParsedSummary() {
if ( vcData.summary === '' ) {
vcData.parsedSummary = '';
return;
}
const params = {
action: 'parse',
summary: vcData.summary,
prop: ''
};
const data = await api.get( params );
vcData.parsedSummary = data.parse.parsedsummary[ '*' ];
}
async function createSecondConfigPanel() {
await mw.loader.using( [
'oojs-ui.styles.icons-accessibility',
'oojs-ui.styles.icons-alerts',
'oojs-ui.styles.icons-moderation'
] );
this.secondConfigPanel.$element.empty();
const earliestEditDate =
new Date( vcData.earliestEditTimestamp ).toLocaleString(
i18n( 'dateFormat' ),
{
dateStyle: 'long',
timeStyle: 'short',
hourCycle: 'h23'
}
);
const reviewConfigHtml = new OO.ui.HtmlSnippet( `
<p>${ i18n( 'reviewConfigHeading' ) }</p>
<ul id="vandal-cleaner-config-list">
<li>
${ i18n( 'reviewConfigVandal' ) }
<strong>${ vcData.prettifiedVandal }</strong>
</li>
<li>
${ i18n( 'reviewConfigNumOfDays' ) }
<strong>${ vcData.numOfDays }</strong><br />
<span id="vandal-cleaner-earliest-edit-date">
${ i18n( 'reviewConfigEarliestDate', [ earliestEditDate ] ) }
</span>
</li>
<li>
${ i18n( 'reviewConfigNumOfActions' ) }
<strong>${ vcData.numOfActions }</strong>
</li>
<li>
${ i18n( 'reviewConfigSummary' ) }
${ vcData.parsedSummary
? `<span class="comment">${ vcData.parsedSummary }</span>`
: i18n( 'reviewConfigNoSummary' )
}
</li>
</ul>
` );
const reviewConfigMsg = new OO.ui.MessageWidget( {
icon: 'lightbulb',
id: 'vandal-cleaner-review-config-msg',
label: reviewConfigHtml,
type: 'notice'
} );
const $configList =
reviewConfigMsg.$element.find( '#vandal-cleaner-config-list' );
const isMobile = mw.config.get( 'skin' ) === 'minerva';
let shouldConfigListBeHidden;
if ( isMobile ) {
shouldConfigListBeHidden = getPref( 'hideConfigListOnMobile' );
} else {
shouldConfigListBeHidden = getPref( 'hideConfigListOnDesktop' );
}
if ( shouldConfigListBeHidden ) {
$configList.hide();
skipReviewConfigMsg( reviewConfigMsg.$element );
}
const configListToggleBtn = new OO.ui.ButtonWidget( {
framed: false,
id: 'vandal-cleaner-config-list-toggle-btn',
indicator: shouldConfigListBeHidden ? 'down' : 'up',
invisibleLabel: true,
label: shouldConfigListBeHidden
? i18n( 'configListShow' )
: i18n( 'configListHide' ),
title: shouldConfigListBeHidden
? i18n( 'configListShow' )
: i18n( 'configListHide' )
} );
configListToggleBtn.on( 'click', () =>
toggleConfigList( $configList, configListToggleBtn, isMobile )
);
configListToggleBtn.$element.prependTo( reviewConfigMsg.$element );
const canRollback = vcData.editorRights.includes( 'rollback' );
vcData.inputs.rollbackTog = new OO.ui.ToggleSwitchWidget( {
classes: [ 'vandal-cleaner-action-tog' ],
disabled: !canRollback,
value: canRollback
} );
const $rollbackNoEdits = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'rollbackNoEdits' ) );
const $rollbackTooMany = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'rollbackTooMany', [ vcData.revertibleEditsCount ] ) );
const $rollbackNoPermission = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'rollbackNoPermission' ) );
const rollbackField = new OO.ui.FieldLayout(
vcData.inputs.rollbackTog,
{
classes: [ 'vandal-cleaner-action-field' ],
help:
`${ i18n( 'rollbackHelp', [ vcData.numOfDays ] ) }
${ vcData.revertibleEditsCount > 0
? i18n( 'rollbackHelpCount', [ vcData.revertibleEditsCount ] )
: ''
}`,
helpInline: true,
label: $( '<span>' )
.addClass( [
'vandal-cleaner-action-label',
'vandal-cleaner-prominent-label'
] )
.text( i18n( 'rollbackLabel' ) ),
notices: [ $rollbackNoEdits ],
warnings: [ $rollbackTooMany, $rollbackNoPermission ]
}
);
const rollbackFieldset = new OO.ui.FieldsetLayout( {
classes: [
'vandal-cleaner-action-fieldset',
'vandal-cleaner-fancy-border-bottom'
],
icon: 'editUndo',
items: [ rollbackField ]
} );
if ( canRollback ) {
rollbackFieldset.$element.on( 'click', e =>
simulateLabelClick( e, vcData.inputs.rollbackTog )
);
const $rollbackFieldExtra =
rollbackField.$element.find( '.oo-ui-fieldLayout-messages' );
vcData.inputs.rollbackTog.on( 'change', () =>
toggleFieldExtra( $rollbackFieldExtra )
);
if ( vcData.revertibleEditsCount === 0 ) {
displayFieldMsg( $rollbackNoEdits );
}
if ( vcData.tooManyRevertibleEdits ) {
displayFieldMsg( $rollbackTooMany );
}
} else {
displayFieldMsg( $rollbackNoPermission );
}
const canDelete = vcData.editorRights.includes( 'delete' );
vcData.inputs.deleteTog = new OO.ui.ToggleSwitchWidget( {
classes: [ 'vandal-cleaner-action-tog' ],
disabled: !canDelete,
value: canDelete
} );
const $deleteNoPages = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'deleteNoPages' ) );
const $deleteTooMany = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'deleteTooMany', [ vcData.deletablePagesCount ] ) );
const $deleteNoPermission = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'deleteNoPermission' ) );
const deleteField = new OO.ui.FieldLayout(
vcData.inputs.deleteTog,
{
classes: [ 'vandal-cleaner-action-field' ],
help:
`${ i18n( 'deleteHelp', [ vcData.numOfDays ] ) }
${ vcData.deletablePagesCount > 0
? i18n( 'deleteHelpCount', [ vcData.deletablePagesCount ] )
: ''
}`,
helpInline: true,
label: $( '<span>' )
.addClass( [
'vandal-cleaner-action-label',
'vandal-cleaner-prominent-label'
] )
.text( i18n( 'deleteLabel' ) ),
notices: [ $deleteNoPages ],
warnings: [ $deleteTooMany, $deleteNoPermission ]
}
);
const deleteFieldset = new OO.ui.FieldsetLayout( {
classes: [
'vandal-cleaner-action-fieldset',
'vandal-cleaner-fancy-border-bottom'
],
icon: 'trash',
items: [ deleteField ]
} );
if ( canDelete ) {
deleteFieldset.$element.on( 'click', e =>
simulateLabelClick( e, vcData.inputs.deleteTog )
);
const $deleteFieldExtra =
deleteField.$element.find( '.oo-ui-fieldLayout-messages' );
vcData.inputs.deleteTog.on( 'change', () =>
toggleFieldExtra( $deleteFieldExtra )
);
if ( vcData.deletablePagesCount === 0 ) {
displayFieldMsg( $deleteNoPages );
}
if ( vcData.tooManyDeletablePages ) {
displayFieldMsg( $deleteTooMany );
}
} else {
displayFieldMsg( $deleteNoPermission );
}
const canBlock = vcData.editorRights.includes( 'block' );
vcData.inputs.blockTog = new OO.ui.ToggleSwitchWidget( {
classes: [ 'vandal-cleaner-action-tog' ],
disabled: !canBlock,
value: canBlock
} );
const $blockNoPermission = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'blockNoPermission' ) );
const blockField = new OO.ui.FieldLayout(
vcData.inputs.blockTog,
{
classes: [ 'vandal-cleaner-action-field' ],
help: i18n( 'blockHelp' ),
helpInline: true,
label: $( '<span>' )
.addClass( [
'vandal-cleaner-action-label',
'vandal-cleaner-prominent-label'
] )
.text( i18n( 'blockLabel' ) ),
warnings: [ $blockNoPermission ]
}
);
const blockFieldset = new OO.ui.FieldsetLayout( {
classes: [
'vandal-cleaner-action-fieldset',
'vandal-cleaner-fancy-border-bottom'
],
icon: 'block',
items: [ blockField ]
} );
if ( canBlock ) {
blockFieldset.$element.on( 'click', e =>
simulateLabelClick( e, vcData.inputs.blockTog )
);
const $blockFieldExtra =
blockField.$element.find( '.oo-ui-fieldLayout-messages' );
vcData.inputs.blockTog.on( 'change', () =>
toggleFieldExtra( $blockFieldExtra )
);
const blockOptions = [
{ data: '2 hours' },
{ data: '1 day' },
{ data: '3 days' },
{ data: '1 week' },
{ data: '2 weeks' },
{ data: '1 month' },
{ data: '3 months' },
{ data: '6 months' },
{ data: '1 year' },
{ data: 'infinite' }
];
const blockOptionsMsg = i18n( 'blockOptions' );
blockOptions.forEach( option =>
option.label = blockOptionsMsg[ option.data ]
);
vcData.inputs.blockDurationDropdown = new OO.ui.DropdownInputWidget( {
id: 'vandal-cleaner-block-duration-dropdown',
options: blockOptions,
value: isAnon ? '1 day' : 'infinite'
} ).toggle( false );
const blockDurationLabel = new OO.ui.LabelWidget( {
id: 'vandal-cleaner-block-duration-label',
input: vcData.inputs.blockDurationDropdown,
label: new OO.ui.HtmlSnippet( `
${ i18n( 'blockDurationLabel' ) }
${ blockOptionsMsg[ vcData.inputs.blockDurationDropdown.getValue() ] }
(<a id="vandal-cleaner-block-duration-change-btn"
href="#" role="button">${ i18n( 'blockDurationChange' ) }</a>)
` )
} );
blockDurationLabel.$element
.find( '#vandal-cleaner-block-duration-change-btn' )
.on( 'click', e => {
e.preventDefault();
blockDurationLabel.setLabel( i18n( 'blockDurationLabel' ) );
vcData.inputs.blockDurationDropdown.toggle( true );
} );
const blockDurationLayout = new OO.ui.HorizontalLayout( {
id: 'vandal-cleaner-block-duration-layout',
items: [ blockDurationLabel, vcData.inputs.blockDurationDropdown ]
} );
blockDurationLayout.$element.prependTo( $blockFieldExtra )
.on( 'click', e => e.stopPropagation() );
} else {
displayFieldMsg( $blockNoPermission );
}
const canRevDelete = vcData.editorRights.includes( 'deleterevision' );
vcData.inputs.revDeleteTog = new OO.ui.ToggleSwitchWidget( {
classes: [ 'vandal-cleaner-action-tog' ],
disabled: !canRevDelete,
value: false
} );
const $revDeleteNoRevs = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'revDeleteNoRevs' ) );
const $revDeleteTooMany = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'revDeleteTooMany', [ vcData.hideableRevsCount ] ) );
const $revDeleteNoPermission = $( '<div>' )
.addClass( 'vandal-cleaner-field-msg' )
.text( i18n( 'revDeleteNoPermission' ) );
const revDeleteField = new OO.ui.FieldLayout(
vcData.inputs.revDeleteTog,
{
classes: [ 'vandal-cleaner-action-field' ],
help:
`${ i18n( 'revDeleteHelp', [ vcData.numOfDays ] ) }
${ vcData.hideableRevsCount > 0
? i18n( 'revDeleteHelpCount', [ vcData.hideableRevsCount ] )
: ''
}`,
helpInline: true,
label: $( '<span>' )
.addClass( [
'vandal-cleaner-action-label',
'vandal-cleaner-prominent-label'
] )
.text( i18n( 'revDeleteLabel' ) ),
notices: [ $revDeleteNoRevs ],
warnings: [ $revDeleteTooMany, $revDeleteNoPermission ]
}
);
const revDeleteFieldset = new OO.ui.FieldsetLayout( {
classes: [ 'vandal-cleaner-action-fieldset' ],
icon: 'eyeClosed',
items: [ revDeleteField ]
} );
if ( canRevDelete ) {
revDeleteFieldset.$element.on( 'click', e =>
simulateLabelClick( e, vcData.inputs.revDeleteTog )
);
const $revDeleteFieldExtra =
revDeleteField.$element.find( '.oo-ui-fieldLayout-messages' ).hide();
vcData.inputs.revDeleteTog.on( 'change', () =>
toggleFieldExtra( $revDeleteFieldExtra )
);
if ( vcData.hideableRevsCount === 0 ) {
displayFieldMsg( $revDeleteNoRevs );
}
if ( vcData.tooManyHideableRevs ) {
displayFieldMsg( $revDeleteTooMany );
}
vcData.inputs.hideContentsCbx = new OO.ui.CheckboxInputWidget( {
classes: [ 'vandal-cleaner-checkbox' ],
selected: true
} );
const hideContentsField = new OO.ui.FieldLayout(
vcData.inputs.hideContentsCbx,
{
align: 'inline',
label: i18n( 'hideContentsLabel' )
}
);
vcData.inputs.hideSummariesCbx = new OO.ui.CheckboxInputWidget( {
classes: [ 'vandal-cleaner-checkbox' ]
} );
const hideSummariesField = new OO.ui.FieldLayout(
vcData.inputs.hideSummariesCbx,
{
align: 'inline',
label: i18n( 'hideSummariesLabel' )
}
);
const revDeleteOptionsLayout = new OO.ui.HorizontalLayout( {
id: 'vandal-cleaner-revdelete-options-layout',
items: [ hideContentsField, hideSummariesField ]
} );
revDeleteOptionsLayout.$element.prependTo( $revDeleteFieldExtra )
.on( 'click', e => e.stopPropagation() );
} else {
displayFieldMsg( $revDeleteNoPermission );
}
const $actionSelectionHeader = $( '<header>' )
.attr( 'id', 'vandal-cleaner-action-selection-header' )
.addClass( 'vandal-cleaner-fancy-border-bottom' )
.text( i18n( 'actionSelectionHeading' ) );
const $actionSelectionContainer = $( '<div>' )
.attr( 'id', 'vandal-cleaner-action-selection-container' )
.append(
$actionSelectionHeader,
rollbackFieldset.$element,
deleteFieldset.$element,
blockFieldset.$element,
revDeleteFieldset.$element
);
this.secondConfigPanel.$element.append(
reviewConfigMsg.$element,
$actionSelectionContainer
);
}
function skipReviewConfigMsg( $msgElement ) {
const observer = new IntersectionObserver( entries => {
if ( entries[ 0 ].isIntersecting ) {
document.querySelector( '#vandal-cleaner-dialog .oo-ui-window-body' )
.scroll( {
top: $msgElement.innerHeight() - 8,
behavior: 'smooth'
} );
observer.unobserve( $msgElement[ 0 ] );
}
} );
observer.observe( $msgElement[ 0 ] );
}
function toggleConfigList( $configList, configListToggleBtn, isMobile ) {
$configList.slideToggle( 'fast', () => {
if ( configListToggleBtn.getIndicator() === 'down' ) {
configListToggleBtn
.setIndicator( 'up' )
.setLabel( i18n( 'configListHide' ) )
.setTitle( i18n( 'configListHide' ) );
setPref(
isMobile ? 'hideConfigListOnMobile' : 'hideConfigListOnDesktop',
0
);
} else {
configListToggleBtn
.setIndicator( 'down' )
.setLabel( i18n( 'configListShow' ) )
.setTitle( i18n( 'configListShow' ) );
setPref(
isMobile ? 'hideConfigListOnMobile' : 'hideConfigListOnDesktop',
1
);
}
} );
}
function simulateLabelClick( e, targetOouiWidget ) {
if (
e.pointerType !== 'mouse' ||
e.target.nodeName === 'LABEL' ||
e.target.classList.contains( 'vandal-cleaner-action-label' )
) {
return;
}
targetOouiWidget.simulateLabelClick();
}
function toggleFieldExtra( $element ) {
$element.slideToggle( 'fast', () =>
scrollIntoViewIfNeeded(
$element,
45,
$element.children( ':visible:last' ),
'end'
)
);
}
function displayFieldMsg( $msg ) {
$msg.closest( '.oo-ui-messageWidget' ).css( 'display', 'block' );
}
function processSecondConfigInput() {
const defer = $.Deferred();
vcData.isRollbackChecked = vcData.inputs.rollbackTog.getValue();
vcData.isDeleteChecked = vcData.inputs.deleteTog.getValue();
vcData.isBlockChecked = vcData.inputs.blockTog.getValue();
if ( vcData.isBlockChecked ) {
vcData.blockDuration = vcData.inputs.blockDurationDropdown.getValue();
}
vcData.isRevDeleteChecked = vcData.inputs.revDeleteTog.getValue();
if ( vcData.isRevDeleteChecked ) {
vcData.isHideContentsChecked = vcData.inputs.hideContentsCbx.isSelected();
vcData.isHideSummariesChecked = vcData.inputs.hideSummariesCbx.isSelected();
if ( !vcData.isHideContentsChecked && !vcData.isHideSummariesChecked ) {
vcData.isRevDeleteChecked = false;
}
}
if (
vcData.isRollbackChecked ||
vcData.isDeleteChecked ||
vcData.isBlockChecked ||
vcData.isRevDeleteChecked
) {
return defer.resolve();
} else {
return defer.reject( new OO.ui.Error( i18n( 'noActions' ) ) );
}
}
async function createWorkingPanel() {
await mw.loader.using( 'oojs-ui.styles.icons-media' );
vcData.inputs.progressBar = new OO.ui.ProgressBarWidget( {
classes: [ 'vandal-cleaner-progress-bar' ],
progress: 0
} ).pushPending();
const progressField = new OO.ui.FieldLayout(
vcData.inputs.progressBar,
{
align: 'top',
id: 'vandal-cleaner-progress-field',
label: $( '<span>' )
.addClass( 'vandal-cleaner-prominent-label' )
.text( i18n( 'progressLabel' ) )
}
);
const progressFieldset = new OO.ui.FieldsetLayout( {
items: [ progressField ]
} );
vcData.statusPane = {};
vcData.statusPane.$blockStatus = $( '<p>' )
.addClass( 'vandal-cleaner-status' )
.attr( 'data-percent', '0' )
.append(
$( '<span>' ).html(
i18n( 'blockStatus', [ vcData.prettifiedVandal ] )
)
);
const blockError = new OO.ui.MessageWidget( {
classes: [ 'vandal-cleaner-error' ],
inline: true,
label: i18n( 'blockError' ),
type: 'error'
} );
vcData.statusPane.$blockError = blockError.$element;
vcData.statusPane.$blockContainer = $( '<div>' )
.addClass( 'vandal-cleaner-status-action-container' )
.append(
vcData.statusPane.$blockStatus,
vcData.statusPane.$blockError
);
vcData.statusPane.$rollbackStatus = $( '<p>' )
.addClass( 'vandal-cleaner-status' )
.attr( 'data-percent', '0' )
.append(
$( '<span>' ).html(
i18n( 'rollbackStatus', [ vcData.prettifiedVandal ] )
)
);
const rollbackError = new OO.ui.MessageWidget( {
classes: [ 'vandal-cleaner-error' ],
inline: true,
label: i18n( 'rollbackError' ),
type: 'error'
} );
vcData.statusPane.$rollbackError = rollbackError.$element;
vcData.statusPane.$rollbackContainer = $( '<div>' )
.addClass( 'vandal-cleaner-status-action-container' )
.append(
vcData.statusPane.$rollbackStatus,
vcData.statusPane.$rollbackError
);
vcData.statusPane.$deleteStatus = $( '<p>' )
.addClass( 'vandal-cleaner-status' )
.attr( 'data-percent', '0' )
.append(
$( '<span>' ).html(
i18n( 'deleteStatus', [ vcData.prettifiedVandal ] )
)
);
const deleteError = new OO.ui.MessageWidget( {
classes: [ 'vandal-cleaner-error' ],
inline: true,
label: i18n( 'deleteError' ),
type: 'error'
} );
vcData.statusPane.$deleteError = deleteError.$element;
vcData.statusPane.$deleteContainer = $( '<div>' )
.addClass( 'vandal-cleaner-status-action-container' )
.append(
vcData.statusPane.$deleteStatus,
vcData.statusPane.$deleteError
);
vcData.statusPane.$revDeleteStatus = $( '<p>' )
.addClass( 'vandal-cleaner-status' )
.attr( 'data-percent', '0' )
.append(
$( '<span>' ).html(
i18n( 'revDeleteStatus', [ vcData.prettifiedVandal ] )
)
);
const revDeleteError = new OO.ui.MessageWidget( {
classes: [ 'vandal-cleaner-error' ],
inline: true,
label: i18n( 'revDeleteError' ),
type: 'error'
} );
vcData.statusPane.$revDeleteError = revDeleteError.$element;
vcData.statusPane.$revDeleteContainer = $( '<div>' )
.addClass( 'vandal-cleaner-status-action-container' )
.append(
vcData.statusPane.$revDeleteStatus,
vcData.statusPane.$revDeleteError
);
vcData.statusPane.$leftoverVandalismStatus = $( '<p>' )
.addClass( 'vandal-cleaner-status' )
.attr( 'data-percent', '0' )
.append( $( '<span>' ).text( i18n( 'leftoverVandalismStatus' ) ) );
vcData.statusPane.$leftoverVandalismContainer = $( '<div>' )
.addClass( [
'vandal-cleaner-status-action-container',
'vandal-cleaner-fancy-border-top'
] )
.append( vcData.statusPane.$leftoverVandalismStatus );
const $statusPane = $( '<div>' )
.attr( 'id', 'vandal-cleaner-status-pane' )
.append(
vcData.statusPane.$blockContainer,
vcData.statusPane.$rollbackContainer,
vcData.statusPane.$deleteContainer,
vcData.statusPane.$revDeleteContainer,
vcData.statusPane.$leftoverVandalismContainer
);
this.workingPanel.$element.append(
progressFieldset.$element,
$statusPane
);
}
async function runCleaner( ProcessDialog ) {
vcData.totalTasksCount = getTotalTasksCount();
vcData.completedTasksCount = 0;
vcData.hasFailedActions = false;
vcData.doesTagExist = await checkIfTagExists();
await doBlock();
await doRollback();
await doDelete();
await doRevDelete();
await checkForLeftoverVandalism();
vcData.inputs.progressBar.popPending();
await waitBeforeProceeding( 180 );
this.workingPanel.$element.fadeTo( '_default', 0.15, () => {
createFinalPanel.call( this );
adaptFinalPanelHeight.call( this, ProcessDialog );
ProcessDialog.static.escapable = true;
} );
}
function getTotalTasksCount() {
let count = 0;
if ( vcData.isBlockChecked ) {
count++;
}
if ( vcData.isRollbackChecked ) {
if ( vcData.revertibleEditsCount === 0 ) {
count++;
} else {
count += vcData.revertibleEditsCount;
}
}
if ( vcData.isDeleteChecked ) {
if ( vcData.deletablePagesCount === 0 ) {
count++;
} else {
count += vcData.deletablePagesCount;
}
}
if ( vcData.isRevDeleteChecked ) {
if ( vcData.hideableRevsCount === 0 ) {
count++;
} else {
count += vcData.hideableRevsCount;
}
}
return count;
}
async function checkIfTagExists() {
const data = await api.get( { titles: 'MediaWiki:Tag-VandalCleaner' } );
if ( data.query.pages[ -1 ] ) {
return false;
}
return true;
}
async function doBlock() {
if ( !vcData.isBlockChecked ) {
return;
}
vcData.statusPane.$blockContainer.fadeIn();
const params = {
action: 'block',
user: vandal,
expiry: vcData.blockDuration,
nocreate: true,
autoblock: true,
noemail: true,
allowusertalk: isAnon,
reblock: true,
reason: vcData.summary,
tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
};
try {
await api.postWithToken( 'csrf', params );
} catch ( e ) {
handleError(
`${ vandal } could not be blocked`,
e,
'block',
[ 'alreadyblocked' ]
);
} finally {
vcData.statusPane.$blockStatus.attr( 'data-percent', '100' );
updateProgressBar();
await waitBeforeProceeding( 300 );
}
}
async function doRollback() {
if ( !vcData.isRollbackChecked ) {
return;
}
timer_start = new Date().getTime();
addTopBorderIfNeeded( vcData.statusPane.$rollbackContainer );
vcData.statusPane.$rollbackContainer.fadeIn();
if ( vcData.revertibleEditsCount === 0 ) {
vcData.statusPane.$rollbackStatus.attr( 'data-percent', '100' );
updateProgressBar();
await waitBeforeProceeding( 250 );
return;
}
let millisecondsBetweenRollbacks = 300;
if (
vcData.revertibleEditsCount > 50 &&
!vcData.editorRights.includes( 'noratelimit' )
) {
millisecondsBetweenRollbacks = 650;
}
const params = {
summary: vcData.summary,
tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
};
let currentEditIndex = 0;
do {
const title = vcData.revertibleEdits[ currentEditIndex ].title;
try {
await api.rollback( title, vandal, params );
} catch ( e ) {
handleError(
`${ vandal }'s edits on page ${ title } could not be rollbacked`,
e,
'rollback',
[ 'alreadyrolled', 'onlyauthor' ]
);
} finally {
currentEditIndex++;
vcData.statusPane.$rollbackStatus.attr(
'data-percent',
( currentEditIndex / vcData.revertibleEditsCount * 100 ).toFixed( 0 )
);
updateProgressBar();
if(currentEditIndex === vcData.revertibleEditsCount) {
var timer_end = new Date().getTime();
var timer = (timer_end - timer_start) / vcData.revertibleEditsCount / 1000;
timer_values.push(timer);
mw.storage.setObject('vc_timer',{values:timer_values});
mw.notify(`לקח ${timer.toFixed(2)} שניות לעריכה`,{autoHide: false});
var sum = timer_values.reduce((a,b)=>a+b,0);
var avg = (sum / timer_values.length).toFixed(2);
mw.notify(`ממוצע במכשיר זה: ${avg} שניות`,{autoHide: false});
mw.notify(`עד כה התבצעו במכשיר זה ${timer_values.length} בדיקות זמן`,{autoHide:false});
}
await waitBeforeProceeding( millisecondsBetweenRollbacks );
}
} while ( currentEditIndex < vcData.revertibleEditsCount );
}
async function doDelete() {
if ( !vcData.isDeleteChecked ) {
return;
}
addTopBorderIfNeeded( vcData.statusPane.$deleteContainer );
vcData.statusPane.$deleteContainer.fadeIn();
if ( vcData.deletablePagesCount === 0 ) {
vcData.statusPane.$deleteStatus.attr( 'data-percent', '100' );
updateProgressBar();
await waitBeforeProceeding( 250 );
return;
}
const params = {
action: 'delete',
reason: vcData.summary,
tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
};
let currentPageIndex = 0;
do {
params.title = vcData.deletablePages[ currentPageIndex ].title;
try {
await api.postWithToken( 'csrf', params );
} catch ( e ) {
handleError(
`Page ${ params.title } could not be deleted`,
e,
'delete',
[ 'missingtitle' ]
);
} finally {
currentPageIndex++;
vcData.statusPane.$deleteStatus.attr(
'data-percent',
( currentPageIndex / vcData.deletablePagesCount * 100 ).toFixed( 0 )
);
updateProgressBar();
await waitBeforeProceeding( 300 );
}
} while ( currentPageIndex < vcData.deletablePagesCount );
}
async function doRevDelete() {
if ( !vcData.isRevDeleteChecked ) {
return;
}
addTopBorderIfNeeded( vcData.statusPane.$revDeleteContainer );
vcData.statusPane.$revDeleteContainer.fadeIn();
if ( vcData.hideableRevsCount === 0 ) {
vcData.statusPane.$revDeleteStatus.attr( 'data-percent', '100' );
updateProgressBar();
await waitBeforeProceeding( 250 );
return;
}
let itemsToHide = 'content|comment';
if ( !vcData.isHideSummariesChecked ) {
itemsToHide = 'content';
} else if ( !vcData.isHideContentsChecked ) {
itemsToHide = 'comment';
}
const params = {
action: 'revisiondelete',
type: 'revision',
hide: itemsToHide,
reason: vcData.summary,
tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
};
let currentRevIndex = 0;
do {
params.ids = vcData.hideableRevs[ currentRevIndex ].revid;
params.target = vcData.hideableRevs[ currentRevIndex ].title;
try {
await api.postWithToken( 'csrf', params );
} catch ( e ) {
handleError(
`Revision ${ params.ids } on page ${ params.target } could not be hidden`,
e,
'revDelete'
);
} finally {
currentRevIndex++;
vcData.statusPane.$revDeleteStatus.attr(
'data-percent',
( currentRevIndex / vcData.hideableRevsCount * 100 ).toFixed( 0 )
);
updateProgressBar();
await waitBeforeProceeding( 300 );
}
} while ( currentRevIndex < vcData.hideableRevsCount );
}
async function checkForLeftoverVandalism() {
if (
!vcData.isRollbackChecked ||
vcData.revertibleEditsCount === 0 ||
(
!vcData.editorRights.includes( 'patrol' ) &&
!vcData.editorRights.includes( 'patrolmarks' )
)
) {
return;
}
vcData.statusPane.$leftoverVandalismContainer.fadeIn();
vcData.leftoverVandalismSuspects = [];
const params = {
list: 'recentchanges',
rcexcludeuser: vandal,
rcprop: 'ids|patrolled',
rcshow: '!bot',
rclimit: 1,
rctype: 'edit'
};
for ( const [ index, edit ] of vcData.revertibleEdits.entries() ) {
if ( ![ 0, 10, 12, 14, 100, 828 ].includes( edit.ns ) ) {
continue;
}
const daysFromEdit =
( new Date( mw.now() ) - new Date( edit.timestamp ) ) / 1000 / 60 / 60 / 24;
if ( daysFromEdit >= 30 ) {
break;
}
params.rcstart = edit.timestamp;
params.rcend = subtractDaysFromTimestamp( edit.timestamp, 5 );
params.rctitle = edit.title;
const data = await api.get( params );
if (
data.query.recentchanges[ 0 ] &&
data.query.recentchanges[ 0 ].unpatrolled === ''
) {
vcData.leftoverVandalismSuspects.push( {
title: edit.title,
id: data.query.recentchanges[ 0 ].revid
} );
}
vcData.statusPane.$leftoverVandalismStatus.attr(
'data-percent',
( ( index + 1 ) / vcData.revertibleEditsCount * 100 ).toFixed( 0 )
);
}
vcData.statusPane.$leftoverVandalismStatus.attr( 'data-percent', '100' );
}
function handleError( errorMsg, errorCode, action, ignoredErrorCodes = [] ) {
console.log(
`⚠️ Vandal Cleaner: ${ errorMsg } (%c${ errorCode }%c)`,
'font-weight: bold;',
''
);
if ( ignoredErrorCodes.includes( errorCode ) ) {
return;
}
vcData.statusPane[ `$${ action }Status` ]
.addClass( 'vandal-cleaner-status-failure' );
vcData.statusPane[ `$${ action }Error` ].fadeIn();
if ( vcData.hasFailedActions ) {
return;
}
vcData.inputs.progressBar.$element
.addClass( 'vandal-cleaner-progress-bar-failure' );
vcData.hasFailedActions = true;
}
function addTopBorderIfNeeded( $element ) {
if (
$element.siblings( '.vandal-cleaner-status-action-container:visible' )
.length
) {
$element.addClass( 'vandal-cleaner-fancy-border-top' );
}
}
function updateProgressBar() {
vcData.completedTasksCount++;
vcData.inputs.progressBar.setProgress(
vcData.completedTasksCount / vcData.totalTasksCount * 100
);
}
function waitBeforeProceeding( milliseconds ) {
const defer = $.Deferred();
setTimeout( () => defer.resolve(), milliseconds );
return defer;
}
function createFinalPanel() {
const successMsg = new OO.ui.MessageWidget( {
icon: 'success',
id: 'vandal-cleaner-success-msg',
label: i18n( 'finishedRunning' ),
type: 'success'
} );
const $thumbImg = $( '<img>' ).attr( {
alt: i18n( 'finishedRunning' ),
id: 'vandal-cleaner-thumb-img',
src: 'https://upload.wikimedia.org/wikipedia/commons/c/ce/Emoji_u1f44d.svg'
} );
const $outroText = $( '<div>' )
.attr( 'id', 'vandal-cleaner-outro-text' );
if ( vcData.hasFailedActions ) {
const unrevertedEditsUrl = mw.util.getUrl( 'Special:Contributions', {
target: vandal,
topOnly: '1'
} );
const unrevertedEditsError = new OO.ui.MessageWidget( {
classes: [ 'vandal-cleaner-error' ],
id: 'vandal-cleaner-unreverted-edits-error',
inline: true,
label: new OO.ui.HtmlSnippet(
i18n( 'unrevertedEditsError', [ unrevertedEditsUrl ] )
),
type: 'error'
} );
$outroText.append( unrevertedEditsError.$element );
}
if (
vcData.leftoverVandalismSuspects &&
vcData.leftoverVandalismSuspects.length > 0
) {
const $leftoverVandalismOpeningText = $( '<p>' ).text(
i18n(
'leftoverVandalismOpeningText',
[ vcData.leftoverVandalismSuspects.length ]
)
);
const $leftoverVandalismList = $( '<ul>' )
.attr( 'id', 'vandal-cleaner-leftover-vandalism-list' );
const $leftoverVandalismContainer = $( '<div>' )
.attr( 'id', 'vandal-cleaner-leftover-vandalism-container' )
.append( $leftoverVandalismOpeningText, $leftoverVandalismList );
vcData.leftoverVandalismSuspects.forEach( item => {
const pageUrl = mw.util.getUrl( item.title );
const $pageLink = $( '<a>' )
.addClass( 'vandal-cleaner-leftover-vandalism-page-link' )
.attr( { href: pageUrl, target: '_blank' } );
const $pageLinkText = $( '<bdi>' )
.addClass( 'vandal-cleaner-leftover-vandalism-page-link-text' )
.attr( 'title', item.title )
.text( item.title );
$pageLink.append( $pageLinkText );
const diffUrl = mw.util.getUrl( `Special:Diff/${ item.id }` );
const $diffLink = $( '<a>' )
.attr( { href: diffUrl, target: '_blank' } )
.text( i18n( 'leftoverVandalismDiff' ) );
const histUrl = mw.util.getUrl( item.title, { action: 'history' } );
const $histLink = $( '<a>' )
.attr( { href: histUrl, target: '_blank' } )
.text( i18n( 'leftoverVandalismHist' ) );
const $li = $( '<li>' )
.addClass( 'vandal-cleaner-leftover-vandalism-list-item' )
.append( $pageLink, ' (', $diffLink, ' | ', $histLink, ')' );
$leftoverVandalismList.append( $li );
} );
$outroText.append( $leftoverVandalismContainer );
}
const recentChangesUrl = mw.util.getUrl( 'Special:RecentChanges', {
days: '30',
enhanced: null,
hidebyothers: '1',
tagfilter: vcData.doesTagExist ? 'VandalCleaner' : null,
urlversion: '2'
} );
$outroText.append(
$( '<p>' ).text( i18n( 'outroThankYou' ) ),
$( '<p>' ).html( i18n( 'outroReviewActions', [ recentChangesUrl ] ) ),
$( '<p>' ).text( i18n( 'outroKeepItUp', [ mw.config.get( 'wgSiteName' ) ] ) ),
$( '<p>' ).text( i18n( 'outroClose' ) )
);
const $outroContainer = $( '<div>' )
.attr( 'id', 'vandal-cleaner-outro-container' )
.append( $thumbImg, $outroText );
this.finalPanel.$element.append( successMsg.$element, $outroContainer );
this.stackLayout.setItem( this.finalPanel );
this.actions.setMode( 'final' );
this.actions.list.find( item => item.modes === 'final' ).focus();
// Allow non-sysops to submit block requests
// (on Hebrew WMF projects only, for now).
// TODO: Implement this feature in a better way.
if (
!vcData.editorRights.includes( 'block' ) &&
mw.config.get( 'wgWikiID' ).slice( 0, 5 ) === 'hewik'
) {
const blockRequestReasonInput = new OO.ui.TextInputWidget( {
id: 'vandal-cleaner-block-request-reason-input-widget',
inputId: 'vandal-cleaner-block-request-reason-input-element',
placeholder: i18n( 'blockRequestPlaceholder' )
} );
const blockRequestReasonLabel = new OO.ui.LabelWidget( {
input: blockRequestReasonInput,
label: i18n( 'blockRequestLabel' )
} );
const blockRequestSubmitBtn = new OO.ui.ButtonWidget( {
id: 'vandal-cleaner-block-request-submit-btn',
label: i18n( 'blockRequestSubmitBtn' )
} );
blockRequestSubmitBtn.on( 'click', () => {
blockRequestReasonInput.setDisabled( true );
blockRequestSubmitBtn
.setDisabled( true )
.setLabel( i18n( 'blockRequestSubmitting' ) );
const reason = blockRequestReasonInput.getValue().trim();
const params = {
action: 'edit',
title: 'Project:בקשות ממפעילים',
redirect: true,
section: 2,
appendtext: `${ '\n\n' }* {${ '{' }לחסום|${ vandal }}} – ${ reason } ~~${ '~~' }`,
summary: `/* בקשות חסימה / הסרת חסימה */ [[משתמש:${ vandal }|${ vandal }]] ([[שיחת משתמש:${ vandal }|ש]]|[[מיוחד:תרומות/${ vandal }|ת]]|[[מיוחד:חסימה/${ vandal }|ח]])`,
tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
};
api.postWithEditToken( params ).then(
() => blockRequestSubmitBtn.setLabel( i18n( 'blockRequestSubmitDone' ) ),
() => blockRequestSubmitBtn.setLabel( i18n( 'blockRequestSubmitError' ) )
);
} );
blockRequestReasonInput.on( 'enter', () =>
blockRequestSubmitBtn.$element
.children( '.oo-ui-buttonElement-button' )[ 0 ].click()
);
const blockRequestLayout = new OO.ui.HorizontalLayout( {
id: 'vandal-cleaner-block-request-layout',
items: [ blockRequestReasonInput, blockRequestSubmitBtn ]
} );
const $blockRequestContainer = $( '<div>' )
.attr( 'id', 'vandal-cleaner-block-request-container' )
.append( blockRequestReasonLabel.$element, blockRequestLayout.$element );
$outroText.append( $blockRequestContainer );
}
}
function adaptFinalPanelHeight( ProcessDialog ) {
const imgElement = document.getElementById( 'vandal-cleaner-thumb-img' );
const observer = new IntersectionObserver( entries => {
if ( entries[ 0 ].isIntersecting ) {
const currentHeight = this.$body[ 0 ].scrollHeight;
ProcessDialog.prototype.getBodyHeight = () => currentHeight;
this.updateSize();
const newHeight = this.finalPanel.$element[ 0 ].scrollHeight + 20;
ProcessDialog.prototype.getBodyHeight = () => newHeight;
this.updateSize();
observer.unobserve( imgElement );
}
} );
observer.observe( imgElement );
}
function scrollIntoViewIfNeeded(
$watchTarget, watchThreshold, $scrollTarget, scrollBlock
) {
if (
$scrollTarget[ 0 ] &&
window.innerHeight - $watchTarget[ 0 ].getBoundingClientRect().bottom
< watchThreshold
) {
$scrollTarget[ 0 ].scrollIntoView( {
block: scrollBlock,
behavior: 'smooth'
} );
}
}
function refineErrorDialog( ProcessDialog ) {
const dismissBtnSelector =
`#vandal-cleaner-dialog .oo-ui-processDialog-errors-actions >
.oo-ui-buttonWidget:first-child .oo-ui-buttonElement-button`;
const dismissBtn = document.querySelector( dismissBtnSelector );
$( dismissBtn ).find( '.oo-ui-labelElement-label' )
.text( i18n( 'errorDialogDismissBtnLabel' ) );
const observer = new IntersectionObserver( entries => {
if ( entries[ 0 ].isIntersecting ) {
ProcessDialog.static.escapable = false;
entries[ 0 ].target.focus();
$( document ).on( 'keydown.closeErrorDialog', e => {
if ( e.key === 'Escape' ) {
entries[ 0 ].target.click();
}
} );
} else {
$( document ).off( 'keydown.closeErrorDialog' );
ProcessDialog.static.escapable = true;
}
} );
observer.observe( dismissBtn );
}
function getPref( key ) {
return mw.user.options.get( `userjs-VandalCleaner-${ key }` );
}
function setPref( key, value ) {
api.saveOption( `userjs-VandalCleaner-${ key }`, value );
}
} )();