@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 1217 lines 36 kB view raw
1<?php 2 3abstract class HeraldAdapter extends Phobject { 4 5 const CONDITION_CONTAINS = 'contains'; 6 const CONDITION_NOT_CONTAINS = '!contains'; 7 const CONDITION_IS = 'is'; 8 const CONDITION_IS_NOT = '!is'; 9 const CONDITION_IS_ANY = 'isany'; 10 const CONDITION_IS_NOT_ANY = '!isany'; 11 const CONDITION_INCLUDE_ALL = 'all'; 12 const CONDITION_INCLUDE_ANY = 'any'; 13 const CONDITION_INCLUDE_NONE = 'none'; 14 const CONDITION_IS_ME = 'me'; 15 const CONDITION_IS_NOT_ME = '!me'; 16 const CONDITION_REGEXP = 'regexp'; 17 const CONDITION_NOT_REGEXP = '!regexp'; 18 const CONDITION_RULE = 'conditions'; 19 const CONDITION_NOT_RULE = '!conditions'; 20 const CONDITION_EXISTS = 'exists'; 21 const CONDITION_NOT_EXISTS = '!exists'; 22 const CONDITION_UNCONDITIONALLY = 'unconditionally'; 23 const CONDITION_NEVER = 'never'; 24 const CONDITION_REGEXP_PAIR = 'regexp-pair'; 25 const CONDITION_HAS_BIT = 'bit'; 26 const CONDITION_NOT_BIT = '!bit'; 27 const CONDITION_IS_TRUE = 'true'; 28 const CONDITION_IS_FALSE = 'false'; 29 30 private $contentSource; 31 private $isNewObject; 32 private $applicationEmail; 33 private $appliedTransactions = array(); 34 private $queuedTransactions = array(); 35 private $emailPHIDs = array(); 36 private $forcedEmailPHIDs = array(); 37 private $fieldMap; 38 private $actionMap; 39 private $edgeCache = array(); 40 private $forbiddenActions = array(); 41 private $viewer; 42 private $mustEncryptReasons = array(); 43 private $actingAsPHID; 44 private $webhookMap = array(); 45 46 public function getEmailPHIDs() { 47 return array_values($this->emailPHIDs); 48 } 49 50 public function getForcedEmailPHIDs() { 51 return array_values($this->forcedEmailPHIDs); 52 } 53 54 final public function setActingAsPHID($acting_as_phid) { 55 $this->actingAsPHID = $acting_as_phid; 56 return $this; 57 } 58 59 final public function getActingAsPHID() { 60 return $this->actingAsPHID; 61 } 62 63 public function addEmailPHID($phid, $force) { 64 $this->emailPHIDs[$phid] = $phid; 65 if ($force) { 66 $this->forcedEmailPHIDs[$phid] = $phid; 67 } 68 return $this; 69 } 70 71 public function setViewer(PhabricatorUser $viewer) { 72 $this->viewer = $viewer; 73 return $this; 74 } 75 76 public function getViewer() { 77 // See PHI276. Normally, Herald runs without regard for policy checks. 78 // However, we use a real viewer during test console runs: this makes 79 // intracluster calls to Diffusion APIs work even if web nodes don't 80 // have privileged credentials. 81 82 if ($this->viewer) { 83 return $this->viewer; 84 } 85 86 return PhabricatorUser::getOmnipotentUser(); 87 } 88 89 public function setContentSource(PhabricatorContentSource $content_source) { 90 $this->contentSource = $content_source; 91 return $this; 92 } 93 94 public function getContentSource() { 95 return $this->contentSource; 96 } 97 98 public function getIsNewObject() { 99 if (is_bool($this->isNewObject)) { 100 return $this->isNewObject; 101 } 102 103 throw new Exception( 104 pht( 105 'You must %s to a boolean first!', 106 'setIsNewObject()')); 107 } 108 public function setIsNewObject($new) { 109 $this->isNewObject = (bool)$new; 110 return $this; 111 } 112 113 public function supportsApplicationEmail() { 114 return false; 115 } 116 117 public function setApplicationEmail( 118 PhabricatorMetaMTAApplicationEmail $email) { 119 $this->applicationEmail = $email; 120 return $this; 121 } 122 123 public function getApplicationEmail() { 124 return $this->applicationEmail; 125 } 126 127 public function getPHID() { 128 return $this->getObject()->getPHID(); 129 } 130 131 abstract public function getHeraldName(); 132 133 final public function willGetHeraldField($field_key) { 134 // This method is called during rule evaluation, before we engage the 135 // Herald profiler. We make sure we have a concrete implementation so time 136 // spent loading fields out of the classmap is not mistakenly attributed to 137 // whichever field happens to evaluate first. 138 $this->requireFieldImplementation($field_key); 139 } 140 141 public function getHeraldField($field_key) { 142 return $this->requireFieldImplementation($field_key) 143 ->getHeraldFieldValue($this->getObject()); 144 } 145 146 /** 147 * @param array<HeraldEffect> $effects 148 */ 149 public function applyHeraldEffects(array $effects) { 150 assert_instances_of($effects, HeraldEffect::class); 151 152 $result = array(); 153 foreach ($effects as $effect) { 154 $result[] = $this->applyStandardEffect($effect); 155 } 156 157 return $result; 158 } 159 160 public function isAvailableToUser(PhabricatorUser $viewer) { 161 $applications = id(new PhabricatorApplicationQuery()) 162 ->setViewer($viewer) 163 ->withInstalled(true) 164 ->withClasses(array($this->getAdapterApplicationClass())) 165 ->execute(); 166 167 return !empty($applications); 168 } 169 170 171 /** 172 * Set the list of transactions which just took effect. 173 * 174 * These transactions are set by @{class:PhabricatorApplicationEditor} 175 * automatically, before it invokes Herald. 176 * 177 * @param array<PhabricatorApplicationTransaction> $xactions List of 178 * transactions. 179 * @return $this 180 */ 181 final public function setAppliedTransactions(array $xactions) { 182 assert_instances_of($xactions, PhabricatorApplicationTransaction::class); 183 $this->appliedTransactions = $xactions; 184 return $this; 185 } 186 187 188 /** 189 * Get a list of transactions which just took effect. 190 * 191 * When an object is edited normally, transactions are applied and then 192 * Herald executes. You can call this method to examine the transactions 193 * if you want to react to them. 194 * 195 * @return list<PhabricatorApplicationTransaction> List of transactions. 196 */ 197 final public function getAppliedTransactions() { 198 return $this->appliedTransactions; 199 } 200 201 final public function queueTransaction( 202 PhabricatorApplicationTransaction $transaction) { 203 $this->queuedTransactions[] = $transaction; 204 } 205 206 final public function getQueuedTransactions() { 207 return $this->queuedTransactions; 208 } 209 210 final public function newTransaction() { 211 $object = $this->newObject(); 212 213 if (!($object instanceof PhabricatorApplicationTransactionInterface)) { 214 throw new Exception( 215 pht( 216 'Unable to build a new transaction for adapter object; it does '. 217 'not implement "%s".', 218 'PhabricatorApplicationTransactionInterface')); 219 } 220 221 $xaction = $object->getApplicationTransactionTemplate(); 222 223 if (!($xaction instanceof PhabricatorApplicationTransaction)) { 224 throw new Exception( 225 pht( 226 'Expected object (of class "%s") to return a transaction template '. 227 '(of class "%s"), but it returned something else ("%s").', 228 get_class($object), 229 'PhabricatorApplicationTransaction', 230 phutil_describe_type($xaction))); 231 } 232 233 return $xaction; 234 } 235 236 237 /** 238 * NOTE: You generally should not override this; it exists to support legacy 239 * adapters which had hard-coded content types. 240 */ 241 public function getAdapterContentType() { 242 return get_class($this); 243 } 244 245 abstract public function getAdapterContentName(); 246 abstract public function getAdapterContentDescription(); 247 abstract public function getAdapterApplicationClass(); 248 abstract public function getObject(); 249 250 public function getAdapterContentIcon() { 251 $application_class = $this->getAdapterApplicationClass(); 252 $application = newv($application_class, array()); 253 return $application->getIcon(); 254 } 255 256 /** 257 * Return a new characteristic object for this adapter. 258 * 259 * The adapter will use this object to test for interfaces, generate 260 * transactions, and interact with custom fields. 261 * 262 * Adapters must return an object from this method to enable custom 263 * field rules and various implicit actions. 264 * 265 * Normally, you'll return an empty version of the adapted object: 266 * 267 * return new ApplicationObject(); 268 * 269 * @return null|object Template object. 270 */ 271 protected function newObject() { 272 return null; 273 } 274 275 public function supportsRuleType($rule_type) { 276 return false; 277 } 278 279 public function canTriggerOnObject($object) { 280 return false; 281 } 282 283 public function isTestAdapterForObject($object) { 284 return false; 285 } 286 287 public function canCreateTestAdapterForObject($object) { 288 return $this->isTestAdapterForObject($object); 289 } 290 291 public function newTestAdapter(PhabricatorUser $viewer, $object) { 292 return id(clone $this) 293 ->setObject($object); 294 } 295 296 public function getAdapterTestDescription() { 297 return null; 298 } 299 300 public function explainValidTriggerObjects() { 301 return pht('This adapter can not trigger on objects.'); 302 } 303 304 public function getTriggerObjectPHIDs() { 305 return array($this->getPHID()); 306 } 307 308 public function getAdapterSortKey() { 309 return sprintf( 310 '%08d%s', 311 $this->getAdapterSortOrder(), 312 $this->getAdapterContentName()); 313 } 314 315 public function getAdapterSortOrder() { 316 return 1000; 317 } 318 319 320/* -( Fields )------------------------------------------------------------- */ 321 322 private function getFieldImplementationMap() { 323 if ($this->fieldMap === null) { 324 // We can't use PhutilClassMapQuery here because field expansion 325 // depends on the adapter and object. 326 327 $object = $this->getObject(); 328 329 $map = array(); 330 $all = HeraldField::getAllFields(); 331 foreach ($all as $key => $field) { 332 $field = id(clone $field)->setAdapter($this); 333 334 if (!$field->supportsObject($object)) { 335 continue; 336 } 337 $subfields = $field->getFieldsForObject($object); 338 foreach ($subfields as $subkey => $subfield) { 339 if (isset($map[$subkey])) { 340 throw new Exception( 341 pht( 342 'Two HeraldFields (of classes "%s" and "%s") have the same '. 343 'field key ("%s") after expansion for an object of class '. 344 '"%s" inside adapter "%s". Each field must have a unique '. 345 'field key.', 346 get_class($subfield), 347 get_class($map[$subkey]), 348 $subkey, 349 get_class($object), 350 get_class($this))); 351 } 352 353 $subfield = id(clone $subfield)->setAdapter($this); 354 355 $map[$subkey] = $subfield; 356 } 357 } 358 $this->fieldMap = $map; 359 } 360 361 return $this->fieldMap; 362 } 363 364 private function getFieldImplementation($key) { 365 return idx($this->getFieldImplementationMap(), $key); 366 } 367 368 public function getFields() { 369 return array_keys($this->getFieldImplementationMap()); 370 } 371 372 public function getFieldNameMap() { 373 return mpull($this->getFieldImplementationMap(), 'getHeraldFieldName'); 374 } 375 376 public function getFieldGroupKey($field_key) { 377 $field = $this->getFieldImplementation($field_key); 378 379 if (!$field) { 380 return null; 381 } 382 383 return $field->getFieldGroupKey(); 384 } 385 386 public function isFieldAvailable($field_key) { 387 $field = $this->getFieldImplementation($field_key); 388 389 if (!$field) { 390 return null; 391 } 392 393 return $field->isFieldAvailable(); 394 } 395 396 397/* -( Conditions )--------------------------------------------------------- */ 398 399 400 public function getConditionNameMap() { 401 return array( 402 self::CONDITION_CONTAINS => pht('contains'), 403 self::CONDITION_NOT_CONTAINS => pht('does not contain'), 404 self::CONDITION_IS => pht('is'), 405 self::CONDITION_IS_NOT => pht('is not'), 406 self::CONDITION_IS_ANY => pht('is any of'), 407 self::CONDITION_IS_TRUE => pht('is true'), 408 self::CONDITION_IS_FALSE => pht('is false'), 409 self::CONDITION_IS_NOT_ANY => pht('is not any of'), 410 self::CONDITION_INCLUDE_ALL => pht('include all of'), 411 self::CONDITION_INCLUDE_ANY => pht('include any of'), 412 self::CONDITION_INCLUDE_NONE => pht('include none of'), 413 self::CONDITION_IS_ME => pht('is myself'), 414 self::CONDITION_IS_NOT_ME => pht('is not myself'), 415 self::CONDITION_REGEXP => pht('matches regexp'), 416 self::CONDITION_NOT_REGEXP => pht('does not match regexp'), 417 self::CONDITION_RULE => pht('matches:'), 418 self::CONDITION_NOT_RULE => pht('does not match:'), 419 self::CONDITION_EXISTS => pht('exists'), 420 self::CONDITION_NOT_EXISTS => pht('does not exist'), 421 self::CONDITION_UNCONDITIONALLY => '', // don't show anything! 422 self::CONDITION_NEVER => '', // don't show anything! 423 self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), 424 self::CONDITION_HAS_BIT => pht('has bit'), 425 self::CONDITION_NOT_BIT => pht('lacks bit'), 426 ); 427 } 428 429 public function getConditionsForField($field) { 430 return $this->requireFieldImplementation($field) 431 ->getHeraldFieldConditions(); 432 } 433 434 private function requireFieldImplementation($field_key) { 435 $field = $this->getFieldImplementation($field_key); 436 437 if (!$field) { 438 throw new Exception( 439 pht( 440 'No field with key "%s" is available to Herald adapter "%s".', 441 $field_key, 442 get_class($this))); 443 } 444 445 return $field; 446 } 447 448 public function doesConditionMatch( 449 HeraldEngine $engine, 450 HeraldRule $rule, 451 HeraldCondition $condition, 452 $field_value) { 453 454 $condition_type = $condition->getFieldCondition(); 455 $condition_value = $condition->getValue(); 456 457 switch ($condition_type) { 458 case self::CONDITION_CONTAINS: 459 case self::CONDITION_NOT_CONTAINS: 460 // "Contains and "does not contain" can take an array of strings, as in 461 // "Any changed filename" for diffs. 462 463 $result_if_match = ($condition_type == self::CONDITION_CONTAINS); 464 465 foreach ((array)$field_value as $value) { 466 if (stripos($value, $condition_value) !== false) { 467 return $result_if_match; 468 } 469 } 470 return !$result_if_match; 471 case self::CONDITION_IS: 472 return ($field_value == $condition_value); 473 case self::CONDITION_IS_NOT: 474 return ($field_value != $condition_value); 475 case self::CONDITION_IS_ME: 476 return ($field_value == $rule->getAuthorPHID()); 477 case self::CONDITION_IS_NOT_ME: 478 return ($field_value != $rule->getAuthorPHID()); 479 case self::CONDITION_IS_ANY: 480 if (!is_array($condition_value)) { 481 throw new HeraldInvalidConditionException( 482 pht('Expected condition value to be an array.')); 483 } 484 $condition_value = array_fuse($condition_value); 485 return isset($condition_value[$field_value]); 486 case self::CONDITION_IS_NOT_ANY: 487 if (!is_array($condition_value)) { 488 throw new HeraldInvalidConditionException( 489 pht('Expected condition value to be an array.')); 490 } 491 $condition_value = array_fuse($condition_value); 492 return !isset($condition_value[$field_value]); 493 case self::CONDITION_INCLUDE_ALL: 494 if (!is_array($field_value)) { 495 throw new HeraldInvalidConditionException( 496 pht('Object produced non-array value!')); 497 } 498 if (!is_array($condition_value)) { 499 throw new HeraldInvalidConditionException( 500 pht('Expected condition value to be an array.')); 501 } 502 503 $have = array_select_keys(array_fuse($field_value), $condition_value); 504 return (count($have) == count($condition_value)); 505 case self::CONDITION_INCLUDE_ANY: 506 return (bool)array_select_keys( 507 array_fuse($field_value), 508 $condition_value); 509 case self::CONDITION_INCLUDE_NONE: 510 return !array_select_keys( 511 array_fuse($field_value), 512 $condition_value); 513 case self::CONDITION_EXISTS: 514 case self::CONDITION_IS_TRUE: 515 case self::CONDITION_UNCONDITIONALLY: 516 return (bool)$field_value; 517 case self::CONDITION_NOT_EXISTS: 518 case self::CONDITION_IS_FALSE: 519 return !$field_value; 520 case self::CONDITION_NEVER: 521 return false; 522 case self::CONDITION_REGEXP: 523 case self::CONDITION_NOT_REGEXP: 524 $result_if_match = ($condition_type == self::CONDITION_REGEXP); 525 526 // We add the 'S' flag because we use the regexp multiple times. 527 // It shouldn't cause any troubles if the flag is already there 528 // - /.*/S is evaluated same as /.*/SS. 529 $condition_pattern = $condition_value.'S'; 530 531 foreach ((array)$field_value as $value) { 532 try { 533 $result = phutil_preg_match($condition_pattern, $value); 534 } catch (PhutilRegexException $ex) { 535 $message = array(); 536 $message[] = pht( 537 'Regular expression "%s" in Herald rule "%s" is not valid, '. 538 'or exceeded backtracking or recursion limits while '. 539 'executing. Verify the expression and correct it or rewrite '. 540 'it with less backtracking.', 541 $condition_value, 542 $rule->getMonogram()); 543 $message[] = $ex->getMessage(); 544 $message = implode("\n\n", $message); 545 546 throw new HeraldInvalidConditionException($message); 547 } 548 549 if ($result) { 550 return $result_if_match; 551 } 552 } 553 return !$result_if_match; 554 case self::CONDITION_REGEXP_PAIR: 555 // Match a JSON-encoded pair of regular expressions against a 556 // dictionary. The first regexp must match the dictionary key, and the 557 // second regexp must match the dictionary value. If any key/value pair 558 // in the dictionary matches both regexps, the condition is satisfied. 559 $regexp_pair = null; 560 try { 561 $regexp_pair = phutil_json_decode($condition_value); 562 } catch (PhutilJSONParserException $ex) { 563 throw new HeraldInvalidConditionException( 564 pht('Regular expression pair is not valid JSON!')); 565 } 566 if (count($regexp_pair) != 2) { 567 throw new HeraldInvalidConditionException( 568 pht('Regular expression pair is not a pair!')); 569 } 570 571 $key_regexp = array_shift($regexp_pair); 572 $value_regexp = array_shift($regexp_pair); 573 574 foreach ((array)$field_value as $key => $value) { 575 $key_matches = @preg_match($key_regexp, $key); 576 if ($key_matches === false) { 577 throw new HeraldInvalidConditionException( 578 pht('First regular expression is invalid!')); 579 } 580 if ($key_matches) { 581 $value_matches = @preg_match($value_regexp, $value); 582 if ($value_matches === false) { 583 throw new HeraldInvalidConditionException( 584 pht('Second regular expression is invalid!')); 585 } 586 if ($value_matches) { 587 return true; 588 } 589 } 590 } 591 return false; 592 case self::CONDITION_RULE: 593 case self::CONDITION_NOT_RULE: 594 $rule = $engine->getRule($condition_value); 595 if (!$rule) { 596 throw new HeraldInvalidConditionException( 597 pht('Condition references a rule which does not exist!')); 598 } 599 600 $is_not = ($condition_type == self::CONDITION_NOT_RULE); 601 $result = $engine->doesRuleMatch($rule, $this); 602 if ($is_not) { 603 $result = !$result; 604 } 605 return $result; 606 case self::CONDITION_HAS_BIT: 607 return (($condition_value & $field_value) === (int)$condition_value); 608 case self::CONDITION_NOT_BIT: 609 return (($condition_value & $field_value) !== (int)$condition_value); 610 default: 611 throw new HeraldInvalidConditionException( 612 pht("Unknown condition '%s'.", $condition_type)); 613 } 614 } 615 616 public function willSaveCondition(HeraldCondition $condition) { 617 $condition_type = $condition->getFieldCondition(); 618 $condition_value = $condition->getValue(); 619 620 switch ($condition_type) { 621 case self::CONDITION_REGEXP: 622 case self::CONDITION_NOT_REGEXP: 623 $ok = @preg_match($condition_value, ''); 624 if ($ok === false) { 625 throw new HeraldInvalidConditionException( 626 pht( 627 'The regular expression "%s" is not valid. Regular expressions '. 628 'must have enclosing characters (e.g. "@/path/to/file@", not '. 629 '"/path/to/file") and be syntactically correct.', 630 $condition_value)); 631 } 632 break; 633 case self::CONDITION_REGEXP_PAIR: 634 $json = null; 635 try { 636 $json = phutil_json_decode($condition_value); 637 } catch (PhutilJSONParserException $ex) { 638 throw new HeraldInvalidConditionException( 639 pht( 640 'The regular expression pair "%s" is not valid JSON. Enter a '. 641 'valid JSON array with two elements.', 642 $condition_value)); 643 } 644 645 if (count($json) != 2) { 646 throw new HeraldInvalidConditionException( 647 pht( 648 'The regular expression pair "%s" must have exactly two '. 649 'elements.', 650 $condition_value)); 651 } 652 653 $key_regexp = array_shift($json); 654 $val_regexp = array_shift($json); 655 656 $key_ok = @preg_match($key_regexp, ''); 657 if ($key_ok === false) { 658 throw new HeraldInvalidConditionException( 659 pht( 660 'The first regexp in the regexp pair, "%s", is not a valid '. 661 'regexp.', 662 $key_regexp)); 663 } 664 665 $val_ok = @preg_match($val_regexp, ''); 666 if ($val_ok === false) { 667 throw new HeraldInvalidConditionException( 668 pht( 669 'The second regexp in the regexp pair, "%s", is not a valid '. 670 'regexp.', 671 $val_regexp)); 672 } 673 break; 674 case self::CONDITION_CONTAINS: 675 case self::CONDITION_NOT_CONTAINS: 676 case self::CONDITION_IS: 677 case self::CONDITION_IS_NOT: 678 case self::CONDITION_IS_ANY: 679 case self::CONDITION_IS_NOT_ANY: 680 case self::CONDITION_INCLUDE_ALL: 681 case self::CONDITION_INCLUDE_ANY: 682 case self::CONDITION_INCLUDE_NONE: 683 case self::CONDITION_IS_ME: 684 case self::CONDITION_IS_NOT_ME: 685 case self::CONDITION_RULE: 686 case self::CONDITION_NOT_RULE: 687 case self::CONDITION_EXISTS: 688 case self::CONDITION_NOT_EXISTS: 689 case self::CONDITION_UNCONDITIONALLY: 690 case self::CONDITION_NEVER: 691 case self::CONDITION_HAS_BIT: 692 case self::CONDITION_NOT_BIT: 693 case self::CONDITION_IS_TRUE: 694 case self::CONDITION_IS_FALSE: 695 // No explicit validation for these types, although there probably 696 // should be in some cases. 697 break; 698 default: 699 throw new HeraldInvalidConditionException( 700 pht( 701 'Unknown condition "%s"!', 702 $condition_type)); 703 } 704 } 705 706 707/* -( Actions )------------------------------------------------------------ */ 708 709 private function getActionImplementationMap() { 710 if ($this->actionMap === null) { 711 // We can't use PhutilClassMapQuery here because action expansion 712 // depends on the adapter and object. 713 714 $object = $this->getObject(); 715 716 $map = array(); 717 $all = HeraldAction::getAllActions(); 718 foreach ($all as $key => $action) { 719 $action = id(clone $action)->setAdapter($this); 720 721 if (!$action->supportsObject($object)) { 722 continue; 723 } 724 725 $subactions = $action->getActionsForObject($object); 726 foreach ($subactions as $subkey => $subaction) { 727 if (isset($map[$subkey])) { 728 throw new Exception( 729 pht( 730 'Two HeraldActions (of classes "%s" and "%s") have the same '. 731 'action key ("%s") after expansion for an object of class '. 732 '"%s" inside adapter "%s". Each action must have a unique '. 733 'action key.', 734 get_class($subaction), 735 get_class($map[$subkey]), 736 $subkey, 737 get_class($object), 738 get_class($this))); 739 } 740 741 $subaction = id(clone $subaction)->setAdapter($this); 742 743 $map[$subkey] = $subaction; 744 } 745 } 746 $this->actionMap = $map; 747 } 748 749 return $this->actionMap; 750 } 751 752 private function requireActionImplementation($action_key) { 753 $action = $this->getActionImplementation($action_key); 754 755 if (!$action) { 756 throw new Exception( 757 pht( 758 'No action with key "%s" is available to Herald adapter "%s".', 759 $action_key, 760 get_class($this))); 761 } 762 763 return $action; 764 } 765 766 private function getActionsForRuleType($rule_type) { 767 $actions = $this->getActionImplementationMap(); 768 769 foreach ($actions as $key => $action) { 770 if (!$action->supportsRuleType($rule_type)) { 771 unset($actions[$key]); 772 } 773 } 774 775 return $actions; 776 } 777 778 public function getActionImplementation($key) { 779 return idx($this->getActionImplementationMap(), $key); 780 } 781 782 public function getActionKeys() { 783 return array_keys($this->getActionImplementationMap()); 784 } 785 786 public function getActionGroupKey($action_key) { 787 $action = $this->getActionImplementation($action_key); 788 if (!$action) { 789 return null; 790 } 791 792 return $action->getActionGroupKey(); 793 } 794 795 public function isActionAvailable($action_key) { 796 $action = $this->getActionImplementation($action_key); 797 798 if (!$action) { 799 return null; 800 } 801 802 return $action->isActionAvailable(); 803 } 804 805 public function getActions($rule_type) { 806 $actions = array(); 807 foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { 808 $actions[] = $key; 809 } 810 811 return $actions; 812 } 813 814 public function getActionNameMap($rule_type) { 815 $map = array(); 816 foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { 817 $map[$key] = $action->getHeraldActionName(); 818 } 819 820 return $map; 821 } 822 823 public function willSaveAction( 824 HeraldRule $rule, 825 HeraldActionRecord $action) { 826 827 $impl = $this->requireActionImplementation($action->getAction()); 828 $target = $action->getTarget(); 829 $target = $impl->willSaveActionValue($target); 830 831 $action->setTarget($target); 832 } 833 834 835 836/* -( Values )------------------------------------------------------------- */ 837 838 839 public function getValueTypeForFieldAndCondition($field, $condition) { 840 return $this->requireFieldImplementation($field) 841 ->getHeraldFieldValueType($condition); 842 } 843 844 public function getValueTypeForAction($action, $rule_type) { 845 $impl = $this->requireActionImplementation($action); 846 return $impl->getHeraldActionValueType(); 847 } 848 849/* -( Repetition )--------------------------------------------------------- */ 850 851 852 public function getRepetitionOptions() { 853 $options = array(); 854 855 $options[] = HeraldRule::REPEAT_EVERY; 856 857 // Some rules, like pre-commit rules, only ever fire once. It doesn't 858 // make sense to use state-based repetition policies like "only the first 859 // time" for these rules. 860 861 if (!$this->isSingleEventAdapter()) { 862 $options[] = HeraldRule::REPEAT_FIRST; 863 $options[] = HeraldRule::REPEAT_CHANGE; 864 } 865 866 return $options; 867 } 868 869 protected function initializeNewAdapter() { 870 $this->setObject($this->newObject()); 871 return $this; 872 } 873 874 /** 875 * Does this adapter's event fire only once? 876 * 877 * Single use adapters (like pre-commit and diff adapters) only fire once, 878 * so fields like "Is new object" don't make sense to apply to their content. 879 * 880 * @return bool 881 */ 882 public function isSingleEventAdapter() { 883 return false; 884 } 885 886 public static function getAllAdapters() { 887 return id(new PhutilClassMapQuery()) 888 ->setAncestorClass(self::class) 889 ->setUniqueMethod('getAdapterContentType') 890 ->setSortMethod('getAdapterSortKey') 891 ->execute(); 892 } 893 894 public static function getAdapterForContentType($content_type) { 895 $adapters = self::getAllAdapters(); 896 897 foreach ($adapters as $adapter) { 898 if ($adapter->getAdapterContentType() == $content_type) { 899 $adapter = id(clone $adapter); 900 $adapter->initializeNewAdapter(); 901 return $adapter; 902 } 903 } 904 905 throw new Exception( 906 pht( 907 'No adapter exists for Herald content type "%s".', 908 $content_type)); 909 } 910 911 public static function getEnabledAdapterMap(PhabricatorUser $viewer) { 912 $map = array(); 913 914 $adapters = self::getAllAdapters(); 915 foreach ($adapters as $adapter) { 916 if (!$adapter->isAvailableToUser($viewer)) { 917 continue; 918 } 919 $type = $adapter->getAdapterContentType(); 920 $name = $adapter->getAdapterContentName(); 921 $map[$type] = $name; 922 } 923 924 return $map; 925 } 926 927 public function getEditorValueForCondition( 928 PhabricatorUser $viewer, 929 HeraldCondition $condition) { 930 931 $field = $this->requireFieldImplementation($condition->getFieldName()); 932 933 return $field->getEditorValue( 934 $viewer, 935 $condition->getFieldCondition(), 936 $condition->getValue()); 937 } 938 939 public function getEditorValueForAction( 940 PhabricatorUser $viewer, 941 HeraldActionRecord $action_record) { 942 943 $action = $this->requireActionImplementation($action_record->getAction()); 944 945 return $action->getEditorValue( 946 $viewer, 947 $action_record->getTarget()); 948 } 949 950 public function renderRuleAsText( 951 HeraldRule $rule, 952 PhabricatorUser $viewer) { 953 954 require_celerity_resource('herald-css'); 955 956 $icon = id(new PHUIIconView()) 957 ->setIcon('fa-chevron-circle-right lightgreytext') 958 ->addClass('herald-list-icon'); 959 960 if ($rule->getMustMatchAll()) { 961 $match_text = pht('When all of these conditions are met:'); 962 } else { 963 $match_text = pht('When any of these conditions are met:'); 964 } 965 966 $match_title = phutil_tag( 967 'p', 968 array( 969 'class' => 'herald-list-description', 970 ), 971 $match_text); 972 973 $match_list = array(); 974 foreach ($rule->getConditions() as $condition) { 975 $match_list[] = phutil_tag( 976 'div', 977 array( 978 'class' => 'herald-list-item', 979 ), 980 array( 981 $icon, 982 $this->renderConditionAsText($condition, $viewer), 983 )); 984 } 985 986 if ($rule->isRepeatFirst()) { 987 $action_text = pht( 988 'Take these actions the first time this rule matches:'); 989 } else if ($rule->isRepeatOnChange()) { 990 $action_text = pht( 991 'Take these actions if this rule did not match the last time:'); 992 } else { 993 $action_text = pht( 994 'Take these actions every time this rule matches:'); 995 } 996 997 $action_title = phutil_tag( 998 'p', 999 array( 1000 'class' => 'herald-list-description', 1001 ), 1002 $action_text); 1003 1004 $action_list = array(); 1005 foreach ($rule->getActions() as $action) { 1006 $action_list[] = phutil_tag( 1007 'div', 1008 array( 1009 'class' => 'herald-list-item', 1010 ), 1011 array( 1012 $icon, 1013 $this->renderActionAsText($viewer, $action), 1014 )); 1015 } 1016 1017 return array( 1018 $match_title, 1019 $match_list, 1020 $action_title, 1021 $action_list, 1022 ); 1023 } 1024 1025 private function renderConditionAsText( 1026 HeraldCondition $condition, 1027 PhabricatorUser $viewer) { 1028 1029 $field_type = $condition->getFieldName(); 1030 $field = $this->getFieldImplementation($field_type); 1031 1032 if (!$field) { 1033 return pht('Unknown Field: "%s"', $field_type); 1034 } 1035 1036 $field_name = $field->getHeraldFieldName(); 1037 1038 $condition_type = $condition->getFieldCondition(); 1039 $condition_name = idx($this->getConditionNameMap(), $condition_type); 1040 1041 $value = $this->renderConditionValueAsText($condition, $viewer); 1042 1043 return array( 1044 $field_name, 1045 ' ', 1046 $condition_name, 1047 ' ', 1048 $value, 1049 ); 1050 } 1051 1052 private function renderActionAsText( 1053 PhabricatorUser $viewer, 1054 HeraldActionRecord $action_record) { 1055 1056 $action_type = $action_record->getAction(); 1057 $action_value = $action_record->getTarget(); 1058 1059 $action = $this->getActionImplementation($action_type); 1060 if (!$action) { 1061 return pht('Unknown Action ("%s")', $action_type); 1062 } 1063 1064 $action->setViewer($viewer); 1065 1066 return $action->renderActionDescription($action_value); 1067 } 1068 1069 private function renderConditionValueAsText( 1070 HeraldCondition $condition, 1071 PhabricatorUser $viewer) { 1072 1073 $field = $this->requireFieldImplementation($condition->getFieldName()); 1074 1075 return $field->renderConditionValue( 1076 $viewer, 1077 $condition->getFieldCondition(), 1078 $condition->getValue()); 1079 } 1080 1081 public function renderFieldTranscriptValue( 1082 PhabricatorUser $viewer, 1083 $field_type, 1084 $field_value) { 1085 1086 $field = $this->getFieldImplementation($field_type); 1087 if ($field) { 1088 return $field->renderTranscriptValue( 1089 $viewer, 1090 $field_value); 1091 } 1092 1093 return phutil_tag( 1094 'em', 1095 array(), 1096 pht( 1097 'Unable to render value for unknown field type ("%s").', 1098 $field_type)); 1099 } 1100 1101 1102/* -( Applying Effects )--------------------------------------------------- */ 1103 1104 1105 /** 1106 * @task apply 1107 */ 1108 protected function applyStandardEffect(HeraldEffect $effect) { 1109 $action = $effect->getAction(); 1110 $rule_type = $effect->getRule()->getRuleType(); 1111 1112 $impl = $this->getActionImplementation($action); 1113 if (!$impl) { 1114 return new HeraldApplyTranscript( 1115 $effect, 1116 false, 1117 array( 1118 array( 1119 HeraldAction::DO_STANDARD_INVALID_ACTION, 1120 $action, 1121 ), 1122 )); 1123 } 1124 1125 if (!$impl->supportsRuleType($rule_type)) { 1126 return new HeraldApplyTranscript( 1127 $effect, 1128 false, 1129 array( 1130 array( 1131 HeraldAction::DO_STANDARD_WRONG_RULE_TYPE, 1132 $rule_type, 1133 ), 1134 )); 1135 } 1136 1137 $impl->applyEffect($this->getObject(), $effect); 1138 return $impl->getApplyTranscript($effect); 1139 } 1140 1141 public function loadEdgePHIDs($type) { 1142 if (!isset($this->edgeCache[$type])) { 1143 $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 1144 $this->getObject()->getPHID(), 1145 $type); 1146 1147 $this->edgeCache[$type] = array_fuse($phids); 1148 } 1149 return $this->edgeCache[$type]; 1150 } 1151 1152 1153/* -( Forbidden Actions )-------------------------------------------------- */ 1154 1155 1156 final public function getForbiddenActions() { 1157 return array_keys($this->forbiddenActions); 1158 } 1159 1160 final public function setForbiddenAction($action, $reason) { 1161 $this->forbiddenActions[$action] = $reason; 1162 return $this; 1163 } 1164 1165 final public function getRequiredFieldStates($field_key) { 1166 return $this->requireFieldImplementation($field_key) 1167 ->getRequiredAdapterStates(); 1168 } 1169 1170 final public function getRequiredActionStates($action_key) { 1171 return $this->requireActionImplementation($action_key) 1172 ->getRequiredAdapterStates(); 1173 } 1174 1175 final public function getForbiddenReason($action) { 1176 if (!isset($this->forbiddenActions[$action])) { 1177 throw new Exception( 1178 pht( 1179 'Action "%s" is not forbidden!', 1180 $action)); 1181 } 1182 1183 return $this->forbiddenActions[$action]; 1184 } 1185 1186 1187/* -( Must Encrypt )------------------------------------------------------- */ 1188 1189 1190 final public function addMustEncryptReason($reason) { 1191 $this->mustEncryptReasons[] = $reason; 1192 return $this; 1193 } 1194 1195 final public function getMustEncryptReasons() { 1196 return $this->mustEncryptReasons; 1197 } 1198 1199 1200/* -( Webhooks )----------------------------------------------------------- */ 1201 1202 1203 public function supportsWebhooks() { 1204 return true; 1205 } 1206 1207 1208 final public function queueWebhook($webhook_phid, $rule_phid) { 1209 $this->webhookMap[$webhook_phid][] = $rule_phid; 1210 return $this; 1211 } 1212 1213 final public function getWebhookMap() { 1214 return $this->webhookMap; 1215 } 1216 1217}