Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/herald/controller/HeraldTranscriptController.php
12256 views
1
<?php
2
3
final class HeraldTranscriptController extends HeraldController {
4
5
private $handles;
6
private $adapter;
7
8
private function getAdapter() {
9
return $this->adapter;
10
}
11
12
public function buildApplicationMenu() {
13
// Use the menu we build in this controller, not the default menu for
14
// Herald.
15
return null;
16
}
17
18
public function handleRequest(AphrontRequest $request) {
19
$viewer = $this->getViewer();
20
21
$xscript = id(new HeraldTranscriptQuery())
22
->setViewer($viewer)
23
->withIDs(array($request->getURIData('id')))
24
->executeOne();
25
if (!$xscript) {
26
return new Aphront404Response();
27
}
28
29
$view_key = $this->getViewKey($request);
30
if (!$view_key) {
31
return new Aphront404Response();
32
}
33
34
$navigation = $this->newSideNavView($xscript, $view_key);
35
36
$object = $xscript->getObject();
37
38
require_celerity_resource('herald-test-css');
39
$content = array();
40
41
$object_xscript = $xscript->getObjectTranscript();
42
if (!$object_xscript) {
43
$notice = id(new PHUIInfoView())
44
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
45
->setTitle(pht('Old Transcript'))
46
->appendChild(phutil_tag(
47
'p',
48
array(),
49
pht('Details of this transcript have been garbage collected.')));
50
$content[] = $notice;
51
} else {
52
$map = HeraldAdapter::getEnabledAdapterMap($viewer);
53
$object_type = $object_xscript->getType();
54
if (empty($map[$object_type])) {
55
// TODO: We should filter these out in the Query, but we have to load
56
// the objectTranscript right now, which is potentially enormous. We
57
// should denormalize the object type, or move the data into a separate
58
// table, and then filter this earlier (and thus raise a better error).
59
// For now, just block access so we don't violate policies.
60
throw new Exception(
61
pht('This transcript has an invalid or inaccessible adapter.'));
62
}
63
64
$this->adapter = HeraldAdapter::getAdapterForContentType($object_type);
65
66
$phids = $this->getTranscriptPHIDs($xscript);
67
$phids = array_unique($phids);
68
$phids = array_filter($phids);
69
70
$handles = $this->loadViewerHandles($phids);
71
$this->handles = $handles;
72
73
$warning_panel = $this->buildWarningPanel($xscript);
74
$content[] = $warning_panel;
75
76
$content[] = $this->newContentView($xscript, $view_key);
77
}
78
79
$crumbs = id($this->buildApplicationCrumbs())
80
->addTextCrumb(
81
pht('Transcripts'),
82
$this->getApplicationURI('/transcript/'))
83
->addTextCrumb(pht('Transcript %d', $xscript->getID()))
84
->setBorder(true);
85
86
$title = pht('Herald Transcript %s', $xscript->getID());
87
$header = $this->newHeaderView($xscript, $title);
88
89
$view = id(new PHUITwoColumnView())
90
->setHeader($header)
91
->setFooter($content);
92
93
return $this->newPage()
94
->setTitle($title)
95
->setCrumbs($crumbs)
96
->setNavigation($navigation)
97
->appendChild($view);
98
}
99
100
protected function renderConditionTestValue($condition, $handles) {
101
// TODO: This is all a hacky mess and should be driven through FieldValue
102
// eventually.
103
104
switch ($condition->getFieldName()) {
105
case HeraldAnotherRuleField::FIELDCONST:
106
$value = array($condition->getTestValue());
107
break;
108
default:
109
$value = $condition->getTestValue();
110
break;
111
}
112
113
if (!is_scalar($value) && $value !== null) {
114
foreach ($value as $key => $phid) {
115
$handle = idx($handles, $phid);
116
if ($handle && $handle->isComplete()) {
117
$value[$key] = $handle->getName();
118
} else {
119
// This happens for things like task priorities, statuses, and
120
// custom fields.
121
$value[$key] = $phid;
122
}
123
}
124
sort($value);
125
$value = implode(', ', $value);
126
}
127
128
return phutil_tag('span', array('class' => 'condition-test-value'), $value);
129
}
130
131
protected function getTranscriptPHIDs($xscript) {
132
$phids = array();
133
134
$object_xscript = $xscript->getObjectTranscript();
135
if (!$object_xscript) {
136
return array();
137
}
138
139
$phids[] = $object_xscript->getPHID();
140
141
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
142
// TODO: This is total hacks. Add another amazing layer of abstraction.
143
$target = (array)$apply_xscript->getTarget();
144
foreach ($target as $phid) {
145
if ($phid) {
146
$phids[] = $phid;
147
}
148
}
149
}
150
151
foreach ($xscript->getRuleTranscripts() as $rule_xscript) {
152
$phids[] = $rule_xscript->getRuleOwner();
153
}
154
155
$condition_xscripts = $xscript->getConditionTranscripts();
156
if ($condition_xscripts) {
157
$condition_xscripts = call_user_func_array(
158
'array_merge',
159
$condition_xscripts);
160
}
161
foreach ($condition_xscripts as $condition_xscript) {
162
switch ($condition_xscript->getFieldName()) {
163
case HeraldAnotherRuleField::FIELDCONST:
164
$phids[] = $condition_xscript->getTestValue();
165
break;
166
default:
167
$value = $condition_xscript->getTestValue();
168
// TODO: Also total hacks.
169
if (is_array($value)) {
170
foreach ($value as $phid) {
171
if ($phid) {
172
// TODO: Probably need to make sure this
173
// "looks like" a PHID or decrease the level of hacks here;
174
// this used to be an is_numeric() check in Facebook land.
175
$phids[] = $phid;
176
}
177
}
178
}
179
break;
180
}
181
}
182
183
return $phids;
184
}
185
186
private function buildWarningPanel(HeraldTranscript $xscript) {
187
$request = $this->getRequest();
188
$panel = null;
189
if ($xscript->getObjectTranscript()) {
190
$handles = $this->handles;
191
$object_xscript = $xscript->getObjectTranscript();
192
$handle = $handles[$object_xscript->getPHID()];
193
if ($handle->getType() ==
194
PhabricatorRepositoryCommitPHIDType::TYPECONST) {
195
$commit = id(new DiffusionCommitQuery())
196
->setViewer($request->getUser())
197
->withPHIDs(array($handle->getPHID()))
198
->executeOne();
199
if ($commit) {
200
$repository = $commit->getRepository();
201
if ($repository->isImporting()) {
202
$title = pht(
203
'The %s repository is still importing.',
204
$repository->getMonogram());
205
$body = pht(
206
'Herald rules will not trigger until import completes.');
207
} else if (!$repository->isTracked()) {
208
$title = pht(
209
'The %s repository is not tracked.',
210
$repository->getMonogram());
211
$body = pht(
212
'Herald rules will not trigger until tracking is enabled.');
213
} else {
214
return $panel;
215
}
216
$panel = id(new PHUIInfoView())
217
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
218
->setTitle($title)
219
->appendChild($body);
220
}
221
}
222
}
223
return $panel;
224
}
225
226
private function buildActionTranscriptPanel(HeraldTranscript $xscript) {
227
$viewer = $this->getViewer();
228
$action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID');
229
230
$adapter = $this->getAdapter();
231
232
$field_names = $adapter->getFieldNameMap();
233
$condition_names = $adapter->getConditionNameMap();
234
235
$handles = $this->handles;
236
237
$action_map = $xscript->getApplyTranscripts();
238
$action_map = mgroup($action_map, 'getRuleID');
239
240
$rule_list = id(new PHUIObjectItemListView())
241
->setNoDataString(pht('No Herald rules applied to this object.'))
242
->setFlush(true);
243
244
$rule_xscripts = $xscript->getRuleTranscripts();
245
$rule_xscripts = msort($rule_xscripts, 'getRuleID');
246
foreach ($rule_xscripts as $rule_xscript) {
247
$rule_id = $rule_xscript->getRuleID();
248
249
$rule_monogram = pht('H%d', $rule_id);
250
$rule_uri = '/'.$rule_monogram;
251
252
$rule_item = id(new PHUIObjectItemView())
253
->setObjectName($rule_monogram)
254
->setHeader($rule_xscript->getRuleName())
255
->setHref($rule_uri);
256
257
$rule_result = $rule_xscript->getRuleResult();
258
259
if (!$rule_result->getShouldApplyActions()) {
260
$rule_item->setDisabled(true);
261
}
262
263
$rule_list->addItem($rule_item);
264
265
// Build the field/condition transcript.
266
267
$cond_xscripts = $xscript->getConditionTranscriptsForRule($rule_id);
268
269
$cond_list = id(new PHUIStatusListView());
270
$cond_list->addItem(
271
id(new PHUIStatusItemView())
272
->setTarget(phutil_tag('strong', array(), pht('Conditions'))));
273
274
foreach ($cond_xscripts as $cond_xscript) {
275
$result = $cond_xscript->getResult();
276
277
$icon = $result->getIconIcon();
278
$color = $result->getIconColor();
279
$name = $result->getName();
280
281
$result_details = $result->newDetailsView($viewer);
282
if ($result_details !== null) {
283
$result_details = phutil_tag(
284
'div',
285
array(
286
'class' => 'herald-condition-note',
287
),
288
$result_details);
289
}
290
291
// TODO: This is not really translatable and should be driven through
292
// HeraldField.
293
$explanation = pht(
294
'%s %s %s',
295
idx($field_names, $cond_xscript->getFieldName(), pht('Unknown')),
296
idx($condition_names, $cond_xscript->getCondition(), pht('Unknown')),
297
$this->renderConditionTestValue($cond_xscript, $handles));
298
299
$cond_item = id(new PHUIStatusItemView())
300
->setIcon($icon, $color)
301
->setTarget($name)
302
->setNote(array($explanation, $result_details));
303
304
$cond_list->addItem($cond_item);
305
}
306
307
$rule_result = $rule_xscript->getRuleResult();
308
309
$last_icon = $rule_result->getIconIcon();
310
$last_color = $rule_result->getIconColor();
311
$last_result = $rule_result->getName();
312
$last_note = $rule_result->getDescription();
313
314
$last_details = $rule_result->newDetailsView($viewer);
315
if ($last_details !== null) {
316
$last_details = phutil_tag(
317
'div',
318
array(
319
'class' => 'herald-condition-note',
320
),
321
$last_details);
322
}
323
324
$cond_last = id(new PHUIStatusItemView())
325
->setIcon($last_icon, $last_color)
326
->setTarget(phutil_tag('strong', array(), $last_result))
327
->setNote(array($last_note, $last_details));
328
$cond_list->addItem($cond_last);
329
330
$cond_box = id(new PHUIBoxView())
331
->appendChild($cond_list)
332
->addMargin(PHUI::MARGIN_LARGE_LEFT);
333
334
$rule_item->appendChild($cond_box);
335
336
// Not all rules will have any action transcripts, but we show them
337
// in general because they may have relevant information even when
338
// rules did not take actions. In particular, state-based actions may
339
// forbid rules from matching.
340
341
$cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM);
342
343
$action_xscripts = idx($action_map, $rule_id, array());
344
foreach ($action_xscripts as $action_xscript) {
345
$action_key = $action_xscript->getAction();
346
$action = $adapter->getActionImplementation($action_key);
347
348
if ($action) {
349
$name = $action->getHeraldActionName();
350
$action->setViewer($this->getViewer());
351
} else {
352
$name = pht('Unknown Action ("%s")', $action_key);
353
}
354
355
$name = pht('Action: %s', $name);
356
357
$action_list = id(new PHUIStatusListView());
358
$action_list->addItem(
359
id(new PHUIStatusItemView())
360
->setTarget(phutil_tag('strong', array(), $name)));
361
362
$action_box = id(new PHUIBoxView())
363
->appendChild($action_list)
364
->addMargin(PHUI::MARGIN_LARGE_LEFT);
365
366
$rule_item->appendChild($action_box);
367
368
$log = $action_xscript->getAppliedReason();
369
370
// Handle older transcripts which used a static string to record
371
// action results.
372
373
if ($xscript->getDryRun()) {
374
$action_list->addItem(
375
id(new PHUIStatusItemView())
376
->setIcon('fa-ban', 'grey')
377
->setTarget(pht('Dry Run'))
378
->setNote(
379
pht(
380
'This was a dry run, so no actions were taken.')));
381
continue;
382
} else if (!is_array($log)) {
383
$action_list->addItem(
384
id(new PHUIStatusItemView())
385
->setIcon('fa-clock-o', 'grey')
386
->setTarget(pht('Old Transcript'))
387
->setNote(
388
pht(
389
'This is an old transcript which uses an obsolete log '.
390
'format. Detailed action information is not available.')));
391
continue;
392
}
393
394
foreach ($log as $entry) {
395
$type = idx($entry, 'type');
396
$data = idx($entry, 'data');
397
398
if ($action) {
399
$icon = $action->renderActionEffectIcon($type, $data);
400
$color = $action->renderActionEffectColor($type, $data);
401
$name = $action->renderActionEffectName($type, $data);
402
$note = $action->renderEffectDescription($type, $data);
403
} else {
404
$icon = 'fa-question-circle';
405
$color = 'indigo';
406
$name = pht('Unknown Effect ("%s")', $type);
407
$note = null;
408
}
409
410
$action_item = id(new PHUIStatusItemView())
411
->setIcon($icon, $color)
412
->setTarget($name)
413
->setNote($note);
414
415
$action_list->addItem($action_item);
416
}
417
}
418
}
419
420
$box = id(new PHUIObjectBoxView())
421
->setHeaderText(pht('Rule Transcript'))
422
->appendChild($rule_list);
423
424
$content = array();
425
426
if ($xscript->getDryRun()) {
427
$notice = new PHUIInfoView();
428
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
429
$notice->setTitle(pht('Dry Run'));
430
$notice->appendChild(
431
pht(
432
'This was a dry run to test Herald rules, '.
433
'no actions were executed.'));
434
$content[] = $notice;
435
}
436
437
$content[] = $box;
438
439
return $content;
440
}
441
442
private function buildObjectTranscriptPanel(HeraldTranscript $xscript) {
443
$viewer = $this->getViewer();
444
$adapter = $this->getAdapter();
445
446
$field_names = $adapter->getFieldNameMap();
447
448
$object_xscript = $xscript->getObjectTranscript();
449
450
$rows = array();
451
if ($object_xscript) {
452
$phid = $object_xscript->getPHID();
453
$handles = $this->handles;
454
455
$rows[] = array(
456
pht('Object Name'),
457
$object_xscript->getName(),
458
);
459
460
$rows[] = array(
461
pht('Object Type'),
462
$object_xscript->getType(),
463
);
464
465
$rows[] = array(
466
pht('Object PHID'),
467
$phid,
468
);
469
470
$rows[] = array(
471
pht('Object Link'),
472
$handles[$phid]->renderLink(),
473
);
474
}
475
476
foreach ($xscript->getMetadataMap() as $key => $value) {
477
$rows[] = array(
478
$key,
479
$value,
480
);
481
}
482
483
if ($object_xscript) {
484
foreach ($object_xscript->getFields() as $field_type => $value) {
485
if (isset($field_names[$field_type])) {
486
$field_name = pht('Field: %s', $field_names[$field_type]);
487
} else {
488
$field_name = pht('Unknown Field ("%s")', $field_type);
489
}
490
491
$field_value = $adapter->renderFieldTranscriptValue(
492
$viewer,
493
$field_type,
494
$value);
495
496
$rows[] = array(
497
$field_name,
498
$field_value,
499
);
500
}
501
}
502
503
$property_list = new PHUIPropertyListView();
504
$property_list->setStacked(true);
505
foreach ($rows as $row) {
506
$property_list->addProperty($row[0], $row[1]);
507
}
508
509
$box = new PHUIObjectBoxView();
510
$box->setHeaderText(pht('Object Transcript'));
511
$box->appendChild($property_list);
512
513
return $box;
514
}
515
516
private function buildTransactionsTranscriptPanel(HeraldTranscript $xscript) {
517
$viewer = $this->getViewer();
518
519
$xaction_phids = $this->getTranscriptTransactionPHIDs($xscript);
520
521
if ($xaction_phids) {
522
$object = $xscript->getObject();
523
$query = PhabricatorApplicationTransactionQuery::newQueryForObject(
524
$object);
525
$xactions = $query
526
->setViewer($viewer)
527
->withPHIDs($xaction_phids)
528
->execute();
529
$xactions = mpull($xactions, null, 'getPHID');
530
} else {
531
$xactions = array();
532
}
533
534
$rows = array();
535
foreach ($xaction_phids as $xaction_phid) {
536
$xaction = idx($xactions, $xaction_phid);
537
538
$xaction_identifier = $xaction_phid;
539
$xaction_date = null;
540
$xaction_display = null;
541
if ($xaction) {
542
$xaction_identifier = $xaction->getID();
543
$xaction_date = phabricator_datetime(
544
$xaction->getDateCreated(),
545
$viewer);
546
547
// Since we don't usually render transactions outside of the context
548
// of objects, some of them might depend on missing object data. Out of
549
// an abundance of caution, catch any rendering issues.
550
try {
551
$xaction_display = $xaction->getTitle();
552
} catch (Exception $ex) {
553
$xaction_display = $ex->getMessage();
554
}
555
}
556
557
$rows[] = array(
558
$xaction_identifier,
559
$xaction_display,
560
$xaction_date,
561
);
562
}
563
564
$table_view = id(new AphrontTableView($rows))
565
->setHeaders(
566
array(
567
pht('ID'),
568
pht('Transaction'),
569
pht('Date'),
570
))
571
->setColumnClasses(
572
array(
573
null,
574
'wide',
575
null,
576
));
577
578
$box_view = id(new PHUIObjectBoxView())
579
->setHeaderText(pht('Transactions'))
580
->setTable($table_view);
581
582
return $box_view;
583
}
584
585
586
private function buildProfilerTranscriptPanel(HeraldTranscript $xscript) {
587
$viewer = $this->getViewer();
588
589
$object_xscript = $xscript->getObjectTranscript();
590
591
$profile = $object_xscript->getProfile();
592
593
// If this is an older transcript without profiler information, don't
594
// show anything.
595
if ($profile === null) {
596
return null;
597
}
598
599
$profile = isort($profile, 'elapsed');
600
$profile = array_reverse($profile);
601
602
$phids = array();
603
foreach ($profile as $frame) {
604
if ($frame['type'] === 'rule') {
605
$phids[] = $frame['key'];
606
}
607
}
608
$handles = $viewer->loadHandles($phids);
609
610
$field_map = HeraldField::getAllFields();
611
612
$rows = array();
613
foreach ($profile as $frame) {
614
$cost = $frame['elapsed'];
615
$cost = 1000000 * $cost;
616
$cost = pht('%sus', new PhutilNumber($cost));
617
618
$type = $frame['type'];
619
switch ($type) {
620
case 'rule':
621
$type_display = pht('Rule');
622
break;
623
case 'field':
624
$type_display = pht('Field');
625
break;
626
default:
627
$type_display = $type;
628
break;
629
}
630
631
$key = $frame['key'];
632
switch ($type) {
633
case 'field':
634
$field_object = idx($field_map, $key);
635
if ($field_object) {
636
$key_display = $field_object->getHeraldFieldName();
637
} else {
638
$key_display = $key;
639
}
640
break;
641
case 'rule':
642
$key_display = $handles[$key]->renderLink();
643
break;
644
default:
645
$key_display = $key;
646
break;
647
}
648
649
$rows[] = array(
650
$type_display,
651
$key_display,
652
$cost,
653
pht('%s', new PhutilNumber($frame['count'])),
654
);
655
}
656
657
$table_view = id(new AphrontTableView($rows))
658
->setHeaders(
659
array(
660
pht('Type'),
661
pht('What'),
662
pht('Cost'),
663
pht('Count'),
664
))
665
->setColumnClasses(
666
array(
667
null,
668
'wide',
669
'right',
670
'right',
671
));
672
673
$box_view = id(new PHUIObjectBoxView())
674
->setHeaderText(pht('Profile'))
675
->setTable($table_view);
676
677
return $box_view;
678
}
679
680
private function getViewKey(AphrontRequest $request) {
681
$view_key = $request->getURIData('view');
682
683
if ($view_key === null) {
684
return 'rules';
685
}
686
687
switch ($view_key) {
688
case 'fields':
689
case 'xactions':
690
case 'profile':
691
return $view_key;
692
default:
693
return null;
694
}
695
}
696
697
private function newSideNavView(
698
HeraldTranscript $xscript,
699
$view_key) {
700
701
$base_uri = urisprintf(
702
'transcript/%d/',
703
$xscript->getID());
704
705
$base_uri = $this->getApplicationURI($base_uri);
706
$base_uri = new PhutilURI($base_uri);
707
708
$nav = id(new AphrontSideNavFilterView())
709
->setBaseURI($base_uri);
710
711
$nav->newLink('rules')
712
->setHref($base_uri)
713
->setName(pht('Rules'))
714
->setIcon('fa-list-ul');
715
716
$nav->newLink('fields')
717
->setName(pht('Field Values'))
718
->setIcon('fa-file-text-o');
719
720
$xaction_phids = $this->getTranscriptTransactionPHIDs($xscript);
721
$has_xactions = (bool)$xaction_phids;
722
723
$nav->newLink('xactions')
724
->setName(pht('Transactions'))
725
->setIcon('fa-forward')
726
->setDisabled(!$has_xactions);
727
728
$nav->newLink('profile')
729
->setName(pht('Profiler'))
730
->setIcon('fa-tachometer');
731
732
$nav->selectFilter($view_key);
733
734
return $nav;
735
}
736
737
private function newContentView(
738
HeraldTranscript $xscript,
739
$view_key) {
740
741
switch ($view_key) {
742
case 'rules':
743
$content = $this->buildActionTranscriptPanel($xscript);
744
break;
745
case 'fields':
746
$content = $this->buildObjectTranscriptPanel($xscript);
747
break;
748
case 'xactions':
749
$content = $this->buildTransactionsTranscriptPanel($xscript);
750
break;
751
case 'profile':
752
$content = $this->buildProfilerTranscriptPanel($xscript);
753
break;
754
default:
755
throw new Exception(pht('Unknown view key "%s".', $view_key));
756
}
757
758
return $content;
759
}
760
761
private function getTranscriptTransactionPHIDs(HeraldTranscript $xscript) {
762
763
$object_xscript = $xscript->getObjectTranscript();
764
$xaction_phids = $object_xscript->getAppliedTransactionPHIDs();
765
766
// If the value is "null", this is an older transcript or this adapter
767
// does not use transactions.
768
//
769
// (If the value is "array()", this is a modern transcript which uses
770
// transactions, there just weren't any applied.)
771
if ($xaction_phids === null) {
772
return array();
773
}
774
775
$object = $xscript->getObject();
776
777
// If this object doesn't implement the right interface, we won't be
778
// able to load the transactions.
779
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
780
return array();
781
}
782
783
return $xaction_phids;
784
}
785
786
private function newHeaderView(HeraldTranscript $xscript, $title) {
787
$header = id(new PHUIHeaderView())
788
->setHeader($title)
789
->setHeaderIcon('fa-list-ul');
790
791
if ($xscript->getDryRun()) {
792
$dry_run_tag = id(new PHUITagView())
793
->setType(PHUITagView::TYPE_SHADE)
794
->setColor(PHUITagView::COLOR_VIOLET)
795
->setName(pht('Dry Run'))
796
->setIcon('fa-exclamation-triangle');
797
798
$header->addTag($dry_run_tag);
799
}
800
801
return $header;
802
}
803
804
}
805
806