mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-22 21:40:55 +01:00
88aba65d54
Summary: This was significantly easier than expected. Here's an example of what an extension class might look like: ``` <?php final class AddRiskReviewHeraldCustomAction extends HeraldCustomAction { public function appliesToAdapter(HeraldAdapter $adapter) { return $adapter instanceof HeraldDifferentialRevisionAdapter; } public function appliesToRuleType($rule_type) { return $rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL || $rule_type == HeraldRuleTypeConfig::RULE_TYPE_OBJECT; } public function getActionKey() { return 'custom:add-risk'; } public function getActionName() { return 'Add risk rating (JSON)'; } public function getActionType() { return HeraldAdapter::VALUE_TEXT; } public function applyEffect( HeraldAdapter $adapter, $object, HeraldEffect $effect) { $key = "phragile:risk-rating"; // Read existing value. $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_VIEW); $field_list->readFieldsFromStorage($object); $field_list = mpull($field_list->getFields(), null, 'getFieldKey'); $field = $field_list[$key]; $field->setObject($object); $field->setViewer(PhabricatorUser::getOmnipotentUser()); $risk = $field->getValue(); $old_risk = $risk; // PHP copies arrays by default! // Add new value to array. $herald_args = phutil_json_decode($effect->getTarget()); $risk[$herald_args['key']] = array( 'value' => $herald_args['value'], 'reason' => $herald_args['reason']); $risk_key = $herald_args['key']; // Set new value. $adapter->queueTransaction( id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) ->setMetadataValue('customfield:key', $key) ->setOldValue($old_risk) ->setNewValue($risk)); return new HeraldApplyTranscript( $effect, true, pht( 'Modifying automatic risk ratings (key: %s)!', $risk_key)); } } ``` Test Plan: Created a custom action for differential revisions, set up a Herald rule to match and trigger the custom action, did 'arc diff' and saw the action trigger in the transcripts. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: locutus, edutibau, ite-klass, epriestley, Korvin Maniphest Tasks: T4884 Differential Revision: https://secure.phabricator.com/D8784
552 lines
16 KiB
PHP
552 lines
16 KiB
PHP
<?php
|
|
|
|
final class HeraldTranscriptController extends HeraldController {
|
|
|
|
const FILTER_AFFECTED = 'affected';
|
|
const FILTER_OWNED = 'owned';
|
|
const FILTER_ALL = 'all';
|
|
|
|
private $id;
|
|
private $filter;
|
|
private $handles;
|
|
private $adapter;
|
|
|
|
public function willProcessRequest(array $data) {
|
|
$this->id = $data['id'];
|
|
$map = $this->getFilterMap();
|
|
$this->filter = idx($data, 'filter');
|
|
if (empty($map[$this->filter])) {
|
|
$this->filter = self::FILTER_ALL;
|
|
}
|
|
}
|
|
|
|
private function getAdapter() {
|
|
return $this->adapter;
|
|
}
|
|
|
|
public function processRequest() {
|
|
$request = $this->getRequest();
|
|
$viewer = $request->getUser();
|
|
|
|
$xscript = id(new HeraldTranscriptQuery())
|
|
->setViewer($viewer)
|
|
->withIDs(array($this->id))
|
|
->executeOne();
|
|
if (!$xscript) {
|
|
return new Aphront404Response();
|
|
}
|
|
|
|
require_celerity_resource('herald-test-css');
|
|
|
|
$nav = $this->buildSideNav();
|
|
|
|
$object_xscript = $xscript->getObjectTranscript();
|
|
if (!$object_xscript) {
|
|
$notice = id(new AphrontErrorView())
|
|
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
|
|
->setTitle(pht('Old Transcript'))
|
|
->appendChild(phutil_tag(
|
|
'p',
|
|
array(),
|
|
pht('Details of this transcript have been garbage collected.')));
|
|
$nav->appendChild($notice);
|
|
} else {
|
|
$map = HeraldAdapter::getEnabledAdapterMap($viewer);
|
|
$object_type = $object_xscript->getType();
|
|
if (empty($map[$object_type])) {
|
|
// TODO: We should filter these out in the Query, but we have to load
|
|
// the objectTranscript right now, which is potentially enormous. We
|
|
// should denormalize the object type, or move the data into a separate
|
|
// table, and then filter this earlier (and thus raise a better error).
|
|
// For now, just block access so we don't violate policies.
|
|
throw new Exception(
|
|
pht('This transcript has an invalid or inaccessible adapter.'));
|
|
}
|
|
|
|
$this->adapter = HeraldAdapter::getAdapterForContentType($object_type);
|
|
|
|
$filter = $this->getFilterPHIDs();
|
|
$this->filterTranscript($xscript, $filter);
|
|
$phids = array_merge($filter, $this->getTranscriptPHIDs($xscript));
|
|
$phids = array_unique($phids);
|
|
$phids = array_filter($phids);
|
|
|
|
$handles = $this->loadViewerHandles($phids);
|
|
$this->handles = $handles;
|
|
|
|
if ($xscript->getDryRun()) {
|
|
$notice = new AphrontErrorView();
|
|
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
|
|
$notice->setTitle(pht('Dry Run'));
|
|
$notice->appendChild(pht('This was a dry run to test Herald '.
|
|
'rules, no actions were executed.'));
|
|
$nav->appendChild($notice);
|
|
}
|
|
|
|
$warning_panel = $this->buildWarningPanel($xscript);
|
|
$nav->appendChild($warning_panel);
|
|
|
|
$apply_xscript_panel = $this->buildApplyTranscriptPanel(
|
|
$xscript);
|
|
$nav->appendChild($apply_xscript_panel);
|
|
|
|
$action_xscript_panel = $this->buildActionTranscriptPanel(
|
|
$xscript);
|
|
$nav->appendChild($action_xscript_panel);
|
|
|
|
$object_xscript_panel = $this->buildObjectTranscriptPanel(
|
|
$xscript);
|
|
$nav->appendChild($object_xscript_panel);
|
|
}
|
|
|
|
$crumbs = id($this->buildApplicationCrumbs())
|
|
->addTextCrumb(
|
|
pht('Transcripts'),
|
|
$this->getApplicationURI('/transcript/'))
|
|
->addTextCrumb($xscript->getID());
|
|
$nav->setCrumbs($crumbs);
|
|
|
|
return $this->buildApplicationPage(
|
|
$nav,
|
|
array(
|
|
'title' => pht('Transcript'),
|
|
));
|
|
}
|
|
|
|
protected function renderConditionTestValue($condition, $handles) {
|
|
switch ($condition->getFieldName()) {
|
|
case HeraldAdapter::FIELD_RULE:
|
|
$value = array($condition->getTestValue());
|
|
break;
|
|
default:
|
|
$value = $condition->getTestValue();
|
|
break;
|
|
}
|
|
|
|
if (!is_scalar($value) && $value !== null) {
|
|
foreach ($value as $key => $phid) {
|
|
$handle = idx($handles, $phid);
|
|
if ($handle) {
|
|
$value[$key] = $handle->getName();
|
|
} else {
|
|
// This shouldn't ever really happen as we are supposed to have
|
|
// grabbed handles for everything, but be super liberal in what
|
|
// we accept here since we expect all sorts of weird issues as we
|
|
// version the system.
|
|
$value[$key] = 'Unknown Object #'.$phid;
|
|
}
|
|
}
|
|
sort($value);
|
|
$value = implode(', ', $value);
|
|
}
|
|
|
|
return phutil_tag('span', array('class' => 'condition-test-value'), $value);
|
|
}
|
|
|
|
private function buildSideNav() {
|
|
$nav = new AphrontSideNavFilterView();
|
|
$nav->setBaseURI(new PhutilURI('/herald/transcript/'.$this->id.'/'));
|
|
|
|
$items = array();
|
|
$filters = $this->getFilterMap();
|
|
foreach ($filters as $key => $name) {
|
|
$nav->addFilter($key, $name);
|
|
}
|
|
$nav->selectFilter($this->filter, null);
|
|
|
|
return $nav;
|
|
}
|
|
|
|
protected function getFilterMap() {
|
|
return array(
|
|
self::FILTER_ALL => pht('All Rules'),
|
|
self::FILTER_OWNED => pht('Rules I Own'),
|
|
self::FILTER_AFFECTED => pht('Rules that Affected Me'),
|
|
);
|
|
}
|
|
|
|
|
|
protected function getFilterPHIDs() {
|
|
return array($this->getRequest()->getUser()->getPHID());
|
|
}
|
|
|
|
protected function getTranscriptPHIDs($xscript) {
|
|
$phids = array();
|
|
|
|
$object_xscript = $xscript->getObjectTranscript();
|
|
if (!$object_xscript) {
|
|
return array();
|
|
}
|
|
|
|
$phids[] = $object_xscript->getPHID();
|
|
|
|
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
|
|
// TODO: This is total hacks. Add another amazing layer of abstraction.
|
|
$target = (array)$apply_xscript->getTarget();
|
|
foreach ($target as $phid) {
|
|
if ($phid) {
|
|
$phids[] = $phid;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($xscript->getRuleTranscripts() as $rule_xscript) {
|
|
$phids[] = $rule_xscript->getRuleOwner();
|
|
}
|
|
|
|
$condition_xscripts = $xscript->getConditionTranscripts();
|
|
if ($condition_xscripts) {
|
|
$condition_xscripts = call_user_func_array(
|
|
'array_merge',
|
|
$condition_xscripts);
|
|
}
|
|
foreach ($condition_xscripts as $condition_xscript) {
|
|
switch ($condition_xscript->getFieldName()) {
|
|
case HeraldAdapter::FIELD_RULE:
|
|
$phids[] = $condition_xscript->getTestValue();
|
|
break;
|
|
default:
|
|
$value = $condition_xscript->getTestValue();
|
|
// TODO: Also total hacks.
|
|
if (is_array($value)) {
|
|
foreach ($value as $phid) {
|
|
if ($phid) { // TODO: Probably need to make sure this
|
|
// "looks like" a PHID or decrease the level of hacks here;
|
|
// this used to be an is_numeric() check in Facebook land.
|
|
$phids[] = $phid;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $phids;
|
|
}
|
|
|
|
protected function filterTranscript($xscript, $filter_phids) {
|
|
$filter_owned = ($this->filter == self::FILTER_OWNED);
|
|
$filter_affected = ($this->filter == self::FILTER_AFFECTED);
|
|
|
|
if (!$filter_owned && !$filter_affected) {
|
|
// No filtering to be done.
|
|
return;
|
|
}
|
|
|
|
if (!$xscript->getObjectTranscript()) {
|
|
return;
|
|
}
|
|
|
|
$user_phid = $this->getRequest()->getUser()->getPHID();
|
|
|
|
$keep_apply_xscripts = array();
|
|
$keep_rule_xscripts = array();
|
|
|
|
$filter_phids = array_fill_keys($filter_phids, true);
|
|
|
|
$rule_xscripts = $xscript->getRuleTranscripts();
|
|
foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) {
|
|
$rule_id = $apply_xscript->getRuleID();
|
|
if ($filter_owned) {
|
|
if (empty($rule_xscripts[$rule_id])) {
|
|
// No associated rule so you can't own this effect.
|
|
continue;
|
|
}
|
|
if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) {
|
|
continue;
|
|
}
|
|
} else if ($filter_affected) {
|
|
$targets = (array)$apply_xscript->getTarget();
|
|
if (!array_select_keys($filter_phids, $targets)) {
|
|
continue;
|
|
}
|
|
}
|
|
$keep_apply_xscripts[$id] = true;
|
|
if ($rule_id) {
|
|
$keep_rule_xscripts[$rule_id] = true;
|
|
}
|
|
}
|
|
|
|
foreach ($rule_xscripts as $rule_id => $rule_xscript) {
|
|
if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) {
|
|
$keep_rule_xscripts[$rule_id] = true;
|
|
}
|
|
}
|
|
|
|
$xscript->setRuleTranscripts(
|
|
array_intersect_key(
|
|
$xscript->getRuleTranscripts(),
|
|
$keep_rule_xscripts));
|
|
|
|
$xscript->setApplyTranscripts(
|
|
array_intersect_key(
|
|
$xscript->getApplyTranscripts(),
|
|
$keep_apply_xscripts));
|
|
|
|
$xscript->setConditionTranscripts(
|
|
array_intersect_key(
|
|
$xscript->getConditionTranscripts(),
|
|
$keep_rule_xscripts));
|
|
}
|
|
|
|
private function buildWarningPanel(HeraldTranscript $xscript) {
|
|
$request = $this->getRequest();
|
|
$panel = null;
|
|
if ($xscript->getObjectTranscript()) {
|
|
$handles = $this->handles;
|
|
$object_xscript = $xscript->getObjectTranscript();
|
|
$handle = $handles[$object_xscript->getPHID()];
|
|
if ($handle->getType() ==
|
|
PhabricatorRepositoryPHIDTypeCommit::TYPECONST) {
|
|
$commit = id(new DiffusionCommitQuery())
|
|
->setViewer($request->getUser())
|
|
->withPHIDs(array($handle->getPHID()))
|
|
->executeOne();
|
|
if ($commit) {
|
|
$repository = $commit->getRepository();
|
|
if ($repository->isImporting()) {
|
|
$title = pht(
|
|
'The %s repository is still importing.',
|
|
$repository->getMonogram());
|
|
$body = pht(
|
|
'Herald rules will not trigger until import completes.');
|
|
} else if (!$repository->isTracked()) {
|
|
$title = pht(
|
|
'The %s repository is not tracked.',
|
|
$repository->getMonogram());
|
|
$body = pht(
|
|
'Herald rules will not trigger until tracking is enabled.');
|
|
} else {
|
|
return $panel;
|
|
}
|
|
$panel = id(new AphrontErrorView())
|
|
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
|
|
->setTitle($title)
|
|
->appendChild($body);
|
|
}
|
|
}
|
|
}
|
|
return $panel;
|
|
}
|
|
|
|
private function buildApplyTranscriptPanel(HeraldTranscript $xscript) {
|
|
$handles = $this->handles;
|
|
$adapter = $this->getAdapter();
|
|
|
|
$rule_type_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
|
|
$action_names = $adapter->getActionNameMap($rule_type_global);
|
|
|
|
$list = new PHUIObjectItemListView();
|
|
$list->setStates(true);
|
|
$list->setNoDataString(pht('No actions were taken.'));
|
|
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
|
|
|
|
$target = $apply_xscript->getTarget();
|
|
switch ($apply_xscript->getAction()) {
|
|
case HeraldAdapter::ACTION_NOTHING:
|
|
$target = null;
|
|
break;
|
|
case HeraldAdapter::ACTION_FLAG:
|
|
$target = PhabricatorFlagColor::getColorName($target);
|
|
break;
|
|
case HeraldAdapter::ACTION_BLOCK:
|
|
// Target is a text string.
|
|
$target = $target;
|
|
break;
|
|
default:
|
|
if (is_array($target) && $target) {
|
|
foreach ($target as $k => $phid) {
|
|
if (isset($handles[$phid])) {
|
|
$target[$k] = $handles[$phid]->getName();
|
|
}
|
|
}
|
|
$target = implode(', ', $target);
|
|
} else if (is_string($target)) {
|
|
$target = $target;
|
|
} else {
|
|
$target = '<empty>';
|
|
}
|
|
break;
|
|
}
|
|
|
|
$item = new PHUIObjectItemView();
|
|
|
|
if ($apply_xscript->getApplied()) {
|
|
$item->setState(PHUIObjectItemView::STATE_SUCCESS);
|
|
} else {
|
|
$item->setState(PHUIObjectItemView::STATE_FAIL);
|
|
}
|
|
|
|
$rule = idx($action_names, $apply_xscript->getAction(), pht('Unknown'));
|
|
|
|
$item->setHeader(pht('%s: %s', $rule, $target));
|
|
$item->addAttribute($apply_xscript->getReason());
|
|
$item->addAttribute(
|
|
pht('Outcome: %s', $apply_xscript->getAppliedReason()));
|
|
|
|
$list->addItem($item);
|
|
}
|
|
|
|
$box = new PHUIObjectBoxView();
|
|
$box->setHeaderText(pht('Actions Taken'));
|
|
$box->appendChild($list);
|
|
|
|
return $box;
|
|
}
|
|
|
|
private function buildActionTranscriptPanel(HeraldTranscript $xscript) {
|
|
$action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID');
|
|
|
|
$adapter = $this->getAdapter();
|
|
|
|
|
|
$field_names = $adapter->getFieldNameMap();
|
|
$condition_names = $adapter->getConditionNameMap();
|
|
|
|
$handles = $this->handles;
|
|
|
|
$rule_markup = array();
|
|
foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) {
|
|
$cond_markup = array();
|
|
foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) {
|
|
if ($cond->getNote()) {
|
|
$note = phutil_tag_div('herald-condition-note', $cond->getNote());
|
|
} else {
|
|
$note = null;
|
|
}
|
|
|
|
if ($cond->getResult()) {
|
|
$result = phutil_tag(
|
|
'span',
|
|
array('class' => 'herald-outcome condition-pass'),
|
|
"\xE2\x9C\x93");
|
|
} else {
|
|
$result = phutil_tag(
|
|
'span',
|
|
array('class' => 'herald-outcome condition-fail'),
|
|
"\xE2\x9C\x98");
|
|
}
|
|
|
|
$cond_markup[] = phutil_tag(
|
|
'li',
|
|
array(),
|
|
pht(
|
|
'%s Condition: %s %s %s%s',
|
|
$result,
|
|
idx($field_names, $cond->getFieldName(), pht('Unknown')),
|
|
idx($condition_names, $cond->getCondition(), pht('Unknown')),
|
|
$this->renderConditionTestValue($cond, $handles),
|
|
$note));
|
|
}
|
|
|
|
if ($rule->getResult()) {
|
|
$result = phutil_tag(
|
|
'span',
|
|
array('class' => 'herald-outcome rule-pass'),
|
|
pht('PASS'));
|
|
$class = 'herald-rule-pass';
|
|
} else {
|
|
$result = phutil_tag(
|
|
'span',
|
|
array('class' => 'herald-outcome rule-fail'),
|
|
pht('FAIL'));
|
|
$class = 'herald-rule-fail';
|
|
}
|
|
|
|
$cond_markup[] = phutil_tag(
|
|
'li',
|
|
array(),
|
|
array($result, $rule->getReason()));
|
|
$user_phid = $this->getRequest()->getUser()->getPHID();
|
|
|
|
$name = $rule->getRuleName();
|
|
|
|
$rule_markup[] =
|
|
phutil_tag(
|
|
'li',
|
|
array(
|
|
'class' => $class,
|
|
),
|
|
phutil_tag_div('rule-name', array(
|
|
phutil_tag('strong', array(), $name),
|
|
' ',
|
|
phutil_tag('ul', array(), $cond_markup),
|
|
)));
|
|
}
|
|
|
|
$box = null;
|
|
if ($rule_markup) {
|
|
$box = new PHUIObjectBoxView();
|
|
$box->setHeaderText(pht('Rule Details'));
|
|
$box->appendChild(phutil_tag(
|
|
'ul',
|
|
array('class' => 'herald-explain-list'),
|
|
$rule_markup));
|
|
}
|
|
return $box;
|
|
}
|
|
|
|
private function buildObjectTranscriptPanel(HeraldTranscript $xscript) {
|
|
|
|
$adapter = $this->getAdapter();
|
|
$field_names = $adapter->getFieldNameMap();
|
|
|
|
$object_xscript = $xscript->getObjectTranscript();
|
|
|
|
$data = array();
|
|
if ($object_xscript) {
|
|
$phid = $object_xscript->getPHID();
|
|
$handles = $this->handles;
|
|
|
|
$data += array(
|
|
pht('Object Name') => $object_xscript->getName(),
|
|
pht('Object Type') => $object_xscript->getType(),
|
|
pht('Object PHID') => $phid,
|
|
pht('Object Link') => $handles[$phid]->renderLink(),
|
|
);
|
|
}
|
|
|
|
$data += $xscript->getMetadataMap();
|
|
|
|
if ($object_xscript) {
|
|
foreach ($object_xscript->getFields() as $field => $value) {
|
|
$field = idx($field_names, $field, '['.$field.'?]');
|
|
$data['Field: '.$field] = $value;
|
|
}
|
|
}
|
|
|
|
$rows = array();
|
|
foreach ($data as $name => $value) {
|
|
if (!($value instanceof PhutilSafeHTML)) {
|
|
if (!is_scalar($value) && !is_null($value)) {
|
|
$value = implode("\n", $value);
|
|
}
|
|
|
|
if (strlen($value) > 256) {
|
|
$value = phutil_tag(
|
|
'textarea',
|
|
array(
|
|
'class' => 'herald-field-value-transcript',
|
|
),
|
|
$value);
|
|
}
|
|
}
|
|
|
|
$rows[] = array($name, $value);
|
|
}
|
|
|
|
$property_list = new PHUIPropertyListView();
|
|
$property_list->setStacked(true);
|
|
foreach ($rows as $row) {
|
|
$property_list->addProperty($row[0], $row[1]);
|
|
}
|
|
|
|
$box = new PHUIObjectBoxView();
|
|
$box->setHeaderText(pht('Object Transcript'));
|
|
$box->appendChild($property_list);
|
|
|
|
return $box;
|
|
}
|
|
|
|
|
|
}
|