Changeset View
Changeset View
Standalone View
Standalone View
src/applications/system/engine/PhabricatorSystemActionEngine.php
<?php | <?php | ||||
final class PhabricatorSystemActionEngine extends Phobject { | final class PhabricatorSystemActionEngine extends Phobject { | ||||
/** | |||||
* Prepare to take an action, throwing an exception if the user has exceeded | |||||
* the rate limit. | |||||
* | |||||
* The `$actors` are a list of strings. Normally this will be a list of | |||||
* user PHIDs, but some systems use other identifiers (like email | |||||
* addresses). Each actor's score threshold is tracked independently. If | |||||
* any actor exceeds the rate limit for the action, this method throws. | |||||
* | |||||
* The `$action` defines the actual thing being rate limited, and sets the | |||||
* limit. | |||||
* | |||||
* You can pass either a positive, zero, or negative `$score` to this method: | |||||
* | |||||
* - If the score is positive, the user is given that many points toward | |||||
* the rate limit after the limit is checked. Over time, this will cause | |||||
* them to hit the rate limit and be prevented from taking further | |||||
* actions. | |||||
* - If the score is zero, the rate limit is checked but no score changes | |||||
* are made. This allows you to check for a rate limit before beginning | |||||
* a workflow, so the user doesn't fill in a form only to get rate limited | |||||
* at the end. | |||||
* - If the score is negative, the user is credited points, allowing them | |||||
* to take more actions than the limit normally permits. By awarding | |||||
* points for failed actions and credits for successful actions, a | |||||
* system can be sensitive to failure without overly restricting | |||||
* legitimate uses. | |||||
* | |||||
* If any actor is exceeding their rate limit, this method throws a | |||||
* @{class:PhabricatorSystemActionRateLimitException}. | |||||
* | |||||
* @param list<string> List of actors. | |||||
* @param PhabricatorSystemAction Action being taken. | |||||
* @param float Score or credit, see above. | |||||
* @return void | |||||
*/ | |||||
public static function willTakeAction( | public static function willTakeAction( | ||||
array $actors, | array $actors, | ||||
PhabricatorSystemAction $action, | PhabricatorSystemAction $action, | ||||
$score) { | $score) { | ||||
// If the score for this action is negative, we're giving the user a credit, | // If the score for this action is negative, we're giving the user a credit, | ||||
// so don't bother checking if they're blocked or not. | // so don't bother checking if they're blocked or not. | ||||
if ($score >= 0) { | if ($score >= 0) { | ||||
$blocked = self::loadBlockedActors($actors, $action, $score); | $blocked = self::loadBlockedActors($actors, $action, $score); | ||||
if ($blocked) { | if ($blocked) { | ||||
foreach ($blocked as $actor => $actor_score) { | foreach ($blocked as $actor => $actor_score) { | ||||
throw new PhabricatorSystemActionRateLimitException( | throw new PhabricatorSystemActionRateLimitException( | ||||
$action, | $action, | ||||
$actor_score); | $actor_score); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
if ($score != 0) { | |||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | ||||
self::recordAction($actors, $action, $score); | self::recordAction($actors, $action, $score); | ||||
unset($unguarded); | unset($unguarded); | ||||
} | } | ||||
} | |||||
public static function loadBlockedActors( | public static function loadBlockedActors( | ||||
array $actors, | array $actors, | ||||
PhabricatorSystemAction $action, | PhabricatorSystemAction $action, | ||||
$score) { | $score) { | ||||
$scores = self::loadScores($actors, $action); | $scores = self::loadScores($actors, $action); | ||||
$window = self::getWindow(); | $window = self::getWindow(); | ||||
$blocked = array(); | $blocked = array(); | ||||
foreach ($scores as $actor => $actor_score) { | foreach ($scores as $actor => $actor_score) { | ||||
$actor_score = $actor_score + ($score / $window); | // For the purposes of checking for a block, we just use the raw | ||||
// persistent score and do not include the score for this action. This | |||||
// allows callers to test for a block without adding any points and get | |||||
// the same result they would if they were adding points: we only | |||||
// trigger a rate limit when the persistent score exceeds the threshold. | |||||
if ($action->shouldBlockActor($actor, $actor_score)) { | if ($action->shouldBlockActor($actor, $actor_score)) { | ||||
$blocked[$actor] = $actor_score; | // When reporting the results, we do include the points for this | ||||
// action. This makes the error messages more clear, since they | |||||
// more accurately report the number of actions the user has really | |||||
// tried to take. | |||||
$blocked[$actor] = $actor_score + ($score / $window); | |||||
} | } | ||||
} | } | ||||
return $blocked; | return $blocked; | ||||
} | } | ||||
public static function loadScores( | public static function loadScores( | ||||
array $actors, | array $actors, | ||||
▲ Show 20 Lines • Show All 76 Lines • Show Last 20 Lines |