מדיה ויקי:סקריפטים/107.js
הערה: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.
- פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
- גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
- אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים 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 ); 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; } 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(); 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 ); } } )();