[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/herald/controller/ -> HeraldRuleController.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1