23 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
33 use Psr\Log\LoggerInterface;
34 use Wikimedia\IPUtils;
50 private $wrstatsFactory;
59 private $hookContainer;
65 private $centralIdLookup;
68 private $userGroupManager;
73 private StatsdDataFactoryInterface $stats;
89 private array $nonLimitableActions = [
121 $this->options = $options;
122 $this->wrstatsFactory = $wrstatsFactory;
123 $this->centralIdLookup = $centralIdLookup;
124 $this->userFactory = $userFactory;
125 $this->userGroupManager = $userGroupManager;
126 $this->hookContainer = $hookContainer;
127 $this->hookRunner =
new HookRunner( $hookContainer );
132 public function setStats( StatsdDataFactoryInterface $stats ) {
133 $this->stats = $stats;
136 private function incrementStats( $name ) {
137 $this->stats->increment(
"RateLimiter.$name" );
151 $ip = $subject->
getIP();
152 if ( $ip && IPUtils::isInRanges( $ip, $rateLimitsExcludedIPs ) ) {
160 return $subject->
is( RateLimitSubject::EXEMPT );
174 if ( $this->nonLimitableActions[$action] ??
false ) {
178 if ( isset( $this->rateLimits[$action] ) ) {
182 if ( $this->hookContainer->isRegistered(
'PingLimiter' ) ) {
209 if ( $this->nonLimitableActions[$action] ??
false ) {
214 $ip = $subject->
getIP();
218 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
219 if ( !$this->hookRunner->onPingLimiter( $legacyUser, $action, $result, $incrBy ) ) {
220 $this->incrementStats(
"limit.$action.result." . ( $result ?
'tripped_by_hook' :
'passed_by_hook' ) );
224 if ( !isset( $this->rateLimits[$action] ) ) {
229 if ( $this->canBypass( $action ) && $this->
isExempt( $subject ) ) {
230 $this->incrementStats(
"limit.$action.result.exempt" );
234 $conds = $this->getConditions( $action );
235 $limiter = $this->wrstatsFactory->createRateLimiter( $conds, [
'limiter', $action ] );
236 $limitBatch = $limiter->createBatch( $incrBy );
237 $this->logger->debug( __METHOD__ .
": limiting $action rate for {$user->getName()}" );
239 $id = $user->getId();
240 $isNewbie = $subject->
is( RateLimitSubject::NEWBIE );
244 if ( isset( $conds[
'anon'] ) ) {
245 $limitBatch->localOp(
'anon', [] );
249 if ( isset( $conds[
'user-global'] ) ) {
251 $centralId = $this->centralIdLookup
257 $realm = $this->centralIdLookup->getProviderId();
258 $limitBatch->globalOp(
'user-global', [ $realm, $centralId ] );
261 $limitBatch->localOp(
'user-global', [
'local', $id ] );
266 if ( $isNewbie && $ip ) {
268 if ( isset( $conds[
'ip'] ) ) {
269 $limitBatch->globalOp(
'ip', $ip );
272 if ( isset( $conds[
'subnet'] ) ) {
273 $subnet = IPUtils::getSubnet( $ip );
274 if ( $subnet !==
false ) {
275 $limitBatch->globalOp(
'subnet', $subnet );
281 $userEntityType =
false;
282 if ( $id !== 0 && isset( $conds[
'user'] ) ) {
284 $userEntityType =
'user';
287 if ( $id !== 0 && $isNewbie && isset( $conds[
'newbie'] ) ) {
288 $userEntityType =
'newbie';
292 $userGroups = $this->userGroupManager->getUserGroups( $user );
293 foreach ( $userGroups as $group ) {
294 if ( isset( $conds[$group] ) ) {
295 if ( $userEntityType ===
false
296 || $conds[$group]->perSecond() > $conds[$userEntityType]->perSecond()
298 $userEntityType = $group;
305 if ( $userEntityType !==
false ) {
306 $limitBatch->localOp( $userEntityType, $id );
310 if ( isset( $conds[
'ip-all'] ) && $ip ) {
312 if ( $isNewbie || $userEntityType ===
false
313 || $conds[
'ip-all']->perSecond() > $conds[$userEntityType]->perSecond()
315 $limitBatch->globalOp(
'ip-all', $ip );
320 if ( isset( $conds[
'subnet-all'] ) && $ip ) {
321 $subnet = IPUtils::getSubnet( $ip );
322 if ( $subnet !==
false ) {
324 if ( $isNewbie || $userEntityType ===
false
325 || $conds[
'ip-all']->perSecond() > $conds[$userEntityType]->perSecond()
327 $limitBatch->globalOp(
'subnet-all', $subnet );
333 'name' => $user->getName(),
337 $batchResult = $limitBatch->tryIncr();
338 foreach ( $batchResult->getFailedResults() as $type => $result ) {
340 'User::pingLimiter: User tripped rate limit',
343 'limit' => $result->condition->limit,
344 'period' => $result->condition->window,
345 'count' => $result->prevTotal,
350 $this->incrementStats(
"limit.$action.tripped_by.$type" );
353 $allowed = $batchResult->isAllowed();
355 $this->incrementStats(
"limit.$action.result." . ( $allowed ?
'passed' :
'tripped' ) );
360 private function canBypass(
string $action ) {
361 return $this->rateLimits[$action][
'&can-bypass'] ??
true;
368 private function getConditions( $action ) {
369 if ( !isset( $this->rateLimits[$action] ) ) {
373 foreach ( $this->rateLimits[$action] as $entityType => $limitInfo ) {
374 if ( $entityType[0] ===
'&' ) {
377 [ $limit, $window ] = $limitInfo;
378 $conds[$entityType] =
new LimitCondition(
A class containing constants representing the names of configuration variables.
const RateLimitsExcludedIPs
Name constant for the RateLimitsExcludedIPs setting, for use with Config::get()
const RateLimits
Name constant for the RateLimits setting, for use with Config::get()