Path: blob/master/src/applications/herald/adapter/HeraldAdapter.php
12256 views
<?php12abstract class HeraldAdapter extends Phobject {34const CONDITION_CONTAINS = 'contains';5const CONDITION_NOT_CONTAINS = '!contains';6const CONDITION_IS = 'is';7const CONDITION_IS_NOT = '!is';8const CONDITION_IS_ANY = 'isany';9const CONDITION_IS_NOT_ANY = '!isany';10const CONDITION_INCLUDE_ALL = 'all';11const CONDITION_INCLUDE_ANY = 'any';12const CONDITION_INCLUDE_NONE = 'none';13const CONDITION_IS_ME = 'me';14const CONDITION_IS_NOT_ME = '!me';15const CONDITION_REGEXP = 'regexp';16const CONDITION_NOT_REGEXP = '!regexp';17const CONDITION_RULE = 'conditions';18const CONDITION_NOT_RULE = '!conditions';19const CONDITION_EXISTS = 'exists';20const CONDITION_NOT_EXISTS = '!exists';21const CONDITION_UNCONDITIONALLY = 'unconditionally';22const CONDITION_NEVER = 'never';23const CONDITION_REGEXP_PAIR = 'regexp-pair';24const CONDITION_HAS_BIT = 'bit';25const CONDITION_NOT_BIT = '!bit';26const CONDITION_IS_TRUE = 'true';27const CONDITION_IS_FALSE = 'false';2829private $contentSource;30private $isNewObject;31private $applicationEmail;32private $appliedTransactions = array();33private $queuedTransactions = array();34private $emailPHIDs = array();35private $forcedEmailPHIDs = array();36private $fieldMap;37private $actionMap;38private $edgeCache = array();39private $forbiddenActions = array();40private $viewer;41private $mustEncryptReasons = array();42private $actingAsPHID;43private $webhookMap = array();4445public function getEmailPHIDs() {46return array_values($this->emailPHIDs);47}4849public function getForcedEmailPHIDs() {50return array_values($this->forcedEmailPHIDs);51}5253final public function setActingAsPHID($acting_as_phid) {54$this->actingAsPHID = $acting_as_phid;55return $this;56}5758final public function getActingAsPHID() {59return $this->actingAsPHID;60}6162public function addEmailPHID($phid, $force) {63$this->emailPHIDs[$phid] = $phid;64if ($force) {65$this->forcedEmailPHIDs[$phid] = $phid;66}67return $this;68}6970public function setViewer(PhabricatorUser $viewer) {71$this->viewer = $viewer;72return $this;73}7475public function getViewer() {76// See PHI276. Normally, Herald runs without regard for policy checks.77// However, we use a real viewer during test console runs: this makes78// intracluster calls to Diffusion APIs work even if web nodes don't79// have privileged credentials.8081if ($this->viewer) {82return $this->viewer;83}8485return PhabricatorUser::getOmnipotentUser();86}8788public function setContentSource(PhabricatorContentSource $content_source) {89$this->contentSource = $content_source;90return $this;91}9293public function getContentSource() {94return $this->contentSource;95}9697public function getIsNewObject() {98if (is_bool($this->isNewObject)) {99return $this->isNewObject;100}101102throw new Exception(103pht(104'You must %s to a boolean first!',105'setIsNewObject()'));106}107public function setIsNewObject($new) {108$this->isNewObject = (bool)$new;109return $this;110}111112public function supportsApplicationEmail() {113return false;114}115116public function setApplicationEmail(117PhabricatorMetaMTAApplicationEmail $email) {118$this->applicationEmail = $email;119return $this;120}121122public function getApplicationEmail() {123return $this->applicationEmail;124}125126public function getPHID() {127return $this->getObject()->getPHID();128}129130abstract public function getHeraldName();131132final public function willGetHeraldField($field_key) {133// This method is called during rule evaluation, before we engage the134// Herald profiler. We make sure we have a concrete implementation so time135// spent loading fields out of the classmap is not mistakenly attributed to136// whichever field happens to evaluate first.137$this->requireFieldImplementation($field_key);138}139140public function getHeraldField($field_key) {141return $this->requireFieldImplementation($field_key)142->getHeraldFieldValue($this->getObject());143}144145public function applyHeraldEffects(array $effects) {146assert_instances_of($effects, 'HeraldEffect');147148$result = array();149foreach ($effects as $effect) {150$result[] = $this->applyStandardEffect($effect);151}152153return $result;154}155156public function isAvailableToUser(PhabricatorUser $viewer) {157$applications = id(new PhabricatorApplicationQuery())158->setViewer($viewer)159->withInstalled(true)160->withClasses(array($this->getAdapterApplicationClass()))161->execute();162163return !empty($applications);164}165166167/**168* Set the list of transactions which just took effect.169*170* These transactions are set by @{class:PhabricatorApplicationEditor}171* automatically, before it invokes Herald.172*173* @param list<PhabricatorApplicationTransaction> List of transactions.174* @return this175*/176final public function setAppliedTransactions(array $xactions) {177assert_instances_of($xactions, 'PhabricatorApplicationTransaction');178$this->appliedTransactions = $xactions;179return $this;180}181182183/**184* Get a list of transactions which just took effect.185*186* When an object is edited normally, transactions are applied and then187* Herald executes. You can call this method to examine the transactions188* if you want to react to them.189*190* @return list<PhabricatorApplicationTransaction> List of transactions.191*/192final public function getAppliedTransactions() {193return $this->appliedTransactions;194}195196final public function queueTransaction(197PhabricatorApplicationTransaction $transaction) {198$this->queuedTransactions[] = $transaction;199}200201final public function getQueuedTransactions() {202return $this->queuedTransactions;203}204205final public function newTransaction() {206$object = $this->newObject();207208if (!($object instanceof PhabricatorApplicationTransactionInterface)) {209throw new Exception(210pht(211'Unable to build a new transaction for adapter object; it does '.212'not implement "%s".',213'PhabricatorApplicationTransactionInterface'));214}215216$xaction = $object->getApplicationTransactionTemplate();217218if (!($xaction instanceof PhabricatorApplicationTransaction)) {219throw new Exception(220pht(221'Expected object (of class "%s") to return a transaction template '.222'(of class "%s"), but it returned something else ("%s").',223get_class($object),224'PhabricatorApplicationTransaction',225phutil_describe_type($xaction)));226}227228return $xaction;229}230231232/**233* NOTE: You generally should not override this; it exists to support legacy234* adapters which had hard-coded content types.235*/236public function getAdapterContentType() {237return get_class($this);238}239240abstract public function getAdapterContentName();241abstract public function getAdapterContentDescription();242abstract public function getAdapterApplicationClass();243abstract public function getObject();244245public function getAdapterContentIcon() {246$application_class = $this->getAdapterApplicationClass();247$application = newv($application_class, array());248return $application->getIcon();249}250251/**252* Return a new characteristic object for this adapter.253*254* The adapter will use this object to test for interfaces, generate255* transactions, and interact with custom fields.256*257* Adapters must return an object from this method to enable custom258* field rules and various implicit actions.259*260* Normally, you'll return an empty version of the adapted object:261*262* return new ApplicationObject();263*264* @return null|object Template object.265*/266protected function newObject() {267return null;268}269270public function supportsRuleType($rule_type) {271return false;272}273274public function canTriggerOnObject($object) {275return false;276}277278public function isTestAdapterForObject($object) {279return false;280}281282public function canCreateTestAdapterForObject($object) {283return $this->isTestAdapterForObject($object);284}285286public function newTestAdapter(PhabricatorUser $viewer, $object) {287return id(clone $this)288->setObject($object);289}290291public function getAdapterTestDescription() {292return null;293}294295public function explainValidTriggerObjects() {296return pht('This adapter can not trigger on objects.');297}298299public function getTriggerObjectPHIDs() {300return array($this->getPHID());301}302303public function getAdapterSortKey() {304return sprintf(305'%08d%s',306$this->getAdapterSortOrder(),307$this->getAdapterContentName());308}309310public function getAdapterSortOrder() {311return 1000;312}313314315/* -( Fields )------------------------------------------------------------- */316317private function getFieldImplementationMap() {318if ($this->fieldMap === null) {319// We can't use PhutilClassMapQuery here because field expansion320// depends on the adapter and object.321322$object = $this->getObject();323324$map = array();325$all = HeraldField::getAllFields();326foreach ($all as $key => $field) {327$field = id(clone $field)->setAdapter($this);328329if (!$field->supportsObject($object)) {330continue;331}332$subfields = $field->getFieldsForObject($object);333foreach ($subfields as $subkey => $subfield) {334if (isset($map[$subkey])) {335throw new Exception(336pht(337'Two HeraldFields (of classes "%s" and "%s") have the same '.338'field key ("%s") after expansion for an object of class '.339'"%s" inside adapter "%s". Each field must have a unique '.340'field key.',341get_class($subfield),342get_class($map[$subkey]),343$subkey,344get_class($object),345get_class($this)));346}347348$subfield = id(clone $subfield)->setAdapter($this);349350$map[$subkey] = $subfield;351}352}353$this->fieldMap = $map;354}355356return $this->fieldMap;357}358359private function getFieldImplementation($key) {360return idx($this->getFieldImplementationMap(), $key);361}362363public function getFields() {364return array_keys($this->getFieldImplementationMap());365}366367public function getFieldNameMap() {368return mpull($this->getFieldImplementationMap(), 'getHeraldFieldName');369}370371public function getFieldGroupKey($field_key) {372$field = $this->getFieldImplementation($field_key);373374if (!$field) {375return null;376}377378return $field->getFieldGroupKey();379}380381public function isFieldAvailable($field_key) {382$field = $this->getFieldImplementation($field_key);383384if (!$field) {385return null;386}387388return $field->isFieldAvailable();389}390391392/* -( Conditions )--------------------------------------------------------- */393394395public function getConditionNameMap() {396return array(397self::CONDITION_CONTAINS => pht('contains'),398self::CONDITION_NOT_CONTAINS => pht('does not contain'),399self::CONDITION_IS => pht('is'),400self::CONDITION_IS_NOT => pht('is not'),401self::CONDITION_IS_ANY => pht('is any of'),402self::CONDITION_IS_TRUE => pht('is true'),403self::CONDITION_IS_FALSE => pht('is false'),404self::CONDITION_IS_NOT_ANY => pht('is not any of'),405self::CONDITION_INCLUDE_ALL => pht('include all of'),406self::CONDITION_INCLUDE_ANY => pht('include any of'),407self::CONDITION_INCLUDE_NONE => pht('include none of'),408self::CONDITION_IS_ME => pht('is myself'),409self::CONDITION_IS_NOT_ME => pht('is not myself'),410self::CONDITION_REGEXP => pht('matches regexp'),411self::CONDITION_NOT_REGEXP => pht('does not match regexp'),412self::CONDITION_RULE => pht('matches:'),413self::CONDITION_NOT_RULE => pht('does not match:'),414self::CONDITION_EXISTS => pht('exists'),415self::CONDITION_NOT_EXISTS => pht('does not exist'),416self::CONDITION_UNCONDITIONALLY => '', // don't show anything!417self::CONDITION_NEVER => '', // don't show anything!418self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),419self::CONDITION_HAS_BIT => pht('has bit'),420self::CONDITION_NOT_BIT => pht('lacks bit'),421);422}423424public function getConditionsForField($field) {425return $this->requireFieldImplementation($field)426->getHeraldFieldConditions();427}428429private function requireFieldImplementation($field_key) {430$field = $this->getFieldImplementation($field_key);431432if (!$field) {433throw new Exception(434pht(435'No field with key "%s" is available to Herald adapter "%s".',436$field_key,437get_class($this)));438}439440return $field;441}442443public function doesConditionMatch(444HeraldEngine $engine,445HeraldRule $rule,446HeraldCondition $condition,447$field_value) {448449$condition_type = $condition->getFieldCondition();450$condition_value = $condition->getValue();451452switch ($condition_type) {453case self::CONDITION_CONTAINS:454case self::CONDITION_NOT_CONTAINS:455// "Contains and "does not contain" can take an array of strings, as in456// "Any changed filename" for diffs.457458$result_if_match = ($condition_type == self::CONDITION_CONTAINS);459460foreach ((array)$field_value as $value) {461if (stripos($value, $condition_value) !== false) {462return $result_if_match;463}464}465return !$result_if_match;466case self::CONDITION_IS:467return ($field_value == $condition_value);468case self::CONDITION_IS_NOT:469return ($field_value != $condition_value);470case self::CONDITION_IS_ME:471return ($field_value == $rule->getAuthorPHID());472case self::CONDITION_IS_NOT_ME:473return ($field_value != $rule->getAuthorPHID());474case self::CONDITION_IS_ANY:475if (!is_array($condition_value)) {476throw new HeraldInvalidConditionException(477pht('Expected condition value to be an array.'));478}479$condition_value = array_fuse($condition_value);480return isset($condition_value[$field_value]);481case self::CONDITION_IS_NOT_ANY:482if (!is_array($condition_value)) {483throw new HeraldInvalidConditionException(484pht('Expected condition value to be an array.'));485}486$condition_value = array_fuse($condition_value);487return !isset($condition_value[$field_value]);488case self::CONDITION_INCLUDE_ALL:489if (!is_array($field_value)) {490throw new HeraldInvalidConditionException(491pht('Object produced non-array value!'));492}493if (!is_array($condition_value)) {494throw new HeraldInvalidConditionException(495pht('Expected condition value to be an array.'));496}497498$have = array_select_keys(array_fuse($field_value), $condition_value);499return (count($have) == count($condition_value));500case self::CONDITION_INCLUDE_ANY:501return (bool)array_select_keys(502array_fuse($field_value),503$condition_value);504case self::CONDITION_INCLUDE_NONE:505return !array_select_keys(506array_fuse($field_value),507$condition_value);508case self::CONDITION_EXISTS:509case self::CONDITION_IS_TRUE:510return (bool)$field_value;511case self::CONDITION_NOT_EXISTS:512case self::CONDITION_IS_FALSE:513return !$field_value;514case self::CONDITION_UNCONDITIONALLY:515return (bool)$field_value;516case self::CONDITION_NEVER:517return false;518case self::CONDITION_REGEXP:519case self::CONDITION_NOT_REGEXP:520$result_if_match = ($condition_type == self::CONDITION_REGEXP);521522// We add the 'S' flag because we use the regexp multiple times.523// It shouldn't cause any troubles if the flag is already there524// - /.*/S is evaluated same as /.*/SS.525$condition_pattern = $condition_value.'S';526527foreach ((array)$field_value as $value) {528try {529$result = phutil_preg_match($condition_pattern, $value);530} catch (PhutilRegexException $ex) {531$message = array();532$message[] = pht(533'Regular expression "%s" in Herald rule "%s" is not valid, '.534'or exceeded backtracking or recursion limits while '.535'executing. Verify the expression and correct it or rewrite '.536'it with less backtracking.',537$condition_value,538$rule->getMonogram());539$message[] = $ex->getMessage();540$message = implode("\n\n", $message);541542throw new HeraldInvalidConditionException($message);543}544545if ($result) {546return $result_if_match;547}548}549return !$result_if_match;550case self::CONDITION_REGEXP_PAIR:551// Match a JSON-encoded pair of regular expressions against a552// dictionary. The first regexp must match the dictionary key, and the553// second regexp must match the dictionary value. If any key/value pair554// in the dictionary matches both regexps, the condition is satisfied.555$regexp_pair = null;556try {557$regexp_pair = phutil_json_decode($condition_value);558} catch (PhutilJSONParserException $ex) {559throw new HeraldInvalidConditionException(560pht('Regular expression pair is not valid JSON!'));561}562if (count($regexp_pair) != 2) {563throw new HeraldInvalidConditionException(564pht('Regular expression pair is not a pair!'));565}566567$key_regexp = array_shift($regexp_pair);568$value_regexp = array_shift($regexp_pair);569570foreach ((array)$field_value as $key => $value) {571$key_matches = @preg_match($key_regexp, $key);572if ($key_matches === false) {573throw new HeraldInvalidConditionException(574pht('First regular expression is invalid!'));575}576if ($key_matches) {577$value_matches = @preg_match($value_regexp, $value);578if ($value_matches === false) {579throw new HeraldInvalidConditionException(580pht('Second regular expression is invalid!'));581}582if ($value_matches) {583return true;584}585}586}587return false;588case self::CONDITION_RULE:589case self::CONDITION_NOT_RULE:590$rule = $engine->getRule($condition_value);591if (!$rule) {592throw new HeraldInvalidConditionException(593pht('Condition references a rule which does not exist!'));594}595596$is_not = ($condition_type == self::CONDITION_NOT_RULE);597$result = $engine->doesRuleMatch($rule, $this);598if ($is_not) {599$result = !$result;600}601return $result;602case self::CONDITION_HAS_BIT:603return (($condition_value & $field_value) === (int)$condition_value);604case self::CONDITION_NOT_BIT:605return (($condition_value & $field_value) !== (int)$condition_value);606default:607throw new HeraldInvalidConditionException(608pht("Unknown condition '%s'.", $condition_type));609}610}611612public function willSaveCondition(HeraldCondition $condition) {613$condition_type = $condition->getFieldCondition();614$condition_value = $condition->getValue();615616switch ($condition_type) {617case self::CONDITION_REGEXP:618case self::CONDITION_NOT_REGEXP:619$ok = @preg_match($condition_value, '');620if ($ok === false) {621throw new HeraldInvalidConditionException(622pht(623'The regular expression "%s" is not valid. Regular expressions '.624'must have enclosing characters (e.g. "@/path/to/file@", not '.625'"/path/to/file") and be syntactically correct.',626$condition_value));627}628break;629case self::CONDITION_REGEXP_PAIR:630$json = null;631try {632$json = phutil_json_decode($condition_value);633} catch (PhutilJSONParserException $ex) {634throw new HeraldInvalidConditionException(635pht(636'The regular expression pair "%s" is not valid JSON. Enter a '.637'valid JSON array with two elements.',638$condition_value));639}640641if (count($json) != 2) {642throw new HeraldInvalidConditionException(643pht(644'The regular expression pair "%s" must have exactly two '.645'elements.',646$condition_value));647}648649$key_regexp = array_shift($json);650$val_regexp = array_shift($json);651652$key_ok = @preg_match($key_regexp, '');653if ($key_ok === false) {654throw new HeraldInvalidConditionException(655pht(656'The first regexp in the regexp pair, "%s", is not a valid '.657'regexp.',658$key_regexp));659}660661$val_ok = @preg_match($val_regexp, '');662if ($val_ok === false) {663throw new HeraldInvalidConditionException(664pht(665'The second regexp in the regexp pair, "%s", is not a valid '.666'regexp.',667$val_regexp));668}669break;670case self::CONDITION_CONTAINS:671case self::CONDITION_NOT_CONTAINS:672case self::CONDITION_IS:673case self::CONDITION_IS_NOT:674case self::CONDITION_IS_ANY:675case self::CONDITION_IS_NOT_ANY:676case self::CONDITION_INCLUDE_ALL:677case self::CONDITION_INCLUDE_ANY:678case self::CONDITION_INCLUDE_NONE:679case self::CONDITION_IS_ME:680case self::CONDITION_IS_NOT_ME:681case self::CONDITION_RULE:682case self::CONDITION_NOT_RULE:683case self::CONDITION_EXISTS:684case self::CONDITION_NOT_EXISTS:685case self::CONDITION_UNCONDITIONALLY:686case self::CONDITION_NEVER:687case self::CONDITION_HAS_BIT:688case self::CONDITION_NOT_BIT:689case self::CONDITION_IS_TRUE:690case self::CONDITION_IS_FALSE:691// No explicit validation for these types, although there probably692// should be in some cases.693break;694default:695throw new HeraldInvalidConditionException(696pht(697'Unknown condition "%s"!',698$condition_type));699}700}701702703/* -( Actions )------------------------------------------------------------ */704705private function getActionImplementationMap() {706if ($this->actionMap === null) {707// We can't use PhutilClassMapQuery here because action expansion708// depends on the adapter and object.709710$object = $this->getObject();711712$map = array();713$all = HeraldAction::getAllActions();714foreach ($all as $key => $action) {715$action = id(clone $action)->setAdapter($this);716717if (!$action->supportsObject($object)) {718continue;719}720721$subactions = $action->getActionsForObject($object);722foreach ($subactions as $subkey => $subaction) {723if (isset($map[$subkey])) {724throw new Exception(725pht(726'Two HeraldActions (of classes "%s" and "%s") have the same '.727'action key ("%s") after expansion for an object of class '.728'"%s" inside adapter "%s". Each action must have a unique '.729'action key.',730get_class($subaction),731get_class($map[$subkey]),732$subkey,733get_class($object),734get_class($this)));735}736737$subaction = id(clone $subaction)->setAdapter($this);738739$map[$subkey] = $subaction;740}741}742$this->actionMap = $map;743}744745return $this->actionMap;746}747748private function requireActionImplementation($action_key) {749$action = $this->getActionImplementation($action_key);750751if (!$action) {752throw new Exception(753pht(754'No action with key "%s" is available to Herald adapter "%s".',755$action_key,756get_class($this)));757}758759return $action;760}761762private function getActionsForRuleType($rule_type) {763$actions = $this->getActionImplementationMap();764765foreach ($actions as $key => $action) {766if (!$action->supportsRuleType($rule_type)) {767unset($actions[$key]);768}769}770771return $actions;772}773774public function getActionImplementation($key) {775return idx($this->getActionImplementationMap(), $key);776}777778public function getActionKeys() {779return array_keys($this->getActionImplementationMap());780}781782public function getActionGroupKey($action_key) {783$action = $this->getActionImplementation($action_key);784if (!$action) {785return null;786}787788return $action->getActionGroupKey();789}790791public function isActionAvailable($action_key) {792$action = $this->getActionImplementation($action_key);793794if (!$action) {795return null;796}797798return $action->isActionAvailable();799}800801public function getActions($rule_type) {802$actions = array();803foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {804$actions[] = $key;805}806807return $actions;808}809810public function getActionNameMap($rule_type) {811$map = array();812foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {813$map[$key] = $action->getHeraldActionName();814}815816return $map;817}818819public function willSaveAction(820HeraldRule $rule,821HeraldActionRecord $action) {822823$impl = $this->requireActionImplementation($action->getAction());824$target = $action->getTarget();825$target = $impl->willSaveActionValue($target);826827$action->setTarget($target);828}829830831832/* -( Values )------------------------------------------------------------- */833834835public function getValueTypeForFieldAndCondition($field, $condition) {836return $this->requireFieldImplementation($field)837->getHeraldFieldValueType($condition);838}839840public function getValueTypeForAction($action, $rule_type) {841$impl = $this->requireActionImplementation($action);842return $impl->getHeraldActionValueType();843}844845private function buildTokenizerFieldValue(846PhabricatorTypeaheadDatasource $datasource) {847848$key = 'action.'.get_class($datasource);849850return id(new HeraldTokenizerFieldValue())851->setKey($key)852->setDatasource($datasource);853}854855/* -( Repetition )--------------------------------------------------------- */856857858public function getRepetitionOptions() {859$options = array();860861$options[] = HeraldRule::REPEAT_EVERY;862863// Some rules, like pre-commit rules, only ever fire once. It doesn't864// make sense to use state-based repetition policies like "only the first865// time" for these rules.866867if (!$this->isSingleEventAdapter()) {868$options[] = HeraldRule::REPEAT_FIRST;869$options[] = HeraldRule::REPEAT_CHANGE;870}871872return $options;873}874875protected function initializeNewAdapter() {876$this->setObject($this->newObject());877return $this;878}879880/**881* Does this adapter's event fire only once?882*883* Single use adapters (like pre-commit and diff adapters) only fire once,884* so fields like "Is new object" don't make sense to apply to their content.885*886* @return bool887*/888public function isSingleEventAdapter() {889return false;890}891892public static function getAllAdapters() {893return id(new PhutilClassMapQuery())894->setAncestorClass(__CLASS__)895->setUniqueMethod('getAdapterContentType')896->setSortMethod('getAdapterSortKey')897->execute();898}899900public static function getAdapterForContentType($content_type) {901$adapters = self::getAllAdapters();902903foreach ($adapters as $adapter) {904if ($adapter->getAdapterContentType() == $content_type) {905$adapter = id(clone $adapter);906$adapter->initializeNewAdapter();907return $adapter;908}909}910911throw new Exception(912pht(913'No adapter exists for Herald content type "%s".',914$content_type));915}916917public static function getEnabledAdapterMap(PhabricatorUser $viewer) {918$map = array();919920$adapters = self::getAllAdapters();921foreach ($adapters as $adapter) {922if (!$adapter->isAvailableToUser($viewer)) {923continue;924}925$type = $adapter->getAdapterContentType();926$name = $adapter->getAdapterContentName();927$map[$type] = $name;928}929930return $map;931}932933public function getEditorValueForCondition(934PhabricatorUser $viewer,935HeraldCondition $condition) {936937$field = $this->requireFieldImplementation($condition->getFieldName());938939return $field->getEditorValue(940$viewer,941$condition->getFieldCondition(),942$condition->getValue());943}944945public function getEditorValueForAction(946PhabricatorUser $viewer,947HeraldActionRecord $action_record) {948949$action = $this->requireActionImplementation($action_record->getAction());950951return $action->getEditorValue(952$viewer,953$action_record->getTarget());954}955956public function renderRuleAsText(957HeraldRule $rule,958PhabricatorUser $viewer) {959960require_celerity_resource('herald-css');961962$icon = id(new PHUIIconView())963->setIcon('fa-chevron-circle-right lightgreytext')964->addClass('herald-list-icon');965966if ($rule->getMustMatchAll()) {967$match_text = pht('When all of these conditions are met:');968} else {969$match_text = pht('When any of these conditions are met:');970}971972$match_title = phutil_tag(973'p',974array(975'class' => 'herald-list-description',976),977$match_text);978979$match_list = array();980foreach ($rule->getConditions() as $condition) {981$match_list[] = phutil_tag(982'div',983array(984'class' => 'herald-list-item',985),986array(987$icon,988$this->renderConditionAsText($condition, $viewer),989));990}991992if ($rule->isRepeatFirst()) {993$action_text = pht(994'Take these actions the first time this rule matches:');995} else if ($rule->isRepeatOnChange()) {996$action_text = pht(997'Take these actions if this rule did not match the last time:');998} else {999$action_text = pht(1000'Take these actions every time this rule matches:');1001}10021003$action_title = phutil_tag(1004'p',1005array(1006'class' => 'herald-list-description',1007),1008$action_text);10091010$action_list = array();1011foreach ($rule->getActions() as $action) {1012$action_list[] = phutil_tag(1013'div',1014array(1015'class' => 'herald-list-item',1016),1017array(1018$icon,1019$this->renderActionAsText($viewer, $action),1020));1021}10221023return array(1024$match_title,1025$match_list,1026$action_title,1027$action_list,1028);1029}10301031private function renderConditionAsText(1032HeraldCondition $condition,1033PhabricatorUser $viewer) {10341035$field_type = $condition->getFieldName();1036$field = $this->getFieldImplementation($field_type);10371038if (!$field) {1039return pht('Unknown Field: "%s"', $field_type);1040}10411042$field_name = $field->getHeraldFieldName();10431044$condition_type = $condition->getFieldCondition();1045$condition_name = idx($this->getConditionNameMap(), $condition_type);10461047$value = $this->renderConditionValueAsText($condition, $viewer);10481049return array(1050$field_name,1051' ',1052$condition_name,1053' ',1054$value,1055);1056}10571058private function renderActionAsText(1059PhabricatorUser $viewer,1060HeraldActionRecord $action_record) {10611062$action_type = $action_record->getAction();1063$action_value = $action_record->getTarget();10641065$action = $this->getActionImplementation($action_type);1066if (!$action) {1067return pht('Unknown Action ("%s")', $action_type);1068}10691070$action->setViewer($viewer);10711072return $action->renderActionDescription($action_value);1073}10741075private function renderConditionValueAsText(1076HeraldCondition $condition,1077PhabricatorUser $viewer) {10781079$field = $this->requireFieldImplementation($condition->getFieldName());10801081return $field->renderConditionValue(1082$viewer,1083$condition->getFieldCondition(),1084$condition->getValue());1085}10861087public function renderFieldTranscriptValue(1088PhabricatorUser $viewer,1089$field_type,1090$field_value) {10911092$field = $this->getFieldImplementation($field_type);1093if ($field) {1094return $field->renderTranscriptValue(1095$viewer,1096$field_value);1097}10981099return phutil_tag(1100'em',1101array(),1102pht(1103'Unable to render value for unknown field type ("%s").',1104$field_type));1105}110611071108/* -( Applying Effects )--------------------------------------------------- */110911101111/**1112* @task apply1113*/1114protected function applyStandardEffect(HeraldEffect $effect) {1115$action = $effect->getAction();1116$rule_type = $effect->getRule()->getRuleType();11171118$impl = $this->getActionImplementation($action);1119if (!$impl) {1120return new HeraldApplyTranscript(1121$effect,1122false,1123array(1124array(1125HeraldAction::DO_STANDARD_INVALID_ACTION,1126$action,1127),1128));1129}11301131if (!$impl->supportsRuleType($rule_type)) {1132return new HeraldApplyTranscript(1133$effect,1134false,1135array(1136array(1137HeraldAction::DO_STANDARD_WRONG_RULE_TYPE,1138$rule_type,1139),1140));1141}11421143$impl->applyEffect($this->getObject(), $effect);1144return $impl->getApplyTranscript($effect);1145}11461147public function loadEdgePHIDs($type) {1148if (!isset($this->edgeCache[$type])) {1149$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(1150$this->getObject()->getPHID(),1151$type);11521153$this->edgeCache[$type] = array_fuse($phids);1154}1155return $this->edgeCache[$type];1156}115711581159/* -( Forbidden Actions )-------------------------------------------------- */116011611162final public function getForbiddenActions() {1163return array_keys($this->forbiddenActions);1164}11651166final public function setForbiddenAction($action, $reason) {1167$this->forbiddenActions[$action] = $reason;1168return $this;1169}11701171final public function getRequiredFieldStates($field_key) {1172return $this->requireFieldImplementation($field_key)1173->getRequiredAdapterStates();1174}11751176final public function getRequiredActionStates($action_key) {1177return $this->requireActionImplementation($action_key)1178->getRequiredAdapterStates();1179}11801181final public function getForbiddenReason($action) {1182if (!isset($this->forbiddenActions[$action])) {1183throw new Exception(1184pht(1185'Action "%s" is not forbidden!',1186$action));1187}11881189return $this->forbiddenActions[$action];1190}119111921193/* -( Must Encrypt )------------------------------------------------------- */119411951196final public function addMustEncryptReason($reason) {1197$this->mustEncryptReasons[] = $reason;1198return $this;1199}12001201final public function getMustEncryptReasons() {1202return $this->mustEncryptReasons;1203}120412051206/* -( Webhooks )----------------------------------------------------------- */120712081209public function supportsWebhooks() {1210return true;1211}121212131214final public function queueWebhook($webhook_phid, $rule_phid) {1215$this->webhookMap[$webhook_phid][] = $rule_phid;1216return $this;1217}12181219final public function getWebhookMap() {1220return $this->webhookMap;1221}12221223}122412251226