2011-03-22 21:22:40 +01:00
|
|
|
<?php
|
|
|
|
|
2012-03-14 00:21:04 +01:00
|
|
|
final class HeraldEngine {
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
protected $rules = array();
|
|
|
|
protected $results = array();
|
|
|
|
protected $stack = array();
|
|
|
|
protected $activeRule = null;
|
|
|
|
|
|
|
|
protected $fieldCache = array();
|
|
|
|
protected $object = null;
|
|
|
|
|
2013-08-05 19:02:40 +02:00
|
|
|
public function getRule($id) {
|
|
|
|
return idx($this->rules, $id);
|
|
|
|
}
|
|
|
|
|
2013-08-02 17:38:17 +02:00
|
|
|
public static function loadAndApplyRules(HeraldAdapter $object) {
|
2013-08-02 21:03:54 +02:00
|
|
|
$content_type = $object->getAdapterContentType();
|
Remove massive "rule applied" query
Summary:
Herald rules may be marked as "one-time". We track this by writing a row with
<ruleID, phid> when we apply a rule.
However, the current test for rule application involves loading every <ruleID,
*> pair. We also always write this row even for rules which are not one-time, so
if there are 100 rules, we'll load 1,000,000 rows after processing 10,000
objects.
Instead, load only the <phid, *> pairs, which are guaranteed to be bounded to at
most the number of rules.
I'll follow up with a diff that causes us to write rows only for one-time rules,
and deletes all historic rows which are not associated with one-time rules.
Test Plan:
Grepped for callsites to loadAllByContentTypeWithFullData(). Ran
rules in test console.
Reviewers: nh, btrahan, jungejason
Reviewed By: nh
CC: aran, epriestley
Differential Revision: https://secure.phabricator.com/D1483
2012-01-25 04:03:30 +01:00
|
|
|
$rules = HeraldRule::loadAllByContentTypeWithFullData(
|
|
|
|
$content_type,
|
|
|
|
$object->getPHID());
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
$engine = new HeraldEngine();
|
|
|
|
$effects = $engine->applyRules($rules, $object);
|
2012-01-25 20:53:39 +01:00
|
|
|
$engine->applyEffects($effects, $object, $rules);
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
return $engine->getTranscript();
|
|
|
|
}
|
|
|
|
|
2013-08-02 17:38:17 +02:00
|
|
|
public function applyRules(array $rules, HeraldAdapter $object) {
|
2012-04-03 21:10:45 +02:00
|
|
|
assert_instances_of($rules, 'HeraldRule');
|
2011-03-22 21:22:40 +01:00
|
|
|
$t_start = microtime(true);
|
|
|
|
|
|
|
|
$rules = mpull($rules, null, 'getID');
|
|
|
|
|
|
|
|
$this->transcript = new HeraldTranscript();
|
2011-03-25 05:32:26 +01:00
|
|
|
$this->transcript->setObjectPHID((string)$object->getPHID());
|
2011-03-22 21:22:40 +01:00
|
|
|
$this->fieldCache = array();
|
|
|
|
$this->results = array();
|
|
|
|
$this->rules = $rules;
|
|
|
|
$this->object = $object;
|
|
|
|
|
|
|
|
$effects = array();
|
|
|
|
foreach ($rules as $id => $rule) {
|
|
|
|
$this->stack = array();
|
|
|
|
try {
|
2011-05-28 00:52:26 +02:00
|
|
|
if (($rule->getRepetitionPolicy() ==
|
|
|
|
HeraldRepetitionPolicyConfig::FIRST) &&
|
|
|
|
$rule->getRuleApplied($object->getPHID())) {
|
|
|
|
// This rule is only supposed to be applied a single time, and it's
|
|
|
|
// aleady been applied, so this is an automatic failure.
|
|
|
|
$xscript = id(new HeraldRuleTranscript())
|
|
|
|
->setRuleID($id)
|
|
|
|
->setResult(false)
|
|
|
|
->setRuleName($rule->getName())
|
|
|
|
->setRuleOwner($rule->getAuthorPHID())
|
|
|
|
->setReason(
|
|
|
|
"This rule is only supposed to be repeated a single time, ".
|
2013-02-19 22:33:10 +01:00
|
|
|
"and it has already been applied.");
|
2011-05-28 00:52:26 +02:00
|
|
|
$this->transcript->addRuleTranscript($xscript);
|
|
|
|
$rule_matches = false;
|
|
|
|
} else {
|
|
|
|
$rule_matches = $this->doesRuleMatch($rule, $object);
|
|
|
|
}
|
2011-03-22 21:22:40 +01:00
|
|
|
} catch (HeraldRecursiveConditionsException $ex) {
|
|
|
|
$names = array();
|
|
|
|
foreach ($this->stack as $rule_id => $ignored) {
|
|
|
|
$names[] = '"'.$rules[$rule_id]->getName().'"';
|
|
|
|
}
|
|
|
|
$names = implode(', ', $names);
|
|
|
|
foreach ($this->stack as $rule_id => $ignored) {
|
|
|
|
$xscript = new HeraldRuleTranscript();
|
|
|
|
$xscript->setRuleID($rule_id);
|
|
|
|
$xscript->setResult(false);
|
|
|
|
$xscript->setReason(
|
|
|
|
"Rules {$names} are recursively dependent upon one another! ".
|
|
|
|
"Don't do this! You have formed an unresolvable cycle in the ".
|
|
|
|
"dependency graph!");
|
|
|
|
$xscript->setRuleName($rules[$rule_id]->getName());
|
2011-03-25 05:32:26 +01:00
|
|
|
$xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
|
2011-03-22 21:22:40 +01:00
|
|
|
$this->transcript->addRuleTranscript($xscript);
|
|
|
|
}
|
|
|
|
$rule_matches = false;
|
|
|
|
}
|
|
|
|
$this->results[$id] = $rule_matches;
|
|
|
|
|
|
|
|
if ($rule_matches) {
|
|
|
|
foreach ($this->getRuleEffects($rule, $object) as $effect) {
|
|
|
|
$effects[] = $effect;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$object_transcript = new HeraldObjectTranscript();
|
2011-03-25 05:32:26 +01:00
|
|
|
$object_transcript->setPHID($object->getPHID());
|
2011-03-22 21:22:40 +01:00
|
|
|
$object_transcript->setName($object->getHeraldName());
|
2013-08-02 21:03:54 +02:00
|
|
|
$object_transcript->setType($object->getAdapterContentType());
|
2011-03-22 21:22:40 +01:00
|
|
|
$object_transcript->setFields($this->fieldCache);
|
|
|
|
|
|
|
|
$this->transcript->setObjectTranscript($object_transcript);
|
|
|
|
|
|
|
|
$t_end = microtime(true);
|
|
|
|
|
|
|
|
$this->transcript->setDuration($t_end - $t_start);
|
|
|
|
|
|
|
|
return $effects;
|
|
|
|
}
|
|
|
|
|
2012-01-25 20:53:39 +01:00
|
|
|
public function applyEffects(
|
|
|
|
array $effects,
|
2013-08-02 17:38:17 +02:00
|
|
|
HeraldAdapter $object,
|
2012-01-25 20:53:39 +01:00
|
|
|
array $rules) {
|
2012-04-03 21:10:45 +02:00
|
|
|
assert_instances_of($effects, 'HeraldEffect');
|
|
|
|
assert_instances_of($rules, 'HeraldRule');
|
2012-01-25 20:53:39 +01:00
|
|
|
|
2013-02-18 17:58:35 +01:00
|
|
|
$this->transcript->setDryRun((int)($object instanceof HeraldDryRunAdapter));
|
2011-05-28 00:52:26 +02:00
|
|
|
|
|
|
|
$xscripts = $object->applyHeraldEffects($effects);
|
|
|
|
foreach ($xscripts as $apply_xscript) {
|
2011-03-22 21:22:40 +01:00
|
|
|
if (!($apply_xscript instanceof HeraldApplyTranscript)) {
|
|
|
|
throw new Exception(
|
|
|
|
"Heraldable must return HeraldApplyTranscripts from ".
|
|
|
|
"applyHeraldEffect().");
|
|
|
|
}
|
|
|
|
$this->transcript->addApplyTranscript($apply_xscript);
|
|
|
|
}
|
2011-05-28 00:52:26 +02:00
|
|
|
|
|
|
|
if (!$this->transcript->getDryRun()) {
|
2012-01-25 20:53:39 +01:00
|
|
|
|
|
|
|
$rules = mpull($rules, null, 'getID');
|
|
|
|
$applied_ids = array();
|
|
|
|
$first_policy = HeraldRepetitionPolicyConfig::toInt(
|
|
|
|
HeraldRepetitionPolicyConfig::FIRST);
|
|
|
|
|
2011-05-28 00:52:26 +02:00
|
|
|
// Mark all the rules that have had their effects applied as having been
|
|
|
|
// executed for the current object.
|
2011-06-09 22:48:39 +02:00
|
|
|
$rule_ids = mpull($xscripts, 'getRuleID');
|
2012-01-25 20:53:39 +01:00
|
|
|
|
2011-06-09 22:48:39 +02:00
|
|
|
foreach ($rule_ids as $rule_id) {
|
|
|
|
if (!$rule_id) {
|
|
|
|
// Some apply transcripts are purely informational and not associated
|
|
|
|
// with a rule, e.g. carryover emails from earlier revisions.
|
|
|
|
continue;
|
|
|
|
}
|
2012-01-25 20:53:39 +01:00
|
|
|
|
|
|
|
$rule = idx($rules, $rule_id);
|
|
|
|
if (!$rule) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($rule->getRepetitionPolicy() == $first_policy) {
|
|
|
|
$applied_ids[] = $rule_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($applied_ids) {
|
|
|
|
$conn_w = id(new HeraldRule())->establishConnection('w');
|
|
|
|
$sql = array();
|
|
|
|
foreach ($applied_ids as $id) {
|
|
|
|
$sql[] = qsprintf(
|
|
|
|
$conn_w,
|
|
|
|
'(%s, %d)',
|
|
|
|
$object->getPHID(),
|
|
|
|
$id);
|
|
|
|
}
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
|
|
|
|
HeraldRule::TABLE_RULE_APPLIED,
|
|
|
|
implode(', ', $sql));
|
2011-05-28 00:52:26 +02:00
|
|
|
}
|
|
|
|
}
|
2011-03-22 21:22:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getTranscript() {
|
|
|
|
$this->transcript->save();
|
|
|
|
return $this->transcript;
|
|
|
|
}
|
|
|
|
|
2013-08-05 19:02:40 +02:00
|
|
|
public function doesRuleMatch(
|
2011-03-25 05:32:26 +01:00
|
|
|
HeraldRule $rule,
|
2013-08-02 17:38:17 +02:00
|
|
|
HeraldAdapter $object) {
|
2011-03-25 05:32:26 +01:00
|
|
|
|
2011-03-22 21:22:40 +01:00
|
|
|
$id = $rule->getID();
|
|
|
|
|
|
|
|
if (isset($this->results[$id])) {
|
|
|
|
// If we've already evaluated this rule because another rule depends
|
|
|
|
// on it, we don't need to reevaluate it.
|
|
|
|
return $this->results[$id];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($this->stack[$id])) {
|
|
|
|
// We've recursed, fail all of the rules on the stack. This happens when
|
|
|
|
// there's a dependency cycle with "Rule conditions match for rule ..."
|
|
|
|
// conditions.
|
|
|
|
foreach ($this->stack as $rule_id => $ignored) {
|
|
|
|
$this->results[$rule_id] = false;
|
|
|
|
}
|
|
|
|
throw new HeraldRecursiveConditionsException();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->stack[$id] = true;
|
|
|
|
|
|
|
|
$all = $rule->getMustMatchAll();
|
|
|
|
|
|
|
|
$conditions = $rule->getConditions();
|
|
|
|
|
|
|
|
$result = null;
|
|
|
|
|
|
|
|
$local_version = id(new HeraldRule())->getConfigVersion();
|
|
|
|
if ($rule->getConfigVersion() > $local_version) {
|
|
|
|
$reason = "Rule could not be processed, it was created with a newer ".
|
|
|
|
"version of Herald.";
|
|
|
|
$result = false;
|
|
|
|
} else if (!$conditions) {
|
|
|
|
$reason = "Rule failed automatically because it has no conditions.";
|
|
|
|
$result = false;
|
Disable personal Herald rules from invalid owners
Summary: Since mailing list rules are now "global", don't run "personal" rules
for disabled/invalid users.
Test Plan: Added a personal rule that matches every revision for a test user.
Created a revision, checked transcript, rule matched. Disabled user, updated
revision, checked transcript, rule got auto-disabled.
Reviewers: btrahan, jungejason, nh, xela
Reviewed By: btrahan
CC: aran, epriestley
Differential Revision: https://secure.phabricator.com/D1517
2012-01-31 21:11:31 +01:00
|
|
|
} else if ($rule->hasInvalidOwner()) {
|
|
|
|
$reason = "Rule failed automatically because its owner is invalid ".
|
|
|
|
"or disabled.";
|
2011-03-22 21:22:40 +01:00
|
|
|
$result = false;
|
|
|
|
} else {
|
|
|
|
foreach ($conditions as $condition) {
|
|
|
|
$match = $this->doesConditionMatch($rule, $condition, $object);
|
|
|
|
|
|
|
|
if (!$all && $match) {
|
|
|
|
$reason = "Any condition matched.";
|
|
|
|
$result = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($all && !$match) {
|
|
|
|
$reason = "Not all conditions matched.";
|
|
|
|
$result = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($result === null) {
|
|
|
|
if ($all) {
|
|
|
|
$reason = "All conditions matched.";
|
|
|
|
$result = true;
|
|
|
|
} else {
|
|
|
|
$reason = "No conditions matched.";
|
|
|
|
$result = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$rule_transcript = new HeraldRuleTranscript();
|
|
|
|
$rule_transcript->setRuleID($rule->getID());
|
|
|
|
$rule_transcript->setResult($result);
|
|
|
|
$rule_transcript->setReason($reason);
|
|
|
|
$rule_transcript->setRuleName($rule->getName());
|
2011-03-25 05:32:26 +01:00
|
|
|
$rule_transcript->setRuleOwner($rule->getAuthorPHID());
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
$this->transcript->addRuleTranscript($rule_transcript);
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function doesConditionMatch(
|
|
|
|
HeraldRule $rule,
|
|
|
|
HeraldCondition $condition,
|
2013-08-02 17:38:17 +02:00
|
|
|
HeraldAdapter $object) {
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
$object_value = $this->getConditionObjectValue($condition, $object);
|
|
|
|
$test_value = $condition->getValue();
|
|
|
|
|
2011-03-25 05:32:26 +01:00
|
|
|
$cond = $condition->getFieldCondition();
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
$transcript = new HeraldConditionTranscript();
|
|
|
|
$transcript->setRuleID($rule->getID());
|
|
|
|
$transcript->setConditionID($condition->getID());
|
|
|
|
$transcript->setFieldName($condition->getFieldName());
|
|
|
|
$transcript->setCondition($cond);
|
|
|
|
$transcript->setTestValue($test_value);
|
|
|
|
|
2013-08-05 19:02:40 +02:00
|
|
|
try {
|
|
|
|
$result = $object->doesConditionMatch(
|
|
|
|
$this,
|
|
|
|
$rule,
|
|
|
|
$condition,
|
|
|
|
$object_value);
|
|
|
|
} catch (HeraldInvalidConditionException $ex) {
|
|
|
|
$result = false;
|
|
|
|
$transcript->setNote($ex->getMessage());
|
2011-03-22 21:22:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$transcript->setResult($result);
|
|
|
|
|
|
|
|
$this->transcript->addConditionTranscript($transcript);
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getConditionObjectValue(
|
|
|
|
HeraldCondition $condition,
|
2013-08-02 17:38:17 +02:00
|
|
|
HeraldAdapter $object) {
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
$field = $condition->getFieldName();
|
|
|
|
|
|
|
|
return $this->getObjectFieldValue($field);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getObjectFieldValue($field) {
|
|
|
|
if (isset($this->fieldCache[$field])) {
|
|
|
|
return $this->fieldCache[$field];
|
|
|
|
}
|
|
|
|
|
2013-08-05 19:02:40 +02:00
|
|
|
$result = $this->object->getHeraldField($field);
|
2011-03-22 21:22:40 +01:00
|
|
|
|
|
|
|
$this->fieldCache[$field] = $result;
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2011-03-25 05:32:26 +01:00
|
|
|
protected function getRuleEffects(
|
|
|
|
HeraldRule $rule,
|
2013-08-02 17:38:17 +02:00
|
|
|
HeraldAdapter $object) {
|
2011-03-25 05:32:26 +01:00
|
|
|
|
2011-03-22 21:22:40 +01:00
|
|
|
$effects = array();
|
|
|
|
foreach ($rule->getActions() as $action) {
|
|
|
|
$effect = new HeraldEffect();
|
2011-03-25 05:32:26 +01:00
|
|
|
$effect->setObjectPHID($object->getPHID());
|
2011-03-22 21:22:40 +01:00
|
|
|
$effect->setAction($action->getAction());
|
|
|
|
$effect->setTarget($action->getTarget());
|
|
|
|
|
|
|
|
$effect->setRuleID($rule->getID());
|
|
|
|
|
|
|
|
$name = $rule->getName();
|
|
|
|
$id = $rule->getID();
|
|
|
|
$effect->setReason(
|
|
|
|
'Conditions were met for Herald rule "'.$name.'" (#'.$id.').');
|
|
|
|
|
|
|
|
$effects[] = $effect;
|
|
|
|
}
|
|
|
|
return $effects;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|