@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 1004 lines 29 kB view raw
1<?php 2 3final class HeraldEngine extends Phobject { 4 5 protected $rules = array(); 6 protected $transcript; 7 8 private $fieldCache = array(); 9 private $fieldExceptions = array(); 10 protected $object; 11 private $dryRun; 12 13 private $forbiddenFields = array(); 14 private $forbiddenActions = array(); 15 private $skipEffects = array(); 16 17 private $profilerStack = array(); 18 private $profilerFrames = array(); 19 20 private $ruleResults; 21 private $ruleStack; 22 23 public function setDryRun($dry_run) { 24 $this->dryRun = $dry_run; 25 return $this; 26 } 27 28 public function getDryRun() { 29 return $this->dryRun; 30 } 31 32 public function getRule($phid) { 33 return idx($this->rules, $phid); 34 } 35 36 public function loadRulesForAdapter(HeraldAdapter $adapter) { 37 return id(new HeraldRuleQuery()) 38 ->setViewer(PhabricatorUser::getOmnipotentUser()) 39 ->withDisabled(false) 40 ->withContentTypes(array($adapter->getAdapterContentType())) 41 ->needConditionsAndActions(true) 42 ->needAppliedToPHIDs(array($adapter->getPHID())) 43 ->needValidateAuthors(true) 44 ->execute(); 45 } 46 47 public static function loadAndApplyRules(HeraldAdapter $adapter) { 48 $engine = new HeraldEngine(); 49 50 $rules = $engine->loadRulesForAdapter($adapter); 51 $effects = $engine->applyRules($rules, $adapter); 52 $engine->applyEffects($effects, $adapter, $rules); 53 54 return $engine->getTranscript(); 55 } 56 57/* -( Rule Stack )--------------------------------------------------------- */ 58 59 private function resetRuleStack() { 60 $this->ruleStack = array(); 61 return $this; 62 } 63 64 private function hasRuleOnStack(HeraldRule $rule) { 65 $phid = $rule->getPHID(); 66 return isset($this->ruleStack[$phid]); 67 } 68 69 private function pushRuleStack(HeraldRule $rule) { 70 $phid = $rule->getPHID(); 71 $this->ruleStack[$phid] = $rule; 72 return $this; 73 } 74 75 private function getRuleStack() { 76 return array_values($this->ruleStack); 77 } 78 79/* -( Rule Results )------------------------------------------------------- */ 80 81 private function resetRuleResults() { 82 $this->ruleResults = array(); 83 return $this; 84 } 85 86 private function setRuleResult( 87 HeraldRule $rule, 88 HeraldRuleResult $result) { 89 90 $phid = $rule->getPHID(); 91 92 if ($this->hasRuleResult($rule)) { 93 throw new Exception( 94 pht( 95 'Herald rule "%s" already has an evaluation result.', 96 $phid)); 97 } 98 99 $this->ruleResults[$phid] = $result; 100 101 $this->newRuleTranscript($rule) 102 ->setRuleResult($result); 103 104 return $this; 105 } 106 107 private function hasRuleResult(HeraldRule $rule) { 108 $phid = $rule->getPHID(); 109 return isset($this->ruleResults[$phid]); 110 } 111 112 private function getRuleResult(HeraldRule $rule) { 113 $phid = $rule->getPHID(); 114 115 if (!$this->hasRuleResult($rule)) { 116 throw new Exception( 117 pht( 118 'Herald rule "%s" does not have an evaluation result.', 119 $phid)); 120 } 121 122 return $this->ruleResults[$phid]; 123 } 124 125 /** 126 * @param array<HeraldRule> $rules 127 * @param HeraldAdapter $object 128 * @return array<HeraldEffect> 129 */ 130 public function applyRules(array $rules, HeraldAdapter $object) { 131 assert_instances_of($rules, HeraldRule::class); 132 $t_start = microtime(true); 133 134 // Rules execute in a well-defined order: sort them into execution order. 135 $rules = msort($rules, 'getRuleExecutionOrderSortKey'); 136 $rules = mpull($rules, null, 'getPHID'); 137 138 $this->transcript = new HeraldTranscript(); 139 $this->transcript->setObjectPHID((string)$object->getPHID()); 140 $this->fieldCache = array(); 141 $this->fieldExceptions = array(); 142 $this->rules = $rules; 143 $this->object = $object; 144 145 $this->resetRuleResults(); 146 147 $effects = array(); 148 foreach ($rules as $phid => $rule) { 149 $this->resetRuleStack(); 150 151 $caught = null; 152 $result = null; 153 try { 154 $is_first_only = $rule->isRepeatFirst(); 155 156 if (!$this->getDryRun() && 157 $is_first_only && 158 $rule->getRuleApplied($object->getPHID())) { 159 160 // This is not a dry run, and this rule is only supposed to be 161 // applied a single time, and it has already been applied. 162 // That means automatic failure. 163 164 $result_code = HeraldRuleResult::RESULT_ALREADY_APPLIED; 165 $result = HeraldRuleResult::newFromResultCode($result_code); 166 } else if ($this->isForbidden($rule, $object)) { 167 $result_code = HeraldRuleResult::RESULT_OBJECT_STATE; 168 $result = HeraldRuleResult::newFromResultCode($result_code); 169 } else { 170 $result = $this->getRuleMatchResult($rule, $object); 171 } 172 } catch (HeraldRecursiveConditionsException $ex) { 173 $cycle_phids = array(); 174 175 $stack = $this->getRuleStack(); 176 foreach ($stack as $stack_rule) { 177 $cycle_phids[] = $stack_rule->getPHID(); 178 } 179 // Add the rule which actually cycled to the list to make the 180 // result more clear when we show it to the user. 181 $cycle_phids[] = $phid; 182 183 foreach ($stack as $stack_rule) { 184 if ($this->hasRuleResult($stack_rule)) { 185 continue; 186 } 187 188 $result_code = HeraldRuleResult::RESULT_RECURSION; 189 $result_data = array( 190 'cyclePHIDs' => $cycle_phids, 191 ); 192 193 $result = HeraldRuleResult::newFromResultCode($result_code) 194 ->setResultData($result_data); 195 $this->setRuleResult($stack_rule, $result); 196 } 197 198 $result = $this->getRuleResult($rule); 199 } catch (HeraldRuleEvaluationException $ex) { 200 // When we encounter an evaluation exception, the condition which 201 // failed to evaluate is responsible for logging the details of the 202 // error. 203 204 $result_code = HeraldRuleResult::RESULT_EVALUATION_EXCEPTION; 205 $result = HeraldRuleResult::newFromResultCode($result_code); 206 } catch (Exception $ex) { 207 $caught = $ex; 208 } catch (Throwable $ex) { 209 $caught = $ex; 210 } 211 212 if ($caught) { 213 // These exceptions are unexpected, and did not arise during rule 214 // evaluation, so we're responsible for handling the details. 215 216 $result_code = HeraldRuleResult::RESULT_EXCEPTION; 217 218 $result_data = array( 219 'exception.class' => get_class($caught), 220 'exception.message' => $ex->getMessage(), 221 ); 222 223 $result = HeraldRuleResult::newFromResultCode($result_code) 224 ->setResultData($result_data); 225 } 226 227 if (!$this->hasRuleResult($rule)) { 228 $this->setRuleResult($rule, $result); 229 } 230 $result = $this->getRuleResult($rule); 231 232 if ($result->getShouldApplyActions()) { 233 foreach ($this->getRuleEffects($rule, $object) as $effect) { 234 $effects[] = $effect; 235 } 236 } 237 } 238 239 $xaction_phids = null; 240 $xactions = $object->getAppliedTransactions(); 241 if ($xactions !== null) { 242 $xaction_phids = mpull($xactions, 'getPHID'); 243 } 244 245 $object_transcript = id(new HeraldObjectTranscript()) 246 ->setPHID($object->getPHID()) 247 ->setName($object->getHeraldName()) 248 ->setType($object->getAdapterContentType()) 249 ->setFields($this->fieldCache) 250 ->setAppliedTransactionPHIDs($xaction_phids) 251 ->setProfile($this->getProfile()); 252 253 $this->transcript->setObjectTranscript($object_transcript); 254 255 $t_end = microtime(true); 256 257 $this->transcript->setDuration($t_end - $t_start); 258 259 return $effects; 260 } 261 262 /** 263 * @param array<HeraldEffect> $effects 264 * @param HeraldAdapter $adapter 265 * @param array<HeraldRule> $rules 266 * @return array 267 */ 268 public function applyEffects( 269 array $effects, 270 HeraldAdapter $adapter, 271 array $rules) { 272 assert_instances_of($effects, HeraldEffect::class); 273 assert_instances_of($rules, HeraldRule::class); 274 275 $this->transcript->setDryRun((int)$this->getDryRun()); 276 277 if ($this->getDryRun()) { 278 $xscripts = array(); 279 foreach ($effects as $effect) { 280 $xscripts[] = new HeraldApplyTranscript( 281 $effect, 282 false, 283 pht('This was a dry run, so no actions were actually taken.')); 284 } 285 } else { 286 $xscripts = $adapter->applyHeraldEffects($effects); 287 } 288 289 assert_instances_of($xscripts, HeraldApplyTranscript::class); 290 foreach ($xscripts as $apply_xscript) { 291 $this->transcript->addApplyTranscript($apply_xscript); 292 } 293 294 // For dry runs, don't mark the rule as having applied to the object. 295 if ($this->getDryRun()) { 296 return; 297 } 298 299 // Update the "applied" state table. How this table works depends on the 300 // repetition policy for the rule. 301 // 302 // REPEAT_EVERY: We delete existing rows for the rule, then write nothing. 303 // This policy doesn't use any state. 304 // 305 // REPEAT_FIRST: We keep existing rows, then write additional rows for 306 // rules which fired. This policy accumulates state over the life of the 307 // object. 308 // 309 // REPEAT_CHANGE: We delete existing rows, then write all the rows which 310 // matched. This policy only uses the state from the previous run. 311 312 $rules = mpull($rules, null, 'getID'); 313 $rule_ids = mpull($xscripts, 'getRuleID'); 314 315 $delete_ids = array(); 316 foreach ($rules as $rule_id => $rule) { 317 if ($rule->isRepeatFirst()) { 318 continue; 319 } 320 $delete_ids[] = $rule_id; 321 } 322 323 $applied_ids = array(); 324 foreach ($rule_ids as $rule_id) { 325 if (!$rule_id) { 326 // Some apply transcripts are purely informational and not associated 327 // with a rule, e.g. carryover emails from earlier revisions. 328 continue; 329 } 330 331 $rule = idx($rules, $rule_id); 332 if (!$rule) { 333 continue; 334 } 335 336 if ($rule->isRepeatFirst() || $rule->isRepeatOnChange()) { 337 $applied_ids[] = $rule_id; 338 } 339 } 340 341 // Also include "only if this rule did not match the last time" rules 342 // which matched but were skipped in the "applied" list. 343 foreach ($this->skipEffects as $rule_id => $ignored) { 344 $applied_ids[] = $rule_id; 345 } 346 347 if ($delete_ids || $applied_ids) { 348 $conn_w = id(new HeraldRule())->establishConnection('w'); 349 350 if ($delete_ids) { 351 queryfx( 352 $conn_w, 353 'DELETE FROM %T WHERE phid = %s AND ruleID IN (%Ld)', 354 HeraldRule::TABLE_RULE_APPLIED, 355 $adapter->getPHID(), 356 $delete_ids); 357 } 358 359 if ($applied_ids) { 360 $sql = array(); 361 foreach ($applied_ids as $id) { 362 $sql[] = qsprintf( 363 $conn_w, 364 '(%s, %d)', 365 $adapter->getPHID(), 366 $id); 367 } 368 queryfx( 369 $conn_w, 370 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %LQ', 371 HeraldRule::TABLE_RULE_APPLIED, 372 $sql); 373 } 374 } 375 } 376 377 public function getTranscript() { 378 $this->transcript->save(); 379 return $this->transcript; 380 } 381 382 public function doesRuleMatch( 383 HeraldRule $rule, 384 HeraldAdapter $object) { 385 $result = $this->getRuleMatchResult($rule, $object); 386 return $result->getShouldApplyActions(); 387 } 388 389 private function getRuleMatchResult( 390 HeraldRule $rule, 391 HeraldAdapter $object) { 392 393 if ($this->hasRuleResult($rule)) { 394 // If we've already evaluated this rule because another rule depends 395 // on it, we don't need to reevaluate it. 396 return $this->getRuleResult($rule); 397 } 398 399 if ($this->hasRuleOnStack($rule)) { 400 // We've recursed, fail all of the rules on the stack. This happens when 401 // there's a dependency cycle with "Rule conditions match for rule ..." 402 // conditions. 403 throw new HeraldRecursiveConditionsException(); 404 } 405 $this->pushRuleStack($rule); 406 407 $all = $rule->getMustMatchAll(); 408 409 $conditions = $rule->getConditions(); 410 411 $result_code = null; 412 $result_data = array(); 413 414 $local_version = id(new HeraldRule())->getConfigVersion(); 415 if ($rule->getConfigVersion() > $local_version) { 416 $result_code = HeraldRuleResult::RESULT_VERSION; 417 } else if (!$conditions) { 418 $result_code = HeraldRuleResult::RESULT_EMPTY; 419 } else if (!$rule->hasValidAuthor()) { 420 $result_code = HeraldRuleResult::RESULT_OWNER; 421 } else if (!$this->canAuthorViewObject($rule, $object)) { 422 $result_code = HeraldRuleResult::RESULT_VIEW_POLICY; 423 } else if (!$this->canRuleApplyToObject($rule, $object)) { 424 $result_code = HeraldRuleResult::RESULT_OBJECT_RULE; 425 } else { 426 foreach ($conditions as $condition) { 427 $caught = null; 428 429 try { 430 $match = $this->doesConditionMatch( 431 $rule, 432 $condition, 433 $object); 434 } catch (HeraldRuleEvaluationException $ex) { 435 throw $ex; 436 } catch (HeraldRecursiveConditionsException $ex) { 437 throw $ex; 438 } catch (Exception $ex) { 439 $caught = $ex; 440 } catch (Throwable $ex) { 441 $caught = $ex; 442 } 443 444 if ($caught) { 445 throw new HeraldRuleEvaluationException(); 446 } 447 448 if (!$all && $match) { 449 $result_code = HeraldRuleResult::RESULT_ANY_MATCHED; 450 break; 451 } 452 453 if ($all && !$match) { 454 $result_code = HeraldRuleResult::RESULT_ANY_FAILED; 455 break; 456 } 457 } 458 459 if ($result_code === null) { 460 if ($all) { 461 $result_code = HeraldRuleResult::RESULT_ALL_MATCHED; 462 } else { 463 $result_code = HeraldRuleResult::RESULT_ALL_FAILED; 464 } 465 } 466 } 467 468 // If this rule matched, and is set to run "if it did not match the last 469 // time", and we matched the last time, we're going to return a special 470 // result code which records a match but doesn't actually apply effects. 471 472 // We need the rule to match so that storage gets updated properly. If we 473 // just pretend the rule didn't match it won't cause any effects (which 474 // is correct), but it also won't set the "it matched" flag in storage, 475 // so the next run after this one would incorrectly trigger again. 476 477 $result = HeraldRuleResult::newFromResultCode($result_code) 478 ->setResultData($result_data); 479 480 $should_apply = $result->getShouldApplyActions(); 481 482 $is_dry_run = $this->getDryRun(); 483 if ($should_apply && !$is_dry_run) { 484 $is_on_change = $rule->isRepeatOnChange(); 485 if ($is_on_change) { 486 $did_apply = $rule->getRuleApplied($object->getPHID()); 487 if ($did_apply) { 488 // Replace the result with our modified result. 489 $result_code = HeraldRuleResult::RESULT_LAST_MATCHED; 490 $result = HeraldRuleResult::newFromResultCode($result_code); 491 492 $this->skipEffects[$rule->getID()] = true; 493 } 494 } 495 } 496 497 $this->setRuleResult($rule, $result); 498 499 return $result; 500 } 501 502 private function doesConditionMatch( 503 HeraldRule $rule, 504 HeraldCondition $condition, 505 HeraldAdapter $adapter) { 506 507 $transcript = $this->newConditionTranscript($rule, $condition); 508 509 $caught = null; 510 $result_data = array(); 511 512 try { 513 $field_key = $condition->getFieldName(); 514 515 $field_value = $this->getProfiledObjectFieldValue( 516 $adapter, 517 $field_key); 518 519 $is_match = $this->getProfiledConditionMatch( 520 $adapter, 521 $rule, 522 $condition, 523 $field_value); 524 if ($is_match) { 525 $result_code = HeraldConditionResult::RESULT_MATCHED; 526 } else { 527 $result_code = HeraldConditionResult::RESULT_FAILED; 528 } 529 } catch (HeraldRecursiveConditionsException $ex) { 530 $result_code = HeraldConditionResult::RESULT_RECURSION; 531 $caught = $ex; 532 } catch (HeraldInvalidConditionException $ex) { 533 $result_code = HeraldConditionResult::RESULT_INVALID; 534 $caught = $ex; 535 } catch (Exception $ex) { 536 $result_code = HeraldConditionResult::RESULT_EXCEPTION; 537 $caught = $ex; 538 } catch (Throwable $ex) { 539 $result_code = HeraldConditionResult::RESULT_EXCEPTION; 540 $caught = $ex; 541 } 542 543 if ($caught) { 544 $result_data = array( 545 'exception.class' => get_class($caught), 546 'exception.message' => $caught->getMessage(), 547 ); 548 phlog(pht('An exception occurred executing Herald rule %s: "%s" Review '. 549 'the Herald transcripts and correct or disable the problematic rule.', 550 $rule->getMonogram(), 551 $caught->getMessage())); 552 } 553 554 $result = HeraldConditionResult::newFromResultCode($result_code) 555 ->setResultData($result_data); 556 557 $transcript->setResult($result); 558 559 if ($caught) { 560 throw $caught; 561 } 562 563 return $result->getIsMatch(); 564 } 565 566 private function getProfiledConditionMatch( 567 HeraldAdapter $adapter, 568 HeraldRule $rule, 569 HeraldCondition $condition, 570 $field_value) { 571 572 // Here, we're profiling the cost to match the condition value against 573 // whatever test is configured. Normally, this cost should be very 574 // small (<<1ms) since it amounts to a single comparison: 575 // 576 // [ Task author ][ is any of ][ alice ] 577 // 578 // However, it may be expensive in some cases, particularly if you 579 // write a rule with a very creative regular expression that backtracks 580 // explosively. 581 // 582 // At time of writing, the "Another Herald Rule" field is also 583 // evaluated inside the matching function. This may be arbitrarily 584 // expensive (it can prompt us to execute any finite number of other 585 // Herald rules), although we'll push the profiler stack appropriately 586 // so we don't count the evaluation time against this rule in the final 587 // profile. 588 589 $this->pushProfilerRule($rule); 590 591 $caught = null; 592 try { 593 $is_match = $adapter->doesConditionMatch( 594 $this, 595 $rule, 596 $condition, 597 $field_value); 598 } catch (Exception $ex) { 599 $caught = $ex; 600 } catch (Throwable $ex) { 601 $caught = $ex; 602 } 603 604 $this->popProfilerRule($rule); 605 606 if ($caught) { 607 phlog(pht('An exception occurred executing Herald rule %s: "%s" Review '. 608 'the Herald transcripts and correct or disable the problematic rule.', 609 $rule->getMonogram(), 610 $caught->getMessage())); 611 throw $caught; 612 } 613 614 return $is_match; 615 } 616 617 private function getProfiledObjectFieldValue( 618 HeraldAdapter $adapter, 619 $field_key) { 620 621 // Before engaging the profiler, make sure the field class is loaded. 622 623 $adapter->willGetHeraldField($field_key); 624 625 // The first time we read a field value, we'll actually generate it, which 626 // may be slow. 627 628 // After it is generated for the first time, this will just read it from a 629 // cache, which should be very fast. 630 631 // We still want to profile the request even if it goes to cache so we can 632 // get an accurate count of how many times we access the field value: when 633 // trying to improve the performance of Herald rules, it's helpful to know 634 // how many rules rely on the value of a field which is slow to generate. 635 636 $caught = null; 637 638 $this->pushProfilerField($field_key); 639 try { 640 $value = $this->getObjectFieldValue($field_key); 641 } catch (Exception $ex) { 642 $caught = $ex; 643 } catch (Throwable $ex) { 644 $caught = $ex; 645 } 646 $this->popProfilerField($field_key); 647 648 if ($caught) { 649 throw $caught; 650 } 651 652 return $value; 653 } 654 655 private function getObjectFieldValue($field_key) { 656 if (array_key_exists($field_key, $this->fieldExceptions)) { 657 throw $this->fieldExceptions[$field_key]; 658 } 659 660 if (array_key_exists($field_key, $this->fieldCache)) { 661 return $this->fieldCache[$field_key]; 662 } 663 664 $adapter = $this->object; 665 666 $caught = null; 667 try { 668 $value = $adapter->getHeraldField($field_key); 669 } catch (Exception $ex) { 670 $caught = $ex; 671 } catch (Throwable $ex) { 672 $caught = $ex; 673 } 674 675 if ($caught) { 676 $this->fieldExceptions[$field_key] = $caught; 677 throw $caught; 678 } 679 680 $this->fieldCache[$field_key] = $value; 681 682 return $value; 683 } 684 685 protected function getRuleEffects( 686 HeraldRule $rule, 687 HeraldAdapter $object) { 688 689 $rule_id = $rule->getID(); 690 if (isset($this->skipEffects[$rule_id])) { 691 return array(); 692 } 693 694 $effects = array(); 695 foreach ($rule->getActions() as $action) { 696 $effect = id(new HeraldEffect()) 697 ->setObjectPHID($object->getPHID()) 698 ->setAction($action->getAction()) 699 ->setTarget($action->getTarget()) 700 ->setRule($rule); 701 702 if ($object->getActionImplementation($action->getAction()) === null) { 703 phlog(pht('An exception occurred executing Herald rule %s: Unknown '. 704 'action: "%s". Review the Herald transcripts and correct or '. 705 'disable the problematic rule.', 706 $rule->getMonogram(), 707 $action->getAction())); 708 } 709 710 $name = $rule->getName(); 711 $id = $rule->getID(); 712 $effect->setReason( 713 pht( 714 'Conditions were met for %s', 715 "H{$id} {$name}")); 716 717 $effects[] = $effect; 718 } 719 return $effects; 720 } 721 722 private function canAuthorViewObject( 723 HeraldRule $rule, 724 HeraldAdapter $adapter) { 725 726 // Authorship is irrelevant for global rules and object rules. 727 if ($rule->isGlobalRule() || $rule->isObjectRule()) { 728 return true; 729 } 730 731 // The author must be able to create rules for the adapter's content type. 732 // In particular, this means that the application must be enabled and 733 // accessible to the user. For example, if a user writes a Differential 734 // rule and then loses access to Differential, this disables the rule. 735 $enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor()); 736 if (empty($enabled[$adapter->getAdapterContentType()])) { 737 return false; 738 } 739 740 // Finally, the author must be able to see the object itself. You can't 741 // write a personal rule that CC's you on revisions you wouldn't otherwise 742 // be able to see, for example. 743 $object = $adapter->getObject(); 744 return PhabricatorPolicyFilter::hasCapability( 745 $rule->getAuthor(), 746 $object, 747 PhabricatorPolicyCapability::CAN_VIEW); 748 } 749 750 private function canRuleApplyToObject( 751 HeraldRule $rule, 752 HeraldAdapter $adapter) { 753 754 // Rules which are not object rules can apply to anything. 755 if (!$rule->isObjectRule()) { 756 return true; 757 } 758 759 $trigger_phid = $rule->getTriggerObjectPHID(); 760 $object_phids = $adapter->getTriggerObjectPHIDs(); 761 762 if ($object_phids) { 763 if (in_array($trigger_phid, $object_phids)) { 764 return true; 765 } 766 } 767 768 return false; 769 } 770 771 private function newRuleTranscript(HeraldRule $rule) { 772 $xscript = id(new HeraldRuleTranscript()) 773 ->setRuleID($rule->getID()) 774 ->setRuleName($rule->getName()) 775 ->setRuleOwner($rule->getAuthorPHID()); 776 777 $this->transcript->addRuleTranscript($xscript); 778 779 return $xscript; 780 } 781 782 private function newConditionTranscript( 783 HeraldRule $rule, 784 HeraldCondition $condition) { 785 786 $xscript = id(new HeraldConditionTranscript()) 787 ->setRuleID($rule->getID()) 788 ->setConditionID($condition->getID()) 789 ->setFieldName($condition->getFieldName()) 790 ->setCondition($condition->getFieldCondition()) 791 ->setTestValue($condition->getValue()); 792 793 $this->transcript->addConditionTranscript($xscript); 794 795 return $xscript; 796 } 797 798 private function newApplyTranscript( 799 HeraldAdapter $adapter, 800 HeraldRule $rule, 801 HeraldActionRecord $action) { 802 803 $effect = id(new HeraldEffect()) 804 ->setObjectPHID($adapter->getPHID()) 805 ->setAction($action->getAction()) 806 ->setTarget($action->getTarget()) 807 ->setRule($rule); 808 809 $xscript = new HeraldApplyTranscript($effect, false); 810 811 $this->transcript->addApplyTranscript($xscript); 812 813 return $xscript; 814 } 815 816 private function isForbidden( 817 HeraldRule $rule, 818 HeraldAdapter $adapter) { 819 820 $forbidden = $adapter->getForbiddenActions(); 821 if (!$forbidden) { 822 return false; 823 } 824 825 $forbidden = array_fuse($forbidden); 826 827 $is_forbidden = false; 828 829 foreach ($rule->getConditions() as $condition) { 830 $field_key = $condition->getFieldName(); 831 832 if (!isset($this->forbiddenFields[$field_key])) { 833 $reason = null; 834 835 try { 836 $states = $adapter->getRequiredFieldStates($field_key); 837 } catch (Exception $ex) { 838 $states = array(); 839 } 840 841 foreach ($states as $state) { 842 if (!isset($forbidden[$state])) { 843 continue; 844 } 845 $reason = $adapter->getForbiddenReason($state); 846 break; 847 } 848 849 $this->forbiddenFields[$field_key] = $reason; 850 } 851 852 $forbidden_reason = $this->forbiddenFields[$field_key]; 853 if ($forbidden_reason !== null) { 854 $result_code = HeraldConditionResult::RESULT_OBJECT_STATE; 855 $result_data = array( 856 'reason' => $forbidden_reason, 857 ); 858 859 $result = HeraldConditionResult::newFromResultCode($result_code) 860 ->setResultData($result_data); 861 862 $this->newConditionTranscript($rule, $condition) 863 ->setResult($result); 864 865 $is_forbidden = true; 866 } 867 } 868 869 foreach ($rule->getActions() as $action_record) { 870 $action_key = $action_record->getAction(); 871 872 if (!isset($this->forbiddenActions[$action_key])) { 873 $reason = null; 874 875 try { 876 $states = $adapter->getRequiredActionStates($action_key); 877 } catch (Exception $ex) { 878 $states = array(); 879 } 880 881 foreach ($states as $state) { 882 if (!isset($forbidden[$state])) { 883 continue; 884 } 885 $reason = $adapter->getForbiddenReason($state); 886 break; 887 } 888 889 $this->forbiddenActions[$action_key] = $reason; 890 } 891 892 $forbidden_reason = $this->forbiddenActions[$action_key]; 893 if ($forbidden_reason !== null) { 894 $this->newApplyTranscript($adapter, $rule, $action_record) 895 ->setAppliedReason( 896 array( 897 array( 898 'type' => HeraldAction::DO_STANDARD_FORBIDDEN, 899 'data' => $forbidden_reason, 900 ), 901 )); 902 903 $is_forbidden = true; 904 } 905 } 906 907 return $is_forbidden; 908 } 909 910/* -( Profiler )----------------------------------------------------------- */ 911 912 private function pushProfilerField($field_key) { 913 return $this->pushProfilerStack('field', $field_key); 914 } 915 916 private function popProfilerField($field_key) { 917 return $this->popProfilerStack('field', $field_key); 918 } 919 920 private function pushProfilerRule(HeraldRule $rule) { 921 return $this->pushProfilerStack('rule', $rule->getPHID()); 922 } 923 924 private function popProfilerRule(HeraldRule $rule) { 925 return $this->popProfilerStack('rule', $rule->getPHID()); 926 } 927 928 private function pushProfilerStack($type, $key) { 929 $this->profilerStack[] = array( 930 'type' => $type, 931 'key' => $key, 932 'start' => microtime(true), 933 ); 934 935 return $this; 936 } 937 938 private function popProfilerStack($type, $key) { 939 if (!$this->profilerStack) { 940 throw new Exception( 941 pht( 942 'Unable to pop profiler stack: profiler stack is empty.')); 943 } 944 945 $frame = last($this->profilerStack); 946 if (($frame['type'] !== $type) || ($frame['key'] !== $key)) { 947 throw new Exception( 948 pht( 949 'Unable to pop profiler stack: expected frame of type "%s" with '. 950 'key "%s", but found frame of type "%s" with key "%s".', 951 $type, 952 $key, 953 $frame['type'], 954 $frame['key'])); 955 } 956 957 // Accumulate the new timing information into the existing profile. If this 958 // is the first time we've seen this particular rule or field, we'll 959 // create a new empty frame first. 960 961 $elapsed = microtime(true) - $frame['start']; 962 $frame_key = sprintf('%s/%s', $type, $key); 963 964 if (!isset($this->profilerFrames[$frame_key])) { 965 $current = array( 966 'type' => $type, 967 'key' => $key, 968 'elapsed' => 0, 969 'count' => 0, 970 ); 971 } else { 972 $current = $this->profilerFrames[$frame_key]; 973 } 974 975 $current['elapsed'] += $elapsed; 976 $current['count']++; 977 978 $this->profilerFrames[$frame_key] = $current; 979 980 array_pop($this->profilerStack); 981 } 982 983 private function getProfile() { 984 if ($this->profilerStack) { 985 $frame = last($this->profilerStack); 986 $frame_type = $frame['type']; 987 $frame_key = $frame['key']; 988 $frame_count = count($this->profilerStack); 989 990 throw new Exception( 991 pht( 992 'Unable to retrieve profile: profiler stack is not empty. The '. 993 'stack has %s frame(s); the final frame has type "%s" and key '. 994 '"%s".', 995 new PhutilNumber($frame_count), 996 $frame_type, 997 $frame_key)); 998 } 999 1000 return array_values($this->profilerFrames); 1001 } 1002 1003 1004}