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