Path: blob/master/src/applications/herald/controller/HeraldRuleController.php
12256 views
<?php12final class HeraldRuleController extends HeraldController {34public function handleRequest(AphrontRequest $request) {5$viewer = $request->getViewer();6$id = $request->getURIData('id');78$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);9$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();1011if ($id) {12$rule = id(new HeraldRuleQuery())13->setViewer($viewer)14->withIDs(array($id))15->requireCapabilities(16array(17PhabricatorPolicyCapability::CAN_VIEW,18PhabricatorPolicyCapability::CAN_EDIT,19))20->executeOne();21if (!$rule) {22return new Aphront404Response();23}24$cancel_uri = '/'.$rule->getMonogram();25} else {26$new_uri = $this->getApplicationURI('new/');2728$rule = new HeraldRule();29$rule->setAuthorPHID($viewer->getPHID());30$rule->setMustMatchAll(1);3132$content_type = $request->getStr('content_type');33$rule->setContentType($content_type);3435$rule_type = $request->getStr('rule_type');36if (!isset($rule_type_map[$rule_type])) {37return $this->newDialog()38->setTitle(pht('Invalid Rule Type'))39->appendParagraph(40pht(41'The selected rule type ("%s") is not recognized by Herald.',42$rule_type))43->addCancelButton($new_uri);44}45$rule->setRuleType($rule_type);4647try {48$adapter = HeraldAdapter::getAdapterForContentType(49$rule->getContentType());50} catch (Exception $ex) {51return $this->newDialog()52->setTitle(pht('Invalid Content Type'))53->appendParagraph(54pht(55'The selected content type ("%s") is not recognized by '.56'Herald.',57$rule->getContentType()))58->addCancelButton($new_uri);59}6061if (!$adapter->supportsRuleType($rule->getRuleType())) {62return $this->newDialog()63->setTitle(pht('Rule/Content Mismatch'))64->appendParagraph(65pht(66'The selected rule type ("%s") is not supported by the selected '.67'content type ("%s").',68$rule->getRuleType(),69$rule->getContentType()))70->addCancelButton($new_uri);71}7273if ($rule->isObjectRule()) {74$rule->setTriggerObjectPHID($request->getStr('targetPHID'));75$object = id(new PhabricatorObjectQuery())76->setViewer($viewer)77->withPHIDs(array($rule->getTriggerObjectPHID()))78->requireCapabilities(79array(80PhabricatorPolicyCapability::CAN_VIEW,81PhabricatorPolicyCapability::CAN_EDIT,82))83->executeOne();84if (!$object) {85throw new Exception(86pht('No valid object provided for object rule!'));87}8889if (!$adapter->canTriggerOnObject($object)) {90throw new Exception(91pht('Object is of wrong type for adapter!'));92}93}9495$cancel_uri = $this->getApplicationURI();96}9798if ($rule->isGlobalRule()) {99$this->requireApplicationCapability(100HeraldManageGlobalRulesCapability::CAPABILITY);101}102103$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());104105$local_version = id(new HeraldRule())->getConfigVersion();106if ($rule->getConfigVersion() > $local_version) {107throw new Exception(108pht(109'This rule was created with a newer version of Herald. You can not '.110'view or edit it in this older version. Upgrade your software.'));111}112113// Upgrade rule version to our version, since we might add newly-defined114// conditions, etc.115$rule->setConfigVersion($local_version);116117$rule_conditions = $rule->loadConditions();118$rule_actions = $rule->loadActions();119120$rule->attachConditions($rule_conditions);121$rule->attachActions($rule_actions);122123$e_name = true;124$errors = array();125if ($request->isFormPost() && $request->getStr('save')) {126list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);127if (!$errors) {128$id = $rule->getID();129$uri = '/'.$rule->getMonogram();130return id(new AphrontRedirectResponse())->setURI($uri);131}132}133134$must_match_selector = $this->renderMustMatchSelector($rule);135$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);136137$handles = $this->loadHandlesForRule($rule);138139require_celerity_resource('herald-css');140141$content_type_name = $content_type_map[$rule->getContentType()];142$rule_type_name = $rule_type_map[$rule->getRuleType()];143144$form = id(new AphrontFormView())145->setUser($viewer)146->setID('herald-rule-edit-form')147->addHiddenInput('content_type', $rule->getContentType())148->addHiddenInput('rule_type', $rule->getRuleType())149->addHiddenInput('save', 1)150->appendChild(151// Build this explicitly (instead of using addHiddenInput())152// so we can add a sigil to it.153javelin_tag(154'input',155array(156'type' => 'hidden',157'name' => 'rule',158'sigil' => 'rule',159)))160->appendChild(161id(new AphrontFormTextControl())162->setLabel(pht('Rule Name'))163->setName('name')164->setError($e_name)165->setValue($rule->getName()));166167$trigger_object_control = false;168if ($rule->isObjectRule()) {169$trigger_object_control = id(new AphrontFormStaticControl())170->setValue(171pht(172'This rule triggers for %s.',173$handles[$rule->getTriggerObjectPHID()]->renderLink()));174}175176177$form178->appendChild(179id(new AphrontFormMarkupControl())180->setValue(pht(181'This %s rule triggers for %s.',182phutil_tag('strong', array(), $rule_type_name),183phutil_tag('strong', array(), $content_type_name))))184->appendChild($trigger_object_control)185->appendChild(186id(new PHUIFormInsetView())187->setTitle(pht('Conditions'))188->setRightButton(javelin_tag(189'a',190array(191'href' => '#',192'class' => 'button button-green',193'sigil' => 'create-condition',194'mustcapture' => true,195),196pht('New Condition')))197->setDescription(198pht('When %s these conditions are met:', $must_match_selector))199->setContent(javelin_tag(200'table',201array(202'sigil' => 'rule-conditions',203'class' => 'herald-condition-table',204),205'')))206->appendChild(207id(new PHUIFormInsetView())208->setTitle(pht('Action'))209->setRightButton(javelin_tag(210'a',211array(212'href' => '#',213'class' => 'button button-green',214'sigil' => 'create-action',215'mustcapture' => true,216),217pht('New Action')))218->setDescription(pht(219'Take these actions %s',220$repetition_selector))221->setContent(javelin_tag(222'table',223array(224'sigil' => 'rule-actions',225'class' => 'herald-action-table',226),227'')))228->appendChild(229id(new AphrontFormSubmitControl())230->setValue(pht('Save Rule'))231->addCancelButton($cancel_uri));232233$this->setupEditorBehavior($rule, $handles, $adapter);234235$title = $rule->getID()236? pht('Edit Herald Rule: %s', $rule->getName())237: pht('Create Herald Rule: %s', idx($content_type_map, $content_type));238239$form_box = id(new PHUIObjectBoxView())240->setHeaderText($title)241->setBackground(PHUIObjectBoxView::WHITE_CONFIG)242->setFormErrors($errors)243->setForm($form);244245$crumbs = $this246->buildApplicationCrumbs()247->addTextCrumb($title)248->setBorder(true);249250$view = id(new PHUITwoColumnView())251->setFooter($form_box);252253return $this->newPage()254->setTitle($title)255->setCrumbs($crumbs)256->appendChild(257array(258$view,259));260}261262private function saveRule(HeraldAdapter $adapter, $rule, $request) {263$new_name = $request->getStr('name');264$match_all = ($request->getStr('must_match') == 'all');265266$repetition_policy = $request->getStr('repetition_policy');267268// If the user selected an invalid policy, or there's only one possible269// value so we didn't render a control, adjust the value to the first270// valid policy value.271$repetition_options = $this->getRepetitionOptionMap($adapter);272if (!isset($repetition_options[$repetition_policy])) {273$repetition_policy = head_key($repetition_options);274}275276$e_name = true;277$errors = array();278279if (!strlen($new_name)) {280$e_name = pht('Required');281$errors[] = pht('Rule must have a name.');282}283284$data = null;285try {286$data = phutil_json_decode($request->getStr('rule'));287} catch (PhutilJSONParserException $ex) {288throw new PhutilProxyException(289pht('Failed to decode rule data.'),290$ex);291}292293if (!is_array($data) ||294!$data['conditions'] ||295!$data['actions']) {296throw new Exception(pht('Failed to decode rule data.'));297}298299$conditions = array();300foreach ($data['conditions'] as $condition) {301if ($condition === null) {302// We manage this as a sparse array on the client, so may receive303// NULL if conditions have been removed.304continue;305}306307$obj = new HeraldCondition();308$obj->setFieldName($condition[0]);309$obj->setFieldCondition($condition[1]);310311if (is_array($condition[2])) {312$obj->setValue(array_keys($condition[2]));313} else {314$obj->setValue($condition[2]);315}316317try {318$adapter->willSaveCondition($obj);319} catch (HeraldInvalidConditionException $ex) {320$errors[] = $ex->getMessage();321}322323$conditions[] = $obj;324}325326$actions = array();327foreach ($data['actions'] as $action) {328if ($action === null) {329// Sparse on the client; removals can give us NULLs.330continue;331}332333if (!isset($action[1])) {334// Legitimate for any action which doesn't need a target, like335// "Do nothing".336$action[1] = null;337}338339$obj = new HeraldActionRecord();340$obj->setAction($action[0]);341$obj->setTarget($action[1]);342343try {344$adapter->willSaveAction($rule, $obj);345} catch (HeraldInvalidActionException $ex) {346$errors[] = $ex->getMessage();347}348349$actions[] = $obj;350}351352if (!$errors) {353$new_state = id(new HeraldRuleSerializer())->serializeRuleComponents(354$match_all,355$conditions,356$actions,357$repetition_policy);358359$xactions = array();360361// Until this moves to EditEngine, manually add a "CREATE" transaction362// if we're creating a new rule. This improves rendering of the initial363// group of transactions.364$is_new = (bool)(!$rule->getID());365if ($is_new) {366$xactions[] = id(new HeraldRuleTransaction())367->setTransactionType(PhabricatorTransactions::TYPE_CREATE);368}369370$xactions[] = id(new HeraldRuleTransaction())371->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE)372->setNewValue($new_state);373$xactions[] = id(new HeraldRuleTransaction())374->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE)375->setNewValue($new_name);376377try {378id(new HeraldRuleEditor())379->setActor($this->getViewer())380->setContinueOnNoEffect(true)381->setContentSourceFromRequest($request)382->applyTransactions($rule, $xactions);383return array(null, null);384} catch (Exception $ex) {385$errors[] = $ex->getMessage();386}387}388389// mutate current rule, so it would be sent to the client in the right state390$rule->setMustMatchAll((int)$match_all);391$rule->setName($new_name);392$rule->setRepetitionPolicyStringConstant($repetition_policy);393$rule->attachConditions($conditions);394$rule->attachActions($actions);395396return array($e_name, $errors);397}398399private function setupEditorBehavior(400HeraldRule $rule,401array $handles,402HeraldAdapter $adapter) {403404$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);405$all_rules = msortv($all_rules, 'getEditorSortVector');406$all_rules = mpull($all_rules, 'getEditorDisplayName', 'getPHID');407408$all_fields = $adapter->getFieldNameMap();409$all_conditions = $adapter->getConditionNameMap();410$all_actions = $adapter->getActionNameMap($rule->getRuleType());411412$fields = $adapter->getFields();413$field_map = array_select_keys($all_fields, $fields);414415// Populate any fields which exist in the rule but which we don't know the416// names of, so that saving a rule without touching anything doesn't change417// it.418foreach ($rule->getConditions() as $condition) {419$field_name = $condition->getFieldName();420421if (empty($field_map[$field_name])) {422$field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);423}424}425426$actions = $adapter->getActions($rule->getRuleType());427$action_map = array_select_keys($all_actions, $actions);428429// Populate any actions which exist in the rule but which we don't know the430// names of, so that saving a rule without touching anything doesn't change431// it.432foreach ($rule->getActions() as $action) {433$action_name = $action->getAction();434435if (empty($action_map[$action_name])) {436$action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);437}438}439440$config_info = array();441$config_info['fields'] = $this->getFieldGroups($adapter, $field_map);442$config_info['conditions'] = $all_conditions;443$config_info['actions'] = $this->getActionGroups($adapter, $action_map);444$config_info['valueMap'] = array();445446foreach ($field_map as $field => $name) {447try {448$field_conditions = $adapter->getConditionsForField($field);449} catch (Exception $ex) {450$field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);451}452$config_info['conditionMap'][$field] = $field_conditions;453}454455foreach ($field_map as $field => $fname) {456foreach ($config_info['conditionMap'][$field] as $condition) {457$value_key = $adapter->getValueTypeForFieldAndCondition(458$field,459$condition);460461if ($value_key instanceof HeraldFieldValue) {462$value_key->setViewer($this->getViewer());463464$spec = $value_key->getControlSpecificationDictionary();465$value_key = $value_key->getFieldValueKey();466$config_info['valueMap'][$value_key] = $spec;467}468469$config_info['values'][$field][$condition] = $value_key;470}471}472473$config_info['rule_type'] = $rule->getRuleType();474475foreach ($action_map as $action => $name) {476try {477$value_key = $adapter->getValueTypeForAction(478$action,479$rule->getRuleType());480} catch (Exception $ex) {481$value_key = new HeraldEmptyFieldValue();482}483484if ($value_key instanceof HeraldFieldValue) {485$value_key->setViewer($this->getViewer());486487$spec = $value_key->getControlSpecificationDictionary();488$value_key = $value_key->getFieldValueKey();489$config_info['valueMap'][$value_key] = $spec;490}491492$config_info['targets'][$action] = $value_key;493}494495$default_group = head($config_info['fields']);496$default_field = head_key($default_group['options']);497$default_condition = head($config_info['conditionMap'][$default_field]);498$default_actions = head($config_info['actions']);499$default_action = head_key($default_actions['options']);500501if ($rule->getConditions()) {502$serial_conditions = array();503foreach ($rule->getConditions() as $condition) {504$value = $adapter->getEditorValueForCondition(505$this->getViewer(),506$condition);507508$serial_conditions[] = array(509$condition->getFieldName(),510$condition->getFieldCondition(),511$value,512);513}514} else {515$serial_conditions = array(516array($default_field, $default_condition, null),517);518}519520if ($rule->getActions()) {521$serial_actions = array();522foreach ($rule->getActions() as $action) {523$value = $adapter->getEditorValueForAction(524$this->getViewer(),525$action);526527$serial_actions[] = array(528$action->getAction(),529$value,530);531}532} else {533$serial_actions = array(534array($default_action, null),535);536}537538Javelin::initBehavior(539'herald-rule-editor',540array(541'root' => 'herald-rule-edit-form',542'default' => array(543'field' => $default_field,544'condition' => $default_condition,545'action' => $default_action,546),547'conditions' => (object)$serial_conditions,548'actions' => (object)$serial_actions,549'template' => $this->buildTokenizerTemplates() + array(550'rules' => $all_rules,551),552'info' => $config_info,553));554}555556private function loadHandlesForRule($rule) {557$phids = array();558559foreach ($rule->getActions() as $action) {560if (!is_array($action->getTarget())) {561continue;562}563foreach ($action->getTarget() as $target) {564$target = (array)$target;565foreach ($target as $phid) {566$phids[] = $phid;567}568}569}570571foreach ($rule->getConditions() as $condition) {572$value = $condition->getValue();573if (is_array($value)) {574foreach ($value as $phid) {575$phids[] = $phid;576}577}578}579580$phids[] = $rule->getAuthorPHID();581582if ($rule->isObjectRule()) {583$phids[] = $rule->getTriggerObjectPHID();584}585586return $this->loadViewerHandles($phids);587}588589590/**591* Render the selector for the "When (all of | any of) these conditions are592* met:" element.593*/594private function renderMustMatchSelector($rule) {595return AphrontFormSelectControl::renderSelectTag(596$rule->getMustMatchAll() ? 'all' : 'any',597array(598'all' => pht('all of'),599'any' => pht('any of'),600),601array(602'name' => 'must_match',603));604}605606607/**608* Render the selector for "Take these actions (every time | only the first609* time) this rule matches..." element.610*/611private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {612$repetition_policy = $rule->getRepetitionPolicyStringConstant();613$repetition_map = $this->getRepetitionOptionMap($adapter);614if (count($repetition_map) < 2) {615return head($repetition_map);616} else {617return AphrontFormSelectControl::renderSelectTag(618$repetition_policy,619$repetition_map,620array(621'name' => 'repetition_policy',622));623}624}625626private function getRepetitionOptionMap(HeraldAdapter $adapter) {627$repetition_options = $adapter->getRepetitionOptions();628$repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap();629return array_select_keys($repetition_names, $repetition_options);630}631632protected function buildTokenizerTemplates() {633$template = new AphrontTokenizerTemplateView();634$template = $template->render();635return array(636'markup' => $template,637);638}639640641/**642* Load rules for the "Another Herald rule..." condition dropdown, which643* allows one rule to depend upon the success or failure of another rule.644*/645private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {646$viewer = $this->getRequest()->getUser();647648// Any rule can depend on a global rule.649$all_rules = id(new HeraldRuleQuery())650->setViewer($viewer)651->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))652->withContentTypes(array($rule->getContentType()))653->execute();654655if ($rule->isObjectRule()) {656// Object rules may depend on other rules for the same object.657$all_rules += id(new HeraldRuleQuery())658->setViewer($viewer)659->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))660->withContentTypes(array($rule->getContentType()))661->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))662->execute();663}664665if ($rule->isPersonalRule()) {666// Personal rules may depend upon your other personal rules.667$all_rules += id(new HeraldRuleQuery())668->setViewer($viewer)669->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))670->withContentTypes(array($rule->getContentType()))671->withAuthorPHIDs(array($rule->getAuthorPHID()))672->execute();673}674675// A rule can not depend upon itself.676unset($all_rules[$rule->getID()]);677678return $all_rules;679}680681private function getFieldGroups(HeraldAdapter $adapter, array $field_map) {682$group_map = array();683foreach ($field_map as $field_key => $field_name) {684$group_key = $adapter->getFieldGroupKey($field_key);685$group_map[$group_key][$field_key] = array(686'name' => $field_name,687'available' => $adapter->isFieldAvailable($field_key),688);689}690691return $this->getGroups(692$group_map,693HeraldFieldGroup::getAllFieldGroups());694}695696private function getActionGroups(HeraldAdapter $adapter, array $action_map) {697$group_map = array();698foreach ($action_map as $action_key => $action_name) {699$group_key = $adapter->getActionGroupKey($action_key);700$group_map[$group_key][$action_key] = array(701'name' => $action_name,702'available' => $adapter->isActionAvailable($action_key),703);704}705706return $this->getGroups(707$group_map,708HeraldActionGroup::getAllActionGroups());709}710711private function getGroups(array $item_map, array $group_list) {712assert_instances_of($group_list, 'HeraldGroup');713714$groups = array();715foreach ($item_map as $group_key => $options) {716asort($options);717718$group_object = idx($group_list, $group_key);719if ($group_object) {720$group_label = $group_object->getGroupLabel();721$group_order = $group_object->getSortKey();722} else {723$group_label = nonempty($group_key, pht('Other'));724$group_order = 'Z';725}726727$groups[] = array(728'label' => $group_label,729'options' => $options,730'order' => $group_order,731);732}733734return array_values(isort($groups, 'order'));735}736737738}739740741