@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
3final 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 = 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 = 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('%s%ss', new PhutilNumber($cost), mb_chr(956, 'UTF-8'));
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 $has_xactions = $xscript->getObjectTranscript()
721 && $this->getTranscriptTransactionPHIDs($xscript);
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}