[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class PhabricatorSystemActionEngine extends Phobject { 4 5 /** 6 * Prepare to take an action, throwing an exception if the user has exceeded 7 * the rate limit. 8 * 9 * The `$actors` are a list of strings. Normally this will be a list of 10 * user PHIDs, but some systems use other identifiers (like email 11 * addresses). Each actor's score threshold is tracked independently. If 12 * any actor exceeds the rate limit for the action, this method throws. 13 * 14 * The `$action` defines the actual thing being rate limited, and sets the 15 * limit. 16 * 17 * You can pass either a positive, zero, or negative `$score` to this method: 18 * 19 * - If the score is positive, the user is given that many points toward 20 * the rate limit after the limit is checked. Over time, this will cause 21 * them to hit the rate limit and be prevented from taking further 22 * actions. 23 * - If the score is zero, the rate limit is checked but no score changes 24 * are made. This allows you to check for a rate limit before beginning 25 * a workflow, so the user doesn't fill in a form only to get rate limited 26 * at the end. 27 * - If the score is negative, the user is credited points, allowing them 28 * to take more actions than the limit normally permits. By awarding 29 * points for failed actions and credits for successful actions, a 30 * system can be sensitive to failure without overly restricting 31 * legitimate uses. 32 * 33 * If any actor is exceeding their rate limit, this method throws a 34 * @{class:PhabricatorSystemActionRateLimitException}. 35 * 36 * @param list<string> List of actors. 37 * @param PhabricatorSystemAction Action being taken. 38 * @param float Score or credit, see above. 39 * @return void 40 */ 41 public static function willTakeAction( 42 array $actors, 43 PhabricatorSystemAction $action, 44 $score) { 45 46 // If the score for this action is negative, we're giving the user a credit, 47 // so don't bother checking if they're blocked or not. 48 if ($score >= 0) { 49 $blocked = self::loadBlockedActors($actors, $action, $score); 50 if ($blocked) { 51 foreach ($blocked as $actor => $actor_score) { 52 throw new PhabricatorSystemActionRateLimitException( 53 $action, 54 $actor_score); 55 } 56 } 57 } 58 59 if ($score != 0) { 60 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 61 self::recordAction($actors, $action, $score); 62 unset($unguarded); 63 } 64 } 65 66 public static function loadBlockedActors( 67 array $actors, 68 PhabricatorSystemAction $action, 69 $score) { 70 71 $scores = self::loadScores($actors, $action); 72 $window = self::getWindow(); 73 74 $blocked = array(); 75 foreach ($scores as $actor => $actor_score) { 76 // For the purposes of checking for a block, we just use the raw 77 // persistent score and do not include the score for this action. This 78 // allows callers to test for a block without adding any points and get 79 // the same result they would if they were adding points: we only 80 // trigger a rate limit when the persistent score exceeds the threshold. 81 if ($action->shouldBlockActor($actor, $actor_score)) { 82 // When reporting the results, we do include the points for this 83 // action. This makes the error messages more clear, since they 84 // more accurately report the number of actions the user has really 85 // tried to take. 86 $blocked[$actor] = $actor_score + ($score / $window); 87 } 88 } 89 90 return $blocked; 91 } 92 93 public static function loadScores( 94 array $actors, 95 PhabricatorSystemAction $action) { 96 97 if (!$actors) { 98 return array(); 99 } 100 101 $actor_hashes = array(); 102 foreach ($actors as $actor) { 103 $actor_hashes[] = PhabricatorHash::digestForIndex($actor); 104 } 105 106 $log = new PhabricatorSystemActionLog(); 107 108 $window = self::getWindow(); 109 110 $conn_r = $log->establishConnection('r'); 111 $scores = queryfx_all( 112 $conn_r, 113 'SELECT actorIdentity, SUM(score) totalScore FROM %T 114 WHERE action = %s AND actorHash IN (%Ls) 115 AND epoch >= %d GROUP BY actorHash', 116 $log->getTableName(), 117 $action->getActionConstant(), 118 $actor_hashes, 119 (time() - $window)); 120 121 $scores = ipull($scores, 'totalScore', 'actorIdentity'); 122 123 foreach ($scores as $key => $score) { 124 $scores[$key] = $score / $window; 125 } 126 127 $scores = $scores + array_fill_keys($actors, 0); 128 129 return $scores; 130 } 131 132 private static function recordAction( 133 array $actors, 134 PhabricatorSystemAction $action, 135 $score) { 136 137 $log = new PhabricatorSystemActionLog(); 138 $conn_w = $log->establishConnection('w'); 139 140 $sql = array(); 141 foreach ($actors as $actor) { 142 $sql[] = qsprintf( 143 $conn_w, 144 '(%s, %s, %s, %f, %d)', 145 PhabricatorHash::digestForIndex($actor), 146 $actor, 147 $action->getActionConstant(), 148 $score, 149 time()); 150 } 151 152 foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { 153 queryfx( 154 $conn_w, 155 'INSERT INTO %T (actorHash, actorIdentity, action, score, epoch) 156 VALUES %Q', 157 $log->getTableName(), 158 $chunk); 159 } 160 } 161 162 private static function getWindow() { 163 // Limit queries to the last hour of data so we don't need to look at as 164 // many rows. We can use an arbitrarily larger window instead (we normalize 165 // scores to actions per second) but all the actions we care about limiting 166 // have a limit much higher than one action per hour. 167 return phutil_units('1 hour in seconds'); 168 } 169 170 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |