[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/system/engine/ -> PhabricatorSystemActionEngine.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1