Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index 300af2c383..cd48abe79a 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1657 +1,1669 @@
<?php
/**
* @task customfield Custom Field Integration
*/
abstract class HeraldAdapter {
const FIELD_TITLE = 'title';
const FIELD_BODY = 'body';
const FIELD_AUTHOR = 'author';
const FIELD_ASSIGNEE = 'assignee';
const FIELD_REVIEWER = 'reviewer';
const FIELD_REVIEWERS = 'reviewers';
const FIELD_COMMITTER = 'committer';
const FIELD_CC = 'cc';
const FIELD_TAGS = 'tags';
const FIELD_DIFF_FILE = 'diff-file';
const FIELD_DIFF_CONTENT = 'diff-content';
const FIELD_DIFF_ADDED_CONTENT = 'diff-added-content';
const FIELD_DIFF_REMOVED_CONTENT = 'diff-removed-content';
const FIELD_DIFF_ENORMOUS = 'diff-enormous';
const FIELD_REPOSITORY = 'repository';
const FIELD_REPOSITORY_PROJECTS = 'repository-projects';
const FIELD_RULE = 'rule';
const FIELD_AFFECTED_PACKAGE = 'affected-package';
const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
const FIELD_CONTENT_SOURCE = 'contentsource';
const FIELD_ALWAYS = 'always';
const FIELD_AUTHOR_PROJECTS = 'authorprojects';
const FIELD_PROJECTS = 'projects';
const FIELD_PUSHER = 'pusher';
const FIELD_PUSHER_PROJECTS = 'pusher-projects';
const FIELD_DIFFERENTIAL_REVISION = 'differential-revision';
const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
const FIELD_DIFFERENTIAL_CCS = 'differential-ccs';
const FIELD_DIFFERENTIAL_ACCEPTED = 'differential-accepted';
const FIELD_IS_MERGE_COMMIT = 'is-merge-commit';
const FIELD_BRANCHES = 'branches';
const FIELD_AUTHOR_RAW = 'author-raw';
const FIELD_COMMITTER_RAW = 'committer-raw';
const FIELD_IS_NEW_OBJECT = 'new-object';
const FIELD_APPLICATION_EMAIL = 'applicaton-email';
const FIELD_TASK_PRIORITY = 'taskpriority';
const FIELD_TASK_STATUS = 'taskstatus';
const FIELD_ARCANIST_PROJECT = 'arcanist-project';
const FIELD_PUSHER_IS_COMMITTER = 'pusher-is-committer';
const FIELD_PATH = 'path';
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_UNCONDITIONALLY = 'unconditionally';
const CONDITION_NEVER = 'never';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
const CONDITION_HAS_BIT = 'bit';
const CONDITION_NOT_BIT = '!bit';
const CONDITION_IS_TRUE = 'true';
const CONDITION_IS_FALSE = 'false';
const ACTION_ADD_CC = 'addcc';
const ACTION_REMOVE_CC = 'remcc';
const ACTION_EMAIL = 'email';
const ACTION_NOTHING = 'nothing';
const ACTION_AUDIT = 'audit';
const ACTION_FLAG = 'flag';
const ACTION_ASSIGN_TASK = 'assigntask';
const ACTION_ADD_PROJECTS = 'addprojects';
const ACTION_REMOVE_PROJECTS = 'removeprojects';
const ACTION_ADD_REVIEWERS = 'addreviewers';
const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
const ACTION_BLOCK = 'block';
const ACTION_REQUIRE_SIGNATURE = 'signature';
const VALUE_TEXT = 'text';
const VALUE_NONE = 'none';
const VALUE_EMAIL = 'email';
const VALUE_USER = 'user';
const VALUE_TAG = 'tag';
const VALUE_RULE = 'rule';
const VALUE_REPOSITORY = 'repository';
const VALUE_OWNERS_PACKAGE = 'package';
const VALUE_PROJECT = 'project';
const VALUE_FLAG_COLOR = 'flagcolor';
const VALUE_CONTENT_SOURCE = 'contentsource';
const VALUE_USER_OR_PROJECT = 'userorproject';
const VALUE_BUILD_PLAN = 'buildplan';
const VALUE_TASK_PRIORITY = 'taskpriority';
const VALUE_TASK_STATUS = 'taskstatus';
const VALUE_ARCANIST_PROJECT = 'arcanistprojects';
const VALUE_LEGAL_DOCUMENTS = 'legaldocuments';
const VALUE_APPLICATION_EMAIL = 'applicationemail';
private $contentSource;
private $isNewObject;
private $applicationEmail;
private $customFields = false;
private $customActions = null;
private $queuedTransactions = array();
private $emailPHIDs = array();
private $forcedEmailPHIDs = array();
public function getEmailPHIDs() {
return array_values($this->emailPHIDs);
}
public function getForcedEmailPHIDs() {
return array_values($this->forcedEmailPHIDs);
}
public function getCustomActions() {
if ($this->customActions === null) {
$custom_actions = id(new PhutilSymbolLoader())
->setAncestorClass('HeraldCustomAction')
->loadObjects();
foreach ($custom_actions as $key => $object) {
if (!$object->appliesToAdapter($this)) {
unset($custom_actions[$key]);
}
}
$this->customActions = array();
foreach ($custom_actions as $action) {
$key = $action->getActionKey();
if (array_key_exists($key, $this->customActions)) {
throw new Exception(
pht(
"More than one Herald custom action implementation ".
"handles the action key: '%s'.",
$key));
}
$this->customActions[$key] = $action;
}
}
return $this->customActions;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function getIsNewObject() {
if (is_bool($this->isNewObject)) {
return $this->isNewObject;
}
throw new Exception(
pht(
'You must %s to a boolean first!',
'setIsNewObject()'));
}
public function setIsNewObject($new) {
$this->isNewObject = (bool)$new;
return $this;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
abstract public function getPHID();
abstract public function getHeraldName();
public function getHeraldField($field_name) {
switch ($field_name) {
case self::FIELD_RULE:
return null;
case self::FIELD_CONTENT_SOURCE:
return $this->getContentSource()->getSource();
case self::FIELD_ALWAYS:
return true;
case self::FIELD_IS_NEW_OBJECT:
return $this->getIsNewObject();
case self::FIELD_APPLICATION_EMAIL:
$value = array();
// while there is only one match by implementation, we do set
// comparisons on phids, so return an array with just the phid
if ($this->getApplicationEmail()) {
$value[] = $this->getApplicationEmail()->getPHID();
}
return $value;
default:
if ($this->isHeraldCustomKey($field_name)) {
return $this->getCustomFieldValue($field_name);
}
throw new Exception(pht("Unknown field '%s'!", $field_name));
}
}
abstract public function applyHeraldEffects(array $effects);
protected function handleCustomHeraldEffect(HeraldEffect $effect) {
$custom_action = idx($this->getCustomActions(), $effect->getAction());
if ($custom_action !== null) {
return $custom_action->applyEffect(
$this,
$this->getObject(),
$effect);
}
return null;
}
public function isAvailableToUser(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array($this->getAdapterApplicationClass()))
->execute();
return !empty($applications);
}
public function queueTransaction($transaction) {
$this->queuedTransactions[] = $transaction;
}
public function getQueuedTransactions() {
return $this->queuedTransactions;
}
protected function newTransaction() {
$object = $this->newObject();
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Unable to build a new transaction for adapter object; it does '.
'not implement "%s".',
'PhabricatorApplicationTransactionInterface'));
}
return $object->getApplicationTransactionTemplate();
}
/**
* NOTE: You generally should not override this; it exists to support legacy
* adapters which had hard-coded content types.
*/
public function getAdapterContentType() {
return get_class($this);
}
abstract public function getAdapterContentName();
abstract public function getAdapterContentDescription();
abstract public function getAdapterApplicationClass();
abstract public function getObject();
/**
* Return a new characteristic object for this adapter.
*
* The adapter will use this object to test for interfaces, generate
* transactions, and interact with custom fields.
*
* Adapters must return an object from this method to enable custom
* field rules and various implicit actions.
*
* Normally, you'll return an empty version of the adapted object:
*
* return new ApplicationObject();
*
* @return null|object Template object.
*/
protected function newObject() {
return null;
}
public function supportsRuleType($rule_type) {
return false;
}
public function canTriggerOnObject($object) {
return false;
}
public function explainValidTriggerObjects() {
return pht('This adapter can not trigger on objects.');
}
public function getTriggerObjectPHIDs() {
return array($this->getPHID());
}
public function getAdapterSortKey() {
return sprintf(
'%08d%s',
$this->getAdapterSortOrder(),
$this->getAdapterContentName());
}
public function getAdapterSortOrder() {
return 1000;
}
/* -( Fields )------------------------------------------------------------- */
public function getFields() {
$fields = array();
$fields[] = self::FIELD_ALWAYS;
$fields[] = self::FIELD_RULE;
$custom_fields = $this->getCustomFields();
if ($custom_fields) {
foreach ($custom_fields->getFields() as $custom_field) {
$key = $custom_field->getFieldKey();
$fields[] = $this->getHeraldKeyFromCustomKey($key);
}
}
return $fields;
}
public function getFieldNameMap() {
return array(
self::FIELD_TITLE => pht('Title'),
self::FIELD_BODY => pht('Body'),
self::FIELD_AUTHOR => pht('Author'),
self::FIELD_ASSIGNEE => pht('Assignee'),
self::FIELD_COMMITTER => pht('Committer'),
self::FIELD_REVIEWER => pht('Reviewer'),
self::FIELD_REVIEWERS => pht('Reviewers'),
self::FIELD_CC => pht('CCs'),
self::FIELD_TAGS => pht('Tags'),
self::FIELD_DIFF_FILE => pht('Any changed filename'),
self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
self::FIELD_REPOSITORY => pht('Repository'),
self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
self::FIELD_RULE => pht('Another Herald rule'),
self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
self::FIELD_AFFECTED_PACKAGE_OWNER =>
pht("Any affected package's owner"),
self::FIELD_CONTENT_SOURCE => pht('Content Source'),
self::FIELD_ALWAYS => pht('Always'),
self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
self::FIELD_PROJECTS => pht('Projects'),
self::FIELD_PUSHER => pht('Pusher'),
self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
self::FIELD_DIFFERENTIAL_ACCEPTED
=> pht('Accepted Differential revision'),
self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
self::FIELD_BRANCHES => pht('Commit\'s branches'),
self::FIELD_AUTHOR_RAW => pht('Raw author name'),
self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
self::FIELD_APPLICATION_EMAIL => pht('Receiving email address'),
self::FIELD_TASK_PRIORITY => pht('Task priority'),
self::FIELD_TASK_STATUS => pht('Task status'),
self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'),
self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
self::FIELD_PATH => pht('Path'),
) + $this->getCustomFieldNameMap();
}
/* -( Conditions )--------------------------------------------------------- */
public function getConditionNameMap() {
return array(
self::CONDITION_CONTAINS => pht('contains'),
self::CONDITION_NOT_CONTAINS => pht('does not contain'),
self::CONDITION_IS => pht('is'),
self::CONDITION_IS_NOT => pht('is not'),
self::CONDITION_IS_ANY => pht('is any of'),
self::CONDITION_IS_TRUE => pht('is true'),
self::CONDITION_IS_FALSE => pht('is false'),
self::CONDITION_IS_NOT_ANY => pht('is not any of'),
self::CONDITION_INCLUDE_ALL => pht('include all of'),
self::CONDITION_INCLUDE_ANY => pht('include any of'),
self::CONDITION_INCLUDE_NONE => pht('do not include'),
self::CONDITION_IS_ME => pht('is myself'),
self::CONDITION_IS_NOT_ME => pht('is not myself'),
self::CONDITION_REGEXP => pht('matches regexp'),
self::CONDITION_RULE => pht('matches:'),
self::CONDITION_NOT_RULE => pht('does not match:'),
self::CONDITION_EXISTS => pht('exists'),
self::CONDITION_NOT_EXISTS => pht('does not exist'),
self::CONDITION_UNCONDITIONALLY => '', // don't show anything!
self::CONDITION_NEVER => '', // don't show anything!
self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),
self::CONDITION_HAS_BIT => pht('has bit'),
self::CONDITION_NOT_BIT => pht('lacks bit'),
);
}
public function getConditionsForField($field) {
switch ($field) {
case self::FIELD_TITLE:
case self::FIELD_BODY:
case self::FIELD_COMMITTER_RAW:
case self::FIELD_AUTHOR_RAW:
case self::FIELD_PATH:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_NOT_CONTAINS,
self::CONDITION_IS,
self::CONDITION_IS_NOT,
self::CONDITION_REGEXP,
);
case self::FIELD_REVIEWER:
case self::FIELD_PUSHER:
case self::FIELD_TASK_PRIORITY:
case self::FIELD_TASK_STATUS:
case self::FIELD_ARCANIST_PROJECT:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
);
case self::FIELD_REPOSITORY:
case self::FIELD_ASSIGNEE:
case self::FIELD_AUTHOR:
case self::FIELD_COMMITTER:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_TAGS:
case self::FIELD_REVIEWERS:
case self::FIELD_CC:
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PROJECTS:
case self::FIELD_AFFECTED_PACKAGE:
case self::FIELD_AFFECTED_PACKAGE_OWNER:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_REPOSITORY_PROJECTS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_APPLICATION_EMAIL:
return array(
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_DIFF_FILE:
case self::FIELD_BRANCHES:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
);
case self::FIELD_DIFF_CONTENT:
case self::FIELD_DIFF_ADDED_CONTENT:
case self::FIELD_DIFF_REMOVED_CONTENT:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
self::CONDITION_REGEXP_PAIR,
);
case self::FIELD_RULE:
return array(
self::CONDITION_RULE,
self::CONDITION_NOT_RULE,
);
case self::FIELD_CONTENT_SOURCE:
return array(
self::CONDITION_IS,
self::CONDITION_IS_NOT,
);
case self::FIELD_ALWAYS:
return array(
self::CONDITION_UNCONDITIONALLY,
);
case self::FIELD_DIFFERENTIAL_REVIEWERS:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_CCS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_REVISION:
case self::FIELD_DIFFERENTIAL_ACCEPTED:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_IS_MERGE_COMMIT:
case self::FIELD_DIFF_ENORMOUS:
case self::FIELD_IS_NEW_OBJECT:
case self::FIELD_PUSHER_IS_COMMITTER:
return array(
self::CONDITION_IS_TRUE,
self::CONDITION_IS_FALSE,
);
default:
if ($this->isHeraldCustomKey($field)) {
return $this->getCustomFieldConditions($field);
}
throw new Exception(
pht(
"This adapter does not define conditions for field '%s'!",
$field));
}
}
public function doesConditionMatch(
HeraldEngine $engine,
HeraldRule $rule,
HeraldCondition $condition,
$field_value) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_CONTAINS:
// "Contains" can take an array of strings, as in "Any changed
// filename" for diffs.
foreach ((array)$field_value as $value) {
if (stripos($value, $condition_value) !== false) {
return true;
}
}
return false;
case self::CONDITION_NOT_CONTAINS:
return (stripos($field_value, $condition_value) === false);
case self::CONDITION_IS:
return ($field_value == $condition_value);
case self::CONDITION_IS_NOT:
return ($field_value != $condition_value);
case self::CONDITION_IS_ME:
return ($field_value == $rule->getAuthorPHID());
case self::CONDITION_IS_NOT_ME:
return ($field_value != $rule->getAuthorPHID());
case self::CONDITION_IS_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return isset($condition_value[$field_value]);
case self::CONDITION_IS_NOT_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return !isset($condition_value[$field_value]);
case self::CONDITION_INCLUDE_ALL:
if (!is_array($field_value)) {
throw new HeraldInvalidConditionException(
pht('Object produced non-array value!'));
}
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$have = array_select_keys(array_fuse($field_value), $condition_value);
return (count($have) == count($condition_value));
case self::CONDITION_INCLUDE_ANY:
return (bool)array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_INCLUDE_NONE:
return !array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_EXISTS:
case self::CONDITION_IS_TRUE:
return (bool)$field_value;
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_IS_FALSE:
return !$field_value;
case self::CONDITION_UNCONDITIONALLY:
return (bool)$field_value;
case self::CONDITION_NEVER:
return false;
case self::CONDITION_REGEXP:
foreach ((array)$field_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($condition_value.'S', $value);
if ($result === false) {
throw new HeraldInvalidConditionException(
pht('Regular expression is not valid!'));
}
if ($result) {
return true;
}
}
return false;
case self::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = null;
try {
$regexp_pair = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not valid JSON!'));
}
if (count($regexp_pair) != 2) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not a pair!'));
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$field_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
throw new HeraldInvalidConditionException(
pht('First regular expression is invalid!'));
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
throw new HeraldInvalidConditionException(
pht('Second regular expression is invalid!'));
}
if ($value_matches) {
return true;
}
}
}
return false;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
$rule = $engine->getRule($condition_value);
if (!$rule) {
throw new HeraldInvalidConditionException(
pht('Condition references a rule which does not exist!'));
}
$is_not = ($condition_type == self::CONDITION_NOT_RULE);
$result = $engine->doesRuleMatch($rule, $this);
if ($is_not) {
$result = !$result;
}
return $result;
case self::CONDITION_HAS_BIT:
return (($condition_value & $field_value) === (int)$condition_value);
case self::CONDITION_NOT_BIT:
return (($condition_value & $field_value) !== (int)$condition_value);
default:
throw new HeraldInvalidConditionException(
pht("Unknown condition '%s'.", $condition_type));
}
}
public function willSaveCondition(HeraldCondition $condition) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_REGEXP:
$ok = @preg_match($condition_value, '');
if ($ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression "%s" is not valid. Regular expressions '.
'must have enclosing characters (e.g. "@/path/to/file@", not '.
'"/path/to/file") and be syntactically correct.',
$condition_value));
}
break;
case self::CONDITION_REGEXP_PAIR:
$json = null;
try {
$json = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" is not valid JSON. Enter a '.
'valid JSON array with two elements.',
$condition_value));
}
if (count($json) != 2) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" must have exactly two '.
'elements.',
$condition_value));
}
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
$key_ok = @preg_match($key_regexp, '');
if ($key_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The first regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$key_regexp));
}
$val_ok = @preg_match($val_regexp, '');
if ($val_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The second regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$val_regexp));
}
break;
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_HAS_BIT:
case self::CONDITION_NOT_BIT:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
// No explicit validation for these types, although there probably
// should be in some cases.
break;
default:
throw new HeraldInvalidConditionException(
pht(
'Unknown condition "%s"!',
$condition_type));
}
}
/* -( Actions )------------------------------------------------------------ */
public function getCustomActionsForRuleType($rule_type) {
$results = array();
foreach ($this->getCustomActions() as $custom_action) {
if ($custom_action->appliesToRuleType($rule_type)) {
$results[] = $custom_action;
}
}
return $results;
}
public function getActions($rule_type) {
$custom_actions = $this->getCustomActionsForRuleType($rule_type);
$custom_actions = mpull($custom_actions, 'getActionKey');
$actions = $custom_actions;
$object = $this->newObject();
if (($object instanceof PhabricatorProjectInterface)) {
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
$actions[] = self::ACTION_ADD_PROJECTS;
$actions[] = self::ACTION_REMOVE_PROJECTS;
}
}
return $actions;
}
public function getActionNameMap($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$standard = array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add emails to CC'),
self::ACTION_REMOVE_CC => pht('Remove emails from CC'),
self::ACTION_EMAIL => pht('Send an email to'),
self::ACTION_AUDIT => pht('Trigger an Audit by'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to'),
self::ACTION_ADD_PROJECTS => pht('Add projects'),
self::ACTION_REMOVE_PROJECTS => pht('Remove projects'),
self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'),
self::ACTION_BLOCK => pht('Block change with message'),
);
break;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$standard = array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add me to CC'),
self::ACTION_REMOVE_CC => pht('Remove me from CC'),
self::ACTION_EMAIL => pht('Send me an email'),
self::ACTION_AUDIT => pht('Trigger an Audit by me'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to me'),
self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
self::ACTION_ADD_BLOCKING_REVIEWERS =>
pht('Add me as a blocking reviewer'),
);
break;
default:
throw new Exception(pht("Unknown rule type '%s'!", $rule_type));
}
$custom_actions = $this->getCustomActionsForRuleType($rule_type);
$standard += mpull($custom_actions, 'getActionName', 'getActionKey');
return $standard;
}
public function willSaveAction(
HeraldRule $rule,
HeraldAction $action) {
$target = $action->getTarget();
if (is_array($target)) {
$target = array_keys($target);
}
$author_phid = $rule->getAuthorPHID();
$rule_type = $rule->getRuleType();
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
switch ($action->getAction()) {
case self::ACTION_EMAIL:
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
// For personal rules, force these actions to target the rule owner.
$target = array($author_phid);
break;
case self::ACTION_FLAG:
// Make sure flag color is valid; set to blue if not.
$color_map = PhabricatorFlagColor::getColorNameMap();
if (empty($color_map[$target])) {
$target = PhabricatorFlagColor::COLOR_BLUE;
}
break;
case self::ACTION_BLOCK:
case self::ACTION_NOTHING:
break;
default:
throw new HeraldInvalidActionException(
pht(
'Unrecognized action type "%s"!',
$action->getAction()));
}
}
$action->setTarget($target);
}
/* -( Values )------------------------------------------------------------- */
public function getValueTypeForFieldAndCondition($field, $condition) {
if ($this->isHeraldCustomKey($field)) {
$value_type = $this->getCustomFieldValueTypeForFieldAndCondition(
$field,
$condition);
if ($value_type !== null) {
return $value_type;
}
}
switch ($condition) {
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_REGEXP:
case self::CONDITION_REGEXP_PAIR:
return self::VALUE_TEXT;
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
switch ($field) {
case self::FIELD_CONTENT_SOURCE:
return self::VALUE_CONTENT_SOURCE;
default:
return self::VALUE_TEXT;
}
break;
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case self::FIELD_TASK_PRIORITY:
return self::VALUE_TASK_PRIORITY;
case self::FIELD_TASK_STATUS:
return self::VALUE_TASK_STATUS;
case self::FIELD_ARCANIST_PROJECT:
return self::VALUE_ARCANIST_PROJECT;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case self::FIELD_CC:
return self::VALUE_EMAIL;
case self::FIELD_TAGS:
return self::VALUE_TAG;
case self::FIELD_AFFECTED_PACKAGE:
return self::VALUE_OWNERS_PACKAGE;
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_PROJECTS:
case self::FIELD_REPOSITORY_PROJECTS:
return self::VALUE_PROJECT;
case self::FIELD_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
case self::FIELD_APPLICATION_EMAIL:
return self::VALUE_APPLICATION_EMAIL;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
return self::VALUE_NONE;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
return self::VALUE_RULE;
default:
throw new Exception(pht("Unknown condition '%s'.", $condition));
}
}
public function getValueTypeForAction($action, $rule_type) {
$is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
if ($is_personal) {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
case self::ACTION_NOTHING:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_NONE;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return self::VALUE_PROJECT;
}
} else {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
return self::VALUE_EMAIL;
case self::ACTION_NOTHING:
return self::VALUE_NONE;
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return self::VALUE_PROJECT;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ASSIGN_TASK:
return self::VALUE_USER;
case self::ACTION_AUDIT:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
case self::ACTION_APPLY_BUILD_PLANS:
return self::VALUE_BUILD_PLAN;
case self::ACTION_REQUIRE_SIGNATURE:
return self::VALUE_LEGAL_DOCUMENTS;
case self::ACTION_BLOCK:
return self::VALUE_TEXT;
}
}
$custom_action = idx($this->getCustomActions(), $action);
if ($custom_action !== null) {
return $custom_action->getActionType();
}
throw new Exception(pht("Unknown or invalid action '%s'.", $action));
}
/* -( Repetition )--------------------------------------------------------- */
public function getRepetitionOptions() {
return array(
HeraldRepetitionPolicyConfig::EVERY,
);
}
public static function getAllAdapters() {
static $adapters;
if (!$adapters) {
$adapters = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$adapters = msort($adapters, 'getAdapterSortKey');
}
return $adapters;
}
public static function getAdapterForContentType($content_type) {
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if ($adapter->getAdapterContentType() == $content_type) {
return $adapter;
}
}
throw new Exception(
pht(
'No adapter exists for Herald content type "%s".',
$content_type));
}
public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
$map = array();
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$type = $adapter->getAdapterContentType();
$name = $adapter->getAdapterContentName();
$map[$type] = $name;
}
return $map;
}
public function renderRuleAsText(
HeraldRule $rule,
PhabricatorHandleList $handles) {
require_celerity_resource('herald-css');
$icon = id(new PHUIIconView())
->setIconFont('fa-chevron-circle-right lightgreytext')
->addClass('herald-list-icon');
if ($rule->getMustMatchAll()) {
$match_text = pht('When all of these conditions are met:');
} else {
$match_text = pht('When any of these conditions are met:');
}
$match_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$match_text);
$match_list = array();
foreach ($rule->getConditions() as $condition) {
$match_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderConditionAsText($condition, $handles),
));
}
$integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::EVERY);
if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
$action_text =
pht('Take these actions every time this rule matches:');
} else {
$action_text =
pht('Take these actions the first time this rule matches:');
}
$action_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$action_text);
$action_list = array();
foreach ($rule->getActions() as $action) {
$action_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderActionAsText($action, $handles),
));
}
return array(
$match_title,
$match_list,
$action_title,
$action_list,
);
}
private function renderConditionAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles) {
$field_type = $condition->getFieldName();
$default = $this->isHeraldCustomKey($field_type)
? pht('(Unknown Custom Field "%s")', $field_type)
: pht('(Unknown Field "%s")', $field_type);
$field_name = idx($this->getFieldNameMap(), $field_type, $default);
$condition_type = $condition->getFieldCondition();
$condition_name = idx($this->getConditionNameMap(), $condition_type);
$value = $this->renderConditionValueAsText($condition, $handles);
return hsprintf(' %s %s %s', $field_name, $condition_name, $value);
}
private function renderActionAsText(
HeraldAction $action,
PhabricatorHandleList $handles) {
$rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_type = $action->getAction();
- $action_name = idx($this->getActionNameMap($rule_global), $action_type);
+
+ $default = $this->isHeraldCustomKey($action_type)
+ ? pht('(Unknown Custom Action "%s") equals', $action_type)
+ : pht('(Unknown Action "%s") equals', $action_type);
+
+ $action_name = idx(
+ $this->getActionNameMap($rule_global),
+ $action_type,
+ $default);
$target = $this->renderActionTargetAsText($action, $handles);
return hsprintf(' %s %s', $action_name, $target);
}
private function renderConditionValueAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
switch ($condition->getFieldName()) {
case self::FIELD_TASK_PRIORITY:
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($value as $index => $val) {
$name = idx($priority_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
case self::FIELD_TASK_STATUS:
$status_map = ManiphestTaskStatus::getTaskStatusMap();
foreach ($value as $index => $val) {
$name = idx($status_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
$change_map =
PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
foreach ($value as $index => $val) {
$name = idx($change_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
default:
foreach ($value as $index => $val) {
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$value[$index] = $handle->renderLink();
}
}
break;
}
$value = phutil_implode_html(', ', $value);
return $value;
}
private function renderActionTargetAsText(
HeraldAction $action,
PhabricatorHandleList $handles) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $index => $val) {
switch ($action->getAction()) {
case self::ACTION_FLAG:
$target[$index] = PhabricatorFlagColor::getColorName($val);
break;
default:
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$target[$index] = $handle->renderLink();
}
break;
}
}
$target = phutil_implode_html(', ', $target);
return $target;
}
/**
* Given a @{class:HeraldRule}, this function extracts all the phids that
* we'll want to load as handles later.
*
* This function performs a somewhat hacky approach to figuring out what
* is and is not a phid - try to get the phid type and if the type is
* *not* unknown assume its a valid phid.
*
* Don't try this at home. Use more strongly typed data at home.
*
* Think of the children.
*/
public static function getHandlePHIDs(HeraldRule $rule) {
$phids = array($rule->getAuthorPHID());
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
foreach ($rule->getActions() as $action) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $phids;
}
/* -( Custom Field Integration )------------------------------------------- */
/**
* Returns the prefix used to namespace Herald fields which are based on
* custom fields.
*
* @return string Key prefix.
* @task customfield
*/
private function getCustomKeyPrefix() {
return 'herald.custom/';
}
/**
* Determine if a field key is based on a custom field or a regular internal
* field.
*
* @param string Field key.
* @return bool True if the field key is based on a custom field.
* @task customfield
*/
private function isHeraldCustomKey($key) {
$prefix = $this->getCustomKeyPrefix();
return (strncmp($key, $prefix, strlen($prefix)) == 0);
}
/**
* Convert a custom field key into a Herald field key.
*
* @param string Custom field key.
* @return string Herald field key.
* @task customfield
*/
private function getHeraldKeyFromCustomKey($key) {
return $this->getCustomKeyPrefix().$key;
}
/**
* Get custom fields for this adapter, if appliable. This will either return
* a field list or `null` if the adapted object does not implement custom
* fields or the adapter does not support them.
*
* @return PhabricatorCustomFieldList|null List of fields, or `null`.
* @task customfield
*/
private function getCustomFields() {
if ($this->customFields === false) {
$this->customFields = null;
$template_object = $this->newObject();
if ($template_object instanceof PhabricatorCustomFieldInterface) {
$object = $this->getObject();
if (!$object) {
$object = $template_object;
}
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_HERALD);
$fields->setViewer(PhabricatorUser::getOmnipotentUser());
$fields->readFieldsFromStorage($object);
$this->customFields = $fields;
}
}
return $this->customFields;
}
/**
* Get a custom field by Herald field key, or `null` if it does not exist
* or custom fields are not supported.
*
* @param string Herald field key.
* @return PhabricatorCustomField|null Matching field, if it exists.
* @task customfield
*/
private function getCustomField($herald_field_key) {
$fields = $this->getCustomFields();
if (!$fields) {
return null;
}
foreach ($fields->getFields() as $custom_field) {
$key = $custom_field->getFieldKey();
if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) {
return $custom_field;
}
}
return null;
}
/**
* Get the field map for custom fields.
*
* @return map<string, string> Map of Herald field keys to field names.
* @task customfield
*/
private function getCustomFieldNameMap() {
$fields = $this->getCustomFields();
if (!$fields) {
return array();
}
$map = array();
foreach ($fields->getFields() as $field) {
$key = $field->getFieldKey();
$name = $field->getHeraldFieldName();
$map[$this->getHeraldKeyFromCustomKey($key)] = $name;
}
return $map;
}
/**
* Get the value for a custom field.
*
* @param string Herald field key.
* @return wild Custom field value.
* @task customfield
*/
private function getCustomFieldValue($field_key) {
$field = $this->getCustomField($field_key);
if (!$field) {
return null;
}
return $field->getHeraldFieldValue();
}
/**
* Get the Herald conditions for a custom field.
*
* @param string Herald field key.
* @return list<const> List of Herald conditions.
* @task customfield
*/
private function getCustomFieldConditions($field_key) {
$field = $this->getCustomField($field_key);
if (!$field) {
return array(
self::CONDITION_NEVER,
);
}
return $field->getHeraldFieldConditions();
}
/**
* Get the Herald value type for a custom field and condition.
*
* @param string Herald field key.
* @param const Herald condition constant.
* @return const|null Herald value type constant, or null to use the default.
* @task customfield
*/
private function getCustomFieldValueTypeForFieldAndCondition(
$field_key,
$condition) {
$field = $this->getCustomField($field_key);
if (!$field) {
return self::VALUE_NONE;
}
return $field->getHeraldFieldValueType($condition);
}
/* -( Applying Effects )--------------------------------------------------- */
/**
* @task apply
*/
protected function applyStandardEffect(HeraldEffect $effect) {
$action = $effect->getAction();
$rule_type = $effect->getRule()->getRuleType();
$supported = $this->getActions($rule_type);
$supported = array_fuse($supported);
if (empty($supported[$action])) {
- throw new Exception(
+ return new HeraldApplyTranscript(
+ $effect,
+ false,
pht(
'Adapter "%s" does not support action "%s" for rule type "%s".',
get_class($this),
$action,
$rule_type));
}
switch ($action) {
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return $this->applyProjectsEffect($effect);
case self::ACTION_FLAG:
return $this->applyFlagEffect($effect);
case self::ACTION_EMAIL:
return $this->applyEmailEffect($effect);
default:
break;
}
$result = $this->handleCustomHeraldEffect($effect);
if (!$result) {
- throw new Exception(
+ return new HeraldApplyTranscript(
+ $effect,
+ false,
pht(
'No custom action exists to handle rule action "%s".',
$action));
}
return $result;
}
/**
* @task apply
*/
private function applyProjectsEffect(HeraldEffect $effect) {
if ($effect->getAction() == self::ACTION_ADD_PROJECTS) {
$kind = '+';
} else {
$kind = '-';
}
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$project_phids = $effect->getTarget();
$xaction = $this->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
$kind => array_fuse($project_phids),
));
$this->queueTransaction($xaction);
return new HeraldApplyTranscript(
$effect,
true,
pht('Added projects.'));
}
/**
* @task apply
*/
private function applyFlagEffect(HeraldEffect $effect) {
$phid = $this->getPHID();
$color = $effect->getTarget();
$rule = $effect->getRule();
$user = $rule->getAuthor();
$flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
if ($flag) {
return new HeraldApplyTranscript(
$effect,
false,
pht('Object already flagged.'));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
$flag = new PhabricatorFlag();
$flag->setOwnerPHID($user->getPHID());
$flag->setType($handle->getType());
$flag->setObjectPHID($handle->getPHID());
// TOOD: Should really be transcript PHID, but it doesn't exist yet.
$flag->setReasonPHID($user->getPHID());
$flag->setColor($color);
$flag->setNote(
pht('Flagged by Herald Rule "%s".', $rule->getName()));
$flag->save();
return new HeraldApplyTranscript(
$effect,
true,
pht('Added flag.'));
}
/**
* @task apply
*/
private function applyEmailEffect(HeraldEffect $effect) {
foreach ($effect->getTarget() as $phid) {
$this->emailPHIDs[$phid] = $phid;
// If this is a personal rule, we'll force delivery of a real email. This
// effect is stronger than notification preferences, so you get an actual
// email even if your preferences are set to "Notify" or "Ignore".
$rule = $effect->getRule();
if ($rule->isPersonalRule()) {
$this->forcedEmailPHIDs[$phid] = $phid;
}
}
return new HeraldApplyTranscript(
$effect,
true,
pht('Added mailable to mail targets.'));
}
}
diff --git a/src/applications/herald/adapter/HeraldDifferentialDiffAdapter.php b/src/applications/herald/adapter/HeraldDifferentialDiffAdapter.php
index 766be528f5..2a7a576e3e 100644
--- a/src/applications/herald/adapter/HeraldDifferentialDiffAdapter.php
+++ b/src/applications/herald/adapter/HeraldDifferentialDiffAdapter.php
@@ -1,164 +1,167 @@
<?php
final class HeraldDifferentialDiffAdapter extends HeraldDifferentialAdapter {
public function getAdapterApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function loadChangesets() {
return $this->loadChangesetsWithHunks();
}
protected function loadChangesetsWithHunks() {
return $this->getDiff()->getChangesets();
}
public function getObject() {
return $this->getDiff();
}
public function getAdapterContentType() {
return 'differential.diff';
}
public function getAdapterContentName() {
return pht('Differential Diffs');
}
public function getAdapterContentDescription() {
return pht(
"React to new diffs being uploaded, before writes occur.\n".
"These rules can reject diffs before they are written to permanent ".
"storage, to prevent users from accidentally uploading private keys or ".
"other sensitive information.");
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
return true;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
default:
return false;
}
}
public function getFields() {
return array_merge(
array(
self::FIELD_AUTHOR,
self::FIELD_AUTHOR_PROJECTS,
self::FIELD_REPOSITORY,
self::FIELD_REPOSITORY_PROJECTS,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_DIFF_ADDED_CONTENT,
self::FIELD_DIFF_REMOVED_CONTENT,
),
parent::getFields());
}
public function getRepetitionOptions() {
return array(
HeraldRepetitionPolicyConfig::FIRST,
);
}
public function getPHID() {
return $this->getObject()->getPHID();
}
public function getHeraldName() {
return pht('New Diff');
}
public function getActionNameMap($rule_type) {
return array(
self::ACTION_BLOCK => pht('Block diff with message'),
);
}
public function getHeraldField($field) {
switch ($field) {
case self::FIELD_AUTHOR:
return $this->getObject()->getAuthorPHID();
break;
case self::FIELD_AUTHOR_PROJECTS:
$author_phid = $this->getHeraldField(self::FIELD_AUTHOR);
if (!$author_phid) {
return array();
}
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withMemberPHIDs(array($author_phid))
->execute();
return mpull($projects, 'getPHID');
case self::FIELD_DIFF_FILE:
return $this->loadAffectedPaths();
case self::FIELD_REPOSITORY:
$repository = $this->loadRepository();
if (!$repository) {
return null;
}
return $repository->getPHID();
case self::FIELD_REPOSITORY_PROJECTS:
$repository = $this->loadRepository();
if (!$repository) {
return array();
}
return $repository->getProjectPHIDs();
case self::FIELD_DIFF_CONTENT:
return $this->loadContentDictionary();
case self::FIELD_DIFF_ADDED_CONTENT:
return $this->loadAddedContentDictionary();
case self::FIELD_DIFF_REMOVED_CONTENT:
return $this->loadRemovedContentDictionary();
}
return parent::getHeraldField($field);
}
public function getActions($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
return array_merge(
array(
self::ACTION_BLOCK,
self::ACTION_NOTHING,
),
parent::getActions($rule_type));
}
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$action = $effect->getAction();
switch ($action) {
case self::ACTION_NOTHING:
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Did nothing.'));
break;
case self::ACTION_BLOCK:
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Blocked diff.'));
break;
default:
- throw new Exception(pht('No rules to handle action "%s"!', $action));
+ $result[] = new HeraldApplyTranscript(
+ $effect,
+ false,
+ pht('No rules to handle action "%s"!', $action));
}
}
return $result;
}
}
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index f52a849d1e..ed72d1eda6 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,683 +1,706 @@
<?php
final class HeraldRuleController extends HeraldController {
private $id;
private $filter;
public function willProcessRequest(array $data) {
$this->id = (int)idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$content_type_map = HeraldAdapter::getEnabledAdapterMap($user);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($this->id) {
$id = $this->id;
$rule = id(new HeraldRuleQuery())
->setViewer($user)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI("rule/{$id}/");
} else {
$rule = new HeraldRule();
$rule->setAuthorPHID($user->getPHID());
$rule->setMustMatchAll(1);
$content_type = $request->getStr('content_type');
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
$rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
}
$rule->setRuleType($rule_type);
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
if (!$adapter->supportsRuleType($rule->getRuleType())) {
throw new Exception(
pht(
"This rule's content type does not support the selected rule ".
"type."));
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY);
}
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
pht(
'This rule was created with a newer version of Herald. You can not '.
'view or edit it in this older version. Upgrade your Phabricator '.
'deployment.'));
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
if (!$errors) {
$id = $rule->getID();
$uri = $this->getApplicationURI("rule/{$id}/");
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($user)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Rule Name'))
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(pht(
'This %s rule triggers for %s.',
phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Conditions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-condition',
'mustcapture' => true,
),
pht('New Condition')))
->setDescription(
pht('When %s these conditions are met:', $must_match_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table',
),
'')))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Action'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s this rule matches:',
$repetition_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Rule'))
->addCancelButton($cancel_uri));
$this->setupEditorBehavior($rule, $handles, $adapter);
$title = $rule->getID()
? pht('Edit Herald Rule')
: pht('Create Herald Rule');
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => pht('Edit Rule'),
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$rule->setName($request->getStr('name'));
$match_all = ($request->getStr('must_match') == 'all');
$rule->setMustMatchAll((int)$match_all);
$repetition_policy_param = $request->getStr('repetition_policy');
$rule->setRepetitionPolicy(
HeraldRepetitionPolicyConfig::toInt($repetition_policy_param));
$e_name = true;
$errors = array();
if (!strlen($rule->getName())) {
$e_name = pht('Required');
$errors[] = pht('Rule must have a name.');
}
$data = null;
try {
$data = phutil_json_decode($request->getStr('rule'));
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to decode rule data.'),
$ex);
}
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
throw new Exception(pht('Failed to decode rule data.'));
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
try {
$adapter->willSaveCondition($obj);
} catch (HeraldInvalidConditionException $ex) {
$errors[] = $ex->getMessage();
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$obj = new HeraldAction();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
- $errors[] = $ex;
+ $errors[] = $ex->getMessage();
}
$actions[] = $obj;
}
$rule->attachConditions($conditions);
$rule->attachActions($actions);
if (!$errors) {
$edit_action = $rule->getID() ? 'edit' : 'create';
$rule->openTransaction();
$rule->save();
$rule->saveConditions($conditions);
$rule->saveActions($actions);
$rule->saveTransaction();
}
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$serial_conditions = array(
array('default', 'default', ''),
);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
-
$value = $condition->getValue();
switch ($condition->getFieldName()) {
case HeraldAdapter::FIELD_TASK_PRIORITY:
$value_map = array();
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($value as $priority) {
$value_map[$priority] = idx($priority_map, $priority);
}
$value = $value_map;
break;
case HeraldAdapter::FIELD_TASK_STATUS:
$value_map = array();
$status_map = ManiphestTaskStatus::getTaskStatusMap();
foreach ($value as $status) {
$value_map[$status] = idx($status_map, $status);
}
$value = $value_map;
break;
default:
if (is_array($value)) {
$value_map = array();
foreach ($value as $k => $fbid) {
$value_map[$fbid] = $handles[$fbid]->getName();
}
$value = $value_map;
}
break;
}
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
}
$serial_actions = array(
array('default', ''),
);
+
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
-
switch ($action->getAction()) {
case HeraldAdapter::ACTION_FLAG:
case HeraldAdapter::ACTION_BLOCK:
$current_value = $action->getTarget();
break;
default:
if (is_array($action->getTarget())) {
$target_map = array();
foreach ((array)$action->getTarget() as $fbid) {
$target_map[$fbid] = $handles[$fbid]->getName();
}
$current_value = $target_map;
} else {
$current_value = $action->getTarget();
}
break;
}
$serial_actions[] = array(
$action->getAction(),
$current_value,
);
}
}
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = mpull($all_rules, 'getName', 'getPHID');
asort($all_rules);
$all_fields = $adapter->getFieldNameMap();
$all_conditions = $adapter->getConditionNameMap();
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
$fields = $adapter->getFields();
$field_map = array_select_keys($all_fields, $fields);
// Populate any fields which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getConditions() as $condition) {
- if (empty($field_map[$condition->getFieldName()])) {
- $field_map[$condition->getFieldName()] = pht('<Unknown Field>');
+ $field_name = $condition->getFieldName();
+
+ if (empty($field_map[$field_name])) {
+ $field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);
}
}
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
+ // Populate any actions which exist in the rule but which we don't know the
+ // names of, so that saving a rule without touching anything doesn't change
+ // it.
+ foreach ($rule->getActions() as $action) {
+ $action_name = $action->getAction();
+
+ if (empty($action_map[$action_name])) {
+ $action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);
+ }
+ }
+
+
$config_info = array();
$config_info['fields'] = $field_map;
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $action_map;
foreach ($config_info['fields'] as $field => $name) {
- $field_conditions = $adapter->getConditionsForField($field);
+ try {
+ $field_conditions = $adapter->getConditionsForField($field);
+ } catch (Exception $ex) {
+ $field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);
+ }
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($config_info['fields'] as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_type = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
$config_info['values'][$field][$condition] = $value_type;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($config_info['actions'] as $action => $name) {
- $config_info['targets'][$action] = $adapter->getValueTypeForAction(
- $action,
- $rule->getRuleType());
+ try {
+ $action_value = $adapter->getValueTypeForAction(
+ $action,
+ $rule->getRuleType());
+ } catch (Exception $ex) {
+ $action_value = array(HeraldAdapter::VALUE_NONE);
+ }
+
+ $config_info['targets'][$action] = $action_value;
}
$changeflag_options =
PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'select' => array(
HeraldAdapter::VALUE_CONTENT_SOURCE => array(
'options' => PhabricatorContentSource::getSourceNameMap(),
'default' => PhabricatorContentSource::SOURCE_WEB,
),
HeraldAdapter::VALUE_FLAG_COLOR => array(
'options' => PhabricatorFlagColor::getColorNameMap(),
'default' => PhabricatorFlagColor::COLOR_BLUE,
),
HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array(
'options' => array(
PhabricatorRepositoryPushLog::REFTYPE_BRANCH
=> pht('branch (git/hg)'),
PhabricatorRepositoryPushLog::REFTYPE_TAG
=> pht('tag (git)'),
PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK
=> pht('bookmark (hg)'),
),
'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH,
),
HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array(
'options' => $changeflag_options,
'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD,
),
),
'template' => $this->buildTokenizerTemplates($handles) + array(
'rules' => $all_rules,
),
'author' => array(
$rule->getAuthorPHID() =>
$handles[$rule->getAuthorPHID()]->getName(),
),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => pht('all of'),
'any' => pht('any of'),
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
$repetition_policy = HeraldRepetitionPolicyConfig::toString(
$rule->getRepetitionPolicy());
$repetition_options = $adapter->getRepetitionOptions();
$repetition_names = HeraldRepetitionPolicyConfig::getMap();
$repetition_map = array_select_keys($repetition_names, $repetition_options);
if (count($repetition_map) < 2) {
return head($repetition_names);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
protected function buildTokenizerTemplates(array $handles) {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
$sources = array(
'repository' => new DiffusionRepositoryDatasource(),
'legaldocuments' => new LegalpadDocumentDatasource(),
'taskpriority' => new ManiphestTaskPriorityDatasource(),
'taskstatus' => new ManiphestTaskStatusDatasource(),
'buildplan' => new HarbormasterBuildPlanDatasource(),
'arcanistprojects' => new DiffusionArcanistProjectDatasource(),
'package' => new PhabricatorOwnersPackageDatasource(),
'project' => new PhabricatorProjectDatasource(),
'user' => new PhabricatorPeopleDatasource(),
'email' => new PhabricatorMetaMTAMailableDatasource(),
'userorproject' => new PhabricatorProjectOrUserDatasource(),
'applicationemail' => new PhabricatorMetaMTAApplicationEmailDatasource(),
);
foreach ($sources as $key => $source) {
$source->setViewer($this->getViewer());
$sources[$key] = array(
'uri' => $source->getDatasourceURI(),
'placeholder' => $source->getPlaceholderText(),
'browseURI' => $source->getBrowseURI(),
);
}
return array(
'source' => $sources,
'username' => $this->getRequest()->getUser()->getUserName(),
'icons' => mpull($handles, 'getTypeIcon', 'getPHID'),
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->isObjectRule()) {
// Object rules may depend on other rules for the same object.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
->withContentTypes(array($rule->getContentType()))
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
->execute();
}
if ($rule->isPersonalRule()) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
// mark disabled rules as disabled since they are not useful as such;
// don't filter though to keep edit cases sane / expected
foreach ($all_rules as $current_rule) {
if ($current_rule->getIsDisabled()) {
$current_rule->makeEphemeral();
$current_rule->setName($rule->getName().' '.pht('(Disabled)'));
}
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
}
diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php
index f20d620cca..a915e55b3d 100644
--- a/src/applications/herald/controller/HeraldTranscriptController.php
+++ b/src/applications/herald/controller/HeraldTranscriptController.php
@@ -1,555 +1,558 @@
<?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 PHUIInfoView())
->setSeverity(PHUIInfoView::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 PHUIInfoView();
$notice->setSeverity(PHUIInfoView::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] = pht('Unknown Object #%s', $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() ==
PhabricatorRepositoryCommitPHIDType::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 PHUIInfoView())
->setSeverity(PHUIInfoView::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'));
+ $rule = idx(
+ $action_names,
+ $apply_xscript->getAction(),
+ pht('Unknown Action "%s"', $apply_xscript->getAction()));
$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;
}
}
diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php
index 5f1189fff6..4e986987d5 100644
--- a/src/applications/herald/engine/HeraldEngine.php
+++ b/src/applications/herald/engine/HeraldEngine.php
@@ -1,441 +1,451 @@
<?php
final class HeraldEngine {
protected $rules = array();
protected $results = array();
protected $stack = array();
protected $activeRule = null;
protected $fieldCache = array();
protected $object = null;
private $dryRun;
public function setDryRun($dry_run) {
$this->dryRun = $dry_run;
return $this;
}
public function getDryRun() {
return $this->dryRun;
}
public function getRule($phid) {
return idx($this->rules, $phid);
}
public function loadRulesForAdapter(HeraldAdapter $adapter) {
return id(new HeraldRuleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDisabled(false)
->withContentTypes(array($adapter->getAdapterContentType()))
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($adapter->getPHID()))
->needValidateAuthors(true)
->execute();
}
public static function loadAndApplyRules(HeraldAdapter $adapter) {
$engine = new HeraldEngine();
$rules = $engine->loadRulesForAdapter($adapter);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
return $engine->getTranscript();
}
public function applyRules(array $rules, HeraldAdapter $object) {
assert_instances_of($rules, 'HeraldRule');
$t_start = microtime(true);
// Rules execute in a well-defined order: sort them into execution order.
$rules = msort($rules, 'getRuleExecutionOrderSortKey');
$rules = mpull($rules, null, 'getPHID');
$this->transcript = new HeraldTranscript();
$this->transcript->setObjectPHID((string)$object->getPHID());
$this->fieldCache = array();
$this->results = array();
$this->rules = $rules;
$this->object = $object;
$effects = array();
foreach ($rules as $phid => $rule) {
$this->stack = array();
$policy_first = HeraldRepetitionPolicyConfig::FIRST;
$policy_first_int = HeraldRepetitionPolicyConfig::toInt($policy_first);
$is_first_only = ($rule->getRepetitionPolicy() == $policy_first_int);
try {
if (!$this->getDryRun() &&
$is_first_only &&
$rule->getRuleApplied($object->getPHID())) {
// This is not a dry run, and this rule is only supposed to be
// applied a single time, and it's already been applied...
// That means automatic failure.
$xscript = id(new HeraldRuleTranscript())
->setRuleID($rule->getID())
->setResult(false)
->setRuleName($rule->getName())
->setRuleOwner($rule->getAuthorPHID())
->setReason(
pht(
'This rule is only supposed to be repeated a single time, '.
'and it has already been applied.'));
$this->transcript->addRuleTranscript($xscript);
$rule_matches = false;
} else {
$rule_matches = $this->doesRuleMatch($rule, $object);
}
} catch (HeraldRecursiveConditionsException $ex) {
$names = array();
foreach ($this->stack as $rule_id => $ignored) {
$names[] = '"'.$rules[$rule_id]->getName().'"';
}
$names = implode(', ', $names);
foreach ($this->stack as $rule_id => $ignored) {
$xscript = new HeraldRuleTranscript();
$xscript->setRuleID($rule_id);
$xscript->setResult(false);
$xscript->setReason(
pht(
"Rules %s are recursively dependent upon one another! ".
"Don't do this! You have formed an unresolvable cycle in the ".
"dependency graph!",
$names));
$xscript->setRuleName($rules[$rule_id]->getName());
$xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
$this->transcript->addRuleTranscript($xscript);
}
$rule_matches = false;
}
$this->results[$phid] = $rule_matches;
if ($rule_matches) {
foreach ($this->getRuleEffects($rule, $object) as $effect) {
$effects[] = $effect;
}
}
}
$object_transcript = new HeraldObjectTranscript();
$object_transcript->setPHID($object->getPHID());
$object_transcript->setName($object->getHeraldName());
$object_transcript->setType($object->getAdapterContentType());
$object_transcript->setFields($this->fieldCache);
$this->transcript->setObjectTranscript($object_transcript);
$t_end = microtime(true);
$this->transcript->setDuration($t_end - $t_start);
return $effects;
}
public function applyEffects(
array $effects,
HeraldAdapter $adapter,
array $rules) {
assert_instances_of($effects, 'HeraldEffect');
assert_instances_of($rules, 'HeraldRule');
$this->transcript->setDryRun((int)$this->getDryRun());
if ($this->getDryRun()) {
$xscripts = array();
foreach ($effects as $effect) {
$xscripts[] = new HeraldApplyTranscript(
$effect,
false,
pht('This was a dry run, so no actions were actually taken.'));
}
} else {
$xscripts = $adapter->applyHeraldEffects($effects);
}
assert_instances_of($xscripts, 'HeraldApplyTranscript');
foreach ($xscripts as $apply_xscript) {
$this->transcript->addApplyTranscript($apply_xscript);
}
// For dry runs, don't mark the rule as having applied to the object.
if ($this->getDryRun()) {
return;
}
$rules = mpull($rules, null, 'getID');
$applied_ids = array();
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
// Mark all the rules that have had their effects applied as having been
// executed for the current object.
$rule_ids = mpull($xscripts, 'getRuleID');
foreach ($rule_ids as $rule_id) {
if (!$rule_id) {
// Some apply transcripts are purely informational and not associated
// with a rule, e.g. carryover emails from earlier revisions.
continue;
}
$rule = idx($rules, $rule_id);
if (!$rule) {
continue;
}
if ($rule->getRepetitionPolicy() == $first_policy) {
$applied_ids[] = $rule_id;
}
}
if ($applied_ids) {
$conn_w = id(new HeraldRule())->establishConnection('w');
$sql = array();
foreach ($applied_ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$adapter->getPHID(),
$id);
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
HeraldRule::TABLE_RULE_APPLIED,
implode(', ', $sql));
}
}
public function getTranscript() {
$this->transcript->save();
return $this->transcript;
}
public function doesRuleMatch(
HeraldRule $rule,
HeraldAdapter $object) {
$phid = $rule->getPHID();
if (isset($this->results[$phid])) {
// If we've already evaluated this rule because another rule depends
// on it, we don't need to reevaluate it.
return $this->results[$phid];
}
if (isset($this->stack[$phid])) {
// We've recursed, fail all of the rules on the stack. This happens when
// there's a dependency cycle with "Rule conditions match for rule ..."
// conditions.
foreach ($this->stack as $rule_phid => $ignored) {
$this->results[$rule_phid] = false;
}
throw new HeraldRecursiveConditionsException();
}
$this->stack[$phid] = true;
$all = $rule->getMustMatchAll();
$conditions = $rule->getConditions();
$result = null;
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
$reason = pht(
'Rule could not be processed, it was created with a newer version '.
'of Herald.');
$result = false;
} else if (!$conditions) {
$reason = pht(
'Rule failed automatically because it has no conditions.');
$result = false;
} else if (!$rule->hasValidAuthor()) {
$reason = pht(
'Rule failed automatically because its owner is invalid '.
'or disabled.');
$result = false;
} else if (!$this->canAuthorViewObject($rule, $object)) {
$reason = pht(
'Rule failed automatically because it is a personal rule and its '.
'owner can not see the object.');
$result = false;
} else if (!$this->canRuleApplyToObject($rule, $object)) {
$reason = pht(
'Rule failed automatically because it is an object rule which is '.
'not relevant for this object.');
$result = false;
} else {
foreach ($conditions as $condition) {
+ try {
+ $object->getHeraldField($condition->getFieldName());
+ } catch (Exception $ex) {
+ $reason = pht(
+ 'Field "%s" does not exist!',
+ $condition->getFieldName());
+ $result = false;
+ break;
+ }
+
$match = $this->doesConditionMatch($rule, $condition, $object);
if (!$all && $match) {
$reason = pht('Any condition matched.');
$result = true;
break;
}
if ($all && !$match) {
$reason = pht('Not all conditions matched.');
$result = false;
break;
}
}
if ($result === null) {
if ($all) {
$reason = pht('All conditions matched.');
$result = true;
} else {
$reason = pht('No conditions matched.');
$result = false;
}
}
}
$rule_transcript = new HeraldRuleTranscript();
$rule_transcript->setRuleID($rule->getID());
$rule_transcript->setResult($result);
$rule_transcript->setReason($reason);
$rule_transcript->setRuleName($rule->getName());
$rule_transcript->setRuleOwner($rule->getAuthorPHID());
$this->transcript->addRuleTranscript($rule_transcript);
return $result;
}
protected function doesConditionMatch(
HeraldRule $rule,
HeraldCondition $condition,
HeraldAdapter $object) {
$object_value = $this->getConditionObjectValue($condition, $object);
$test_value = $condition->getValue();
$cond = $condition->getFieldCondition();
$transcript = new HeraldConditionTranscript();
$transcript->setRuleID($rule->getID());
$transcript->setConditionID($condition->getID());
$transcript->setFieldName($condition->getFieldName());
$transcript->setCondition($cond);
$transcript->setTestValue($test_value);
try {
$result = $object->doesConditionMatch(
$this,
$rule,
$condition,
$object_value);
} catch (HeraldInvalidConditionException $ex) {
$result = false;
$transcript->setNote($ex->getMessage());
}
$transcript->setResult($result);
$this->transcript->addConditionTranscript($transcript);
return $result;
}
protected function getConditionObjectValue(
HeraldCondition $condition,
HeraldAdapter $object) {
$field = $condition->getFieldName();
return $this->getObjectFieldValue($field);
}
public function getObjectFieldValue($field) {
if (isset($this->fieldCache[$field])) {
return $this->fieldCache[$field];
}
$result = $this->object->getHeraldField($field);
$this->fieldCache[$field] = $result;
return $result;
}
protected function getRuleEffects(
HeraldRule $rule,
HeraldAdapter $object) {
$effects = array();
foreach ($rule->getActions() as $action) {
$effect = id(new HeraldEffect())
->setObjectPHID($object->getPHID())
->setAction($action->getAction())
->setTarget($action->getTarget())
->setRule($rule);
$name = $rule->getName();
$id = $rule->getID();
$effect->setReason(
pht(
'Conditions were met for %s',
"H{$id} {$name}"));
$effects[] = $effect;
}
return $effects;
}
private function canAuthorViewObject(
HeraldRule $rule,
HeraldAdapter $adapter) {
// Authorship is irrelevant for global rules and object rules.
if ($rule->isGlobalRule() || $rule->isObjectRule()) {
return true;
}
// The author must be able to create rules for the adapter's content type.
// In particular, this means that the application must be installed and
// accessible to the user. For example, if a user writes a Differential
// rule and then loses access to Differential, this disables the rule.
$enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor());
if (empty($enabled[$adapter->getAdapterContentType()])) {
return false;
}
// Finally, the author must be able to see the object itself. You can't
// write a personal rule that CC's you on revisions you wouldn't otherwise
// be able to see, for example.
$object = $adapter->getObject();
return PhabricatorPolicyFilter::hasCapability(
$rule->getAuthor(),
$object,
PhabricatorPolicyCapability::CAN_VIEW);
}
private function canRuleApplyToObject(
HeraldRule $rule,
HeraldAdapter $adapter) {
// Rules which are not object rules can apply to anything.
if (!$rule->isObjectRule()) {
return true;
}
$trigger_phid = $rule->getTriggerObjectPHID();
$object_phids = $adapter->getTriggerObjectPHIDs();
if ($object_phids) {
if (in_array($trigger_phid, $object_phids)) {
return true;
}
}
return false;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 1, 11:48 PM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
431369
Default Alt Text
(112 KB)

Event Timeline