Skip to content

Commit 8b1d8b6

Browse files
committed
REST: Respond 429 when hitting the edit rate limit
Bug: T374971 Change-Id: I0b519b01f0e42dd494cb4c1f3850b0a67ae878a6
1 parent b454789 commit 8b1d8b6

File tree

15 files changed

+118
-16
lines changed

15 files changed

+118
-16
lines changed

repo/config/Wikibase.ci.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@
7979
$headerMaxSize = $request->getHeader( 'X-Wikibase-CI-MAX-ENTITY-SIZE', WebRequest::GETHEADER_LIST );
8080

8181
$wgWBRepoSettings['maxSerializedEntitySize'] = (int)( $headerMaxSize ?: $originalMaxSize );
82+
83+
if ( $request->getHeader( 'X-Wikibase-CI-Anon-Rate-Limit-Zero', WebRequest::GETHEADER_LIST ) ) {
84+
$wgRateLimits = [ 'edit' => [ 'anon' => [ 0, 60 ] ] ];
85+
}

repo/rest-api/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ The following needs to be correctly set up in order for all the tests to pass:
116116
- `X-Wikibase-CI-Badges`
117117
- `X-Wikibase-CI-Redirect-Badges`
118118
- `X-Wikibase-Ci-Tempuser-Config`
119+
- `X-Wikibase-CI-Anon-Rate-Limit-Zero`
119120

120121

121122
[1]: https://www.mediawiki.org/wiki/MediaWiki_API_integration_tests

repo/rest-api/src/Application/UseCases/RemoveItemDescription/RemoveItemDescription.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Wikibase\Repo\RestApi\Application\UseCases\AssertItemExists;
77
use Wikibase\Repo\RestApi\Application\UseCases\AssertUserIsAuthorized;
88
use Wikibase\Repo\RestApi\Application\UseCases\ItemRedirect;
9+
use Wikibase\Repo\RestApi\Application\UseCases\UpdateExceptionHandler;
910
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
1011
use Wikibase\Repo\RestApi\Domain\Model\DescriptionEditSummary;
1112
use Wikibase\Repo\RestApi\Domain\Model\EditMetadata;
@@ -16,6 +17,7 @@
1617
* @license GPL-2.0-or-later
1718
*/
1819
class RemoveItemDescription {
20+
use UpdateExceptionHandler;
1921

2022
private RemoveItemDescriptionValidator $useCaseValidator;
2123
private AssertItemExists $assertItemExists;
@@ -64,7 +66,7 @@ public function execute( RemoveItemDescriptionRequest $request ): void {
6466
DescriptionEditSummary::newRemoveSummary( $providedEditMetadata->getComment(), $description )
6567
);
6668
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
67-
$this->itemUpdater->update( $item, $editMetadata );
69+
$this->executeWithExceptionHandling( fn() => $this->itemUpdater->update( $item, $editMetadata ) );
6870
}
6971

7072
}

repo/rest-api/src/Application/UseCases/RemoveItemLabel/RemoveItemLabel.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use OutOfBoundsException;
66
use Wikibase\Repo\RestApi\Application\UseCases\AssertItemExists;
77
use Wikibase\Repo\RestApi\Application\UseCases\AssertUserIsAuthorized;
8+
use Wikibase\Repo\RestApi\Application\UseCases\UpdateExceptionHandler;
89
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
910
use Wikibase\Repo\RestApi\Domain\Model\EditMetadata;
1011
use Wikibase\Repo\RestApi\Domain\Model\LabelEditSummary;
@@ -15,6 +16,7 @@
1516
* @license GPL-2.0-or-later
1617
*/
1718
class RemoveItemLabel {
19+
use UpdateExceptionHandler;
1820

1921
private RemoveItemLabelValidator $useCaseValidator;
2022
private AssertItemExists $assertItemExists;
@@ -58,10 +60,10 @@ public function execute( RemoveItemLabelRequest $request ): void {
5860

5961
$summary = LabelEditSummary::newRemoveSummary( $providedEditMetadata->getComment(), $label );
6062

61-
$this->itemUpdater->update(
63+
$this->executeWithExceptionHandling( fn() => $this->itemUpdater->update(
6264
$item, // @phan-suppress-current-line PhanTypeMismatchArgumentNullable
6365
new EditMetadata( $providedEditMetadata->getTags(), $providedEditMetadata->isBot(), $summary )
64-
);
66+
) );
6567
}
6668

6769
}

repo/rest-api/src/Application/UseCases/RemovePropertyDescription/RemovePropertyDescription.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use OutOfBoundsException;
66
use Wikibase\Repo\RestApi\Application\UseCases\AssertPropertyExists;
77
use Wikibase\Repo\RestApi\Application\UseCases\AssertUserIsAuthorized;
8+
use Wikibase\Repo\RestApi\Application\UseCases\UpdateExceptionHandler;
89
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
910
use Wikibase\Repo\RestApi\Domain\Model\DescriptionEditSummary;
1011
use Wikibase\Repo\RestApi\Domain\Model\EditMetadata;
@@ -15,6 +16,7 @@
1516
* @license GPL-2.0-or-later
1617
*/
1718
class RemovePropertyDescription {
19+
use UpdateExceptionHandler;
1820

1921
private RemovePropertyDescriptionValidator $requestValidator;
2022
private AssertPropertyExists $assertPropertyExists;
@@ -60,10 +62,10 @@ public function execute( RemovePropertyDescriptionRequest $request ): void {
6062
$property->getDescriptions()->removeByLanguage( $languageCode );
6163

6264
$summary = DescriptionEditSummary::newRemoveSummary( $providedEditMetadata->getComment(), $description );
63-
$this->propertyUpdater->update(
65+
$this->executeWithExceptionHandling( fn() => $this->propertyUpdater->update(
6466
$property, // @phan-suppress-current-line PhanTypeMismatchArgumentNullable
6567
new EditMetadata( $providedEditMetadata->getTags(), $providedEditMetadata->isBot(), $summary )
66-
);
68+
) );
6769
}
6870

6971
}

repo/rest-api/src/Application/UseCases/RemovePropertyLabel/RemovePropertyLabel.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use OutOfBoundsException;
66
use Wikibase\Repo\RestApi\Application\UseCases\AssertPropertyExists;
77
use Wikibase\Repo\RestApi\Application\UseCases\AssertUserIsAuthorized;
8+
use Wikibase\Repo\RestApi\Application\UseCases\UpdateExceptionHandler;
89
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
910
use Wikibase\Repo\RestApi\Domain\Model\EditMetadata;
1011
use Wikibase\Repo\RestApi\Domain\Model\LabelEditSummary;
@@ -15,6 +16,7 @@
1516
* @license GPL-2.0-or-later
1617
*/
1718
class RemovePropertyLabel {
19+
use UpdateExceptionHandler;
1820

1921
private RemovePropertyLabelValidator $requestValidator;
2022
private AssertPropertyExists $assertPropertyExists;
@@ -60,10 +62,10 @@ public function execute( RemovePropertyLabelRequest $request ): void {
6062

6163
$summary = LabelEditSummary::newRemoveSummary( $providedEditMetadata->getComment(), $label );
6264

63-
$this->propertyUpdater->update(
65+
$this->executeWithExceptionHandling( fn() => $this->propertyUpdater->update(
6466
$property, // @phan-suppress-current-line PhanTypeMismatchArgumentNullable
6567
new EditMetadata( $providedEditMetadata->getTags(), $providedEditMetadata->isBot(), $summary )
66-
);
68+
) );
6769
}
6870

6971
}

repo/rest-api/src/Application/UseCases/RemoveSitelink/RemoveSitelink.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Wikibase\Repo\RestApi\Application\UseCases\AssertItemExists;
66
use Wikibase\Repo\RestApi\Application\UseCases\AssertUserIsAuthorized;
77
use Wikibase\Repo\RestApi\Application\UseCases\ItemRedirect;
8+
use Wikibase\Repo\RestApi\Application\UseCases\UpdateExceptionHandler;
89
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
910
use Wikibase\Repo\RestApi\Domain\Model\EditMetadata;
1011
use Wikibase\Repo\RestApi\Domain\Model\SitelinkEditSummary;
@@ -15,6 +16,7 @@
1516
* @license GPL-2.0-or-later
1617
*/
1718
class RemoveSitelink {
19+
use UpdateExceptionHandler;
1820

1921
private ItemWriteModelRetriever $itemRetriever;
2022
private ItemUpdater $itemUpdater;
@@ -57,14 +59,14 @@ public function execute( RemoveSitelinkRequest $request ): void {
5759

5860
$removedSitelink = $item->getSiteLink( $siteId );
5961
$item->removeSiteLink( $siteId );
60-
$this->itemUpdater->update(
62+
$this->executeWithExceptionHandling( fn() => $this->itemUpdater->update(
6163
$item, // @phan-suppress-current-line PhanTypeMismatchArgumentNullable
6264
new EditMetadata(
6365
$editMetadata->getTags(),
6466
$editMetadata->isBot(),
6567
SitelinkEditSummary::newRemoveSummary( $editMetadata->getComment(), $removedSitelink )
6668
)
67-
);
69+
) );
6870
}
6971

7072
}

repo/rest-api/src/Application/UseCases/RemoveStatement/RemoveStatement.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Wikibase\Repo\RestApi\Application\UseCases\AssertStatementSubjectExists;
66
use Wikibase\Repo\RestApi\Application\UseCases\AssertUserIsAuthorized;
77
use Wikibase\Repo\RestApi\Application\UseCases\ItemRedirect;
8+
use Wikibase\Repo\RestApi\Application\UseCases\UpdateExceptionHandler;
89
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
910
use Wikibase\Repo\RestApi\Domain\Model\EditMetadata;
1011
use Wikibase\Repo\RestApi\Domain\Model\StatementEditSummary;
@@ -15,6 +16,7 @@
1516
* @license GPL-2.0-or-later
1617
*/
1718
class RemoveStatement {
19+
use UpdateExceptionHandler;
1820

1921
private RemoveStatementValidator $validator;
2022
private AssertUserIsAuthorized $assertUserIsAuthorized;
@@ -61,7 +63,9 @@ public function execute( RemoveStatementRequest $request ): void {
6163
StatementEditSummary::newRemoveSummary( $deserializedRequest->getEditMetadata()->getComment(), $statementToRemove )
6264
);
6365

64-
$this->statementRemover->remove( $deserializedRequest->getStatementId(), $editMetadata );
66+
$this->executeWithExceptionHandling(
67+
fn() => $this->statementRemover->remove( $deserializedRequest->getStatementId(), $editMetadata )
68+
);
6569
}
6670

6771
}

repo/rest-api/src/Application/UseCases/UpdateExceptionHandler.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Wikibase\Repo\RestApi\Application\UseCases;
44

55
use Wikibase\Repo\RestApi\Domain\Services\Exceptions\AbuseFilterException;
6+
use Wikibase\Repo\RestApi\Domain\Services\Exceptions\RateLimitReached;
67
use Wikibase\Repo\RestApi\Domain\Services\Exceptions\ResourceTooLargeException;
78

89
/**
@@ -30,6 +31,8 @@ public function executeWithExceptionHandling( callable $callback ) {
3031
'filter_id' => $e->getFilterId(),
3132
'filter_description' => $e->getFilterDescription(),
3233
] );
34+
} catch ( RateLimitReached $e ) {
35+
throw UseCaseError::newRateLimitReached( UseCaseError::REQUEST_LIMIT_REASON_RATE_LIMIT );
3336
}
3437
}
3538

repo/rest-api/src/Application/UseCases/UseCaseError.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class UseCaseError extends UseCaseException {
4141
public const POLICY_VIOLATION_SITELINK_CONFLICT = 'sitelink-conflict';
4242
public const PROPERTY_STATEMENT_ID_MISMATCH = 'property-statement-id-mismatch';
4343
public const REFERENCED_RESOURCE_NOT_FOUND = 'referenced-resource-not-found';
44+
public const REQUEST_LIMIT_REACHED = 'request-limit-reached';
45+
public const REQUEST_LIMIT_REASON_RATE_LIMIT = 'rate-limit-reached';
4446
public const RESOURCE_NOT_FOUND = 'resource-not-found';
4547
public const RESOURCE_TOO_LARGE = 'resource-too-large';
4648
public const STATEMENT_GROUP_PROPERTY_ID_MISMATCH = 'statement-group-property-id-mismatch';
@@ -50,6 +52,8 @@ class UseCaseError extends UseCaseException {
5052
public const CONTEXT_ACTUAL_VALUE = 'actual_value';
5153
public const CONTEXT_CONFLICTING_ITEM_ID = 'conflicting_item_id';
5254
public const CONTEXT_CONFLICTING_PROPERTY_ID = 'conflicting_property_id';
55+
public const CONTEXT_DENIAL_REASON = 'denial_reason';
56+
public const CONTEXT_DENIAL_CONTEXT = 'denial_context';
5357
public const CONTEXT_DESCRIPTION = 'description';
5458
public const CONTEXT_FIELD = 'field';
5559
public const CONTEXT_ITEM_ID = 'item_id';
@@ -60,8 +64,7 @@ class UseCaseError extends UseCaseException {
6064
public const CONTEXT_PARAMETER = 'parameter';
6165
public const CONTEXT_PATH = 'path';
6266
public const CONTEXT_PROPERTY_ID = 'property_id';
63-
public const CONTEXT_DENIAL_REASON = 'denial_reason';
64-
public const CONTEXT_DENIAL_CONTEXT = 'denial_context';
67+
public const CONTEXT_REASON = 'reason';
6568
public const CONTEXT_REDIRECT_TARGET = 'redirect_target';
6669
public const CONTEXT_RESOURCE_TYPE = 'resource_type';
6770
public const CONTEXT_SITE_ID = 'site_id';
@@ -102,6 +105,7 @@ class UseCaseError extends UseCaseException {
102105
self::PERMISSION_DENIED_UNKNOWN_REASON => [],
103106
self::PROPERTY_STATEMENT_ID_MISMATCH => [ self::CONTEXT_PROPERTY_ID, self::CONTEXT_STATEMENT_ID ],
104107
self::REFERENCED_RESOURCE_NOT_FOUND => [ self::CONTEXT_PATH ],
108+
self::REQUEST_LIMIT_REACHED => [ self::CONTEXT_REASON ],
105109
self::RESOURCE_NOT_FOUND => [ self::CONTEXT_RESOURCE_TYPE ],
106110
self::RESOURCE_TOO_LARGE => [ self::CONTEXT_LIMIT ],
107111
self::STATEMENT_GROUP_PROPERTY_ID_MISMATCH => [
@@ -223,6 +227,14 @@ public static function newPermissionDenied( string $reason, array $denialContext
223227
return $error;
224228
}
225229

230+
public static function newRateLimitReached( string $reason ): self {
231+
return new self(
232+
self::REQUEST_LIMIT_REACHED,
233+
'Exceeded the limit of actions that can be performed in a given span of time',
234+
[ self::CONTEXT_REASON => $reason ]
235+
);
236+
}
237+
226238
public static function newResourceNotFound( string $resourceType ): self {
227239
return new self(
228240
self::RESOURCE_NOT_FOUND,

0 commit comments

Comments
 (0)