From 3090a1d1f8ba4e67e0a4b53f5619144a69303513 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gerg=C5=91=20Tisza?= Date: Wed, 21 Sep 2016 01:33:26 +0000 Subject: [PATCH] Add HTMLFormField class for MWRestrictions and use it for bot passwords Change-Id: Ib50238e3be5eec63eb5df97154b60dc4ca33d581 --- autoload.php | 1 + includes/htmlform/HTMLFormField.php | 2 +- .../htmlform/fields/HTMLRestrictionsField.php | 121 ++++++++++++++++++ includes/specials/SpecialBotPasswords.php | 16 +-- includes/utils/MWRestrictions.php | 3 + languages/i18n/en.json | 6 +- languages/i18n/qqq.json | 6 +- .../htmlform/HTMLRestrictionsFieldTest.php | 65 ++++++++++ 8 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 includes/htmlform/fields/HTMLRestrictionsField.php create mode 100644 tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php diff --git a/autoload.php b/autoload.php index 1e98f63147..2c196dd757 100644 --- a/autoload.php +++ b/autoload.php @@ -544,6 +544,7 @@ $wgAutoloadLocalClasses = [ 'HTMLMultiSelectField' => __DIR__ . '/includes/htmlform/fields/HTMLMultiSelectField.php', 'HTMLNestedFilterable' => __DIR__ . '/includes/htmlform/HTMLNestedFilterable.php', 'HTMLRadioField' => __DIR__ . '/includes/htmlform/fields/HTMLRadioField.php', + 'HTMLRestrictionsField' => __DIR__ . '/includes/htmlform/fields/HTMLRestrictionsField.php', 'HTMLSelectAndOtherField' => __DIR__ . '/includes/htmlform/fields/HTMLSelectAndOtherField.php', 'HTMLSelectField' => __DIR__ . '/includes/htmlform/fields/HTMLSelectField.php', 'HTMLSelectLimitField' => __DIR__ . '/includes/htmlform/fields/HTMLSelectLimitField.php', diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index 8604ba2098..4afdea76f8 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -821,7 +821,7 @@ abstract class HTMLFormField { /** * Determine the help text to display * @since 1.20 - * @return string HTML + * @return string|null HTML */ public function getHelpText() { $helptext = null; diff --git a/includes/htmlform/fields/HTMLRestrictionsField.php b/includes/htmlform/fields/HTMLRestrictionsField.php new file mode 100644 index 0000000000..8dc16bff80 --- /dev/null +++ b/includes/htmlform/fields/HTMLRestrictionsField.php @@ -0,0 +1,121 @@ +mLabel ) { + $this->mLabel = $this->msg( 'restrictionsfield-label' )->parse(); + } + } + + public function getHelpText() { + $helpText = parent::getHelpText(); + if ( $helpText === null ) { + $helpText = $this->msg( 'restrictionsfield-help' )->parse(); + } + return $helpText; + } + + /** + * @param WebRequest $request + * @return string|MWRestrictions Restrictions object or original string if invalid + */ + function loadDataFromRequest( $request ) { + if ( !$request->getCheck( $this->mName ) ) { + return $this->getDefault(); + } + + $value = rtrim( $request->getText( $this->mName ), "\r\n" ); + $ips = $value === '' ? [] : explode( PHP_EOL, $value ); + try { + return MWRestrictions::newFromArray( [ 'IPAddresses' => $ips ] ); + } catch ( InvalidArgumentException $e ) { + return $value; + } + } + + /** + * @return MWRestrictions + */ + public function getDefault() { + $default = parent::getDefault(); + if ( $default === null ) { + $default = MWRestrictions::newDefault(); + } + return $default; + } + + /** + * @param string|MWRestrictions $value The value the field was submitted with + * @param array $alldata The data collected from the form + * + * @return bool|string True on success, or String error to display, or + * false to fail validation without displaying an error. + */ + public function validate( $value, $alldata ) { + if ( $this->isHidden( $alldata ) ) { + return true; + } + + if ( + isset( $this->mParams['required'] ) && $this->mParams['required'] !== false + && $value instanceof MWRestrictions && !$value->toArray()['IPAddresses'] + ) { + return $this->msg( 'htmlform-required' )->parse(); + } + + if ( is_string( $value ) ) { + // MWRestrictions::newFromArray failed; one of the IP ranges must be invalid + $status = Status::newGood(); + foreach ( explode( PHP_EOL, $value ) as $range ) { + if ( !\IP::isIPAddress( $range ) ) { + $status->fatal( 'restrictionsfield-badip', $range ); + } + } + if ( $status->isOK() ) { + $status->fatal( 'unknown-error' ); + } + return $status->getMessage()->parse(); + } + + if ( isset( $this->mValidationCallback ) ) { + return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent ); + } + + return true; + } + + /** + * @param string|MWRestrictions $value + * @return string + */ + public function getInputHTML( $value ) { + if ( $value instanceof MWRestrictions ) { + $value = implode( PHP_EOL, $value->toArray()['IPAddresses'] ); + } + return parent::getInputHTML( $value ); + } + + /** + * @param MWRestrictions $value + * @return string + */ + public function getInputOOUI( $value ) { + if ( $value instanceof MWRestrictions ) { + $value = implode( PHP_EOL, $value->toArray()['IPAddresses'] ); + } + return parent::getInputOOUI( $value ); + } +} diff --git a/includes/specials/SpecialBotPasswords.php b/includes/specials/SpecialBotPasswords.php index 9975e41d0b..ed3cd5e7e9 100644 --- a/includes/specials/SpecialBotPasswords.php +++ b/includes/specials/SpecialBotPasswords.php @@ -146,19 +146,9 @@ class SpecialBotPasswords extends FormSpecialPage { ]; $fields['restrictions'] = [ - 'type' => 'textarea', - 'label-message' => 'botpasswords-label-restrictions', + 'class' => 'HTMLRestrictionsField', 'required' => true, - 'default' => $this->botPassword->getRestrictions()->toJson( true ), - 'rows' => 5, - 'validation-callback' => function ( $v ) { - try { - MWRestrictions::newFromJson( $v ); - return true; - } catch ( InvalidArgumentException $ex ) { - return $ex->getMessage(); - } - }, + 'default' => $this->botPassword->getRestrictions(), ]; } else { @@ -282,7 +272,7 @@ class SpecialBotPasswords extends FormSpecialPage { $bp = BotPassword::newUnsaved( [ 'centralId' => $this->userId, 'appId' => $this->par, - 'restrictions' => MWRestrictions::newFromJson( $data['restrictions'] ), + 'restrictions' => $data['restrictions'], 'grants' => array_merge( MWGrants::getHiddenGrants(), preg_replace( '/^grant-/', '', $data['grants'] ) diff --git a/includes/utils/MWRestrictions.php b/includes/utils/MWRestrictions.php index 617e8f59fd..caf88a15f1 100644 --- a/includes/utils/MWRestrictions.php +++ b/includes/utils/MWRestrictions.php @@ -27,6 +27,7 @@ class MWRestrictions { /** * @param array $restrictions + * @throws InvalidArgumentException */ protected function __construct( array $restrictions = null ) { if ( $restrictions !== null ) { @@ -44,6 +45,7 @@ class MWRestrictions { /** * @param array $restrictions * @return MWRestrictions + * @throws InvalidArgumentException */ public static function newFromArray( array $restrictions ) { return new self( $restrictions ); @@ -52,6 +54,7 @@ class MWRestrictions { /** * @param string $json JSON representation of the restrictions * @return MWRestrictions + * @throws InvalidArgumentException */ public static function newFromJson( $json ) { $restrictions = FormatJson::decode( $json, true ); diff --git a/languages/i18n/en.json b/languages/i18n/en.json index b3781c256d..a32d400817 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -565,7 +565,6 @@ "botpasswords-label-resetpassword": "Reset the password", "botpasswords-label-grants": "Applicable grants:", "botpasswords-help-grants": "Each grant gives access to listed user rights that a user account already has. See the [[Special:ListGrants|table of grants]] for more information.", - "botpasswords-label-restrictions": "Usage restrictions:", "botpasswords-label-grants-column": "Granted", "botpasswords-bad-appid": "The bot name \"$1\" is not valid.", "botpasswords-insert-failed": "Failed to add bot name \"$1\". Was it already added?", @@ -4209,5 +4208,8 @@ "unlinkaccounts-success": "The account was unlinked.", "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?", "userjsispublic": "Please note: JavaScript subpages should not contain confidential data as they are viewable by other users.", - "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users." + "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.", + "restrictionsfield-badip": "Invalid IP address or range: $1", + "restrictionsfield-label": "Allowed IP ranges:", + "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use
0.0.0.0/0
::/0" } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 4ec5cec5fa..264ea3951e 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -749,7 +749,6 @@ "botpasswords-label-resetpassword": "Label for the checkbox to reset the actual password for the current bot password.", "botpasswords-label-grants": "Label for the checkmatrix for selecting grants allowed when the bot password is used.\n\ngrant: Vidu http://komputeko.net/index_en.php?vorto=grant sed \"konced/i\" egale funkcius.", "botpasswords-help-grants": "Help text for the grant selection checkmatrix.", - "botpasswords-label-restrictions": "Label for the textarea field in which JSON defining access restrictions (e.g. which IP address ranges are allowed) is entered.", "botpasswords-label-grants-column": "Label for the checkbox column on the checkmatrix for selecting grants allowed when the bot password is used.", "botpasswords-bad-appid": "Used as an error message when an invalid \"bot name\" is supplied on [[Special:BotPasswords]]. Parameters:\n* $1 - The rejected bot name.", "botpasswords-insert-failed": "Error message when saving a new bot password failed. It's likely that the failure was because the user resubmitted the form after a previous successful save. Parameters:\n* $1 - Bot name", @@ -4393,5 +4392,8 @@ "unlinkaccounts-success": "Account unlinking form success message", "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}.", "userjsispublic": "A reminder to users that Javascript subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .js. See also {{msg-mw|usercssispublic}}.", - "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}" + "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}", + "restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.", + "restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).", + "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword)." } diff --git a/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php b/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php new file mode 100644 index 0000000000..9ec4f97fd0 --- /dev/null +++ b/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php @@ -0,0 +1,65 @@ + 'restrictions' ] ); + $this->assertNotEmpty( $field->getLabel(), 'has a default label' ); + $this->assertNotEmpty( $field->getHelpText(), 'has a default help text' ); + $this->assertEquals( MWRestrictions::newDefault(), $field->getDefault(), + 'defaults to the default MWRestrictions object' ); + + $field = new HTMLRestrictionsField( [ + 'fieldname' => 'restrictions', + 'label' => 'foo', + 'help' => 'bar', + 'default' => 'baz', + ] ); + $this->assertEquals( 'foo', $field->getLabel(), 'label can be customized' ); + $this->assertEquals( 'bar', $field->getHelpText(), 'help text can be customized' ); + $this->assertEquals( 'baz', $field->getDefault(), 'default can be customized' ); + } + + /** + * @dataProvider provideValidate + */ + public function testForm( $text, $value ) { + $form = HTMLForm::factory( 'ooui', [ + 'restrictions' => [ 'class' => HTMLRestrictionsField::class ], + ] ); + $request = new FauxRequest( [ 'wprestrictions' => $text ], true ); + $context = new DerivativeContext( RequestContext::getMain() ); + $context->setRequest( $request ); + $form->setContext( $context ); + $form->setTitle( Title::newFromText( 'Main Page' ) )->setSubmitCallback( function () { + return true; + } )->prepareForm(); + $status = $form->trySubmit(); + + if ( $status instanceof StatusValue ) { + $this->assertEquals( $value !== false, $status->isGood() ); + } elseif ( $value === false ) { + $this->assertNotSame( true, $status ); + } else { + $this->assertSame( true, $status ); + } + + if ( $value !== false ) { + $restrictions = $form->mFieldData['restrictions']; + $this->assertInstanceOf( MWRestrictions::class, $restrictions ); + $this->assertEquals( $value, $restrictions->toArray()['IPAddresses'] ); + } + + // sanity + $form->getHTML( $status ); + } + + public function provideValidate() { + return [ + // submitted text, value of 'IPAddresses' key or false for validation error + [ null, [ '0.0.0.0/0', '::/0' ] ], + [ '', [] ], + [ "1.2.3.4\n::/0", [ '1.2.3.4', '::/0' ] ], + [ "1.2.3.4\n::/x", false ], + ]; + } +} -- 2.20.1