[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class HeraldRuleController extends HeraldController { 4 5 private $id; 6 private $filter; 7 8 public function willProcessRequest(array $data) { 9 $this->id = (int)idx($data, 'id'); 10 } 11 12 public function processRequest() { 13 $request = $this->getRequest(); 14 $user = $request->getUser(); 15 16 $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); 17 $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); 18 19 if ($this->id) { 20 $id = $this->id; 21 $rule = id(new HeraldRuleQuery()) 22 ->setViewer($user) 23 ->withIDs(array($id)) 24 ->requireCapabilities( 25 array( 26 PhabricatorPolicyCapability::CAN_VIEW, 27 PhabricatorPolicyCapability::CAN_EDIT, 28 )) 29 ->executeOne(); 30 if (!$rule) { 31 return new Aphront404Response(); 32 } 33 $cancel_uri = $this->getApplicationURI("rule/{$id}/"); 34 } else { 35 $rule = new HeraldRule(); 36 $rule->setAuthorPHID($user->getPHID()); 37 $rule->setMustMatchAll(1); 38 39 $content_type = $request->getStr('content_type'); 40 $rule->setContentType($content_type); 41 42 $rule_type = $request->getStr('rule_type'); 43 if (!isset($rule_type_map[$rule_type])) { 44 $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; 45 } 46 $rule->setRuleType($rule_type); 47 48 $adapter = HeraldAdapter::getAdapterForContentType( 49 $rule->getContentType()); 50 51 if (!$adapter->supportsRuleType($rule->getRuleType())) { 52 throw new Exception( 53 pht( 54 "This rule's content type does not support the selected rule ". 55 "type.")); 56 } 57 58 if ($rule->isObjectRule()) { 59 $rule->setTriggerObjectPHID($request->getStr('targetPHID')); 60 $object = id(new PhabricatorObjectQuery()) 61 ->setViewer($user) 62 ->withPHIDs(array($rule->getTriggerObjectPHID())) 63 ->requireCapabilities( 64 array( 65 PhabricatorPolicyCapability::CAN_VIEW, 66 PhabricatorPolicyCapability::CAN_EDIT, 67 )) 68 ->executeOne(); 69 if (!$object) { 70 throw new Exception( 71 pht('No valid object provided for object rule!')); 72 } 73 74 if (!$adapter->canTriggerOnObject($object)) { 75 throw new Exception( 76 pht('Object is of wrong type for adapter!')); 77 } 78 } 79 80 $cancel_uri = $this->getApplicationURI(); 81 } 82 83 if ($rule->isGlobalRule()) { 84 $this->requireApplicationCapability( 85 HeraldManageGlobalRulesCapability::CAPABILITY); 86 } 87 88 $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); 89 90 $local_version = id(new HeraldRule())->getConfigVersion(); 91 if ($rule->getConfigVersion() > $local_version) { 92 throw new Exception( 93 pht( 94 'This rule was created with a newer version of Herald. You can not '. 95 'view or edit it in this older version. Upgrade your Phabricator '. 96 'deployment.')); 97 } 98 99 // Upgrade rule version to our version, since we might add newly-defined 100 // conditions, etc. 101 $rule->setConfigVersion($local_version); 102 103 $rule_conditions = $rule->loadConditions(); 104 $rule_actions = $rule->loadActions(); 105 106 $rule->attachConditions($rule_conditions); 107 $rule->attachActions($rule_actions); 108 109 $e_name = true; 110 $errors = array(); 111 if ($request->isFormPost() && $request->getStr('save')) { 112 list($e_name, $errors) = $this->saveRule($adapter, $rule, $request); 113 if (!$errors) { 114 $id = $rule->getID(); 115 $uri = $this->getApplicationURI("rule/{$id}/"); 116 return id(new AphrontRedirectResponse())->setURI($uri); 117 } 118 } 119 120 $must_match_selector = $this->renderMustMatchSelector($rule); 121 $repetition_selector = $this->renderRepetitionSelector($rule, $adapter); 122 123 $handles = $this->loadHandlesForRule($rule); 124 125 require_celerity_resource('herald-css'); 126 127 $content_type_name = $content_type_map[$rule->getContentType()]; 128 $rule_type_name = $rule_type_map[$rule->getRuleType()]; 129 130 $form = id(new AphrontFormView()) 131 ->setUser($user) 132 ->setID('herald-rule-edit-form') 133 ->addHiddenInput('content_type', $rule->getContentType()) 134 ->addHiddenInput('rule_type', $rule->getRuleType()) 135 ->addHiddenInput('save', 1) 136 ->appendChild( 137 // Build this explicitly (instead of using addHiddenInput()) 138 // so we can add a sigil to it. 139 javelin_tag( 140 'input', 141 array( 142 'type' => 'hidden', 143 'name' => 'rule', 144 'sigil' => 'rule', 145 ))) 146 ->appendChild( 147 id(new AphrontFormTextControl()) 148 ->setLabel(pht('Rule Name')) 149 ->setName('name') 150 ->setError($e_name) 151 ->setValue($rule->getName())); 152 153 $trigger_object_control = false; 154 if ($rule->isObjectRule()) { 155 $trigger_object_control = id(new AphrontFormStaticControl()) 156 ->setValue( 157 pht( 158 'This rule triggers for %s.', 159 $handles[$rule->getTriggerObjectPHID()]->renderLink())); 160 } 161 162 163 $form 164 ->appendChild( 165 id(new AphrontFormMarkupControl()) 166 ->setValue(pht( 167 'This %s rule triggers for %s.', 168 phutil_tag('strong', array(), $rule_type_name), 169 phutil_tag('strong', array(), $content_type_name)))) 170 ->appendChild($trigger_object_control) 171 ->appendChild( 172 id(new PHUIFormInsetView()) 173 ->setTitle(pht('Conditions')) 174 ->setRightButton(javelin_tag( 175 'a', 176 array( 177 'href' => '#', 178 'class' => 'button green', 179 'sigil' => 'create-condition', 180 'mustcapture' => true, 181 ), 182 pht('New Condition'))) 183 ->setDescription( 184 pht('When %s these conditions are met:', $must_match_selector)) 185 ->setContent(javelin_tag( 186 'table', 187 array( 188 'sigil' => 'rule-conditions', 189 'class' => 'herald-condition-table', 190 ), 191 ''))) 192 ->appendChild( 193 id(new PHUIFormInsetView()) 194 ->setTitle(pht('Action')) 195 ->setRightButton(javelin_tag( 196 'a', 197 array( 198 'href' => '#', 199 'class' => 'button green', 200 'sigil' => 'create-action', 201 'mustcapture' => true, 202 ), 203 pht('New Action'))) 204 ->setDescription(pht( 205 'Take these actions %s this rule matches:', 206 $repetition_selector)) 207 ->setContent(javelin_tag( 208 'table', 209 array( 210 'sigil' => 'rule-actions', 211 'class' => 'herald-action-table', 212 ), 213 ''))) 214 ->appendChild( 215 id(new AphrontFormSubmitControl()) 216 ->setValue(pht('Save Rule')) 217 ->addCancelButton($cancel_uri)); 218 219 $this->setupEditorBehavior($rule, $handles, $adapter); 220 221 $title = $rule->getID() 222 ? pht('Edit Herald Rule') 223 : pht('Create Herald Rule'); 224 225 $form_box = id(new PHUIObjectBoxView()) 226 ->setHeaderText($title) 227 ->setFormErrors($errors) 228 ->setForm($form); 229 230 $crumbs = $this 231 ->buildApplicationCrumbs() 232 ->addTextCrumb($title); 233 234 return $this->buildApplicationPage( 235 array( 236 $crumbs, 237 $form_box, 238 ), 239 array( 240 'title' => pht('Edit Rule'), 241 )); 242 } 243 244 private function saveRule(HeraldAdapter $adapter, $rule, $request) { 245 $rule->setName($request->getStr('name')); 246 $match_all = ($request->getStr('must_match') == 'all'); 247 $rule->setMustMatchAll((int)$match_all); 248 249 $repetition_policy_param = $request->getStr('repetition_policy'); 250 $rule->setRepetitionPolicy( 251 HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)); 252 253 $e_name = true; 254 $errors = array(); 255 256 if (!strlen($rule->getName())) { 257 $e_name = pht('Required'); 258 $errors[] = pht('Rule must have a name.'); 259 } 260 261 $data = json_decode($request->getStr('rule'), true); 262 if (!is_array($data) || 263 !$data['conditions'] || 264 !$data['actions']) { 265 throw new Exception('Failed to decode rule data.'); 266 } 267 268 $conditions = array(); 269 foreach ($data['conditions'] as $condition) { 270 if ($condition === null) { 271 // We manage this as a sparse array on the client, so may receive 272 // NULL if conditions have been removed. 273 continue; 274 } 275 276 $obj = new HeraldCondition(); 277 $obj->setFieldName($condition[0]); 278 $obj->setFieldCondition($condition[1]); 279 280 if (is_array($condition[2])) { 281 $obj->setValue(array_keys($condition[2])); 282 } else { 283 $obj->setValue($condition[2]); 284 } 285 286 try { 287 $adapter->willSaveCondition($obj); 288 } catch (HeraldInvalidConditionException $ex) { 289 $errors[] = $ex->getMessage(); 290 } 291 292 $conditions[] = $obj; 293 } 294 295 $actions = array(); 296 foreach ($data['actions'] as $action) { 297 if ($action === null) { 298 // Sparse on the client; removals can give us NULLs. 299 continue; 300 } 301 302 if (!isset($action[1])) { 303 // Legitimate for any action which doesn't need a target, like 304 // "Do nothing". 305 $action[1] = null; 306 } 307 308 $obj = new HeraldAction(); 309 $obj->setAction($action[0]); 310 $obj->setTarget($action[1]); 311 312 try { 313 $adapter->willSaveAction($rule, $obj); 314 } catch (HeraldInvalidActionException $ex) { 315 $errors[] = $ex; 316 } 317 318 $actions[] = $obj; 319 } 320 321 $rule->attachConditions($conditions); 322 $rule->attachActions($actions); 323 324 if (!$errors) { 325 $edit_action = $rule->getID() ? 'edit' : 'create'; 326 327 $rule->openTransaction(); 328 $rule->save(); 329 $rule->saveConditions($conditions); 330 $rule->saveActions($actions); 331 $rule->logEdit($request->getUser()->getPHID(), $edit_action); 332 $rule->saveTransaction(); 333 } 334 335 return array($e_name, $errors); 336 } 337 338 private function setupEditorBehavior( 339 HeraldRule $rule, 340 array $handles, 341 HeraldAdapter $adapter) { 342 343 $serial_conditions = array( 344 array('default', 'default', ''), 345 ); 346 347 if ($rule->getConditions()) { 348 $serial_conditions = array(); 349 foreach ($rule->getConditions() as $condition) { 350 351 $value = $condition->getValue(); 352 switch ($condition->getFieldName()) { 353 case HeraldAdapter::FIELD_TASK_PRIORITY: 354 $value_map = array(); 355 $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); 356 foreach ($value as $priority) { 357 $value_map[$priority] = idx($priority_map, $priority); 358 } 359 $value = $value_map; 360 break; 361 case HeraldAdapter::FIELD_TASK_STATUS: 362 $value_map = array(); 363 $status_map = ManiphestTaskStatus::getTaskStatusMap(); 364 foreach ($value as $status) { 365 $value_map[$status] = idx($status_map, $status); 366 } 367 $value = $value_map; 368 break; 369 default: 370 if (is_array($value)) { 371 $value_map = array(); 372 foreach ($value as $k => $fbid) { 373 $value_map[$fbid] = $handles[$fbid]->getName(); 374 } 375 $value = $value_map; 376 } 377 break; 378 } 379 $serial_conditions[] = array( 380 $condition->getFieldName(), 381 $condition->getFieldCondition(), 382 $value, 383 ); 384 } 385 } 386 387 $serial_actions = array( 388 array('default', ''), 389 ); 390 if ($rule->getActions()) { 391 $serial_actions = array(); 392 foreach ($rule->getActions() as $action) { 393 394 switch ($action->getAction()) { 395 case HeraldAdapter::ACTION_FLAG: 396 case HeraldAdapter::ACTION_BLOCK: 397 $current_value = $action->getTarget(); 398 break; 399 default: 400 if (is_array($action->getTarget())) { 401 $target_map = array(); 402 foreach ((array)$action->getTarget() as $fbid) { 403 $target_map[$fbid] = $handles[$fbid]->getName(); 404 } 405 $current_value = $target_map; 406 } else { 407 $current_value = $action->getTarget(); 408 } 409 break; 410 } 411 412 $serial_actions[] = array( 413 $action->getAction(), 414 $current_value, 415 ); 416 } 417 } 418 419 $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); 420 $all_rules = mpull($all_rules, 'getName', 'getPHID'); 421 asort($all_rules); 422 423 $all_fields = $adapter->getFieldNameMap(); 424 $all_conditions = $adapter->getConditionNameMap(); 425 $all_actions = $adapter->getActionNameMap($rule->getRuleType()); 426 427 $fields = $adapter->getFields(); 428 $field_map = array_select_keys($all_fields, $fields); 429 430 // Populate any fields which exist in the rule but which we don't know the 431 // names of, so that saving a rule without touching anything doesn't change 432 // it. 433 foreach ($rule->getConditions() as $condition) { 434 if (empty($field_map[$condition->getFieldName()])) { 435 $field_map[$condition->getFieldName()] = pht('<Unknown Field>'); 436 } 437 } 438 439 $actions = $adapter->getActions($rule->getRuleType()); 440 $action_map = array_select_keys($all_actions, $actions); 441 442 $config_info = array(); 443 $config_info['fields'] = $field_map; 444 $config_info['conditions'] = $all_conditions; 445 $config_info['actions'] = $action_map; 446 447 foreach ($config_info['fields'] as $field => $name) { 448 $field_conditions = $adapter->getConditionsForField($field); 449 $config_info['conditionMap'][$field] = $field_conditions; 450 } 451 452 foreach ($config_info['fields'] as $field => $fname) { 453 foreach ($config_info['conditionMap'][$field] as $condition) { 454 $value_type = $adapter->getValueTypeForFieldAndCondition( 455 $field, 456 $condition); 457 $config_info['values'][$field][$condition] = $value_type; 458 } 459 } 460 461 $config_info['rule_type'] = $rule->getRuleType(); 462 463 foreach ($config_info['actions'] as $action => $name) { 464 $config_info['targets'][$action] = $adapter->getValueTypeForAction( 465 $action, 466 $rule->getRuleType()); 467 } 468 469 $changeflag_options = 470 PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions(); 471 Javelin::initBehavior( 472 'herald-rule-editor', 473 array( 474 'root' => 'herald-rule-edit-form', 475 'conditions' => (object)$serial_conditions, 476 'actions' => (object)$serial_actions, 477 'select' => array( 478 HeraldAdapter::VALUE_CONTENT_SOURCE => array( 479 'options' => PhabricatorContentSource::getSourceNameMap(), 480 'default' => PhabricatorContentSource::SOURCE_WEB, 481 ), 482 HeraldAdapter::VALUE_FLAG_COLOR => array( 483 'options' => PhabricatorFlagColor::getColorNameMap(), 484 'default' => PhabricatorFlagColor::COLOR_BLUE, 485 ), 486 HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array( 487 'options' => array( 488 PhabricatorRepositoryPushLog::REFTYPE_BRANCH 489 => pht('branch (git/hg)'), 490 PhabricatorRepositoryPushLog::REFTYPE_TAG 491 => pht('tag (git)'), 492 PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK 493 => pht('bookmark (hg)'), 494 ), 495 'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH, 496 ), 497 HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array( 498 'options' => $changeflag_options, 499 'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD, 500 ), 501 ), 502 'template' => $this->buildTokenizerTemplates($handles) + array( 503 'rules' => $all_rules, 504 ), 505 'author' => array( 506 $rule->getAuthorPHID() => 507 $handles[$rule->getAuthorPHID()]->getName(), 508 ), 509 'info' => $config_info, 510 )); 511 } 512 513 private function loadHandlesForRule($rule) { 514 $phids = array(); 515 516 foreach ($rule->getActions() as $action) { 517 if (!is_array($action->getTarget())) { 518 continue; 519 } 520 foreach ($action->getTarget() as $target) { 521 $target = (array)$target; 522 foreach ($target as $phid) { 523 $phids[] = $phid; 524 } 525 } 526 } 527 528 foreach ($rule->getConditions() as $condition) { 529 $value = $condition->getValue(); 530 if (is_array($value)) { 531 foreach ($value as $phid) { 532 $phids[] = $phid; 533 } 534 } 535 } 536 537 $phids[] = $rule->getAuthorPHID(); 538 539 if ($rule->isObjectRule()) { 540 $phids[] = $rule->getTriggerObjectPHID(); 541 } 542 543 return $this->loadViewerHandles($phids); 544 } 545 546 547 /** 548 * Render the selector for the "When (all of | any of) these conditions are 549 * met:" element. 550 */ 551 private function renderMustMatchSelector($rule) { 552 return AphrontFormSelectControl::renderSelectTag( 553 $rule->getMustMatchAll() ? 'all' : 'any', 554 array( 555 'all' => pht('all of'), 556 'any' => pht('any of'), 557 ), 558 array( 559 'name' => 'must_match', 560 )); 561 } 562 563 564 /** 565 * Render the selector for "Take these actions (every time | only the first 566 * time) this rule matches..." element. 567 */ 568 private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { 569 $repetition_policy = HeraldRepetitionPolicyConfig::toString( 570 $rule->getRepetitionPolicy()); 571 572 $repetition_options = $adapter->getRepetitionOptions(); 573 $repetition_names = HeraldRepetitionPolicyConfig::getMap(); 574 $repetition_map = array_select_keys($repetition_names, $repetition_options); 575 576 if (count($repetition_map) < 2) { 577 return head($repetition_names); 578 } else { 579 return AphrontFormSelectControl::renderSelectTag( 580 $repetition_policy, 581 $repetition_map, 582 array( 583 'name' => 'repetition_policy', 584 )); 585 } 586 } 587 588 589 protected function buildTokenizerTemplates(array $handles) { 590 $template = new AphrontTokenizerTemplateView(); 591 $template = $template->render(); 592 593 $sources = array( 594 'repository' => new DiffusionRepositoryDatasource(), 595 'legaldocuments' => new LegalpadDocumentDatasource(), 596 'taskpriority' => new ManiphestTaskPriorityDatasource(), 597 'taskstatus' => new ManiphestTaskStatusDatasource(), 598 'buildplan' => new HarbormasterBuildPlanDatasource(), 599 'arcanistprojects' => new DiffusionArcanistProjectDatasource(), 600 'package' => new PhabricatorOwnersPackageDatasource(), 601 'project' => new PhabricatorProjectDatasource(), 602 'user' => new PhabricatorPeopleDatasource(), 603 'email' => new PhabricatorMetaMTAMailableDatasource(), 604 'userorproject' => new PhabricatorProjectOrUserDatasource(), 605 ); 606 607 foreach ($sources as $key => $source) { 608 $sources[$key] = array( 609 'uri' => $source->getDatasourceURI(), 610 'placeholder' => $source->getPlaceholderText(), 611 ); 612 } 613 614 return array( 615 'source' => $sources, 616 'username' => $this->getRequest()->getUser()->getUserName(), 617 'icons' => mpull($handles, 'getTypeIcon', 'getPHID'), 618 'markup' => $template, 619 ); 620 } 621 622 623 /** 624 * Load rules for the "Another Herald rule..." condition dropdown, which 625 * allows one rule to depend upon the success or failure of another rule. 626 */ 627 private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) { 628 $viewer = $this->getRequest()->getUser(); 629 630 // Any rule can depend on a global rule. 631 $all_rules = id(new HeraldRuleQuery()) 632 ->setViewer($viewer) 633 ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL)) 634 ->withContentTypes(array($rule->getContentType())) 635 ->execute(); 636 637 if ($rule->isObjectRule()) { 638 // Object rules may depend on other rules for the same object. 639 $all_rules += id(new HeraldRuleQuery()) 640 ->setViewer($viewer) 641 ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT)) 642 ->withContentTypes(array($rule->getContentType())) 643 ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID())) 644 ->execute(); 645 } 646 647 if ($rule->isPersonalRule()) { 648 // Personal rules may depend upon your other personal rules. 649 $all_rules += id(new HeraldRuleQuery()) 650 ->setViewer($viewer) 651 ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL)) 652 ->withContentTypes(array($rule->getContentType())) 653 ->withAuthorPHIDs(array($rule->getAuthorPHID())) 654 ->execute(); 655 } 656 657 // mark disabled rules as disabled since they are not useful as such; 658 // don't filter though to keep edit cases sane / expected 659 foreach ($all_rules as $current_rule) { 660 if ($current_rule->getIsDisabled()) { 661 $current_rule->makeEphemeral(); 662 $current_rule->setName($rule->getName().' '.pht('(Disabled)')); 663 } 664 } 665 666 // A rule can not depend upon itself. 667 unset($all_rules[$rule->getID()]); 668 669 return $all_rules; 670 } 671 672 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |