[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
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 |