Changeset View
Changeset View
Standalone View
Standalone View
src/applications/herald/controller/HeraldNewController.php
<?php | <?php | ||||
final class HeraldNewController extends HeraldController { | final class HeraldNewController extends HeraldController { | ||||
public function handleRequest(AphrontRequest $request) { | public function handleRequest(AphrontRequest $request) { | ||||
$viewer = $request->getViewer(); | $viewer = $this->getViewer(); | ||||
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); | $adapter_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); | ||||
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); | $adapter_type = $request->getStr('adapter'); | ||||
$errors = array(); | |||||
$e_type = null; | if (!isset($adapter_type_map[$adapter_type])) { | ||||
$e_rule = null; | $title = pht('Create Herald Rule'); | ||||
$e_object = null; | $content = $this->newAdapterMenu($title); | ||||
} else { | |||||
$adapter = HeraldAdapter::getAdapterForContentType($adapter_type); | |||||
$step = $request->getInt('step'); | $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); | ||||
if ($request->isFormPost()) { | $rule_type = $request->getStr('type'); | ||||
$content_type = $request->getStr('content_type'); | |||||
if (empty($content_type_map[$content_type])) { | |||||
$errors[] = pht('You must choose a content type for this rule.'); | |||||
$e_type = pht('Required'); | |||||
$step = 0; | |||||
} | |||||
if (!$errors && $step > 1) { | if (!isset($rule_type_map[$rule_type])) { | ||||
$rule_type = $request->getStr('rule_type'); | $title = pht( | ||||
if (empty($rule_type_map[$rule_type])) { | 'Create Herald Rule: %s', | ||||
$errors[] = pht('You must choose a rule type for this rule.'); | $adapter->getAdapterContentName()); | ||||
$e_rule = pht('Required'); | |||||
$step = 1; | |||||
} | |||||
} | |||||
if (!$errors && $step >= 2) { | $content = $this->newTypeMenu($adapter, $title); | ||||
} else { | |||||
if ($rule_type !== HeraldRuleTypeConfig::RULE_TYPE_OBJECT) { | |||||
$target_phid = null; | $target_phid = null; | ||||
$target_okay = true; | |||||
} else { | |||||
$object_name = $request->getStr('objectName'); | $object_name = $request->getStr('objectName'); | ||||
$done = false; | $target_okay = false; | ||||
if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) { | |||||
$done = true; | $errors = array(); | ||||
} else if (strlen($object_name)) { | $e_object = null; | ||||
if ($request->isFormPost()) { | |||||
if (strlen($object_name)) { | |||||
$target_object = id(new PhabricatorObjectQuery()) | $target_object = id(new PhabricatorObjectQuery()) | ||||
->setViewer($viewer) | ->setViewer($viewer) | ||||
->withNames(array($object_name)) | ->withNames(array($object_name)) | ||||
->executeOne(); | ->executeOne(); | ||||
if ($target_object) { | if ($target_object) { | ||||
$can_edit = PhabricatorPolicyFilter::hasCapability( | $can_edit = PhabricatorPolicyFilter::hasCapability( | ||||
$viewer, | $viewer, | ||||
$target_object, | $target_object, | ||||
PhabricatorPolicyCapability::CAN_EDIT); | PhabricatorPolicyCapability::CAN_EDIT); | ||||
if (!$can_edit) { | if (!$can_edit) { | ||||
$errors[] = pht( | $errors[] = pht( | ||||
'You can not create a rule for that object, because you do '. | 'You can not create a rule for that object, because you '. | ||||
'not have permission to edit it. You can only create rules '. | 'do not have permission to edit it. You can only create '. | ||||
'for objects you can edit.'); | 'rules for objects you can edit.'); | ||||
$e_object = pht('Not Editable'); | $e_object = pht('Not Editable'); | ||||
$step = 2; | |||||
} else { | } else { | ||||
$adapter = HeraldAdapter::getAdapterForContentType($content_type); | |||||
if (!$adapter->canTriggerOnObject($target_object)) { | if (!$adapter->canTriggerOnObject($target_object)) { | ||||
$errors[] = pht( | $errors[] = pht( | ||||
'This object is not of an allowed type for the rule. '. | 'This object is not of an allowed type for the rule. '. | ||||
'Rules can only trigger on certain objects.'); | 'Rules can only trigger on certain objects.'); | ||||
$e_object = pht('Invalid'); | $e_object = pht('Invalid'); | ||||
$step = 2; | |||||
} else { | } else { | ||||
$target_phid = $target_object->getPHID(); | $target_phid = $target_object->getPHID(); | ||||
$done = true; | |||||
} | } | ||||
} | } | ||||
} else { | } else { | ||||
$errors[] = pht('No object exists by that name.'); | $errors[] = pht('No object exists by that name.'); | ||||
$e_object = pht('Invalid'); | $e_object = pht('Invalid'); | ||||
$step = 2; | |||||
} | } | ||||
} else if ($step > 2) { | } else { | ||||
$errors[] = pht( | $errors[] = pht( | ||||
'You must choose an object to associate this rule with.'); | 'You must choose an object to associate this rule with.'); | ||||
$e_object = pht('Required'); | $e_object = pht('Required'); | ||||
$step = 2; | |||||
} | } | ||||
if (!$errors && $done) { | $target_okay = !$errors; | ||||
} | |||||
} | |||||
if (!$target_okay) { | |||||
$title = pht('Choose Object'); | |||||
$content = $this->newTargetForm( | |||||
$adapter, | |||||
$rule_type, | |||||
$object_name, | |||||
$errors, | |||||
$e_object, | |||||
$title); | |||||
} else { | |||||
$params = array( | $params = array( | ||||
'content_type' => $content_type, | 'content_type' => $adapter_type, | ||||
'rule_type' => $rule_type, | 'rule_type' => $rule_type, | ||||
'targetPHID' => $target_phid, | 'targetPHID' => $target_phid, | ||||
); | ); | ||||
$uri = new PhutilURI('edit/', $params); | $edit_uri = $this->getApplicationURI('edit/'); | ||||
$uri = $this->getApplicationURI($uri); | $edit_uri = new PhutilURI($edit_uri, $params); | ||||
return id(new AphrontRedirectResponse())->setURI($uri); | |||||
return id(new AphrontRedirectResponse()) | |||||
->setURI($edit_uri); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
$content_type = $request->getStr('content_type'); | $crumbs = $this | ||||
$rule_type = $request->getStr('rule_type'); | ->buildApplicationCrumbs() | ||||
->addTextCrumb(pht('Create Rule')) | |||||
->setBorder(true); | |||||
$form = id(new AphrontFormView()) | $view = id(new PHUITwoColumnView()) | ||||
->setFooter($content); | |||||
return $this->newPage() | |||||
->setTitle($title) | |||||
->setCrumbs($crumbs) | |||||
->appendChild($view); | |||||
} | |||||
private function newAdapterMenu($title) { | |||||
$viewer = $this->getViewer(); | |||||
$types = HeraldAdapter::getEnabledAdapterMap($viewer); | |||||
foreach ($types as $key => $type) { | |||||
$types[$key] = HeraldAdapter::getAdapterForContentType($key); | |||||
} | |||||
$types = msort($types, 'getAdapterContentName'); | |||||
$base_uri = $this->getApplicationURI('create/'); | |||||
$menu = id(new PHUIObjectItemListView()) | |||||
->setViewer($viewer) | |||||
->setBig(true); | |||||
foreach ($types as $key => $adapter) { | |||||
$adapter_uri = id(new PhutilURI($base_uri)) | |||||
->replaceQueryParam('adapter', $key); | |||||
$description = $adapter->getAdapterContentDescription(); | |||||
$description = phutil_escape_html_newlines($description); | |||||
$item = id(new PHUIObjectItemView()) | |||||
->setHeader($adapter->getAdapterContentName()) | |||||
->setImageIcon($adapter->getAdapterContentIcon()) | |||||
->addAttribute($description) | |||||
->setHref($adapter_uri) | |||||
->setClickable(true); | |||||
$menu->addItem($item); | |||||
} | |||||
$box = id(new PHUIObjectBoxView()) | |||||
->setHeaderText($title) | |||||
->setBackground(PHUIObjectBoxView::WHITE_CONFIG) | |||||
->setObjectList($menu); | |||||
return id(new PHUILauncherView()) | |||||
->appendChild($box); | |||||
} | |||||
private function newTypeMenu(HeraldAdapter $adapter, $title) { | |||||
$viewer = $this->getViewer(); | |||||
$global_capability = HeraldManageGlobalRulesCapability::CAPABILITY; | |||||
$can_global = $this->hasApplicationCapability($global_capability); | |||||
if ($can_global) { | |||||
$global_note = pht( | |||||
'You have permission to create and manage global rules.'); | |||||
} else { | |||||
$global_note = pht( | |||||
'You do not have permission to create or manage global rules.'); | |||||
} | |||||
$global_note = phutil_tag('em', array(), $global_note); | |||||
$specs = array( | |||||
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => array( | |||||
'name' => pht('Personal Rule'), | |||||
'icon' => 'fa-user', | |||||
'help' => pht( | |||||
'Personal rules notify you about events. You own them, but they can '. | |||||
'only affect you. Personal rules only trigger for objects you have '. | |||||
'permission to see.'), | |||||
'enabled' => true, | |||||
), | |||||
HeraldRuleTypeConfig::RULE_TYPE_OBJECT => array( | |||||
'name' => pht('Object Rule'), | |||||
'icon' => 'fa-cube', | |||||
'help' => pht( | |||||
'Object rules notify anyone about events. They are bound to an '. | |||||
'object (like a repository) and can only act on that object. You '. | |||||
'must be able to edit an object to create object rules for it. '. | |||||
'Other users who can edit the object can edit its rules.'), | |||||
'enabled' => true, | |||||
), | |||||
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => array( | |||||
'name' => pht('Global Rule'), | |||||
'icon' => 'fa-globe', | |||||
'help' => array( | |||||
pht( | |||||
'Global rules notify anyone about events. Global rules can '. | |||||
'bypass access control policies and act on any object.'), | |||||
$global_note, | |||||
), | |||||
'enabled' => $can_global, | |||||
), | |||||
); | |||||
$adapter_type = $adapter->getAdapterContentType(); | |||||
$base_uri = new PhutilURI($this->getApplicationURI('create/')); | |||||
$adapter_uri = id(clone $base_uri) | |||||
->replaceQueryParam('adapter', $adapter_type); | |||||
$menu = id(new PHUIObjectItemListView()) | |||||
->setUser($viewer) | ->setUser($viewer) | ||||
->setAction($this->getApplicationURI('new/')); | ->setBig(true); | ||||
foreach ($specs as $rule_type => $spec) { | |||||
$type_uri = id(clone $adapter_uri) | |||||
->replaceQueryParam('type', $rule_type); | |||||
$name = $spec['name']; | |||||
$icon = $spec['icon']; | |||||
$description = $spec['help']; | |||||
$description = (array)$description; | |||||
$enabled = $spec['enabled']; | |||||
if ($enabled) { | |||||
$enabled = $adapter->supportsRuleType($rule_type); | |||||
if (!$enabled) { | |||||
$description[] = phutil_tag( | |||||
'em', | |||||
array(), | |||||
pht( | |||||
'This rule type is not supported by the selected '. | |||||
'content type.')); | |||||
} | |||||
} | |||||
$description = phutil_implode_html( | |||||
array( | |||||
phutil_tag('br'), | |||||
phutil_tag('br'), | |||||
), | |||||
$description); | |||||
switch ($step) { | $item = id(new PHUIObjectItemView()) | ||||
case 0: | ->setHeader($name) | ||||
default: | ->setImageIcon($icon) | ||||
$content_types = $this->renderContentTypeControl( | ->addAttribute($description); | ||||
$content_type_map, | |||||
$e_type); | if ($enabled) { | ||||
$item | |||||
$form | ->setHref($type_uri) | ||||
->addHiddenInput('step', 1) | ->setClickable(true); | ||||
->appendChild($content_types); | } else { | ||||
$item->setDisabled(true); | |||||
} | |||||
$cancel_text = null; | $menu->addItem($item); | ||||
$cancel_uri = $this->getApplicationURI(); | } | ||||
$title = pht('Create Herald Rule'); | |||||
break; | $box = id(new PHUIObjectBoxView()) | ||||
case 1: | ->setHeaderText($title) | ||||
$rule_types = $this->renderRuleTypeControl( | ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) | ||||
$rule_type_map, | ->setObjectList($menu); | ||||
$e_rule); | |||||
$box->newTailButton() | |||||
$form | ->setText(pht('Back to Content Types')) | ||||
->addHiddenInput('content_type', $content_type) | ->setIcon('fa-chevron-left') | ||||
->addHiddenInput('step', 2) | ->setHref($base_uri); | ||||
->appendChild($rule_types); | |||||
return id(new PHUILauncherView()) | |||||
->appendChild($box); | |||||
} | |||||
private function newTargetForm( | |||||
HeraldAdapter $adapter, | |||||
$rule_type, | |||||
$object_name, | |||||
$errors, | |||||
$e_object, | |||||
$title) { | |||||
$viewer = $this->getViewer(); | |||||
$content_type = $adapter->getAdapterContentType(); | |||||
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); | |||||
$params = array( | $params = array( | ||||
'content_type' => $content_type, | 'adapter' => $content_type, | ||||
'step' => '0', | 'type' => $rule_type, | ||||
); | ); | ||||
$cancel_text = pht('Back'); | $form = id(new AphrontFormView()) | ||||
$cancel_uri = new PhutilURI('new/', $params); | ->setViewer($viewer) | ||||
$cancel_uri = $this->getApplicationURI($cancel_uri); | |||||
$title = pht('Create Herald Rule: %s', | |||||
idx($content_type_map, $content_type)); | |||||
break; | |||||
case 2: | |||||
$adapter = HeraldAdapter::getAdapterForContentType($content_type); | |||||
$form | |||||
->addHiddenInput('content_type', $content_type) | |||||
->addHiddenInput('rule_type', $rule_type) | |||||
->addHiddenInput('step', 3) | |||||
->appendChild( | ->appendChild( | ||||
id(new AphrontFormStaticControl()) | id(new AphrontFormStaticControl()) | ||||
->setLabel(pht('Rule for')) | ->setLabel(pht('Rule for')) | ||||
->setValue( | ->setValue( | ||||
phutil_tag( | phutil_tag( | ||||
'strong', | 'strong', | ||||
array(), | array(), | ||||
idx($content_type_map, $content_type)))) | $adapter->getAdapterContentName()))) | ||||
->appendChild( | ->appendChild( | ||||
id(new AphrontFormStaticControl()) | id(new AphrontFormStaticControl()) | ||||
->setLabel(pht('Rule Type')) | ->setLabel(pht('Rule Type')) | ||||
->setValue( | ->setValue( | ||||
phutil_tag( | phutil_tag( | ||||
'strong', | 'strong', | ||||
array(), | array(), | ||||
idx($rule_type_map, $rule_type)))) | idx($rule_type_map, $rule_type)))) | ||||
->appendRemarkupInstructions( | ->appendRemarkupInstructions( | ||||
pht( | pht( | ||||
'Choose the object this rule will act on (for example, enter '. | 'Choose the object this rule will act on (for example, enter '. | ||||
'`rX` to act on the `rX` repository, or `#project` to act on '. | '`rX` to act on the `rX` repository, or `#project` to act on '. | ||||
'a project).')) | 'a project).')) | ||||
->appendRemarkupInstructions( | ->appendRemarkupInstructions( | ||||
$adapter->explainValidTriggerObjects()) | $adapter->explainValidTriggerObjects()) | ||||
->appendChild( | ->appendChild( | ||||
id(new AphrontFormTextControl()) | id(new AphrontFormTextControl()) | ||||
->setName('objectName') | ->setName('objectName') | ||||
->setError($e_object) | ->setError($e_object) | ||||
->setValue($request->getStr('objectName')) | ->setValue($object_name) | ||||
->setLabel(pht('Object'))); | ->setLabel(pht('Object'))); | ||||
$params = array( | foreach ($params as $key => $value) { | ||||
'content_type' => $content_type, | $form->addHiddenInput($key, $value); | ||||
'rule_type' => $rule_type, | |||||
'step' => 1, | |||||
); | |||||
$cancel_text = pht('Back'); | |||||
$cancel_uri = new PhutilURI('new/', $params); | |||||
$cancel_uri = $this->getApplicationURI($cancel_uri); | |||||
$title = pht('Create Herald Rule: %s', | |||||
idx($content_type_map, $content_type)); | |||||
break; | |||||
} | } | ||||
$form | $cancel_params = $params; | ||||
->appendChild( | unset($cancel_params['type']); | ||||
$cancel_uri = $this->getApplicationURI('new/'); | |||||
$cancel_uri = new PhutilURI($cancel_uri, $params); | |||||
$form->appendChild( | |||||
id(new AphrontFormSubmitControl()) | id(new AphrontFormSubmitControl()) | ||||
->setValue(pht('Continue')) | ->setValue(pht('Continue')) | ||||
->addCancelButton($cancel_uri, $cancel_text)); | ->addCancelButton($cancel_uri, pht('Back'))); | ||||
$form_box = id(new PHUIObjectBoxView()) | $form_box = id(new PHUIObjectBoxView()) | ||||
->setHeaderText($title) | ->setHeaderText($title) | ||||
->setFormErrors($errors) | ->setFormErrors($errors) | ||||
->setBackground(PHUIObjectBoxView::WHITE_CONFIG) | ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) | ||||
->setForm($form); | ->setForm($form); | ||||
$crumbs = $this | return $form_box; | ||||
->buildApplicationCrumbs() | |||||
->addTextCrumb(pht('Create Rule')) | |||||
->setBorder(true); | |||||
$view = id(new PHUITwoColumnView()) | |||||
->setFooter($form_box); | |||||
return $this->newPage() | |||||
->setTitle($title) | |||||
->setCrumbs($crumbs) | |||||
->appendChild( | |||||
array( | |||||
$view, | |||||
)); | |||||
} | |||||
private function renderContentTypeControl(array $content_type_map, $e_type) { | |||||
$request = $this->getRequest(); | |||||
$radio = id(new AphrontFormRadioButtonControl()) | |||||
->setLabel(pht('New Rule for')) | |||||
->setName('content_type') | |||||
->setValue($request->getStr('content_type')) | |||||
->setError($e_type); | |||||
foreach ($content_type_map as $value => $name) { | |||||
$adapter = HeraldAdapter::getAdapterForContentType($value); | |||||
$radio->addButton( | |||||
$value, | |||||
$name, | |||||
phutil_escape_html_newlines($adapter->getAdapterContentDescription())); | |||||
} | |||||
return $radio; | |||||
} | |||||
private function renderRuleTypeControl(array $rule_type_map, $e_rule) { | |||||
$request = $this->getRequest(); | |||||
// Reorder array to put less powerful rules first. | |||||
$rule_type_map = array_select_keys( | |||||
$rule_type_map, | |||||
array( | |||||
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, | |||||
HeraldRuleTypeConfig::RULE_TYPE_OBJECT, | |||||
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, | |||||
)) + $rule_type_map; | |||||
list($can_global, $global_link) = $this->explainApplicationCapability( | |||||
HeraldManageGlobalRulesCapability::CAPABILITY, | |||||
pht('You have permission to create and manage global rules.'), | |||||
pht('You do not have permission to create or manage global rules.')); | |||||
$captions = array( | |||||
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => | |||||
pht( | |||||
'Personal rules notify you about events. You own them, but they can '. | |||||
'only affect you. Personal rules only trigger for objects you have '. | |||||
'permission to see.'), | |||||
HeraldRuleTypeConfig::RULE_TYPE_OBJECT => | |||||
pht( | |||||
'Object rules notify anyone about events. They are bound to an '. | |||||
'object (like a repository) and can only act on that object. You '. | |||||
'must be able to edit an object to create object rules for it. '. | |||||
'Other users who can edit the object can edit its rules.'), | |||||
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => | |||||
array( | |||||
pht( | |||||
'Global rules notify anyone about events. Global rules can '. | |||||
'bypass access control policies and act on any object.'), | |||||
$global_link, | |||||
), | |||||
); | |||||
$radio = id(new AphrontFormRadioButtonControl()) | |||||
->setLabel(pht('Rule Type')) | |||||
->setName('rule_type') | |||||
->setValue($request->getStr('rule_type')) | |||||
->setError($e_rule); | |||||
$adapter = HeraldAdapter::getAdapterForContentType( | |||||
$request->getStr('content_type')); | |||||
foreach ($rule_type_map as $value => $name) { | |||||
$caption = idx($captions, $value); | |||||
$disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) && | |||||
(!$can_global); | |||||
if (!$adapter->supportsRuleType($value)) { | |||||
$disabled = true; | |||||
$caption = array( | |||||
$caption, | |||||
"\n\n", | |||||
phutil_tag( | |||||
'em', | |||||
array(), | |||||
pht( | |||||
'This rule type is not supported by the selected content type.')), | |||||
); | |||||
} | |||||
$radio->addButton( | |||||
$value, | |||||
$name, | |||||
phutil_escape_html_newlines($caption), | |||||
$disabled ? 'disabled' : null, | |||||
$disabled); | |||||
} | |||||
return $radio; | |||||
} | } | ||||
} | } |