diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index bfcd8e3ccb..e0e15352b6 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -1,366 +1,366 @@ setViewer($viewer) ->withPHIDs(array($object->getPHID())) ->needCommitData(true) ->executeOne(); if (!$object) { throw new Exception( pht( 'Failed to reload commit ("%s") to fetch commit data.', $object->getPHID())); } return id(clone $this) ->setObject($object); } protected function initializeNewAdapter() { $this->commit = $this->newObject(); } public function setObject($object) { $this->commit = $object; return $this; } public function getObject() { return $this->commit; } public function getAdapterContentType() { return 'commit'; } public function getAdapterContentName() { return pht('Commits'); } public function getAdapterContentDescription() { return pht( "React to new commits appearing in tracked repositories.\n". "Commit rules can send email, flag commits, trigger audits, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; default: return false; } } public function canTriggerOnObject($object) { if ($object instanceof PhabricatorRepository) { return true; } if ($object instanceof PhabricatorProject) { return true; } return false; } public function getTriggerObjectPHIDs() { $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $repository_phid = $this->getRepository()->getPHID(); $commit_phid = $this->getObject()->getPHID(); $phids = array(); $phids[] = $commit_phid; $phids[] = $repository_phid; // NOTE: This is projects for the repository, not for the commit. When // Herald evaluates, commits normally can not have any project tags yet. $repository_project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $repository_phid, $project_type); foreach ($repository_project_phids as $phid) { $phids[] = $phid; } $phids = array_unique($phids); $phids = array_values($phids); return $phids; } public function explainValidTriggerObjects() { return pht('This rule can trigger for **repositories** and **projects**.'); } public function getHeraldName() { return $this->commit->getMonogram(); } public function loadAffectedPaths() { if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->getRepository(), $this->commit, PhabricatorUser::getOmnipotentUser()); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->getRepository(), $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackages() { if ($this->auditNeededPackages === null) { $status_arr = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::CONCERNED, ); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s AND auditStatus IN (%Ls)', $this->commit->getPHID(), $status_arr); $this->auditNeededPackages = $requests; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { if ($this->affectedRevision === null) { $this->affectedRevision = false; $commit = $this->getObject(); $data = $commit->getCommitData(); $revision_id = $data->getCommitDetail('differential.revisionID'); if ($revision_id) { // NOTE: The Herald rule owner might not actually have access to // the revision, and can control which revision a commit is // associated with by putting text in the commit message. However, // the rules they can write against revisions don't actually expose // anything interesting, so it seems reasonable to load unconditionally // here. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needReviewers(true) ->executeOne(); if ($revision) { $this->affectedRevision = $revision; } } } return $this->affectedRevision; } public static function getEnormousByteLimit() { return 1024 * 1024 * 1024; // 1GB } public static function getEnormousTimeLimit() { return 60 * 15; // 15 Minutes } private function loadCommitDiff() { - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $byte_limit = self::getEnormousByteLimit(); $time_limit = self::getEnormousTimeLimit(); $diff_info = $this->callConduit( 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => $time_limit, 'byteLimit' => $byte_limit, 'linesOfContext' => 0, )); if ($diff_info['tooHuge']) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %s byte(s)). '. 'Herald can not process it.', new PhutilNumber($byte_limit))); } if ($diff_info['tooSlow']) { throw new Exception( pht( 'The raw text of this change took too long to process (longer '. 'than %s second(s)). Herald can not process it.', new PhutilNumber($time_limit))); } $file_phid = $diff_info['filePHID']; $diff_file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$diff_file) { throw new Exception( pht( 'Failed to load diff ("%s") for this change.', $file_phid)); } $raw = $diff_file->loadFileData(); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes); return $diff; } public function isDiffEnormous() { $this->loadDiffContent('*'); return ($this->commitDiff instanceof Exception); } public function loadDiffContent($type) { if ($this->commitDiff === null) { try { $this->commitDiff = $this->loadCommitDiff(); } catch (Exception $ex) { $this->commitDiff = $ex; phlog($ex); } } if ($this->commitDiff instanceof Exception) { $ex = $this->commitDiff; $ex_class = get_class($ex); $ex_message = pht('Failed to load changes: %s', $ex->getMessage()); return array( '<'.$ex_class.'>' => $ex_message, ); } $changes = $this->commitDiff->getChangesets(); $result = array(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { switch ($type) { case '-': $lines[] = $hunk->makeOldFile(); break; case '+': $lines[] = $hunk->makeNewFile(); break; case '*': $lines[] = $hunk->makeChanges(); break; default: throw new Exception(pht("Unknown content selection '%s'!", $type)); } } $result[$change->getFilename()] = implode("\n", $lines); } return $result; } public function loadIsMergeCommit() { $parents = $this->callConduit( 'diffusion.commitparentsquery', array( 'commit' => $this->getObject()->getCommitIdentifier(), )); return (count($parents) > 1); } private function callConduit($method, array $params) { $viewer = PhabricatorUser::getOmnipotentUser(); $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $viewer, 'repository' => $this->getRepository(), 'commit' => $this->commit->getCommitIdentifier(), )); return DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, $method, $params); } private function getRepository() { return $this->getObject()->getRepository(); } /* -( HarbormasterBuildableAdapterInterface )------------------------------ */ public function getHarbormasterBuildablePHID() { return $this->getObject()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getObject()->getRepository()->getPHID(); } public function getQueuedHarbormasterBuildRequests() { return $this->buildRequests; } public function queueHarbormasterBuildRequest( HarbormasterBuildRequest $request) { $this->buildRequests[] = $request; } } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index d67c69f584..9d56f474ff 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,1165 +1,1185 @@ emailPHIDs); } public function getForcedEmailPHIDs() { return array_values($this->forcedEmailPHIDs); } public function addEmailPHID($phid, $force) { $this->emailPHIDs[$phid] = $phid; if ($force) { $this->forcedEmailPHIDs[$phid] = $phid; } return $this; } + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + // See PHI276. Normally, Herald runs without regard for policy checks. + // However, we use a real viewer during test console runs: this makes + // intracluster calls to Diffusion APIs work even if web nodes don't + // have privileged credentials. + + if ($this->viewer) { + return $this->viewer; + } + + return PhabricatorUser::getOmnipotentUser(); + } + 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 supportsApplicationEmail() { return false; } public function setApplicationEmail( PhabricatorMetaMTAApplicationEmail $email) { $this->applicationEmail = $email; return $this; } public function getApplicationEmail() { return $this->applicationEmail; } public function getPHID() { return $this->getObject()->getPHID(); } abstract public function getHeraldName(); public function getHeraldField($field_key) { return $this->requireFieldImplementation($field_key) ->getHeraldFieldValue($this->getObject()); } public function applyHeraldEffects(array $effects) { assert_instances_of($effects, 'HeraldEffect'); $result = array(); foreach ($effects as $effect) { $result[] = $this->applyStandardEffect($effect); } return $result; } public function isAvailableToUser(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array($this->getAdapterApplicationClass())) ->execute(); return !empty($applications); } /** * Set the list of transactions which just took effect. * * These transactions are set by @{class:PhabricatorApplicationEditor} * automatically, before it invokes Herald. * * @param list List of transactions. * @return this */ final public function setAppliedTransactions(array $xactions) { assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); $this->appliedTransactions = $xactions; return $this; } /** * Get a list of transactions which just took effect. * * When an object is edited normally, transactions are applied and then * Herald executes. You can call this method to examine the transactions * if you want to react to them. * * @return list List of transactions. */ final public function getAppliedTransactions() { return $this->appliedTransactions; } public function queueTransaction($transaction) { $this->queuedTransactions[] = $transaction; } public function getQueuedTransactions() { return $this->queuedTransactions; } public 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 isTestAdapterForObject($object) { return false; } public function canCreateTestAdapterForObject($object) { return $this->isTestAdapterForObject($object); } public function newTestAdapter(PhabricatorUser $viewer, $object) { return id(clone $this) ->setObject($object); } public function getAdapterTestDescription() { return null; } 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 )------------------------------------------------------------- */ private function getFieldImplementationMap() { if ($this->fieldMap === null) { // We can't use PhutilClassMapQuery here because field expansion // depends on the adapter and object. $object = $this->getObject(); $map = array(); $all = HeraldField::getAllFields(); foreach ($all as $key => $field) { $field = id(clone $field)->setAdapter($this); if (!$field->supportsObject($object)) { continue; } $subfields = $field->getFieldsForObject($object); foreach ($subfields as $subkey => $subfield) { if (isset($map[$subkey])) { throw new Exception( pht( 'Two HeraldFields (of classes "%s" and "%s") have the same '. 'field key ("%s") after expansion for an object of class '. '"%s" inside adapter "%s". Each field must have a unique '. 'field key.', get_class($subfield), get_class($map[$subkey]), $subkey, get_class($object), get_class($this))); } $subfield = id(clone $subfield)->setAdapter($this); $map[$subkey] = $subfield; } } $this->fieldMap = $map; } return $this->fieldMap; } private function getFieldImplementation($key) { return idx($this->getFieldImplementationMap(), $key); } public function getFields() { return array_keys($this->getFieldImplementationMap()); } public function getFieldNameMap() { return mpull($this->getFieldImplementationMap(), 'getHeraldFieldName'); } public function getFieldGroupKey($field_key) { $field = $this->getFieldImplementation($field_key); if (!$field) { return null; } return $field->getFieldGroupKey(); } /* -( 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_NOT_REGEXP => pht('does not match 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) { return $this->requireFieldImplementation($field) ->getHeraldFieldConditions(); } private function requireFieldImplementation($field_key) { $field = $this->getFieldImplementation($field_key); if (!$field) { throw new Exception( pht( 'No field with key "%s" is available to Herald adapter "%s".', $field_key, get_class($this))); } return $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: case self::CONDITION_NOT_CONTAINS: // "Contains and "does not contain" can take an array of strings, as in // "Any changed filename" for diffs. $result_if_match = ($condition_type == self::CONDITION_CONTAINS); foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return $result_if_match; } } return !$result_if_match; 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: case self::CONDITION_NOT_REGEXP: $result_if_match = ($condition_type == 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 $result_if_match; } } return !$result_if_match; 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: case self::CONDITION_NOT_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 )------------------------------------------------------------ */ private function getActionImplementationMap() { if ($this->actionMap === null) { // We can't use PhutilClassMapQuery here because action expansion // depends on the adapter and object. $object = $this->getObject(); $map = array(); $all = HeraldAction::getAllActions(); foreach ($all as $key => $action) { $action = id(clone $action)->setAdapter($this); if (!$action->supportsObject($object)) { continue; } $subactions = $action->getActionsForObject($object); foreach ($subactions as $subkey => $subaction) { if (isset($map[$subkey])) { throw new Exception( pht( 'Two HeraldActions (of classes "%s" and "%s") have the same '. 'action key ("%s") after expansion for an object of class '. '"%s" inside adapter "%s". Each action must have a unique '. 'action key.', get_class($subaction), get_class($map[$subkey]), $subkey, get_class($object), get_class($this))); } $subaction = id(clone $subaction)->setAdapter($this); $map[$subkey] = $subaction; } } $this->actionMap = $map; } return $this->actionMap; } private function requireActionImplementation($action_key) { $action = $this->getActionImplementation($action_key); if (!$action) { throw new Exception( pht( 'No action with key "%s" is available to Herald adapter "%s".', $action_key, get_class($this))); } return $action; } private function getActionsForRuleType($rule_type) { $actions = $this->getActionImplementationMap(); foreach ($actions as $key => $action) { if (!$action->supportsRuleType($rule_type)) { unset($actions[$key]); } } return $actions; } public function getActionImplementation($key) { return idx($this->getActionImplementationMap(), $key); } public function getActionKeys() { return array_keys($this->getActionImplementationMap()); } public function getActionGroupKey($action_key) { $action = $this->getActionImplementation($action_key); if (!$action) { return null; } return $action->getActionGroupKey(); } public function getActions($rule_type) { $actions = array(); foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { $actions[] = $key; } return $actions; } public function getActionNameMap($rule_type) { $map = array(); foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { $map[$key] = $action->getHeraldActionName(); } return $map; } public function willSaveAction( HeraldRule $rule, HeraldActionRecord $action) { $impl = $this->requireActionImplementation($action->getAction()); $target = $action->getTarget(); $target = $impl->willSaveActionValue($target); $action->setTarget($target); } /* -( Values )------------------------------------------------------------- */ public function getValueTypeForFieldAndCondition($field, $condition) { return $this->requireFieldImplementation($field) ->getHeraldFieldValueType($condition); } public function getValueTypeForAction($action, $rule_type) { $impl = $this->requireActionImplementation($action); return $impl->getHeraldActionValueType(); } private function buildTokenizerFieldValue( PhabricatorTypeaheadDatasource $datasource) { $key = 'action.'.get_class($datasource); return id(new HeraldTokenizerFieldValue()) ->setKey($key) ->setDatasource($datasource); } /* -( Repetition )--------------------------------------------------------- */ public function getRepetitionOptions() { $options = array(); $options[] = HeraldRule::REPEAT_EVERY; // Some rules, like pre-commit rules, only ever fire once. It doesn't // make sense to use state-based repetition policies like "only the first // time" for these rules. if (!$this->isSingleEventAdapter()) { $options[] = HeraldRule::REPEAT_FIRST; $options[] = HeraldRule::REPEAT_CHANGE; } return $options; } protected function initializeNewAdapter() { $this->setObject($this->newObject()); return $this; } /** * Does this adapter's event fire only once? * * Single use adapters (like pre-commit and diff adapters) only fire once, * so fields like "Is new object" don't make sense to apply to their content. * * @return bool */ public function isSingleEventAdapter() { return false; } public static function getAllAdapters() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getAdapterContentType') ->setSortMethod('getAdapterSortKey') ->execute(); } public static function getAdapterForContentType($content_type) { $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if ($adapter->getAdapterContentType() == $content_type) { $adapter = id(clone $adapter); $adapter->initializeNewAdapter(); 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 getEditorValueForCondition( PhabricatorUser $viewer, HeraldCondition $condition) { $field = $this->requireFieldImplementation($condition->getFieldName()); return $field->getEditorValue( $viewer, $condition->getFieldCondition(), $condition->getValue()); } public function getEditorValueForAction( PhabricatorUser $viewer, HeraldActionRecord $action_record) { $action = $this->requireActionImplementation($action_record->getAction()); return $action->getEditorValue( $viewer, $action_record->getTarget()); } public function renderRuleAsText( HeraldRule $rule, PhabricatorHandleList $handles, PhabricatorUser $viewer) { require_celerity_resource('herald-css'); $icon = id(new PHUIIconView()) ->setIcon('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, $viewer), )); } if ($rule->isRepeatFirst()) { $action_text = pht( 'Take these actions the first time this rule matches:'); } else if ($rule->isRepeatOnChange()) { $action_text = pht( 'Take these actions if this rule did not match the last time:'); } else { $action_text = pht( 'Take these actions every 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($viewer, $action, $handles), )); } return array( $match_title, $match_list, $action_title, $action_list, ); } private function renderConditionAsText( HeraldCondition $condition, PhabricatorHandleList $handles, PhabricatorUser $viewer) { $field_type = $condition->getFieldName(); $field = $this->getFieldImplementation($field_type); if (!$field) { return pht('Unknown Field: "%s"', $field_type); } $field_name = $field->getHeraldFieldName(); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition, $handles, $viewer); return array( $field_name, ' ', $condition_name, ' ', $value, ); } private function renderActionAsText( PhabricatorUser $viewer, HeraldActionRecord $action, PhabricatorHandleList $handles) { $impl = $this->getActionImplementation($action->getAction()); if ($impl) { $impl->setViewer($viewer); $value = $action->getTarget(); return $impl->renderActionDescription($value); } $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_type = $action->getAction(); $default = 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, PhabricatorUser $viewer) { $field = $this->requireFieldImplementation($condition->getFieldName()); return $field->renderConditionValue( $viewer, $condition->getFieldCondition(), $condition->getValue()); } private function renderActionTargetAsText( HeraldActionRecord $action, PhabricatorHandleList $handles) { // TODO: This should be driven through HeraldAction. $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $index => $val) { switch ($action->getAction()) { 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; } /* -( Applying Effects )--------------------------------------------------- */ /** * @task apply */ protected function applyStandardEffect(HeraldEffect $effect) { $action = $effect->getAction(); $rule_type = $effect->getRule()->getRuleType(); $impl = $this->getActionImplementation($action); if (!$impl) { return new HeraldApplyTranscript( $effect, false, array( array( HeraldAction::DO_STANDARD_INVALID_ACTION, $action, ), )); } if (!$impl->supportsRuleType($rule_type)) { return new HeraldApplyTranscript( $effect, false, array( array( HeraldAction::DO_STANDARD_WRONG_RULE_TYPE, $rule_type, ), )); } $impl->applyEffect($this->getObject(), $effect); return $impl->getApplyTranscript($effect); } public function loadEdgePHIDs($type) { if (!isset($this->edgeCache[$type])) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getObject()->getPHID(), $type); $this->edgeCache[$type] = array_fuse($phids); } return $this->edgeCache[$type]; } /* -( Forbidden Actions )-------------------------------------------------- */ final public function getForbiddenActions() { return array_keys($this->forbiddenActions); } final public function setForbiddenAction($action, $reason) { $this->forbiddenActions[$action] = $reason; return $this; } final public function getRequiredFieldStates($field_key) { return $this->requireFieldImplementation($field_key) ->getRequiredAdapterStates(); } final public function getRequiredActionStates($action_key) { return $this->requireActionImplementation($action_key) ->getRequiredAdapterStates(); } final public function getForbiddenReason($action) { if (!isset($this->forbiddenActions[$action])) { throw new Exception( pht( 'Action "%s" is not forbidden!', $action)); } return $this->forbiddenActions[$action]; } } diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 21bedcd848..8a7a94963d 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -1,218 +1,220 @@ testObject = $test_object; return $this; } public function getTestObject() { return $this->testObject; } public function setTestAdapter(HeraldAdapter $test_adapter) { $this->testAdapter = $test_adapter; return $this; } public function getTestAdapter() { return $this->testAdapter; } public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $response = $this->loadTestObject($request); if ($response) { return $response; } $response = $this->loadAdapter($request); if ($response) { return $response; } $object = $this->getTestObject(); $adapter = $this->getTestAdapter(); - $adapter->setIsNewObject(false); + $adapter + ->setIsNewObject(false) + ->setViewer($viewer); $rules = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withContentTypes(array($adapter->getAdapterContentType())) ->withDisabled(false) ->needConditionsAndActions(true) ->needAppliedToPHIDs(array($object->getPHID())) ->needValidateAuthors(true) ->execute(); $engine = id(new HeraldEngine()) ->setDryRun(true); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); return id(new AphrontRedirectResponse()) ->setURI('/herald/transcript/'.$xscript->getID().'/'); } private function loadTestObject(AphrontRequest $request) { $viewer = $this->getViewer(); $e_name = true; $v_name = null; $errors = array(); if ($request->isFormPost()) { $v_name = trim($request->getStr('object_name')); if (!$v_name) { $e_name = pht('Required'); $errors[] = pht('An object name is required.'); } if (!$errors) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($v_name)) ->executeOne(); if (!$object) { $e_name = pht('Invalid'); $errors[] = pht('No object exists with that name.'); } } if (!$errors) { $this->setTestObject($object); return null; } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( 'Enter an object to test rules for, like a Diffusion commit (e.g., '. '`rX123`) or a Differential revision (e.g., `D123`). You will be '. 'shown the results of a dry run on the object.')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Object Name')) ->setName('object_name') ->setError($e_name) ->setValue($v_name)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue'))); return $this->buildTestConsoleResponse($form, $errors); } private function loadAdapter(AphrontRequest $request) { $viewer = $this->getViewer(); $object = $this->getTestObject(); $adapter_key = $request->getStr('adapter'); $adapters = HeraldAdapter::getAllAdapters(); $can_select = array(); $display_adapters = array(); foreach ($adapters as $key => $adapter) { if (!$adapter->isTestAdapterForObject($object)) { continue; } if (!$adapter->isAvailableToUser($viewer)) { continue; } $display_adapters[$key] = $adapter; if ($adapter->canCreateTestAdapterForObject($object)) { $can_select[$key] = $adapter; } } if ($request->isFormPost() && $adapter_key) { if (isset($can_select[$adapter_key])) { $adapter = $can_select[$adapter_key]->newTestAdapter( $viewer, $object); $this->setTestAdapter($adapter); return null; } } $form = id(new AphrontFormView()) ->addHiddenInput('object_name', $request->getStr('object_name')) ->setViewer($viewer); $cancel_uri = $this->getApplicationURI(); if (!$display_adapters) { $form ->appendRemarkupInstructions( pht('//There are no available Herald events for this object.//')) ->appendControl( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri)); } else { $adapter_control = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Event')) ->setName('adapter') ->setValue(head_key($can_select)); foreach ($display_adapters as $adapter_key => $adapter) { $is_disabled = empty($can_select[$adapter_key]); $adapter_control->addButton( $adapter_key, $adapter->getAdapterContentName(), $adapter->getAdapterTestDescription(), null, $is_disabled); } $form ->appendControl($adapter_control) ->appendControl( id(new AphrontFormSubmitControl()) ->setValue(pht('Run Test'))); } return $this->buildTestConsoleResponse($form, array()); } private function buildTestConsoleResponse($form, array $errors) { $box = id(new PHUIObjectBoxView()) ->setFormErrors($errors) ->setForm($form); $crumbs = id($this->buildApplicationCrumbs()) ->addTextCrumb(pht('Test Console')) ->setBorder(true); $title = pht('Test Console'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-desktop'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } }