[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/herald/engine/ -> HeraldEngine.php (source)

   1  <?php
   2  
   3  final class HeraldEngine {
   4  
   5    protected $rules = array();
   6    protected $results = array();
   7    protected $stack = array();
   8    protected $activeRule = null;
   9  
  10    protected $fieldCache = array();
  11    protected $object = null;
  12    private $dryRun;
  13  
  14    public function setDryRun($dry_run) {
  15      $this->dryRun = $dry_run;
  16      return $this;
  17    }
  18  
  19    public function getDryRun() {
  20      return $this->dryRun;
  21    }
  22  
  23    public function getRule($phid) {
  24      return idx($this->rules, $phid);
  25    }
  26  
  27    public function loadRulesForAdapter(HeraldAdapter $adapter) {
  28      return id(new HeraldRuleQuery())
  29        ->setViewer(PhabricatorUser::getOmnipotentUser())
  30        ->withDisabled(false)
  31        ->withContentTypes(array($adapter->getAdapterContentType()))
  32        ->needConditionsAndActions(true)
  33        ->needAppliedToPHIDs(array($adapter->getPHID()))
  34        ->needValidateAuthors(true)
  35        ->execute();
  36    }
  37  
  38    public static function loadAndApplyRules(HeraldAdapter $adapter) {
  39      $engine = new HeraldEngine();
  40  
  41      $rules = $engine->loadRulesForAdapter($adapter);
  42      $effects = $engine->applyRules($rules, $adapter);
  43      $engine->applyEffects($effects, $adapter, $rules);
  44  
  45      return $engine->getTranscript();
  46    }
  47  
  48    public function applyRules(array $rules, HeraldAdapter $object) {
  49      assert_instances_of($rules, 'HeraldRule');
  50      $t_start = microtime(true);
  51  
  52      // Rules execute in a well-defined order: sort them into execution order.
  53      $rules = msort($rules, 'getRuleExecutionOrderSortKey');
  54      $rules = mpull($rules, null, 'getPHID');
  55  
  56      $this->transcript = new HeraldTranscript();
  57      $this->transcript->setObjectPHID((string)$object->getPHID());
  58      $this->fieldCache = array();
  59      $this->results = array();
  60      $this->rules   = $rules;
  61      $this->object  = $object;
  62  
  63      $effects = array();
  64      foreach ($rules as $phid => $rule) {
  65        $this->stack = array();
  66  
  67        $policy_first = HeraldRepetitionPolicyConfig::FIRST;
  68        $policy_first_int = HeraldRepetitionPolicyConfig::toInt($policy_first);
  69        $is_first_only = ($rule->getRepetitionPolicy() == $policy_first_int);
  70  
  71        try {
  72          if (!$this->getDryRun() &&
  73              $is_first_only &&
  74              $rule->getRuleApplied($object->getPHID())) {
  75            // This is not a dry run, and this rule is only supposed to be
  76            // applied a single time, and it's already been applied...
  77            // That means automatic failure.
  78            $xscript = id(new HeraldRuleTranscript())
  79              ->setRuleID($rule->getID())
  80              ->setResult(false)
  81              ->setRuleName($rule->getName())
  82              ->setRuleOwner($rule->getAuthorPHID())
  83              ->setReason(
  84                'This rule is only supposed to be repeated a single time, '.
  85                'and it has already been applied.');
  86            $this->transcript->addRuleTranscript($xscript);
  87            $rule_matches = false;
  88          } else {
  89            $rule_matches = $this->doesRuleMatch($rule, $object);
  90          }
  91        } catch (HeraldRecursiveConditionsException $ex) {
  92          $names = array();
  93          foreach ($this->stack as $rule_id => $ignored) {
  94            $names[] = '"'.$rules[$rule_id]->getName().'"';
  95          }
  96          $names = implode(', ', $names);
  97          foreach ($this->stack as $rule_id => $ignored) {
  98            $xscript = new HeraldRuleTranscript();
  99            $xscript->setRuleID($rule_id);
 100            $xscript->setResult(false);
 101            $xscript->setReason(
 102              "Rules {$names} are recursively dependent upon one another! ".
 103              "Don't do this! You have formed an unresolvable cycle in the ".
 104              "dependency graph!");
 105            $xscript->setRuleName($rules[$rule_id]->getName());
 106            $xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
 107            $this->transcript->addRuleTranscript($xscript);
 108          }
 109          $rule_matches = false;
 110        }
 111        $this->results[$phid] = $rule_matches;
 112  
 113        if ($rule_matches) {
 114          foreach ($this->getRuleEffects($rule, $object) as $effect) {
 115            $effects[] = $effect;
 116          }
 117        }
 118      }
 119  
 120      $object_transcript = new HeraldObjectTranscript();
 121      $object_transcript->setPHID($object->getPHID());
 122      $object_transcript->setName($object->getHeraldName());
 123      $object_transcript->setType($object->getAdapterContentType());
 124      $object_transcript->setFields($this->fieldCache);
 125  
 126      $this->transcript->setObjectTranscript($object_transcript);
 127  
 128      $t_end = microtime(true);
 129  
 130      $this->transcript->setDuration($t_end - $t_start);
 131  
 132      return $effects;
 133    }
 134  
 135    public function applyEffects(
 136      array $effects,
 137      HeraldAdapter $adapter,
 138      array $rules) {
 139      assert_instances_of($effects, 'HeraldEffect');
 140      assert_instances_of($rules, 'HeraldRule');
 141  
 142      $this->transcript->setDryRun((int)$this->getDryRun());
 143  
 144      if ($this->getDryRun()) {
 145        $xscripts = array();
 146        foreach ($effects as $effect) {
 147          $xscripts[] = new HeraldApplyTranscript(
 148            $effect,
 149            false,
 150            pht('This was a dry run, so no actions were actually taken.'));
 151        }
 152      } else {
 153        $xscripts = $adapter->applyHeraldEffects($effects);
 154      }
 155  
 156      assert_instances_of($xscripts, 'HeraldApplyTranscript');
 157      foreach ($xscripts as $apply_xscript) {
 158        $this->transcript->addApplyTranscript($apply_xscript);
 159      }
 160  
 161      // For dry runs, don't mark the rule as having applied to the object.
 162      if ($this->getDryRun()) {
 163        return;
 164      }
 165  
 166      $rules = mpull($rules, null, 'getID');
 167      $applied_ids = array();
 168      $first_policy = HeraldRepetitionPolicyConfig::toInt(
 169        HeraldRepetitionPolicyConfig::FIRST);
 170  
 171      // Mark all the rules that have had their effects applied as having been
 172      // executed for the current object.
 173      $rule_ids = mpull($xscripts, 'getRuleID');
 174  
 175      foreach ($rule_ids as $rule_id) {
 176        if (!$rule_id) {
 177          // Some apply transcripts are purely informational and not associated
 178          // with a rule, e.g. carryover emails from earlier revisions.
 179          continue;
 180        }
 181  
 182        $rule = idx($rules, $rule_id);
 183        if (!$rule) {
 184          continue;
 185        }
 186  
 187        if ($rule->getRepetitionPolicy() == $first_policy) {
 188          $applied_ids[] = $rule_id;
 189        }
 190      }
 191  
 192      if ($applied_ids) {
 193        $conn_w = id(new HeraldRule())->establishConnection('w');
 194        $sql = array();
 195        foreach ($applied_ids as $id) {
 196          $sql[] = qsprintf(
 197            $conn_w,
 198            '(%s, %d)',
 199            $adapter->getPHID(),
 200            $id);
 201        }
 202        queryfx(
 203          $conn_w,
 204          'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
 205          HeraldRule::TABLE_RULE_APPLIED,
 206          implode(', ', $sql));
 207      }
 208    }
 209  
 210    public function getTranscript() {
 211      $this->transcript->save();
 212      return $this->transcript;
 213    }
 214  
 215    public function doesRuleMatch(
 216      HeraldRule $rule,
 217      HeraldAdapter $object) {
 218  
 219      $phid = $rule->getPHID();
 220  
 221      if (isset($this->results[$phid])) {
 222        // If we've already evaluated this rule because another rule depends
 223        // on it, we don't need to reevaluate it.
 224        return $this->results[$phid];
 225      }
 226  
 227      if (isset($this->stack[$phid])) {
 228        // We've recursed, fail all of the rules on the stack. This happens when
 229        // there's a dependency cycle with "Rule conditions match for rule ..."
 230        // conditions.
 231        foreach ($this->stack as $rule_phid => $ignored) {
 232          $this->results[$rule_phid] = false;
 233        }
 234        throw new HeraldRecursiveConditionsException();
 235      }
 236  
 237      $this->stack[$phid] = true;
 238  
 239      $all = $rule->getMustMatchAll();
 240  
 241      $conditions = $rule->getConditions();
 242  
 243      $result = null;
 244  
 245      $local_version = id(new HeraldRule())->getConfigVersion();
 246      if ($rule->getConfigVersion() > $local_version) {
 247        $reason = pht(
 248          'Rule could not be processed, it was created with a newer version '.
 249          'of Herald.');
 250        $result = false;
 251      } else if (!$conditions) {
 252        $reason = pht(
 253          'Rule failed automatically because it has no conditions.');
 254        $result = false;
 255      } else if (!$rule->hasValidAuthor()) {
 256        $reason = pht(
 257          'Rule failed automatically because its owner is invalid '.
 258          'or disabled.');
 259        $result = false;
 260      } else if (!$this->canAuthorViewObject($rule, $object)) {
 261        $reason = pht(
 262          'Rule failed automatically because it is a personal rule and its '.
 263          'owner can not see the object.');
 264        $result = false;
 265      } else if (!$this->canRuleApplyToObject($rule, $object)) {
 266        $reason = pht(
 267          'Rule failed automatically because it is an object rule which is '.
 268          'not relevant for this object.');
 269        $result = false;
 270      } else {
 271        foreach ($conditions as $condition) {
 272          $match = $this->doesConditionMatch($rule, $condition, $object);
 273  
 274          if (!$all && $match) {
 275            $reason = 'Any condition matched.';
 276            $result = true;
 277            break;
 278          }
 279  
 280          if ($all && !$match) {
 281            $reason = 'Not all conditions matched.';
 282            $result = false;
 283            break;
 284          }
 285        }
 286  
 287        if ($result === null) {
 288          if ($all) {
 289            $reason = 'All conditions matched.';
 290            $result = true;
 291          } else {
 292            $reason = 'No conditions matched.';
 293            $result = false;
 294          }
 295        }
 296      }
 297  
 298      $rule_transcript = new HeraldRuleTranscript();
 299      $rule_transcript->setRuleID($rule->getID());
 300      $rule_transcript->setResult($result);
 301      $rule_transcript->setReason($reason);
 302      $rule_transcript->setRuleName($rule->getName());
 303      $rule_transcript->setRuleOwner($rule->getAuthorPHID());
 304  
 305      $this->transcript->addRuleTranscript($rule_transcript);
 306  
 307      return $result;
 308    }
 309  
 310    protected function doesConditionMatch(
 311      HeraldRule $rule,
 312      HeraldCondition $condition,
 313      HeraldAdapter $object) {
 314  
 315      $object_value = $this->getConditionObjectValue($condition, $object);
 316      $test_value   = $condition->getValue();
 317  
 318      $cond = $condition->getFieldCondition();
 319  
 320      $transcript = new HeraldConditionTranscript();
 321      $transcript->setRuleID($rule->getID());
 322      $transcript->setConditionID($condition->getID());
 323      $transcript->setFieldName($condition->getFieldName());
 324      $transcript->setCondition($cond);
 325      $transcript->setTestValue($test_value);
 326  
 327      try {
 328        $result = $object->doesConditionMatch(
 329          $this,
 330          $rule,
 331          $condition,
 332          $object_value);
 333      } catch (HeraldInvalidConditionException $ex) {
 334        $result = false;
 335        $transcript->setNote($ex->getMessage());
 336      }
 337  
 338      $transcript->setResult($result);
 339  
 340      $this->transcript->addConditionTranscript($transcript);
 341  
 342      return $result;
 343    }
 344  
 345    protected function getConditionObjectValue(
 346      HeraldCondition $condition,
 347      HeraldAdapter $object) {
 348  
 349      $field = $condition->getFieldName();
 350  
 351      return $this->getObjectFieldValue($field);
 352    }
 353  
 354    public function getObjectFieldValue($field) {
 355      if (isset($this->fieldCache[$field])) {
 356        return $this->fieldCache[$field];
 357      }
 358  
 359      $result = $this->object->getHeraldField($field);
 360  
 361      $this->fieldCache[$field] = $result;
 362      return $result;
 363    }
 364  
 365    protected function getRuleEffects(
 366      HeraldRule $rule,
 367      HeraldAdapter $object) {
 368  
 369      $effects = array();
 370      foreach ($rule->getActions() as $action) {
 371        $effect = new HeraldEffect();
 372        $effect->setObjectPHID($object->getPHID());
 373        $effect->setAction($action->getAction());
 374        $effect->setTarget($action->getTarget());
 375  
 376        $effect->setRuleID($rule->getID());
 377        $effect->setRulePHID($rule->getPHID());
 378  
 379        $name = $rule->getName();
 380        $id   = $rule->getID();
 381        $effect->setReason(
 382          pht(
 383            'Conditions were met for %s',
 384            "H{$id} {$name}"));
 385  
 386        $effects[] = $effect;
 387      }
 388      return $effects;
 389    }
 390  
 391    private function canAuthorViewObject(
 392      HeraldRule $rule,
 393      HeraldAdapter $adapter) {
 394  
 395      // Authorship is irrelevant for global rules and object rules.
 396      if ($rule->isGlobalRule() || $rule->isObjectRule()) {
 397        return true;
 398      }
 399  
 400      // The author must be able to create rules for the adapter's content type.
 401      // In particular, this means that the application must be installed and
 402      // accessible to the user. For example, if a user writes a Differential
 403      // rule and then loses access to Differential, this disables the rule.
 404      $enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor());
 405      if (empty($enabled[$adapter->getAdapterContentType()])) {
 406        return false;
 407      }
 408  
 409      // Finally, the author must be able to see the object itself. You can't
 410      // write a personal rule that CC's you on revisions you wouldn't otherwise
 411      // be able to see, for example.
 412      $object = $adapter->getObject();
 413      return PhabricatorPolicyFilter::hasCapability(
 414        $rule->getAuthor(),
 415        $object,
 416        PhabricatorPolicyCapability::CAN_VIEW);
 417    }
 418  
 419    private function canRuleApplyToObject(
 420      HeraldRule $rule,
 421      HeraldAdapter $adapter) {
 422  
 423      // Rules which are not object rules can apply to anything.
 424      if (!$rule->isObjectRule()) {
 425        return true;
 426      }
 427  
 428      $trigger_phid = $rule->getTriggerObjectPHID();
 429      $object_phids = $adapter->getTriggerObjectPHIDs();
 430  
 431      if ($object_phids) {
 432        if (in_array($trigger_phid, $object_phids)) {
 433          return true;
 434        }
 435      }
 436  
 437      return false;
 438    }
 439  
 440  }


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