Path: blob/master/src/applications/herald/controller/HeraldTranscriptController.php
12256 views
<?php12final class HeraldTranscriptController extends HeraldController {34private $handles;5private $adapter;67private function getAdapter() {8return $this->adapter;9}1011public function buildApplicationMenu() {12// Use the menu we build in this controller, not the default menu for13// Herald.14return null;15}1617public function handleRequest(AphrontRequest $request) {18$viewer = $this->getViewer();1920$xscript = id(new HeraldTranscriptQuery())21->setViewer($viewer)22->withIDs(array($request->getURIData('id')))23->executeOne();24if (!$xscript) {25return new Aphront404Response();26}2728$view_key = $this->getViewKey($request);29if (!$view_key) {30return new Aphront404Response();31}3233$navigation = $this->newSideNavView($xscript, $view_key);3435$object = $xscript->getObject();3637require_celerity_resource('herald-test-css');38$content = array();3940$object_xscript = $xscript->getObjectTranscript();41if (!$object_xscript) {42$notice = id(new PHUIInfoView())43->setSeverity(PHUIInfoView::SEVERITY_NOTICE)44->setTitle(pht('Old Transcript'))45->appendChild(phutil_tag(46'p',47array(),48pht('Details of this transcript have been garbage collected.')));49$content[] = $notice;50} else {51$map = HeraldAdapter::getEnabledAdapterMap($viewer);52$object_type = $object_xscript->getType();53if (empty($map[$object_type])) {54// TODO: We should filter these out in the Query, but we have to load55// the objectTranscript right now, which is potentially enormous. We56// should denormalize the object type, or move the data into a separate57// table, and then filter this earlier (and thus raise a better error).58// For now, just block access so we don't violate policies.59throw new Exception(60pht('This transcript has an invalid or inaccessible adapter.'));61}6263$this->adapter = HeraldAdapter::getAdapterForContentType($object_type);6465$phids = $this->getTranscriptPHIDs($xscript);66$phids = array_unique($phids);67$phids = array_filter($phids);6869$handles = $this->loadViewerHandles($phids);70$this->handles = $handles;7172$warning_panel = $this->buildWarningPanel($xscript);73$content[] = $warning_panel;7475$content[] = $this->newContentView($xscript, $view_key);76}7778$crumbs = id($this->buildApplicationCrumbs())79->addTextCrumb(80pht('Transcripts'),81$this->getApplicationURI('/transcript/'))82->addTextCrumb(pht('Transcript %d', $xscript->getID()))83->setBorder(true);8485$title = pht('Herald Transcript %s', $xscript->getID());86$header = $this->newHeaderView($xscript, $title);8788$view = id(new PHUITwoColumnView())89->setHeader($header)90->setFooter($content);9192return $this->newPage()93->setTitle($title)94->setCrumbs($crumbs)95->setNavigation($navigation)96->appendChild($view);97}9899protected function renderConditionTestValue($condition, $handles) {100// TODO: This is all a hacky mess and should be driven through FieldValue101// eventually.102103switch ($condition->getFieldName()) {104case HeraldAnotherRuleField::FIELDCONST:105$value = array($condition->getTestValue());106break;107default:108$value = $condition->getTestValue();109break;110}111112if (!is_scalar($value) && $value !== null) {113foreach ($value as $key => $phid) {114$handle = idx($handles, $phid);115if ($handle && $handle->isComplete()) {116$value[$key] = $handle->getName();117} else {118// This happens for things like task priorities, statuses, and119// custom fields.120$value[$key] = $phid;121}122}123sort($value);124$value = implode(', ', $value);125}126127return phutil_tag('span', array('class' => 'condition-test-value'), $value);128}129130protected function getTranscriptPHIDs($xscript) {131$phids = array();132133$object_xscript = $xscript->getObjectTranscript();134if (!$object_xscript) {135return array();136}137138$phids[] = $object_xscript->getPHID();139140foreach ($xscript->getApplyTranscripts() as $apply_xscript) {141// TODO: This is total hacks. Add another amazing layer of abstraction.142$target = (array)$apply_xscript->getTarget();143foreach ($target as $phid) {144if ($phid) {145$phids[] = $phid;146}147}148}149150foreach ($xscript->getRuleTranscripts() as $rule_xscript) {151$phids[] = $rule_xscript->getRuleOwner();152}153154$condition_xscripts = $xscript->getConditionTranscripts();155if ($condition_xscripts) {156$condition_xscripts = call_user_func_array(157'array_merge',158$condition_xscripts);159}160foreach ($condition_xscripts as $condition_xscript) {161switch ($condition_xscript->getFieldName()) {162case HeraldAnotherRuleField::FIELDCONST:163$phids[] = $condition_xscript->getTestValue();164break;165default:166$value = $condition_xscript->getTestValue();167// TODO: Also total hacks.168if (is_array($value)) {169foreach ($value as $phid) {170if ($phid) {171// TODO: Probably need to make sure this172// "looks like" a PHID or decrease the level of hacks here;173// this used to be an is_numeric() check in Facebook land.174$phids[] = $phid;175}176}177}178break;179}180}181182return $phids;183}184185private function buildWarningPanel(HeraldTranscript $xscript) {186$request = $this->getRequest();187$panel = null;188if ($xscript->getObjectTranscript()) {189$handles = $this->handles;190$object_xscript = $xscript->getObjectTranscript();191$handle = $handles[$object_xscript->getPHID()];192if ($handle->getType() ==193PhabricatorRepositoryCommitPHIDType::TYPECONST) {194$commit = id(new DiffusionCommitQuery())195->setViewer($request->getUser())196->withPHIDs(array($handle->getPHID()))197->executeOne();198if ($commit) {199$repository = $commit->getRepository();200if ($repository->isImporting()) {201$title = pht(202'The %s repository is still importing.',203$repository->getMonogram());204$body = pht(205'Herald rules will not trigger until import completes.');206} else if (!$repository->isTracked()) {207$title = pht(208'The %s repository is not tracked.',209$repository->getMonogram());210$body = pht(211'Herald rules will not trigger until tracking is enabled.');212} else {213return $panel;214}215$panel = id(new PHUIInfoView())216->setSeverity(PHUIInfoView::SEVERITY_WARNING)217->setTitle($title)218->appendChild($body);219}220}221}222return $panel;223}224225private function buildActionTranscriptPanel(HeraldTranscript $xscript) {226$viewer = $this->getViewer();227$action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID');228229$adapter = $this->getAdapter();230231$field_names = $adapter->getFieldNameMap();232$condition_names = $adapter->getConditionNameMap();233234$handles = $this->handles;235236$action_map = $xscript->getApplyTranscripts();237$action_map = mgroup($action_map, 'getRuleID');238239$rule_list = id(new PHUIObjectItemListView())240->setNoDataString(pht('No Herald rules applied to this object.'))241->setFlush(true);242243$rule_xscripts = $xscript->getRuleTranscripts();244$rule_xscripts = msort($rule_xscripts, 'getRuleID');245foreach ($rule_xscripts as $rule_xscript) {246$rule_id = $rule_xscript->getRuleID();247248$rule_monogram = pht('H%d', $rule_id);249$rule_uri = '/'.$rule_monogram;250251$rule_item = id(new PHUIObjectItemView())252->setObjectName($rule_monogram)253->setHeader($rule_xscript->getRuleName())254->setHref($rule_uri);255256$rule_result = $rule_xscript->getRuleResult();257258if (!$rule_result->getShouldApplyActions()) {259$rule_item->setDisabled(true);260}261262$rule_list->addItem($rule_item);263264// Build the field/condition transcript.265266$cond_xscripts = $xscript->getConditionTranscriptsForRule($rule_id);267268$cond_list = id(new PHUIStatusListView());269$cond_list->addItem(270id(new PHUIStatusItemView())271->setTarget(phutil_tag('strong', array(), pht('Conditions'))));272273foreach ($cond_xscripts as $cond_xscript) {274$result = $cond_xscript->getResult();275276$icon = $result->getIconIcon();277$color = $result->getIconColor();278$name = $result->getName();279280$result_details = $result->newDetailsView($viewer);281if ($result_details !== null) {282$result_details = phutil_tag(283'div',284array(285'class' => 'herald-condition-note',286),287$result_details);288}289290// TODO: This is not really translatable and should be driven through291// HeraldField.292$explanation = pht(293'%s %s %s',294idx($field_names, $cond_xscript->getFieldName(), pht('Unknown')),295idx($condition_names, $cond_xscript->getCondition(), pht('Unknown')),296$this->renderConditionTestValue($cond_xscript, $handles));297298$cond_item = id(new PHUIStatusItemView())299->setIcon($icon, $color)300->setTarget($name)301->setNote(array($explanation, $result_details));302303$cond_list->addItem($cond_item);304}305306$rule_result = $rule_xscript->getRuleResult();307308$last_icon = $rule_result->getIconIcon();309$last_color = $rule_result->getIconColor();310$last_result = $rule_result->getName();311$last_note = $rule_result->getDescription();312313$last_details = $rule_result->newDetailsView($viewer);314if ($last_details !== null) {315$last_details = phutil_tag(316'div',317array(318'class' => 'herald-condition-note',319),320$last_details);321}322323$cond_last = id(new PHUIStatusItemView())324->setIcon($last_icon, $last_color)325->setTarget(phutil_tag('strong', array(), $last_result))326->setNote(array($last_note, $last_details));327$cond_list->addItem($cond_last);328329$cond_box = id(new PHUIBoxView())330->appendChild($cond_list)331->addMargin(PHUI::MARGIN_LARGE_LEFT);332333$rule_item->appendChild($cond_box);334335// Not all rules will have any action transcripts, but we show them336// in general because they may have relevant information even when337// rules did not take actions. In particular, state-based actions may338// forbid rules from matching.339340$cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM);341342$action_xscripts = idx($action_map, $rule_id, array());343foreach ($action_xscripts as $action_xscript) {344$action_key = $action_xscript->getAction();345$action = $adapter->getActionImplementation($action_key);346347if ($action) {348$name = $action->getHeraldActionName();349$action->setViewer($this->getViewer());350} else {351$name = pht('Unknown Action ("%s")', $action_key);352}353354$name = pht('Action: %s', $name);355356$action_list = id(new PHUIStatusListView());357$action_list->addItem(358id(new PHUIStatusItemView())359->setTarget(phutil_tag('strong', array(), $name)));360361$action_box = id(new PHUIBoxView())362->appendChild($action_list)363->addMargin(PHUI::MARGIN_LARGE_LEFT);364365$rule_item->appendChild($action_box);366367$log = $action_xscript->getAppliedReason();368369// Handle older transcripts which used a static string to record370// action results.371372if ($xscript->getDryRun()) {373$action_list->addItem(374id(new PHUIStatusItemView())375->setIcon('fa-ban', 'grey')376->setTarget(pht('Dry Run'))377->setNote(378pht(379'This was a dry run, so no actions were taken.')));380continue;381} else if (!is_array($log)) {382$action_list->addItem(383id(new PHUIStatusItemView())384->setIcon('fa-clock-o', 'grey')385->setTarget(pht('Old Transcript'))386->setNote(387pht(388'This is an old transcript which uses an obsolete log '.389'format. Detailed action information is not available.')));390continue;391}392393foreach ($log as $entry) {394$type = idx($entry, 'type');395$data = idx($entry, 'data');396397if ($action) {398$icon = $action->renderActionEffectIcon($type, $data);399$color = $action->renderActionEffectColor($type, $data);400$name = $action->renderActionEffectName($type, $data);401$note = $action->renderEffectDescription($type, $data);402} else {403$icon = 'fa-question-circle';404$color = 'indigo';405$name = pht('Unknown Effect ("%s")', $type);406$note = null;407}408409$action_item = id(new PHUIStatusItemView())410->setIcon($icon, $color)411->setTarget($name)412->setNote($note);413414$action_list->addItem($action_item);415}416}417}418419$box = id(new PHUIObjectBoxView())420->setHeaderText(pht('Rule Transcript'))421->appendChild($rule_list);422423$content = array();424425if ($xscript->getDryRun()) {426$notice = new PHUIInfoView();427$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);428$notice->setTitle(pht('Dry Run'));429$notice->appendChild(430pht(431'This was a dry run to test Herald rules, '.432'no actions were executed.'));433$content[] = $notice;434}435436$content[] = $box;437438return $content;439}440441private function buildObjectTranscriptPanel(HeraldTranscript $xscript) {442$viewer = $this->getViewer();443$adapter = $this->getAdapter();444445$field_names = $adapter->getFieldNameMap();446447$object_xscript = $xscript->getObjectTranscript();448449$rows = array();450if ($object_xscript) {451$phid = $object_xscript->getPHID();452$handles = $this->handles;453454$rows[] = array(455pht('Object Name'),456$object_xscript->getName(),457);458459$rows[] = array(460pht('Object Type'),461$object_xscript->getType(),462);463464$rows[] = array(465pht('Object PHID'),466$phid,467);468469$rows[] = array(470pht('Object Link'),471$handles[$phid]->renderLink(),472);473}474475foreach ($xscript->getMetadataMap() as $key => $value) {476$rows[] = array(477$key,478$value,479);480}481482if ($object_xscript) {483foreach ($object_xscript->getFields() as $field_type => $value) {484if (isset($field_names[$field_type])) {485$field_name = pht('Field: %s', $field_names[$field_type]);486} else {487$field_name = pht('Unknown Field ("%s")', $field_type);488}489490$field_value = $adapter->renderFieldTranscriptValue(491$viewer,492$field_type,493$value);494495$rows[] = array(496$field_name,497$field_value,498);499}500}501502$property_list = new PHUIPropertyListView();503$property_list->setStacked(true);504foreach ($rows as $row) {505$property_list->addProperty($row[0], $row[1]);506}507508$box = new PHUIObjectBoxView();509$box->setHeaderText(pht('Object Transcript'));510$box->appendChild($property_list);511512return $box;513}514515private function buildTransactionsTranscriptPanel(HeraldTranscript $xscript) {516$viewer = $this->getViewer();517518$xaction_phids = $this->getTranscriptTransactionPHIDs($xscript);519520if ($xaction_phids) {521$object = $xscript->getObject();522$query = PhabricatorApplicationTransactionQuery::newQueryForObject(523$object);524$xactions = $query525->setViewer($viewer)526->withPHIDs($xaction_phids)527->execute();528$xactions = mpull($xactions, null, 'getPHID');529} else {530$xactions = array();531}532533$rows = array();534foreach ($xaction_phids as $xaction_phid) {535$xaction = idx($xactions, $xaction_phid);536537$xaction_identifier = $xaction_phid;538$xaction_date = null;539$xaction_display = null;540if ($xaction) {541$xaction_identifier = $xaction->getID();542$xaction_date = phabricator_datetime(543$xaction->getDateCreated(),544$viewer);545546// Since we don't usually render transactions outside of the context547// of objects, some of them might depend on missing object data. Out of548// an abundance of caution, catch any rendering issues.549try {550$xaction_display = $xaction->getTitle();551} catch (Exception $ex) {552$xaction_display = $ex->getMessage();553}554}555556$rows[] = array(557$xaction_identifier,558$xaction_display,559$xaction_date,560);561}562563$table_view = id(new AphrontTableView($rows))564->setHeaders(565array(566pht('ID'),567pht('Transaction'),568pht('Date'),569))570->setColumnClasses(571array(572null,573'wide',574null,575));576577$box_view = id(new PHUIObjectBoxView())578->setHeaderText(pht('Transactions'))579->setTable($table_view);580581return $box_view;582}583584585private function buildProfilerTranscriptPanel(HeraldTranscript $xscript) {586$viewer = $this->getViewer();587588$object_xscript = $xscript->getObjectTranscript();589590$profile = $object_xscript->getProfile();591592// If this is an older transcript without profiler information, don't593// show anything.594if ($profile === null) {595return null;596}597598$profile = isort($profile, 'elapsed');599$profile = array_reverse($profile);600601$phids = array();602foreach ($profile as $frame) {603if ($frame['type'] === 'rule') {604$phids[] = $frame['key'];605}606}607$handles = $viewer->loadHandles($phids);608609$field_map = HeraldField::getAllFields();610611$rows = array();612foreach ($profile as $frame) {613$cost = $frame['elapsed'];614$cost = 1000000 * $cost;615$cost = pht('%sus', new PhutilNumber($cost));616617$type = $frame['type'];618switch ($type) {619case 'rule':620$type_display = pht('Rule');621break;622case 'field':623$type_display = pht('Field');624break;625default:626$type_display = $type;627break;628}629630$key = $frame['key'];631switch ($type) {632case 'field':633$field_object = idx($field_map, $key);634if ($field_object) {635$key_display = $field_object->getHeraldFieldName();636} else {637$key_display = $key;638}639break;640case 'rule':641$key_display = $handles[$key]->renderLink();642break;643default:644$key_display = $key;645break;646}647648$rows[] = array(649$type_display,650$key_display,651$cost,652pht('%s', new PhutilNumber($frame['count'])),653);654}655656$table_view = id(new AphrontTableView($rows))657->setHeaders(658array(659pht('Type'),660pht('What'),661pht('Cost'),662pht('Count'),663))664->setColumnClasses(665array(666null,667'wide',668'right',669'right',670));671672$box_view = id(new PHUIObjectBoxView())673->setHeaderText(pht('Profile'))674->setTable($table_view);675676return $box_view;677}678679private function getViewKey(AphrontRequest $request) {680$view_key = $request->getURIData('view');681682if ($view_key === null) {683return 'rules';684}685686switch ($view_key) {687case 'fields':688case 'xactions':689case 'profile':690return $view_key;691default:692return null;693}694}695696private function newSideNavView(697HeraldTranscript $xscript,698$view_key) {699700$base_uri = urisprintf(701'transcript/%d/',702$xscript->getID());703704$base_uri = $this->getApplicationURI($base_uri);705$base_uri = new PhutilURI($base_uri);706707$nav = id(new AphrontSideNavFilterView())708->setBaseURI($base_uri);709710$nav->newLink('rules')711->setHref($base_uri)712->setName(pht('Rules'))713->setIcon('fa-list-ul');714715$nav->newLink('fields')716->setName(pht('Field Values'))717->setIcon('fa-file-text-o');718719$xaction_phids = $this->getTranscriptTransactionPHIDs($xscript);720$has_xactions = (bool)$xaction_phids;721722$nav->newLink('xactions')723->setName(pht('Transactions'))724->setIcon('fa-forward')725->setDisabled(!$has_xactions);726727$nav->newLink('profile')728->setName(pht('Profiler'))729->setIcon('fa-tachometer');730731$nav->selectFilter($view_key);732733return $nav;734}735736private function newContentView(737HeraldTranscript $xscript,738$view_key) {739740switch ($view_key) {741case 'rules':742$content = $this->buildActionTranscriptPanel($xscript);743break;744case 'fields':745$content = $this->buildObjectTranscriptPanel($xscript);746break;747case 'xactions':748$content = $this->buildTransactionsTranscriptPanel($xscript);749break;750case 'profile':751$content = $this->buildProfilerTranscriptPanel($xscript);752break;753default:754throw new Exception(pht('Unknown view key "%s".', $view_key));755}756757return $content;758}759760private function getTranscriptTransactionPHIDs(HeraldTranscript $xscript) {761762$object_xscript = $xscript->getObjectTranscript();763$xaction_phids = $object_xscript->getAppliedTransactionPHIDs();764765// If the value is "null", this is an older transcript or this adapter766// does not use transactions.767//768// (If the value is "array()", this is a modern transcript which uses769// transactions, there just weren't any applied.)770if ($xaction_phids === null) {771return array();772}773774$object = $xscript->getObject();775776// If this object doesn't implement the right interface, we won't be777// able to load the transactions.778if (!($object instanceof PhabricatorApplicationTransactionInterface)) {779return array();780}781782return $xaction_phids;783}784785private function newHeaderView(HeraldTranscript $xscript, $title) {786$header = id(new PHUIHeaderView())787->setHeader($title)788->setHeaderIcon('fa-list-ul');789790if ($xscript->getDryRun()) {791$dry_run_tag = id(new PHUITagView())792->setType(PHUITagView::TYPE_SHADE)793->setColor(PHUITagView::COLOR_VIOLET)794->setName(pht('Dry Run'))795->setIcon('fa-exclamation-triangle');796797$header->addTag($dry_run_tag);798}799800return $header;801}802803}804805806