Create distinct builders for plain and ace editor

Change-Id: I9d2b7572fed6e0b3660d3b0d5dad324d6b75fde9
This commit is contained in:
Daimona Eaytoy 2021-03-04 12:24:20 +01:00
parent 507e3fb0c5
commit 6ba8e93537
5 changed files with 316 additions and 154 deletions

View file

@ -0,0 +1,110 @@
<?php
namespace MediaWiki\Extension\AbuseFilter\EditBox;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWiki\Extension\AbuseFilter\Parser\AbuseFilterParser;
use MediaWiki\Extension\AbuseFilter\Parser\AbuseFilterTokenizer;
use MessageLocalizer;
use OOUI\ButtonWidget;
use OOUI\HorizontalLayout;
use OOUI\Widget;
use OutputPage;
use User;
use Xml;
/**
* Class responsible for building filter edit boxes with both the Ace and the plain version
*/
class AceEditBoxBuiler extends EditBoxBuilder {
/** @var PlainEditBoxBuiler */
private $plainBuilder;
/**
* @inheritDoc
* @param PlainEditBoxBuiler $plainBuilder
*/
public function __construct(
AbuseFilterPermissionManager $afPermManager,
KeywordsManager $keywordsManager,
MessageLocalizer $messageLocalizer,
User $user,
OutputPage $output,
PlainEditBoxBuiler $plainBuilder
) {
parent::__construct( $afPermManager, $keywordsManager, $messageLocalizer, $user, $output );
$this->plainBuilder = $plainBuilder;
}
/**
* @inheritDoc
*/
protected function getEditBox( string $rules, bool $isUserAllowed, bool $externalForm ) : string {
$rules = rtrim( $rules ) . "\n";
$attribs = [
// Rules are in English
'dir' => 'ltr',
'name' => 'wpAceFilterEditor',
'id' => 'wpAceFilterEditor',
'class' => 'mw-abusefilter-editor'
];
$rulesContainer = Xml::element( 'div', $attribs, $rules );
$editorConfig = $this->getAceConfig( $isUserAllowed );
$this->output->addJsConfigVars( 'aceConfig', $editorConfig );
return $rulesContainer . $this->plainBuilder->getEditBox( $rules, $isUserAllowed, $externalForm );
}
/**
* @inheritDoc
*/
protected function getEditorControls() : Widget {
$base = parent::getEditorControls();
$switchEditor = new ButtonWidget(
[
'label' => $this->localizer->msg( 'abusefilter-edit-switch-editor' )->text(),
'id' => 'mw-abusefilter-switcheditor'
]
);
return new Widget( [
'content' => new HorizontalLayout( [
'items' => [ $switchEditor, $base ]
] )
] );
}
/**
* Extract values for syntax highlight
*
* @param bool $canEdit
* @return array
*/
private function getAceConfig( bool $canEdit ): array {
$values = $this->keywordsManager->getBuilderValues();
$deprecatedVars = $this->keywordsManager->getDeprecatedVariables();
$builderVariables = implode( '|', array_keys( $values['vars'] ) );
$builderFunctions = implode( '|', array_keys( AbuseFilterParser::FUNCTIONS ) );
// AbuseFilterTokenizer::KEYWORDS also includes constants (true, false and null),
// but Ace redefines these constants afterwards so this will not be an issue
$builderKeywords = implode( '|', AbuseFilterTokenizer::KEYWORDS );
// Extract operators from tokenizer like we do in AbuseFilterParserTest
$operators = implode( '|', array_map( function ( $op ) {
return preg_quote( $op, '/' );
}, AbuseFilterTokenizer::OPERATORS ) );
$deprecatedVariables = implode( '|', array_keys( $deprecatedVars ) );
$disabledVariables = implode( '|', array_keys( $this->keywordsManager->getDisabledVariables() ) );
return [
'variables' => $builderVariables,
'functions' => $builderFunctions,
'keywords' => $builderKeywords,
'operators' => $operators,
'deprecated' => $deprecatedVariables,
'disabled' => $disabledVariables,
'aceReadOnly' => !$canEdit
];
}
}

View file

@ -4,47 +4,38 @@ namespace MediaWiki\Extension\AbuseFilter\EditBox;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWiki\Extension\AbuseFilter\Parser\AbuseFilterParser;
use MediaWiki\Extension\AbuseFilter\Parser\AbuseFilterTokenizer;
use MessageLocalizer;
use OOUI\ButtonWidget;
use OOUI\DropdownInputWidget;
use OOUI\FieldLayout;
use OOUI\FieldsetLayout;
use OOUI\HorizontalLayout;
use OOUI\Widget;
use OutputPage;
use User;
use Xml;
/**
* Class responsible for building filter edit boxes
* @todo Consider splitting to different classes for each editor (plain, Ace, etc.)
* Base class for classes responsible for building filter edit boxes
*/
class EditBoxBuilder {
abstract class EditBoxBuilder {
/** @var AbuseFilterPermissionManager */
private $afPermManager;
protected $afPermManager;
/** @var KeywordsManager */
private $keywordsManager;
/** @var bool */
private $isCodeEditorLoaded;
protected $keywordsManager;
/** @var MessageLocalizer */
private $localizer;
protected $localizer;
/** @var User */
private $user;
protected $user;
/** @var OutputPage */
private $output;
protected $output;
/**
* @param AbuseFilterPermissionManager $afPermManager
* @param KeywordsManager $keywordsManager
* @param bool $isCodeEditorLoaded
* @param MessageLocalizer $messageLocalizer
* @param User $user
* @param OutputPage $output
@ -52,14 +43,12 @@ class EditBoxBuilder {
public function __construct(
AbuseFilterPermissionManager $afPermManager,
KeywordsManager $keywordsManager,
bool $isCodeEditorLoaded,
MessageLocalizer $messageLocalizer,
User $user,
OutputPage $output
) {
$this->afPermManager = $afPermManager;
$this->keywordsManager = $keywordsManager;
$this->isCodeEditorLoaded = $isCodeEditorLoaded;
$this->localizer = $messageLocalizer;
$this->user = $user;
$this->output = $output;
@ -79,165 +68,106 @@ class EditBoxBuilder {
bool $externalForm = false,
bool $needsModifyRights = true
) : string {
$this->output->enableOOUI();
$this->output->addModules( 'ext.abuseFilter.edit' );
$this->output->enableOOUI();
// Rules are in English
$editorAttribs = [ 'dir' => 'ltr' ];
$noTestAttrib = [];
$isUserAllowed = $needsModifyRights ?
$this->afPermManager->canEdit( $this->user ) :
$this->afPermManager->canUseTestTools( $this->user );
if ( !$isUserAllowed ) {
$noTestAttrib['disabled'] = 'disabled';
$addResultDiv = false;
}
$rules = rtrim( $rules ) . "\n";
$switchEditor = null;
$rulesContainer = '';
if ( $this->isCodeEditorLoaded ) {
$aceAttribs = [
'name' => 'wpAceFilterEditor',
'id' => 'wpAceFilterEditor',
'class' => 'mw-abusefilter-editor'
];
$attribs = array_merge( $editorAttribs, $aceAttribs );
$switchEditor = new ButtonWidget(
[
'label' => $this->localizer->msg( 'abusefilter-edit-switch-editor' )->text(),
'id' => 'mw-abusefilter-switcheditor'
] + $noTestAttrib
);
$rulesContainer .= Xml::element( 'div', $attribs, $rules );
// Add Ace configuration variable
$editorConfig = $this->getAceConfig( $isUserAllowed );
$this->output->addJsConfigVars( 'aceConfig', $editorConfig );
}
// Build a dummy textarea to be used: for submitting form if CodeEditor isn't installed,
// and in case JS is disabled (with or without CodeEditor)
if ( !$isUserAllowed ) {
$editorAttribs['readonly'] = 'readonly';
}
if ( $externalForm ) {
$editorAttribs['form'] = 'wpFilterForm';
}
$rulesContainer .= Xml::textarea( 'wpFilterRules', $rules, 40, 15, $editorAttribs );
$output = $this->getEditBox( $rules, $isUserAllowed, $externalForm );
if ( $isUserAllowed ) {
// Generate builder drop-down
$rawDropDown = $this->keywordsManager->getBuilderValues();
$dropDown = $this->getSuggestionsDropdown();
// The array needs to be rearranged to be understood by OOUI. It comes with the format
// [ group-msg-key => [ text-to-add => text-msg-key ] ] and we need it as
// [ group-msg => [ text-msg => text-to-add ] ]
// Also, the 'other' element must be the first one.
$dropDownOptions = [ $this->localizer->msg( 'abusefilter-edit-builder-select' )->text() => 'other' ];
foreach ( $rawDropDown as $group => $values ) {
// Give grep a chance to find the usages:
// abusefilter-edit-builder-group-op-arithmetic, abusefilter-edit-builder-group-op-comparison,
// abusefilter-edit-builder-group-op-bool, abusefilter-edit-builder-group-misc,
// abusefilter-edit-builder-group-funcs, abusefilter-edit-builder-group-vars
$localisedGroup = $this->localizer->msg( "abusefilter-edit-builder-group-$group" )->text();
$dropDownOptions[ $localisedGroup ] = array_flip( $values );
$newKeys = array_map(
function ( $key ) use ( $group ) {
return $this->localizer->msg( "abusefilter-edit-builder-$group-$key" )->text();
},
array_keys( $dropDownOptions[ $localisedGroup ] )
);
$dropDownOptions[ $localisedGroup ] = array_combine(
$newKeys,
$dropDownOptions[ $localisedGroup ]
);
}
$dropDownList = Xml::listDropDownOptionsOoui( $dropDownOptions );
$dropDown = new DropdownInputWidget( [
'name' => 'wpFilterBuilder',
'inputId' => 'wpFilterBuilder',
'options' => $dropDownList
] );
$formElements = [ new FieldLayout( $dropDown ) ];
// Button for syntax check
$syntaxCheck = new ButtonWidget(
[
'label' => $this->localizer->msg( 'abusefilter-edit-check' )->text(),
'id' => 'mw-abusefilter-syntaxcheck'
] + $noTestAttrib
);
// Button for switching editor (if Ace is used)
if ( $switchEditor !== null ) {
$formElements[] = new FieldLayout(
new Widget( [
'content' => new HorizontalLayout( [
'items' => [ $switchEditor, $syntaxCheck ]
] )
] )
);
} else {
$formElements[] = new FieldLayout( $syntaxCheck );
}
$formElements = [
new FieldLayout( $dropDown ),
new FieldLayout( $this->getEditorControls() )
];
$fieldSet = new FieldsetLayout( [
'items' => $formElements,
'classes' => [ 'mw-abusefilter-edit-buttons', 'mw-abusefilter-javascript-tools' ]
] );
$rulesContainer .= $fieldSet;
$output .= $fieldSet;
}
if ( $addResultDiv ) {
$rulesContainer .= Xml::element(
$output .= Xml::element(
'div',
[ 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ],
'&#160;'
);
}
return $rulesContainer;
return $output;
}
/**
* Extract values for syntax highlight
*
* @param bool $canEdit
* @return array
* @return DropdownInputWidget
*/
private function getAceConfig( bool $canEdit ): array {
$values = $this->keywordsManager->getBuilderValues();
$deprecatedVars = $this->keywordsManager->getDeprecatedVariables();
private function getSuggestionsDropdown() : DropdownInputWidget {
$rawDropDown = $this->keywordsManager->getBuilderValues();
$builderVariables = implode( '|', array_keys( $values['vars'] ) );
$builderFunctions = implode( '|', array_keys( AbuseFilterParser::FUNCTIONS ) );
// AbuseFilterTokenizer::KEYWORDS also includes constants (true, false and null),
// but Ace redefines these constants afterwards so this will not be an issue
$builderKeywords = implode( '|', AbuseFilterTokenizer::KEYWORDS );
// Extract operators from tokenizer like we do in AbuseFilterParserTest
$operators = implode( '|', array_map( function ( $op ) {
return preg_quote( $op, '/' );
}, AbuseFilterTokenizer::OPERATORS ) );
$deprecatedVariables = implode( '|', array_keys( $deprecatedVars ) );
$disabledVariables = implode( '|', array_keys( $this->keywordsManager->getDisabledVariables() ) );
// The array needs to be rearranged to be understood by OOUI. It comes with the format
// [ group-msg-key => [ text-to-add => text-msg-key ] ] and we need it as
// [ group-msg => [ text-msg => text-to-add ] ]
// Also, the 'other' element must be the first one.
$dropDownOptions = [ $this->localizer->msg( 'abusefilter-edit-builder-select' )->text() => 'other' ];
foreach ( $rawDropDown as $group => $values ) {
// Give grep a chance to find the usages:
// abusefilter-edit-builder-group-op-arithmetic, abusefilter-edit-builder-group-op-comparison,
// abusefilter-edit-builder-group-op-bool, abusefilter-edit-builder-group-misc,
// abusefilter-edit-builder-group-funcs, abusefilter-edit-builder-group-vars
$localisedGroup = $this->localizer->msg( "abusefilter-edit-builder-group-$group" )->text();
$dropDownOptions[ $localisedGroup ] = array_flip( $values );
$newKeys = array_map(
function ( $key ) use ( $group ) {
return $this->localizer->msg( "abusefilter-edit-builder-$group-$key" )->text();
},
array_keys( $dropDownOptions[ $localisedGroup ] )
);
$dropDownOptions[ $localisedGroup ] = array_combine(
$newKeys,
$dropDownOptions[ $localisedGroup ]
);
}
return [
'variables' => $builderVariables,
'functions' => $builderFunctions,
'keywords' => $builderKeywords,
'operators' => $operators,
'deprecated' => $deprecatedVariables,
'disabled' => $disabledVariables,
'aceReadOnly' => !$canEdit
];
$dropDownList = Xml::listDropDownOptionsOoui( $dropDownOptions );
return new DropdownInputWidget( [
'name' => 'wpFilterBuilder',
'inputId' => 'wpFilterBuilder',
'options' => $dropDownList
] );
}
/**
* Get an additional widget that "controls" the editor, and is placed next to it
* Precondition: the user has full rights.
*
* @return Widget
*/
protected function getEditorControls() : Widget {
return new ButtonWidget(
[
'label' => $this->localizer->msg( 'abusefilter-edit-check' )->text(),
'id' => 'mw-abusefilter-syntaxcheck'
]
);
}
/**
* Generate the HTML for the actual edit box
*
* @param string $rules
* @param bool $isUserAllowed
* @param bool $externalForm
* @return string
*/
abstract protected function getEditBox( string $rules, bool $isUserAllowed, bool $externalForm ) : string;
}

View file

@ -2,6 +2,7 @@
namespace MediaWiki\Extension\AbuseFilter\EditBox;
use BadMethodCallException;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MessageLocalizer;
@ -40,6 +41,7 @@ class EditBoxBuilderFactory {
}
/**
* Returns a builder, preferring the Ace version if available
* @param MessageLocalizer $messageLocalizer
* @param User $user
* @param OutputPage $output
@ -50,14 +52,57 @@ class EditBoxBuilderFactory {
User $user,
OutputPage $output
) : EditBoxBuilder {
return new EditBoxBuilder(
return $this->isCodeEditorLoaded
? $this->newAceBoxBuilder( $messageLocalizer, $user, $output )
: $this->newPlainBoxBuilder( $messageLocalizer, $user, $output );
}
/**
* @param MessageLocalizer $messageLocalizer
* @param User $user
* @param OutputPage $output
* @return PlainEditBoxBuiler
*/
public function newPlainBoxBuilder(
MessageLocalizer $messageLocalizer,
User $user,
OutputPage $output
) : PlainEditBoxBuiler {
return new PlainEditBoxBuiler(
$this->afPermManager,
$this->keywordsManager,
$this->isCodeEditorLoaded,
$messageLocalizer,
$user,
$output
);
}
/**
* @param MessageLocalizer $messageLocalizer
* @param User $user
* @param OutputPage $output
* @return AceEditBoxBuiler
*/
public function newAceBoxBuilder(
MessageLocalizer $messageLocalizer,
User $user,
OutputPage $output
) : AceEditBoxBuiler {
if ( !$this->isCodeEditorLoaded ) {
throw new BadMethodCallException( 'Cannot create Ace box without CodeEditor' );
}
return new AceEditBoxBuiler(
$this->afPermManager,
$this->keywordsManager,
$messageLocalizer,
$user,
$output,
$this->newPlainBoxBuilder(
$messageLocalizer,
$user,
$output
)
);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Extension\AbuseFilter\EditBox;
use Xml;
/**
* Class responsible for building a plain text filter edit box
*/
class PlainEditBoxBuiler extends EditBoxBuilder {
/**
* @inheritDoc
*/
public function getEditBox( string $rules, bool $isUserAllowed, bool $externalForm ) : string {
$rules = rtrim( $rules ) . "\n";
// Rules are in English
$editorAttribs = [ 'dir' => 'ltr' ];
if ( !$isUserAllowed ) {
$editorAttribs['readonly'] = 'readonly';
}
if ( $externalForm ) {
$editorAttribs['form'] = 'wpFilterForm';
}
return Xml::textarea( 'wpFilterRules', $rules, 40, 15, $editorAttribs );
}
}

View file

@ -2,9 +2,11 @@
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use BadMethodCallException;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilder;
use MediaWiki\Extension\AbuseFilter\EditBox\AceEditBoxBuiler;
use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory;
use MediaWiki\Extension\AbuseFilter\EditBox\PlainEditBoxBuiler;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWikiUnitTestCase;
use MessageLocalizer;
@ -16,6 +18,18 @@ use User;
*/
class EditBoxBuilderFactoryTest extends MediaWikiUnitTestCase {
/**
* @param bool $isCodeEditorLoaded
* @return EditBoxBuilderFactory
*/
private function getFactory( bool $isCodeEditorLoaded ) : EditBoxBuilderFactory {
return new EditBoxBuilderFactory(
$this->createMock( AbuseFilterPermissionManager::class ),
$this->createMock( KeywordsManager::class ),
$isCodeEditorLoaded
);
}
/**
* @covers ::__construct
* @covers ::newEditBoxBuilder
@ -24,17 +38,14 @@ class EditBoxBuilderFactoryTest extends MediaWikiUnitTestCase {
* @param bool $isCodeEditorLoaded
*/
public function testNewEditBoxBuilder( bool $isCodeEditorLoaded ) {
$factory = new EditBoxBuilderFactory(
$this->createMock( AbuseFilterPermissionManager::class ),
$this->createMock( KeywordsManager::class ),
$isCodeEditorLoaded
);
$builder = $factory->newEditBoxBuilder(
$builder = $this->getFactory( $isCodeEditorLoaded )->newEditBoxBuilder(
$this->createMock( MessageLocalizer::class ),
$this->createMock( User::class ),
$this->createMock( OutputPage::class )
);
$this->assertInstanceOf( EditBoxBuilder::class, $builder );
$isCodeEditorLoaded
? $this->assertInstanceOf( AceEditBoxBuiler::class, $builder )
: $this->assertInstanceOf( PlainEditBoxBuiler::class, $builder );
}
public function provideNewEditBoxBuilder() : array {
@ -44,4 +55,43 @@ class EditBoxBuilderFactoryTest extends MediaWikiUnitTestCase {
];
}
/**
* @covers ::newPlainBoxBuilder
*/
public function testNewPlainBoxBuilder() {
$this->assertInstanceOf(
PlainEditBoxBuiler::class,
$this->getFactory( false )->newPlainBoxBuilder(
$this->createMock( MessageLocalizer::class ),
$this->createMock( User::class ),
$this->createMock( OutputPage::class )
)
);
}
/**
* @covers ::newAceBoxBuilder
*/
public function testNewAceBoxBuilder() {
$this->assertInstanceOf(
AceEditBoxBuiler::class,
$this->getFactory( true )->newAceBoxBuilder(
$this->createMock( MessageLocalizer::class ),
$this->createMock( User::class ),
$this->createMock( OutputPage::class )
)
);
}
/**
* @covers ::newAceBoxBuilder
*/
public function testNewAceBoxBuilder__invalid() {
$this->expectException( BadMethodCallException::class );
$this->getFactory( false )->newAceBoxBuilder(
$this->createMock( MessageLocalizer::class ),
$this->createMock( User::class ),
$this->createMock( OutputPage::class )
);
}
}