diff options
Diffstat (limited to 'includes/editpage')
-rw-r--r-- | includes/editpage/Constraint/AuthorizationConstraint.php | 88 | ||||
-rw-r--r-- | includes/editpage/Constraint/ContentModelChangeConstraint.php | 35 | ||||
-rw-r--r-- | includes/editpage/Constraint/EditConstraintFactory.php | 63 | ||||
-rw-r--r-- | includes/editpage/Constraint/EditRightConstraint.php | 105 | ||||
-rw-r--r-- | includes/editpage/Constraint/LinkPurgeRateLimitConstraint.php (renamed from includes/editpage/Constraint/UserRateLimitConstraint.php) | 23 | ||||
-rw-r--r-- | includes/editpage/Constraint/UserBlockConstraint.php | 79 | ||||
-rw-r--r-- | includes/editpage/EditPage.php | 44 |
7 files changed, 141 insertions, 296 deletions
diff --git a/includes/editpage/Constraint/AuthorizationConstraint.php b/includes/editpage/Constraint/AuthorizationConstraint.php new file mode 100644 index 000000000000..73247160fa7b --- /dev/null +++ b/includes/editpage/Constraint/AuthorizationConstraint.php @@ -0,0 +1,88 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +namespace MediaWiki\EditPage\Constraint; + +use MediaWiki\Page\PageIdentity; +use MediaWiki\Permissions\Authority; +use MediaWiki\Permissions\PermissionStatus; +use StatusValue; + +/** + * Verify authorization to edit the page (user rights, rate limits, blocks). + * + * @since 1.44 + * @internal + */ +class AuthorizationConstraint implements IEditConstraint { + + private PermissionStatus $status; + + private Authority $performer; + private PageIdentity $target; + private bool $new; + + public function __construct( + Authority $performer, + PageIdentity $target, + bool $new + ) { + $this->performer = $performer; + $this->target = $target; + $this->new = $new; + } + + public function checkConstraint(): string { + $this->status = PermissionStatus::newEmpty(); + + if ( $this->new && !$this->performer->authorizeWrite( 'create', $this->target, $this->status ) ) { + return self::CONSTRAINT_FAILED; + } + + if ( !$this->performer->authorizeWrite( 'edit', $this->target, $this->status ) ) { + return self::CONSTRAINT_FAILED; + } + + return self::CONSTRAINT_PASSED; + } + + public function getLegacyStatus(): StatusValue { + $statusValue = StatusValue::newGood(); + + if ( !$this->status->isGood() ) { + // Report the most specific errors first + if ( $this->status->isBlocked() ) { + $statusValue->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); + } elseif ( $this->status->isRateLimitExceeded() ) { + $statusValue->setResult( false, self::AS_RATE_LIMITED ); + } elseif ( $this->status->getPermission() === 'create' ) { + $statusValue->setResult( false, self::AS_NO_CREATE_PERMISSION ); + } elseif ( !$this->performer->isRegistered() ) { + $statusValue->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); + } else { + $statusValue->setResult( false, self::AS_READ_ONLY_PAGE_LOGGED ); + } + } + + // TODO: Use error messages from the PermissionStatus ($this->status) here - T384399 + return $statusValue; + } + +} diff --git a/includes/editpage/Constraint/ContentModelChangeConstraint.php b/includes/editpage/Constraint/ContentModelChangeConstraint.php index 8ed2e56debde..2581ebab3c14 100644 --- a/includes/editpage/Constraint/ContentModelChangeConstraint.php +++ b/includes/editpage/Constraint/ContentModelChangeConstraint.php @@ -21,6 +21,7 @@ namespace MediaWiki\EditPage\Constraint; use MediaWiki\Permissions\Authority; +use MediaWiki\Permissions\PermissionStatus; use MediaWiki\Title\Title; use StatusValue; @@ -28,6 +29,7 @@ use StatusValue; * Verify user permissions if changing content model: * Must have editcontentmodel rights * Must be able to edit under the new content model + * Must not have exceeded the rate limit * * @since 1.36 * @internal @@ -35,10 +37,11 @@ use StatusValue; */ class ContentModelChangeConstraint implements IEditConstraint { + private PermissionStatus $status; + private Authority $performer; private Title $title; private string $newContentModel; - private string $result; /** * @param Authority $performer @@ -56,45 +59,43 @@ class ContentModelChangeConstraint implements IEditConstraint { } public function checkConstraint(): string { + $this->status = PermissionStatus::newEmpty(); + if ( $this->newContentModel === $this->title->getContentModel() ) { // No change - $this->result = self::CONSTRAINT_PASSED; return self::CONSTRAINT_PASSED; } - if ( !$this->performer->isAllowed( 'editcontentmodel' ) ) { - $this->result = self::CONSTRAINT_FAILED; + if ( !$this->performer->authorizeWrite( 'editcontentmodel', $this->title, $this->status ) ) { return self::CONSTRAINT_FAILED; } - // Make sure the user can edit the page under the new content model too + // Make sure the user can edit the page under the new content model too. + // We rely on caching in UserAuthority to avoid bumping the rate limit counter twice. $titleWithNewContentModel = clone $this->title; $titleWithNewContentModel->setContentModel( $this->newContentModel ); - - $canEditModel = $this->performer->authorizeWrite( - 'editcontentmodel', - $titleWithNewContentModel - ); - if ( - !$canEditModel - || !$this->performer->authorizeWrite( 'edit', $titleWithNewContentModel ) + !$this->performer->authorizeWrite( 'editcontentmodel', $titleWithNewContentModel, $this->status ) + || !$this->performer->authorizeWrite( 'edit', $titleWithNewContentModel, $this->status ) ) { - $this->result = self::CONSTRAINT_FAILED; return self::CONSTRAINT_FAILED; } - $this->result = self::CONSTRAINT_PASSED; return self::CONSTRAINT_PASSED; } public function getLegacyStatus(): StatusValue { $statusValue = StatusValue::newGood(); - if ( $this->result === self::CONSTRAINT_FAILED ) { - $statusValue->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + if ( !$this->status->isGood() ) { + if ( $this->status->isRateLimitExceeded() ) { + $statusValue->setResult( false, self::AS_RATE_LIMITED ); + } else { + $statusValue->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + } } + // TODO: Use error messages from the PermissionStatus ($this->status) here - T384399 return $statusValue; } diff --git a/includes/editpage/Constraint/EditConstraintFactory.php b/includes/editpage/Constraint/EditConstraintFactory.php index 750c977d2312..e9fede9f256c 100644 --- a/includes/editpage/Constraint/EditConstraintFactory.php +++ b/includes/editpage/Constraint/EditConstraintFactory.php @@ -26,10 +26,8 @@ use MediaWiki\Context\IContextSource; use MediaWiki\EditPage\SpamChecker; use MediaWiki\HookContainer\HookContainer; use MediaWiki\Language\Language; -use MediaWiki\Linker\LinkTarget; use MediaWiki\Logger\Spi; use MediaWiki\MainConfigNames; -use MediaWiki\Permissions\PermissionManager; use MediaWiki\Permissions\RateLimiter; use MediaWiki\Permissions\RateLimitSubject; use MediaWiki\Title\Title; @@ -54,7 +52,6 @@ class EditConstraintFactory { private ServiceOptions $options; private Spi $loggerFactory; - private PermissionManager $permissionManager; private HookContainer $hookContainer; private ReadOnlyMode $readOnlyMode; private SpamChecker $spamRegexChecker; @@ -74,7 +71,6 @@ class EditConstraintFactory { * * @param ServiceOptions $options * @param Spi $loggerFactory - * @param PermissionManager $permissionManager * @param HookContainer $hookContainer * @param ReadOnlyMode $readOnlyMode * @param SpamChecker $spamRegexChecker @@ -83,7 +79,6 @@ class EditConstraintFactory { public function __construct( ServiceOptions $options, Spi $loggerFactory, - PermissionManager $permissionManager, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, SpamChecker $spamRegexChecker, @@ -95,9 +90,6 @@ class EditConstraintFactory { $this->options = $options; $this->loggerFactory = $loggerFactory; - // UserBlockConstraint - $this->permissionManager = $permissionManager; - // EditFilterMergedContentHookConstraint $this->hookContainer = $hookContainer; @@ -107,7 +99,7 @@ class EditConstraintFactory { // SpamRegexConstraint $this->spamRegexChecker = $spamRegexChecker; - // UserRateLimitConstraint + // LinkPurgeRateLimitConstraint $this->rateLimiter = $rateLimiter; } @@ -163,21 +155,15 @@ class EditConstraintFactory { /** * @param RateLimitSubject $subject - * @param string $oldModel - * @param string $newModel * - * @return UserRateLimitConstraint + * @return LinkPurgeRateLimitConstraint */ - public function newUserRateLimitConstraint( - RateLimitSubject $subject, - string $oldModel, - string $newModel - ): UserRateLimitConstraint { - return new UserRateLimitConstraint( + public function newLinkPurgeRateLimitConstraint( + RateLimitSubject $subject + ): LinkPurgeRateLimitConstraint { + return new LinkPurgeRateLimitConstraint( $this->rateLimiter, - $subject, - $oldModel, - $newModel + $subject ); } @@ -226,39 +212,4 @@ class EditConstraintFactory { ); } - /** - * @param LinkTarget $title - * @param User $user - * @return UserBlockConstraint - */ - public function newUserBlockConstraint( - LinkTarget $title, - User $user - ): UserBlockConstraint { - return new UserBlockConstraint( - $this->permissionManager, - $title, - $user - ); - } - - /** - * @param User $performer - * @param Title $title - * @param bool $new - * @return EditRightConstraint - */ - public function newEditRightConstraint( - User $performer, - Title $title, - bool $new - ): EditRightConstraint { - return new EditRightConstraint( - $performer, - $this->permissionManager, - $title, - $new - ); - } - } diff --git a/includes/editpage/Constraint/EditRightConstraint.php b/includes/editpage/Constraint/EditRightConstraint.php deleted file mode 100644 index 47be037e4dad..000000000000 --- a/includes/editpage/Constraint/EditRightConstraint.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php -/** - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -namespace MediaWiki\EditPage\Constraint; - -use MediaWiki\Permissions\PermissionManager; -use MediaWiki\Title\Title; -use MediaWiki\User\User; -use StatusValue; - -/** - * Verify user permissions: - * Must have edit rights - * - * @since 1.36 - * @internal - * @author DannyS712 - */ -class EditRightConstraint implements IEditConstraint { - - private User $performer; - private PermissionManager $permManager; - private Title $title; - private string $result; - private bool $new; - - /** - * @param User $performer - * @param PermissionManager $permManager - * @param Title $title - * @param bool $new - */ - public function __construct( - User $performer, - PermissionManager $permManager, - Title $title, - bool $new - ) { - $this->performer = $performer; - $this->permManager = $permManager; - $this->title = $title; - $this->new = $new; - } - - public function checkConstraint(): string { - if ( $this->new ) { - // Check isn't simple enough to just repeat when getting the status - if ( !$this->performer->authorizeWrite( 'create', $this->title ) ) { - $this->result = (string)self::AS_NO_CREATE_PERMISSION; - return self::CONSTRAINT_FAILED; - } - } - - // Check isn't simple enough to just repeat when getting the status - // Prior to 1.41 this checked if the user had edit rights in general - // instead of for the specific page in question. - if ( !$this->permManager->userCan( - 'edit', - $this->performer, - $this->title - ) ) { - $this->result = self::CONSTRAINT_FAILED; - return self::CONSTRAINT_FAILED; - } - - $this->result = self::CONSTRAINT_PASSED; - return self::CONSTRAINT_PASSED; - } - - public function getLegacyStatus(): StatusValue { - $statusValue = StatusValue::newGood(); - - if ( $this->result === self::CONSTRAINT_FAILED ) { - if ( !$this->performer->isRegistered() ) { - $statusValue->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); - } else { - $statusValue->fatal( 'readonlytext' ); - $statusValue->value = self::AS_READ_ONLY_PAGE_LOGGED; - } - } elseif ( $this->result === (string)self::AS_NO_CREATE_PERMISSION ) { - $statusValue->fatal( 'nocreatetext' ); - $statusValue->value = self::AS_NO_CREATE_PERMISSION; - } - - return $statusValue; - } - -} diff --git a/includes/editpage/Constraint/UserRateLimitConstraint.php b/includes/editpage/Constraint/LinkPurgeRateLimitConstraint.php index 8f35dd5879d0..d25cf9336d88 100644 --- a/includes/editpage/Constraint/UserRateLimitConstraint.php +++ b/includes/editpage/Constraint/LinkPurgeRateLimitConstraint.php @@ -25,31 +25,26 @@ use MediaWiki\Permissions\RateLimitSubject; use StatusValue; /** - * Verify user doesn't exceed rate limits + * Verify that the user doesn't exceed 'linkpurge' limits, which are weird and special. + * Other rate limits have been integrated into their respective permission checks. * - * @since 1.36 + * @since 1.44 * @internal * @author DannyS712 */ -class UserRateLimitConstraint implements IEditConstraint { +class LinkPurgeRateLimitConstraint implements IEditConstraint { private RateLimitSubject $subject; - private string $oldContentModel; - private string $newContentModel; private RateLimiter $limiter; private string $result; public function __construct( RateLimiter $limiter, - RateLimitSubject $subject, - string $oldContentModel, - string $newContentModel + RateLimitSubject $subject ) { $this->limiter = $limiter; $this->subject = $subject; - $this->oldContentModel = $oldContentModel; - $this->newContentModel = $newContentModel; } private function limit( string $action, int $inc = 1 ): bool { @@ -57,16 +52,10 @@ class UserRateLimitConstraint implements IEditConstraint { } public function checkConstraint(): string { - // Need to check for rate limits on `editcontentmodel` if it is changing - $contentModelChange = ( $this->newContentModel !== $this->oldContentModel ); - // TODO inject and use a ThrottleStore once available, see T261744 // Checking if the user is rate limited increments the counts, so we cannot perform // the check again when getting the status; thus, store the result - if ( $this->limit( 'edit' ) - || $this->limit( 'linkpurge', 0 ) // only counted after the fact - || ( $contentModelChange && $this->limit( 'editcontentmodel' ) ) - ) { + if ( $this->limit( 'linkpurge', /* only counted after the fact */ 0 ) ) { $this->result = self::CONSTRAINT_FAILED; } else { $this->result = self::CONSTRAINT_PASSED; diff --git a/includes/editpage/Constraint/UserBlockConstraint.php b/includes/editpage/Constraint/UserBlockConstraint.php deleted file mode 100644 index bdd33262d434..000000000000 --- a/includes/editpage/Constraint/UserBlockConstraint.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -namespace MediaWiki\EditPage\Constraint; - -use MediaWiki\Linker\LinkTarget; -use MediaWiki\Permissions\PermissionManager; -use MediaWiki\User\User; -use StatusValue; - -/** - * Verify user permissions: - * Must not be blocked from the page - * - * @since 1.36 - * @internal - * @author DannyS712 - */ -class UserBlockConstraint implements IEditConstraint { - - private PermissionManager $permissionManager; - private LinkTarget $title; - private User $user; - private string $result; - - /** - * @param PermissionManager $permissionManager - * @param LinkTarget $title - * @param User $user - */ - public function __construct( - PermissionManager $permissionManager, - LinkTarget $title, - User $user - ) { - $this->permissionManager = $permissionManager; - $this->title = $title; - $this->user = $user; - } - - public function checkConstraint(): string { - // Check isn't simple enough to just repeat when getting the status - if ( $this->permissionManager->isBlockedFrom( $this->user, $this->title ) ) { - $this->result = self::CONSTRAINT_FAILED; - return self::CONSTRAINT_FAILED; - } - - $this->result = self::CONSTRAINT_PASSED; - return self::CONSTRAINT_PASSED; - } - - public function getLegacyStatus(): StatusValue { - $statusValue = StatusValue::newGood(); - - if ( $this->result === self::CONSTRAINT_FAILED ) { - $statusValue->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); - } - - return $statusValue; - } - -} diff --git a/includes/editpage/EditPage.php b/includes/editpage/EditPage.php index 93a3ad5a28e8..f08196894d88 100644 --- a/includes/editpage/EditPage.php +++ b/includes/editpage/EditPage.php @@ -37,6 +37,7 @@ use MediaWiki\Context\IContextSource; use MediaWiki\Debug\DeprecationHelper; use MediaWiki\Deferred\DeferredUpdates; use MediaWiki\EditPage\Constraint\AccidentalRecreationConstraint; +use MediaWiki\EditPage\Constraint\AuthorizationConstraint; use MediaWiki\EditPage\Constraint\BrokenRedirectConstraint; use MediaWiki\EditPage\Constraint\ChangeTagsConstraint; use MediaWiki\EditPage\Constraint\ContentModelChangeConstraint; @@ -54,7 +55,6 @@ use MediaWiki\EditPage\Constraint\PageSizeConstraint; use MediaWiki\EditPage\Constraint\SelfRedirectConstraint; use MediaWiki\EditPage\Constraint\SpamRegexConstraint; use MediaWiki\EditPage\Constraint\UnicodeConstraint; -use MediaWiki\EditPage\Constraint\UserBlockConstraint; use MediaWiki\Exception\ErrorPageError; use MediaWiki\Exception\MWContentSerializationException; use MediaWiki\Exception\MWException; @@ -2189,24 +2189,31 @@ class EditPage implements IEditObject { ) ); $constraintRunner->addConstraint( - $constraintFactory->newUserBlockConstraint( $this->mTitle, $requestUser ) + $constraintFactory->newReadOnlyConstraint() ); + + // Load the page data from the primary DB. If anything changes in the meantime, + // we detect it by using page_latest like a token in a 1 try compare-and-swap. + $this->page->loadPageData( IDBAccessObject::READ_LATEST ); + $new = !$this->page->exists(); + $constraintRunner->addConstraint( - new ContentModelChangeConstraint( + new AuthorizationConstraint( $authority, $this->mTitle, - $this->contentModel + $new ) ); - $constraintRunner->addConstraint( - $constraintFactory->newReadOnlyConstraint() + new ContentModelChangeConstraint( + $authority, + $this->mTitle, + $this->contentModel + ) ); $constraintRunner->addConstraint( - $constraintFactory->newUserRateLimitConstraint( - $requestUser->toRateLimitSubject(), - $this->mTitle->getContentModel(), - $this->contentModel + $constraintFactory->newLinkPurgeRateLimitConstraint( + $requestUser->toRateLimitSubject() ) ); $constraintRunner->addConstraint( @@ -2230,16 +2237,6 @@ class EditPage implements IEditObject { ) ); - // Load the page data from the primary DB. If anything changes in the meantime, - // we detect it by using page_latest like a token in a 1 try compare-and-swap. - $this->page->loadPageData( IDBAccessObject::READ_LATEST ); - $new = !$this->page->exists(); - - // We do this last, as some of the other constraints are more specific - $constraintRunner->addConstraint( - $constraintFactory->newEditRightConstraint( $this->getUserForPermissions(), $this->mTitle, $new ) - ); - // Check the constraints if ( !$constraintRunner->checkConstraints() ) { $failed = $constraintRunner->getFailedConstraint(); @@ -2625,9 +2622,12 @@ class EditPage implements IEditObject { * result from the backend. */ private function handleFailedConstraint( IEditConstraint $failed ): void { - if ( $failed instanceof UserBlockConstraint ) { + if ( $failed instanceof AuthorizationConstraint ) { // Auto-block user's IP if the account was "hard" blocked - if ( !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { + if ( + !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() + && $failed->getLegacyStatus()->value === self::AS_BLOCKED_PAGE_FOR_USER + ) { $this->context->getUser()->spreadAnyEditBlock(); } } elseif ( $failed instanceof DefaultTextConstraint ) { |