Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/herald/controller/HeraldRuleController.php
12256 views
1
<?php
2
3
final class HeraldRuleController extends HeraldController {
4
5
public function handleRequest(AphrontRequest $request) {
6
$viewer = $request->getViewer();
7
$id = $request->getURIData('id');
8
9
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
10
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
11
12
if ($id) {
13
$rule = id(new HeraldRuleQuery())
14
->setViewer($viewer)
15
->withIDs(array($id))
16
->requireCapabilities(
17
array(
18
PhabricatorPolicyCapability::CAN_VIEW,
19
PhabricatorPolicyCapability::CAN_EDIT,
20
))
21
->executeOne();
22
if (!$rule) {
23
return new Aphront404Response();
24
}
25
$cancel_uri = '/'.$rule->getMonogram();
26
} else {
27
$new_uri = $this->getApplicationURI('new/');
28
29
$rule = new HeraldRule();
30
$rule->setAuthorPHID($viewer->getPHID());
31
$rule->setMustMatchAll(1);
32
33
$content_type = $request->getStr('content_type');
34
$rule->setContentType($content_type);
35
36
$rule_type = $request->getStr('rule_type');
37
if (!isset($rule_type_map[$rule_type])) {
38
return $this->newDialog()
39
->setTitle(pht('Invalid Rule Type'))
40
->appendParagraph(
41
pht(
42
'The selected rule type ("%s") is not recognized by Herald.',
43
$rule_type))
44
->addCancelButton($new_uri);
45
}
46
$rule->setRuleType($rule_type);
47
48
try {
49
$adapter = HeraldAdapter::getAdapterForContentType(
50
$rule->getContentType());
51
} catch (Exception $ex) {
52
return $this->newDialog()
53
->setTitle(pht('Invalid Content Type'))
54
->appendParagraph(
55
pht(
56
'The selected content type ("%s") is not recognized by '.
57
'Herald.',
58
$rule->getContentType()))
59
->addCancelButton($new_uri);
60
}
61
62
if (!$adapter->supportsRuleType($rule->getRuleType())) {
63
return $this->newDialog()
64
->setTitle(pht('Rule/Content Mismatch'))
65
->appendParagraph(
66
pht(
67
'The selected rule type ("%s") is not supported by the selected '.
68
'content type ("%s").',
69
$rule->getRuleType(),
70
$rule->getContentType()))
71
->addCancelButton($new_uri);
72
}
73
74
if ($rule->isObjectRule()) {
75
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
76
$object = id(new PhabricatorObjectQuery())
77
->setViewer($viewer)
78
->withPHIDs(array($rule->getTriggerObjectPHID()))
79
->requireCapabilities(
80
array(
81
PhabricatorPolicyCapability::CAN_VIEW,
82
PhabricatorPolicyCapability::CAN_EDIT,
83
))
84
->executeOne();
85
if (!$object) {
86
throw new Exception(
87
pht('No valid object provided for object rule!'));
88
}
89
90
if (!$adapter->canTriggerOnObject($object)) {
91
throw new Exception(
92
pht('Object is of wrong type for adapter!'));
93
}
94
}
95
96
$cancel_uri = $this->getApplicationURI();
97
}
98
99
if ($rule->isGlobalRule()) {
100
$this->requireApplicationCapability(
101
HeraldManageGlobalRulesCapability::CAPABILITY);
102
}
103
104
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
105
106
$local_version = id(new HeraldRule())->getConfigVersion();
107
if ($rule->getConfigVersion() > $local_version) {
108
throw new Exception(
109
pht(
110
'This rule was created with a newer version of Herald. You can not '.
111
'view or edit it in this older version. Upgrade your software.'));
112
}
113
114
// Upgrade rule version to our version, since we might add newly-defined
115
// conditions, etc.
116
$rule->setConfigVersion($local_version);
117
118
$rule_conditions = $rule->loadConditions();
119
$rule_actions = $rule->loadActions();
120
121
$rule->attachConditions($rule_conditions);
122
$rule->attachActions($rule_actions);
123
124
$e_name = true;
125
$errors = array();
126
if ($request->isFormPost() && $request->getStr('save')) {
127
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
128
if (!$errors) {
129
$id = $rule->getID();
130
$uri = '/'.$rule->getMonogram();
131
return id(new AphrontRedirectResponse())->setURI($uri);
132
}
133
}
134
135
$must_match_selector = $this->renderMustMatchSelector($rule);
136
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
137
138
$handles = $this->loadHandlesForRule($rule);
139
140
require_celerity_resource('herald-css');
141
142
$content_type_name = $content_type_map[$rule->getContentType()];
143
$rule_type_name = $rule_type_map[$rule->getRuleType()];
144
145
$form = id(new AphrontFormView())
146
->setUser($viewer)
147
->setID('herald-rule-edit-form')
148
->addHiddenInput('content_type', $rule->getContentType())
149
->addHiddenInput('rule_type', $rule->getRuleType())
150
->addHiddenInput('save', 1)
151
->appendChild(
152
// Build this explicitly (instead of using addHiddenInput())
153
// so we can add a sigil to it.
154
javelin_tag(
155
'input',
156
array(
157
'type' => 'hidden',
158
'name' => 'rule',
159
'sigil' => 'rule',
160
)))
161
->appendChild(
162
id(new AphrontFormTextControl())
163
->setLabel(pht('Rule Name'))
164
->setName('name')
165
->setError($e_name)
166
->setValue($rule->getName()));
167
168
$trigger_object_control = false;
169
if ($rule->isObjectRule()) {
170
$trigger_object_control = id(new AphrontFormStaticControl())
171
->setValue(
172
pht(
173
'This rule triggers for %s.',
174
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
175
}
176
177
178
$form
179
->appendChild(
180
id(new AphrontFormMarkupControl())
181
->setValue(pht(
182
'This %s rule triggers for %s.',
183
phutil_tag('strong', array(), $rule_type_name),
184
phutil_tag('strong', array(), $content_type_name))))
185
->appendChild($trigger_object_control)
186
->appendChild(
187
id(new PHUIFormInsetView())
188
->setTitle(pht('Conditions'))
189
->setRightButton(javelin_tag(
190
'a',
191
array(
192
'href' => '#',
193
'class' => 'button button-green',
194
'sigil' => 'create-condition',
195
'mustcapture' => true,
196
),
197
pht('New Condition')))
198
->setDescription(
199
pht('When %s these conditions are met:', $must_match_selector))
200
->setContent(javelin_tag(
201
'table',
202
array(
203
'sigil' => 'rule-conditions',
204
'class' => 'herald-condition-table',
205
),
206
'')))
207
->appendChild(
208
id(new PHUIFormInsetView())
209
->setTitle(pht('Action'))
210
->setRightButton(javelin_tag(
211
'a',
212
array(
213
'href' => '#',
214
'class' => 'button button-green',
215
'sigil' => 'create-action',
216
'mustcapture' => true,
217
),
218
pht('New Action')))
219
->setDescription(pht(
220
'Take these actions %s',
221
$repetition_selector))
222
->setContent(javelin_tag(
223
'table',
224
array(
225
'sigil' => 'rule-actions',
226
'class' => 'herald-action-table',
227
),
228
'')))
229
->appendChild(
230
id(new AphrontFormSubmitControl())
231
->setValue(pht('Save Rule'))
232
->addCancelButton($cancel_uri));
233
234
$this->setupEditorBehavior($rule, $handles, $adapter);
235
236
$title = $rule->getID()
237
? pht('Edit Herald Rule: %s', $rule->getName())
238
: pht('Create Herald Rule: %s', idx($content_type_map, $content_type));
239
240
$form_box = id(new PHUIObjectBoxView())
241
->setHeaderText($title)
242
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
243
->setFormErrors($errors)
244
->setForm($form);
245
246
$crumbs = $this
247
->buildApplicationCrumbs()
248
->addTextCrumb($title)
249
->setBorder(true);
250
251
$view = id(new PHUITwoColumnView())
252
->setFooter($form_box);
253
254
return $this->newPage()
255
->setTitle($title)
256
->setCrumbs($crumbs)
257
->appendChild(
258
array(
259
$view,
260
));
261
}
262
263
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
264
$new_name = $request->getStr('name');
265
$match_all = ($request->getStr('must_match') == 'all');
266
267
$repetition_policy = $request->getStr('repetition_policy');
268
269
// If the user selected an invalid policy, or there's only one possible
270
// value so we didn't render a control, adjust the value to the first
271
// valid policy value.
272
$repetition_options = $this->getRepetitionOptionMap($adapter);
273
if (!isset($repetition_options[$repetition_policy])) {
274
$repetition_policy = head_key($repetition_options);
275
}
276
277
$e_name = true;
278
$errors = array();
279
280
if (!strlen($new_name)) {
281
$e_name = pht('Required');
282
$errors[] = pht('Rule must have a name.');
283
}
284
285
$data = null;
286
try {
287
$data = phutil_json_decode($request->getStr('rule'));
288
} catch (PhutilJSONParserException $ex) {
289
throw new PhutilProxyException(
290
pht('Failed to decode rule data.'),
291
$ex);
292
}
293
294
if (!is_array($data) ||
295
!$data['conditions'] ||
296
!$data['actions']) {
297
throw new Exception(pht('Failed to decode rule data.'));
298
}
299
300
$conditions = array();
301
foreach ($data['conditions'] as $condition) {
302
if ($condition === null) {
303
// We manage this as a sparse array on the client, so may receive
304
// NULL if conditions have been removed.
305
continue;
306
}
307
308
$obj = new HeraldCondition();
309
$obj->setFieldName($condition[0]);
310
$obj->setFieldCondition($condition[1]);
311
312
if (is_array($condition[2])) {
313
$obj->setValue(array_keys($condition[2]));
314
} else {
315
$obj->setValue($condition[2]);
316
}
317
318
try {
319
$adapter->willSaveCondition($obj);
320
} catch (HeraldInvalidConditionException $ex) {
321
$errors[] = $ex->getMessage();
322
}
323
324
$conditions[] = $obj;
325
}
326
327
$actions = array();
328
foreach ($data['actions'] as $action) {
329
if ($action === null) {
330
// Sparse on the client; removals can give us NULLs.
331
continue;
332
}
333
334
if (!isset($action[1])) {
335
// Legitimate for any action which doesn't need a target, like
336
// "Do nothing".
337
$action[1] = null;
338
}
339
340
$obj = new HeraldActionRecord();
341
$obj->setAction($action[0]);
342
$obj->setTarget($action[1]);
343
344
try {
345
$adapter->willSaveAction($rule, $obj);
346
} catch (HeraldInvalidActionException $ex) {
347
$errors[] = $ex->getMessage();
348
}
349
350
$actions[] = $obj;
351
}
352
353
if (!$errors) {
354
$new_state = id(new HeraldRuleSerializer())->serializeRuleComponents(
355
$match_all,
356
$conditions,
357
$actions,
358
$repetition_policy);
359
360
$xactions = array();
361
362
// Until this moves to EditEngine, manually add a "CREATE" transaction
363
// if we're creating a new rule. This improves rendering of the initial
364
// group of transactions.
365
$is_new = (bool)(!$rule->getID());
366
if ($is_new) {
367
$xactions[] = id(new HeraldRuleTransaction())
368
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
369
}
370
371
$xactions[] = id(new HeraldRuleTransaction())
372
->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE)
373
->setNewValue($new_state);
374
$xactions[] = id(new HeraldRuleTransaction())
375
->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE)
376
->setNewValue($new_name);
377
378
try {
379
id(new HeraldRuleEditor())
380
->setActor($this->getViewer())
381
->setContinueOnNoEffect(true)
382
->setContentSourceFromRequest($request)
383
->applyTransactions($rule, $xactions);
384
return array(null, null);
385
} catch (Exception $ex) {
386
$errors[] = $ex->getMessage();
387
}
388
}
389
390
// mutate current rule, so it would be sent to the client in the right state
391
$rule->setMustMatchAll((int)$match_all);
392
$rule->setName($new_name);
393
$rule->setRepetitionPolicyStringConstant($repetition_policy);
394
$rule->attachConditions($conditions);
395
$rule->attachActions($actions);
396
397
return array($e_name, $errors);
398
}
399
400
private function setupEditorBehavior(
401
HeraldRule $rule,
402
array $handles,
403
HeraldAdapter $adapter) {
404
405
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
406
$all_rules = msortv($all_rules, 'getEditorSortVector');
407
$all_rules = mpull($all_rules, 'getEditorDisplayName', 'getPHID');
408
409
$all_fields = $adapter->getFieldNameMap();
410
$all_conditions = $adapter->getConditionNameMap();
411
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
412
413
$fields = $adapter->getFields();
414
$field_map = array_select_keys($all_fields, $fields);
415
416
// Populate any fields which exist in the rule but which we don't know the
417
// names of, so that saving a rule without touching anything doesn't change
418
// it.
419
foreach ($rule->getConditions() as $condition) {
420
$field_name = $condition->getFieldName();
421
422
if (empty($field_map[$field_name])) {
423
$field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);
424
}
425
}
426
427
$actions = $adapter->getActions($rule->getRuleType());
428
$action_map = array_select_keys($all_actions, $actions);
429
430
// Populate any actions which exist in the rule but which we don't know the
431
// names of, so that saving a rule without touching anything doesn't change
432
// it.
433
foreach ($rule->getActions() as $action) {
434
$action_name = $action->getAction();
435
436
if (empty($action_map[$action_name])) {
437
$action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);
438
}
439
}
440
441
$config_info = array();
442
$config_info['fields'] = $this->getFieldGroups($adapter, $field_map);
443
$config_info['conditions'] = $all_conditions;
444
$config_info['actions'] = $this->getActionGroups($adapter, $action_map);
445
$config_info['valueMap'] = array();
446
447
foreach ($field_map as $field => $name) {
448
try {
449
$field_conditions = $adapter->getConditionsForField($field);
450
} catch (Exception $ex) {
451
$field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);
452
}
453
$config_info['conditionMap'][$field] = $field_conditions;
454
}
455
456
foreach ($field_map as $field => $fname) {
457
foreach ($config_info['conditionMap'][$field] as $condition) {
458
$value_key = $adapter->getValueTypeForFieldAndCondition(
459
$field,
460
$condition);
461
462
if ($value_key instanceof HeraldFieldValue) {
463
$value_key->setViewer($this->getViewer());
464
465
$spec = $value_key->getControlSpecificationDictionary();
466
$value_key = $value_key->getFieldValueKey();
467
$config_info['valueMap'][$value_key] = $spec;
468
}
469
470
$config_info['values'][$field][$condition] = $value_key;
471
}
472
}
473
474
$config_info['rule_type'] = $rule->getRuleType();
475
476
foreach ($action_map as $action => $name) {
477
try {
478
$value_key = $adapter->getValueTypeForAction(
479
$action,
480
$rule->getRuleType());
481
} catch (Exception $ex) {
482
$value_key = new HeraldEmptyFieldValue();
483
}
484
485
if ($value_key instanceof HeraldFieldValue) {
486
$value_key->setViewer($this->getViewer());
487
488
$spec = $value_key->getControlSpecificationDictionary();
489
$value_key = $value_key->getFieldValueKey();
490
$config_info['valueMap'][$value_key] = $spec;
491
}
492
493
$config_info['targets'][$action] = $value_key;
494
}
495
496
$default_group = head($config_info['fields']);
497
$default_field = head_key($default_group['options']);
498
$default_condition = head($config_info['conditionMap'][$default_field]);
499
$default_actions = head($config_info['actions']);
500
$default_action = head_key($default_actions['options']);
501
502
if ($rule->getConditions()) {
503
$serial_conditions = array();
504
foreach ($rule->getConditions() as $condition) {
505
$value = $adapter->getEditorValueForCondition(
506
$this->getViewer(),
507
$condition);
508
509
$serial_conditions[] = array(
510
$condition->getFieldName(),
511
$condition->getFieldCondition(),
512
$value,
513
);
514
}
515
} else {
516
$serial_conditions = array(
517
array($default_field, $default_condition, null),
518
);
519
}
520
521
if ($rule->getActions()) {
522
$serial_actions = array();
523
foreach ($rule->getActions() as $action) {
524
$value = $adapter->getEditorValueForAction(
525
$this->getViewer(),
526
$action);
527
528
$serial_actions[] = array(
529
$action->getAction(),
530
$value,
531
);
532
}
533
} else {
534
$serial_actions = array(
535
array($default_action, null),
536
);
537
}
538
539
Javelin::initBehavior(
540
'herald-rule-editor',
541
array(
542
'root' => 'herald-rule-edit-form',
543
'default' => array(
544
'field' => $default_field,
545
'condition' => $default_condition,
546
'action' => $default_action,
547
),
548
'conditions' => (object)$serial_conditions,
549
'actions' => (object)$serial_actions,
550
'template' => $this->buildTokenizerTemplates() + array(
551
'rules' => $all_rules,
552
),
553
'info' => $config_info,
554
));
555
}
556
557
private function loadHandlesForRule($rule) {
558
$phids = array();
559
560
foreach ($rule->getActions() as $action) {
561
if (!is_array($action->getTarget())) {
562
continue;
563
}
564
foreach ($action->getTarget() as $target) {
565
$target = (array)$target;
566
foreach ($target as $phid) {
567
$phids[] = $phid;
568
}
569
}
570
}
571
572
foreach ($rule->getConditions() as $condition) {
573
$value = $condition->getValue();
574
if (is_array($value)) {
575
foreach ($value as $phid) {
576
$phids[] = $phid;
577
}
578
}
579
}
580
581
$phids[] = $rule->getAuthorPHID();
582
583
if ($rule->isObjectRule()) {
584
$phids[] = $rule->getTriggerObjectPHID();
585
}
586
587
return $this->loadViewerHandles($phids);
588
}
589
590
591
/**
592
* Render the selector for the "When (all of | any of) these conditions are
593
* met:" element.
594
*/
595
private function renderMustMatchSelector($rule) {
596
return AphrontFormSelectControl::renderSelectTag(
597
$rule->getMustMatchAll() ? 'all' : 'any',
598
array(
599
'all' => pht('all of'),
600
'any' => pht('any of'),
601
),
602
array(
603
'name' => 'must_match',
604
));
605
}
606
607
608
/**
609
* Render the selector for "Take these actions (every time | only the first
610
* time) this rule matches..." element.
611
*/
612
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
613
$repetition_policy = $rule->getRepetitionPolicyStringConstant();
614
$repetition_map = $this->getRepetitionOptionMap($adapter);
615
if (count($repetition_map) < 2) {
616
return head($repetition_map);
617
} else {
618
return AphrontFormSelectControl::renderSelectTag(
619
$repetition_policy,
620
$repetition_map,
621
array(
622
'name' => 'repetition_policy',
623
));
624
}
625
}
626
627
private function getRepetitionOptionMap(HeraldAdapter $adapter) {
628
$repetition_options = $adapter->getRepetitionOptions();
629
$repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap();
630
return array_select_keys($repetition_names, $repetition_options);
631
}
632
633
protected function buildTokenizerTemplates() {
634
$template = new AphrontTokenizerTemplateView();
635
$template = $template->render();
636
return array(
637
'markup' => $template,
638
);
639
}
640
641
642
/**
643
* Load rules for the "Another Herald rule..." condition dropdown, which
644
* allows one rule to depend upon the success or failure of another rule.
645
*/
646
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
647
$viewer = $this->getRequest()->getUser();
648
649
// Any rule can depend on a global rule.
650
$all_rules = id(new HeraldRuleQuery())
651
->setViewer($viewer)
652
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
653
->withContentTypes(array($rule->getContentType()))
654
->execute();
655
656
if ($rule->isObjectRule()) {
657
// Object rules may depend on other rules for the same object.
658
$all_rules += id(new HeraldRuleQuery())
659
->setViewer($viewer)
660
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
661
->withContentTypes(array($rule->getContentType()))
662
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
663
->execute();
664
}
665
666
if ($rule->isPersonalRule()) {
667
// Personal rules may depend upon your other personal rules.
668
$all_rules += id(new HeraldRuleQuery())
669
->setViewer($viewer)
670
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
671
->withContentTypes(array($rule->getContentType()))
672
->withAuthorPHIDs(array($rule->getAuthorPHID()))
673
->execute();
674
}
675
676
// A rule can not depend upon itself.
677
unset($all_rules[$rule->getID()]);
678
679
return $all_rules;
680
}
681
682
private function getFieldGroups(HeraldAdapter $adapter, array $field_map) {
683
$group_map = array();
684
foreach ($field_map as $field_key => $field_name) {
685
$group_key = $adapter->getFieldGroupKey($field_key);
686
$group_map[$group_key][$field_key] = array(
687
'name' => $field_name,
688
'available' => $adapter->isFieldAvailable($field_key),
689
);
690
}
691
692
return $this->getGroups(
693
$group_map,
694
HeraldFieldGroup::getAllFieldGroups());
695
}
696
697
private function getActionGroups(HeraldAdapter $adapter, array $action_map) {
698
$group_map = array();
699
foreach ($action_map as $action_key => $action_name) {
700
$group_key = $adapter->getActionGroupKey($action_key);
701
$group_map[$group_key][$action_key] = array(
702
'name' => $action_name,
703
'available' => $adapter->isActionAvailable($action_key),
704
);
705
}
706
707
return $this->getGroups(
708
$group_map,
709
HeraldActionGroup::getAllActionGroups());
710
}
711
712
private function getGroups(array $item_map, array $group_list) {
713
assert_instances_of($group_list, 'HeraldGroup');
714
715
$groups = array();
716
foreach ($item_map as $group_key => $options) {
717
asort($options);
718
719
$group_object = idx($group_list, $group_key);
720
if ($group_object) {
721
$group_label = $group_object->getGroupLabel();
722
$group_order = $group_object->getSortKey();
723
} else {
724
$group_label = nonempty($group_key, pht('Other'));
725
$group_order = 'Z';
726
}
727
728
$groups[] = array(
729
'label' => $group_label,
730
'options' => $options,
731
'order' => $group_order,
732
);
733
}
734
735
return array_values(isort($groups, 'order'));
736
}
737
738
739
}
740
741