mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-12-01 00:56:26 +00:00
62adeb3ce5
The editing view is currently full of tech debt, brittle and surprising code and whatnot. It's basically a miracle if it works without problem, and it'd be an even bigger miracle if you could change something there without breaking anything. For these reasons, and because that class must be refactored as part of the upcoming overhaul, this patch adds a bunch of selenium tests to test the main functionality of that page. In particular, these tests cover all possible cases (each corresponding to a data source) for which buildFilterEditor can be called, which FTR are: 1 - View the result of importing a filter 2 - Create a new filter 3 - Load the current version of an existing filter 4 - Load an old version of an existing filter 5 - Show the user input again if saving fails after one of the steps above Having automated tests to cover these cases means that we don't have to manually test all the scenarios manually each time the class is touched. Bug: T201193 Change-Id: I408e0a132905416effe0d6d6dc0921991edd66bd
574 lines
17 KiB
JavaScript
574 lines
17 KiB
JavaScript
/**
|
|
* AbuseFilter editing JavaScript
|
|
*
|
|
* @author John Du Hart
|
|
* @author Marius Hoch <hoo@online.de>
|
|
*/
|
|
/* global ace */
|
|
|
|
( function () {
|
|
'use strict';
|
|
|
|
// @var {jQuery} Filter editor for JS and jQuery handling
|
|
var $filterBox,
|
|
// Filter editor for Ace specific functions
|
|
filterEditor,
|
|
// @var {jQuery} Hidden textarea for submitting form
|
|
$plainTextBox,
|
|
// @var {boolean} To determine what editor to use
|
|
useAce = false,
|
|
// Infused OOUI elements
|
|
toggleWarnPreviewButton, warnMessageExisting, warnMessageOther,
|
|
toggleDisallowPreviewButton, disallowMessageExisting, disallowMessageOther;
|
|
|
|
/**
|
|
* Returns the currently selected warning or disallow message.
|
|
*
|
|
* @param {string} action The action to get the message for
|
|
* @return {string} current warning message
|
|
*/
|
|
function getCurrentMessage( action ) {
|
|
var existing = action === 'warn' ? warnMessageExisting : disallowMessageExisting,
|
|
other = action === 'warn' ? warnMessageOther : disallowMessageOther,
|
|
message = existing.getValue();
|
|
|
|
if ( message === 'other' ) {
|
|
message = other.getValue();
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Things always needed after syntax checks
|
|
*
|
|
* @param {string} resultText The message to show, telling if the syntax is valid
|
|
* @param {string} className Class to add
|
|
* @param {boolean} syntaxOk Is the syntax ok?
|
|
*/
|
|
function processSyntaxResultAlways( resultText, className, syntaxOk ) {
|
|
$.removeSpinner( 'abusefilter-syntaxcheck' );
|
|
$( '#mw-abusefilter-syntaxcheck' ).prop( 'disabled', false );
|
|
|
|
$( '#mw-abusefilter-syntaxresult' )
|
|
.show()
|
|
.attr( 'class', className )
|
|
.text( resultText )
|
|
.data( 'syntaxOk', syntaxOk );
|
|
}
|
|
|
|
/**
|
|
* Switch between Ace Editor and classic textarea
|
|
*/
|
|
function switchEditor() {
|
|
if ( useAce ) {
|
|
useAce = false;
|
|
$filterBox.hide();
|
|
$plainTextBox.show();
|
|
} else {
|
|
useAce = true;
|
|
filterEditor.session.setValue( $plainTextBox.val() );
|
|
$filterBox.show();
|
|
$plainTextBox.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes the data retrieved in doSyntaxCheck and processes it.
|
|
*
|
|
* @param {Object} response Data returned from the AJAX request
|
|
*/
|
|
function processSyntaxResult( response ) {
|
|
var position,
|
|
data = response.abusefilterchecksyntax;
|
|
|
|
if ( data.status === 'ok' ) {
|
|
// Successful
|
|
processSyntaxResultAlways(
|
|
mw.msg( 'abusefilter-edit-syntaxok' ),
|
|
'mw-abusefilter-syntaxresult-ok',
|
|
true
|
|
);
|
|
} else {
|
|
// Set a custom error message as we're aware of the actual problem
|
|
processSyntaxResultAlways(
|
|
mw.message( 'abusefilter-edit-syntaxerr', data.message ).toString(),
|
|
'mw-abusefilter-syntaxresult-error',
|
|
false
|
|
);
|
|
|
|
if ( useAce ) {
|
|
filterEditor.focus();
|
|
// Convert index (used in textareas) in position {row, column} for ace
|
|
position = filterEditor.session.getDocument().indexToPosition( data.character );
|
|
filterEditor.navigateTo( position.row, position.column );
|
|
filterEditor.scrollToRow( position.row );
|
|
} else {
|
|
$plainTextBox
|
|
.trigger( 'focus' )
|
|
.textSelection( 'setSelection', { start: data.character } );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Acts on errors after doSyntaxCheck.
|
|
*
|
|
* @param {string} error Error code returned from the AJAX request
|
|
* @param {Object} details Details about the error
|
|
*/
|
|
function processSyntaxResultFailure( error, details ) {
|
|
processSyntaxResultAlways(
|
|
mw.msg(
|
|
error === 'http' ? 'abusefilter-http-error' : 'unknown-error',
|
|
details && details.exception
|
|
),
|
|
'mw-abusefilter-syntaxresult-error',
|
|
false
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sends the current filter text to be checked for syntax issues.
|
|
*
|
|
* @this HTMLElement
|
|
* @param {jQuery.Event} e The event fired when the function is called
|
|
*/
|
|
function doSyntaxCheck() {
|
|
var filter = $plainTextBox.val(),
|
|
api = new mw.Api();
|
|
|
|
$( this )
|
|
.prop( 'disabled', true )
|
|
.injectSpinner( { id: 'abusefilter-syntaxcheck', size: 'large' } );
|
|
|
|
api.post( {
|
|
action: 'abusefilterchecksyntax',
|
|
filter: filter
|
|
} )
|
|
.done( processSyntaxResult )
|
|
.fail( processSyntaxResultFailure );
|
|
}
|
|
|
|
/**
|
|
* Adds text to the filter textarea.
|
|
* Fired by a change event from the #wpFilterBuilder dropdown
|
|
*/
|
|
function addText() {
|
|
var $filterBuilder = $( '#wpFilterBuilder' );
|
|
|
|
if ( $filterBuilder.prop( 'selectedIndex' ) === 0 ) {
|
|
return;
|
|
}
|
|
|
|
if ( useAce ) {
|
|
filterEditor.insert( $filterBuilder.val() + ' ' );
|
|
$plainTextBox.val( filterEditor.getSession().getValue() );
|
|
filterEditor.focus();
|
|
} else {
|
|
$plainTextBox.textSelection(
|
|
'encapsulateSelection', { pre: $filterBuilder.val() + ' ' }
|
|
);
|
|
}
|
|
$filterBuilder.prop( 'selectedIndex', 0 );
|
|
}
|
|
|
|
/**
|
|
* Fetches a filter from the API and inserts it into the filter box.
|
|
*
|
|
* @this HTMLElement
|
|
* @param {jQuery.Event} e The event fired when the function is called
|
|
*/
|
|
function fetchFilter() {
|
|
var filterId = $( '#mw-abusefilter-load-filter input' ).val().trim(),
|
|
api;
|
|
|
|
if ( filterId === '' ) {
|
|
return;
|
|
}
|
|
|
|
$( this ).injectSpinner( { id: 'fetch-spinner', size: 'large' } );
|
|
|
|
// We just ignore errors or unexisting filters over here
|
|
api = new mw.Api();
|
|
api.get( {
|
|
action: 'query',
|
|
list: 'abusefilters',
|
|
abfprop: 'pattern',
|
|
abfstartid: filterId,
|
|
abfendid: filterId,
|
|
abflimit: 1
|
|
} )
|
|
.always( function removeSpinner() {
|
|
$.removeSpinner( 'fetch-spinner' );
|
|
} )
|
|
.done( function insertFilter( data ) {
|
|
if ( data.query.abusefilters[ 0 ] !== undefined ) {
|
|
if ( useAce ) {
|
|
filterEditor.setValue( data.query.abusefilters[ 0 ].pattern );
|
|
}
|
|
$plainTextBox.val( data.query.abusefilters[ 0 ].pattern );
|
|
}
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Cycles through all action checkboxes and hides parameter divs.
|
|
* that don't have checked boxes
|
|
*/
|
|
function hideDeselectedActions() {
|
|
$( '.mw-abusefilter-action-checkbox input' ).each( function showHideParams() {
|
|
// mw-abusefilter-action-checkbox-{$action}
|
|
var action = this.parentNode.id.substr( 31 ),
|
|
$params = $( '#mw-abusefilter-' + action + '-parameters' );
|
|
|
|
if ( $params.length ) {
|
|
if ( this.checked ) {
|
|
$params.show();
|
|
} else {
|
|
$params.hide();
|
|
}
|
|
}
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Fetches the selected warning message for previewing.
|
|
*
|
|
* @param {string} action The action the message refers to
|
|
*/
|
|
function previewMessage( action ) {
|
|
var api = new mw.Api(),
|
|
args = [
|
|
'<nowiki>' + $( 'input[name=wpFilterDescription]' ).val() + '</nowiki>',
|
|
$( '#mw-abusefilter-edit-id' ).children().last().text()
|
|
],
|
|
message = getCurrentMessage( action ),
|
|
// mw-abusefilter-warn-preview, mw-abusefilter-disallow-preview
|
|
$element = $( '#mw-abusefilter-' + action + '-preview' ),
|
|
previewButton = action === 'warn' ? toggleWarnPreviewButton : toggleDisallowPreviewButton;
|
|
|
|
if ( $element.css( 'display' ) !== 'none' ) {
|
|
$element.hide();
|
|
previewButton.setFlags( { destructive: false, progressive: true } );
|
|
} else {
|
|
api.get( {
|
|
action: 'query',
|
|
meta: 'allmessages',
|
|
ammessages: message,
|
|
amargs: args.join( '|' )
|
|
} )
|
|
.done( function parseMessage( data ) {
|
|
api.parse( data.query.allmessages[ 0 ][ '*' ], {
|
|
disablelimitreport: '',
|
|
preview: '',
|
|
prop: 'text',
|
|
title: 'MediaWiki:' + message
|
|
} )
|
|
.done( function showMessage( html ) {
|
|
$element.show().html( html );
|
|
previewButton.setFlags(
|
|
{ destructive: true, progressive: false }
|
|
);
|
|
} );
|
|
} );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redirects the browser to the message for editing.
|
|
*
|
|
* @param {string} action The action for which the message is used
|
|
*/
|
|
function editMessage( action ) {
|
|
var message = getCurrentMessage( action ),
|
|
defaultMsg = action === 'warn' ? 'warning' : 'disallowed',
|
|
url = mw.util.getUrl( 'MediaWiki:' + message, {
|
|
action: 'edit',
|
|
preload: 'MediaWiki:abusefilter-' + defaultMsg
|
|
} );
|
|
|
|
window.open( url, '_blank' );
|
|
}
|
|
|
|
/**
|
|
* Called if the filter group (#mw-abusefilter-edit-group-input select) is changed.
|
|
*
|
|
* @this HTMLELement
|
|
* @param {jQuery.Event} e The event fired when the function is called
|
|
*/
|
|
function onFilterGroupChange() {
|
|
var $afWarnMessageExisting, $afDisallowMessageExisting, newVal;
|
|
|
|
if ( !$( '#mw-abusefilter-action-warn-checkbox' ).is( ':checked' ) ) {
|
|
$afWarnMessageExisting = $( '#mw-abusefilter-warn-message-existing select' );
|
|
newVal = mw.config.get( 'wgAbuseFilterDefaultWarningMessage' )[ $( this ).val() ];
|
|
|
|
if ( $afWarnMessageExisting.find( 'option[value=\'' + newVal + '\']' ).length ) {
|
|
$afWarnMessageExisting.val( newVal );
|
|
warnMessageOther.setValue( '' );
|
|
} else {
|
|
$afWarnMessageExisting.val( 'other' );
|
|
warnMessageOther.setValue( newVal );
|
|
}
|
|
}
|
|
|
|
if ( !$( '#mw-abusefilter-action-disallow-checkbox' ).is( ':checked' ) ) {
|
|
$afDisallowMessageExisting = $( '#mw-abusefilter-disallow-message-existing select' );
|
|
newVal = mw.config.get( 'wgAbuseFilterDefaultDisallowMessage' )[ $( this ).val() ];
|
|
|
|
if ( $afDisallowMessageExisting.find( 'option[value=\'' + newVal + '\']' ).length ) {
|
|
$afDisallowMessageExisting.val( newVal );
|
|
disallowMessageOther.setValue( '' );
|
|
} else {
|
|
$afDisallowMessageExisting.val( 'other' );
|
|
disallowMessageOther.setValue( newVal );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the options for warning and disallow messages if the filter is set to global.
|
|
*/
|
|
function toggleCustomMessages() {
|
|
// Use the table over here as hideDeselectedActions might alter the visibility of the div
|
|
var $warnOptions = $( '#mw-abusefilter-warn-parameters > table' ),
|
|
$disallowOptions = $( '#mw-abusefilter-disallow-parameters > table' );
|
|
|
|
if ( $( '#wpFilterGlobal' ).is( ':checked' ) ) {
|
|
// It's a global filter, so use the default message and hide the option from the user
|
|
warnMessageExisting.setValue( 'abusefilter-warning' );
|
|
disallowMessageExisting.setValue( 'abusefilter-disallowed' );
|
|
|
|
$warnOptions.hide();
|
|
$disallowOptions.hide();
|
|
} else {
|
|
$warnOptions.show();
|
|
$disallowOptions.show();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called if the user presses a key in the load filter field.
|
|
*
|
|
* @this HTMLELement
|
|
* @param {jQuery.Event} e The event fired when the function is called
|
|
*/
|
|
function onFilterKeypress( e ) {
|
|
if ( e.type === 'keypress' && e.which === 13 ) {
|
|
e.preventDefault();
|
|
$( '#mw-abusefilter-load' ).trigger( 'click' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Warn if the user changed anything and tries to leave the window
|
|
*/
|
|
function setWarnOnLeave() {
|
|
var warnOnLeave,
|
|
$form = $( '#mw-abusefilter-editing-form' ),
|
|
origValues = $form.serialize();
|
|
|
|
warnOnLeave = mw.confirmCloseWindow( {
|
|
test: function () {
|
|
return $form.serialize() !== origValues;
|
|
},
|
|
message: mw.msg( 'abusefilter-edit-warn-leave' )
|
|
} );
|
|
|
|
$form.on( 'submit', function () {
|
|
warnOnLeave.release();
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Builds a TagMultiselectWidget, to be used both for throttle groups and change tags
|
|
*
|
|
* @param {string} action Either 'throttle' or 'tag', will be used to build element IDs
|
|
* @param {Array} config The array with configuration passed from PHP code
|
|
*/
|
|
function buildSelector( action, config ) {
|
|
// mw-abusefilter-throttle-parameters, mw-abusefilter-tag-parameters
|
|
var $container = $( '#mw-abusefilter-' + action + '-parameters' ),
|
|
// Character used to separate elements in the textarea.
|
|
separator = action === 'throttle' ? '\n' : ',',
|
|
selector, field, fieldOpts, hiddenField;
|
|
|
|
selector =
|
|
new OO.ui.TagMultiselectWidget( {
|
|
inputPosition: 'outline',
|
|
allowArbitrary: true,
|
|
allowEditTags: true,
|
|
selected: config.values,
|
|
// The following messages are used here:
|
|
// * abusefilter-edit-throttle-placeholder
|
|
// * abusefilter-edit-tag-placeholder
|
|
placeholder: OO.ui.msg( 'abusefilter-edit-' + action + '-placeholder' ),
|
|
disabled: config.disabled
|
|
} );
|
|
|
|
fieldOpts = {
|
|
label: $( $.parseHTML( config.label ) ),
|
|
align: 'top'
|
|
};
|
|
if ( action === 'throttle' ) {
|
|
fieldOpts.help = new OO.ui.HtmlSnippet( config.help );
|
|
}
|
|
|
|
field = new OO.ui.FieldLayout( selector, fieldOpts );
|
|
|
|
// mw-abusefilter-hidden-throttle-field, mw-abusefilter-hidden-tag-field
|
|
hiddenField = OO.ui.infuse( $( '#mw-abusefilter-hidden-' + action + '-field' ) );
|
|
selector.on( 'change', function () {
|
|
hiddenField.setValue( selector.getValue().join( separator ) );
|
|
} );
|
|
|
|
// mw-abusefilter-hidden-throttle, mw-abusefilter-hidden-tag
|
|
$( '#mw-abusefilter-hidden-' + action ).hide();
|
|
$container.append( field.$element );
|
|
}
|
|
|
|
// On ready initialization
|
|
$( function () {
|
|
var basePath, readOnly,
|
|
$exportBox = $( '#mw-abusefilter-export' ),
|
|
isFilterEditor = mw.config.get( 'isFilterEditor' ),
|
|
tagConfig = mw.config.get( 'tagConfig' ),
|
|
throttleConfig = mw.config.get( 'throttleConfig' ),
|
|
$switchEditorBtn = $( '#mw-abusefilter-switcheditor' ),
|
|
cbEnabled, cbDeleted;
|
|
|
|
if ( isFilterEditor ) {
|
|
// Configure the actual editing interface
|
|
if ( tagConfig ) {
|
|
// Build the tag selector
|
|
buildSelector( 'tag', tagConfig );
|
|
}
|
|
if ( throttleConfig ) {
|
|
// Build the throttle groups selector
|
|
buildSelector( 'throttle', throttleConfig );
|
|
}
|
|
|
|
toggleWarnPreviewButton = OO.ui.infuse( $( '#mw-abusefilter-warn-preview-button' ) );
|
|
warnMessageExisting = OO.ui.infuse( $( '#mw-abusefilter-warn-message-existing' ) );
|
|
warnMessageOther = OO.ui.infuse( $( '#mw-abusefilter-warn-message-other' ) );
|
|
toggleDisallowPreviewButton = OO.ui.infuse( $( '#mw-abusefilter-disallow-preview-button' ) );
|
|
disallowMessageExisting = OO.ui.infuse( $( '#mw-abusefilter-disallow-message-existing' ) );
|
|
disallowMessageOther = OO.ui.infuse( $( '#mw-abusefilter-disallow-message-other' ) );
|
|
setWarnOnLeave();
|
|
}
|
|
|
|
$plainTextBox = $( '#wpFilterRules' );
|
|
|
|
if ( $( '#wpAceFilterEditor' ).length ) {
|
|
// CodeEditor is installed.
|
|
mw.loader.using( [ 'ext.abuseFilter.ace' ] ).then( function () {
|
|
$filterBox = $( '#wpAceFilterEditor' );
|
|
|
|
filterEditor = ace.edit( 'wpAceFilterEditor' );
|
|
filterEditor.session.setMode( 'ace/mode/abusefilter' );
|
|
|
|
// Ace setup from codeEditor extension
|
|
basePath = mw.config.get( 'wgExtensionAssetsPath', '' );
|
|
if ( basePath.slice( 0, 2 ) === '//' ) {
|
|
// ACE uses web workers, which have importScripts, which don't like
|
|
// relative links. This is a problem only when the assets are on another
|
|
// server, so this rewrite should suffice.
|
|
basePath = window.location.protocol + basePath;
|
|
}
|
|
ace.config.set( 'basePath', basePath + '/CodeEditor/modules/ace' );
|
|
|
|
// Settings for Ace editor box
|
|
readOnly = mw.config.get( 'aceConfig' ).aceReadOnly;
|
|
|
|
filterEditor.setTheme( 'ace/theme/textmate' );
|
|
filterEditor.session.setOption( 'useWorker', false );
|
|
filterEditor.setReadOnly( readOnly );
|
|
filterEditor.$blockScrolling = Infinity;
|
|
|
|
// Display Ace editor
|
|
switchEditor();
|
|
|
|
// Hide the syntax ok message when the text changes and sync dummy box
|
|
filterEditor.on( 'change', function () {
|
|
var $el = $( '#mw-abusefilter-syntaxresult' );
|
|
|
|
if ( $el.data( 'syntaxOk' ) ) {
|
|
$el.hide();
|
|
}
|
|
|
|
$plainTextBox.val( filterEditor.getSession().getValue() );
|
|
} );
|
|
|
|
$switchEditorBtn.on( 'click', switchEditor );
|
|
$switchEditorBtn.show();
|
|
} );
|
|
}
|
|
|
|
// Hide the syntax ok message when the text changes
|
|
$plainTextBox.on( 'change', function () {
|
|
var $el = $( '#mw-abusefilter-syntaxresult' );
|
|
|
|
if ( $el.data( 'syntaxOk' ) ) {
|
|
$el.hide();
|
|
}
|
|
} );
|
|
|
|
$( '#mw-abusefilter-load' ).on( 'click', fetchFilter );
|
|
$( '#mw-abusefilter-load-filter' ).on( 'keypress', onFilterKeypress );
|
|
|
|
if ( isFilterEditor ) {
|
|
// Add logic for flags and consequences
|
|
$( '#mw-abusefilter-warn-preview-button' ).on( 'click',
|
|
function () { previewMessage( 'warn' ); }
|
|
);
|
|
$( '#mw-abusefilter-disallow-preview-button' ).on( 'click',
|
|
function () { previewMessage( 'disallow' ); }
|
|
);
|
|
$( '#mw-abusefilter-warn-edit-button' ).on( 'click',
|
|
function () { editMessage( 'warn' ); }
|
|
);
|
|
$( '#mw-abusefilter-disallow-edit-button' ).on( 'click',
|
|
function () { editMessage( 'disallow' ); }
|
|
);
|
|
$( '.mw-abusefilter-action-checkbox input' ).on( 'click', hideDeselectedActions );
|
|
hideDeselectedActions();
|
|
|
|
$( '#wpFilterGlobal' ).on( 'change', toggleCustomMessages );
|
|
toggleCustomMessages();
|
|
|
|
cbEnabled = OO.ui.infuse( $( '#wpFilterEnabled' ) );
|
|
cbDeleted = OO.ui.infuse( $( '#wpFilterDeleted' ) );
|
|
OO.ui.infuse( $( '#wpFilterDeletedLabel' ) );
|
|
cbEnabled.on( 'change',
|
|
function () {
|
|
cbDeleted.setDisabled( cbEnabled.isSelected() );
|
|
if ( cbEnabled.isSelected() ) {
|
|
cbDeleted.setSelected( false );
|
|
}
|
|
}
|
|
);
|
|
|
|
cbDeleted.on( 'change',
|
|
function () {
|
|
if ( cbDeleted.isSelected() ) {
|
|
cbEnabled.setSelected( false );
|
|
}
|
|
}
|
|
);
|
|
|
|
$( '#mw-abusefilter-edit-group-input select' ).on( 'change', onFilterGroupChange );
|
|
|
|
$( '#mw-abusefilter-export-link' ).on( 'click',
|
|
function ( e ) {
|
|
e.preventDefault();
|
|
$exportBox.toggle();
|
|
}
|
|
);
|
|
}
|
|
|
|
$( '#mw-abusefilter-syntaxcheck' ).on( 'click', doSyntaxCheck );
|
|
$( '#wpFilterBuilder' ).on( 'change', addText );
|
|
} );
|
|
}() );
|