@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<?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}