diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php
index c3a277c087..78b9f96e0c 100644
--- a/src/applications/config/controller/PhabricatorConfigEditController.php
+++ b/src/applications/config/controller/PhabricatorConfigEditController.php
@@ -1,492 +1,503 @@
key = $data['key'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
if (empty($options[$this->key])) {
// This may be a dead config entry, which existed in the past but no
// longer exists. Allow it to be edited so it can be reviewed and
// deleted.
$option = id(new PhabricatorConfigOption())
->setKey($this->key)
->setType('wild')
->setDefault(null)
->setDescription(
pht(
"This configuration option is unknown. It may be misspelled, ".
"or have existed in a previous version of Phabricator."));
$group = null;
$group_uri = $this->getApplicationURI();
} else {
$option = $options[$this->key];
$group = $option->getGroup();
$group_uri = $this->getApplicationURI('group/'.$group->getKey().'/');
}
$issue = $request->getStr('issue');
if ($issue) {
// If the user came here from an open setup issue, send them back.
$done_uri = $this->getApplicationURI('issue/'.$issue.'/');
} else {
$done_uri = $group_uri;
}
// Check if the config key is already stored in the database.
// Grab the value if it is.
$config_entry = id(new PhabricatorConfigEntry())
->loadOneWhere(
'configKey = %s AND namespace = %s',
$this->key,
'default');
if (!$config_entry) {
$config_entry = id(new PhabricatorConfigEntry())
->setConfigKey($this->key)
->setNamespace('default')
->setIsDeleted(true);
}
$e_value = null;
$errors = array();
if ($request->isFormPost() && !$option->getLocked()) {
$result = $this->readRequest(
$option,
$request);
list($e_value, $value_errors, $display_value, $xaction) = $result;
$errors = array_merge($errors, $value_errors);
if (!$errors) {
$editor = id(new PhabricatorConfigEditor())
->setActor($user)
->setContinueOnNoEffect(true)
->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
)));
try {
$editor->applyTransactions($config_entry, array($xaction));
return id(new AphrontRedirectResponse())->setURI($done_uri);
} catch (PhabricatorConfigValidationException $ex) {
$e_value = pht('Invalid');
$errors[] = $ex->getMessage();
}
}
} else {
$display_value = $this->getDisplayValue($option, $config_entry);
}
$form = new AphrontFormView();
$form->setFlexible(true);
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle(pht('You broke everything!'))
->setErrors($errors);
} else if ($option->getHidden()) {
$msg = pht(
"This configuration is hidden and can not be edited or viewed from ".
"the web interface.");
$error_view = id(new AphrontErrorView())
->setTitle(pht('Configuration Hidden'))
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->appendChild('
'.phutil_escape_html($msg).'
');
} else if ($option->getLocked()) {
$msg = pht(
"This configuration is locked and can not be edited from the web ".
"interface.");
$error_view = id(new AphrontErrorView())
->setTitle(pht('Configuration Locked'))
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->appendChild(''.phutil_escape_html($msg).'
');
}
if ($option->getHidden()) {
$control = null;
} else {
$control = $this->renderControl(
$option,
$display_value,
$e_value);
}
$engine = new PhabricatorMarkupEngine();
$engine->addObject($option, 'description');
$engine->process();
$description = phutil_render_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($option, 'description'));
$form
->setUser($user)
->addHiddenInput('issue', $request->getStr('issue'))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Description'))
->setValue($description))
->appendChild($control);
$submit_control = id(new AphrontFormSubmitControl())
->addCancelButton($done_uri);
if (!$option->getLocked()) {
$submit_control->setValue(pht('Save Config Entry'));
}
$form->appendChild($submit_control);
$examples = $this->renderExamples($option);
if ($examples) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Examples'))
->setValue($examples));
}
if (!$option->getHidden()) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Default'))
->setValue($this->renderDefaults($option)));
}
$title = pht('Edit %s', $this->key);
$short = pht('Edit');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Config'))
->setHref($this->getApplicationURI()));
if ($group) {
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($group->getName())
->setHref($group_uri));
}
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($this->key)
->setHref('/config/edit/'.$this->key));
$xactions = id(new PhabricatorConfigTransactionQuery())
->withObjectPHIDs(array($config_entry->getPHID()))
->setViewer($user)
->execute();
$xaction_view = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setTransactions($xactions);
return $this->buildApplicationPage(
array(
$crumbs,
id(new PhabricatorHeaderView())->setHeader($title),
$error_view,
$form,
$xaction_view,
),
array(
'title' => $title,
'device' => true,
));
}
private function readRequest(
PhabricatorConfigOption $option,
AphrontRequest $request) {
$xaction = new PhabricatorConfigTransaction();
$xaction->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT);
$e_value = null;
$errors = array();
$value = $request->getStr('value');
if (!strlen($value)) {
$value = null;
$xaction->setNewValue(
array(
'deleted' => true,
'value' => null,
));
return array($e_value, $errors, $value, $xaction);
}
$type = $option->getType();
$set_value = null;
switch ($type) {
case 'int':
if (preg_match('/^-?[0-9]+$/', trim($value))) {
$set_value = (int)$value;
} else {
$e_value = pht('Invalid');
$errors[] = pht('Value must be an integer.');
}
break;
case 'string':
+ case 'enum':
$set_value = (string)$value;
break;
case 'list':
$set_value = $request->getStrList('value');
break;
case 'set':
$set_value = array_fill_keys($request->getStrList('value'), true);
break;
case 'bool':
switch ($value) {
case 'true':
$set_value = true;
break;
case 'false':
$set_value = false;
break;
default:
$e_value = pht('Invalid');
$errors[] = pht('Value must be boolean, "true" or "false".');
break;
}
break;
case 'class':
if (!class_exists($value)) {
$e_value = pht('Invalid');
$errors[] = pht('Class does not exist.');
} else {
$base = $option->getBaseClass();
if (!is_subclass_of($value, $base)) {
$e_value = pht('Invalid');
$errors[] = pht('Class is not of valid type.');
} else {
$set_value = $value;
}
}
break;
default:
$json = json_decode($value, true);
if ($json === null && strtolower($value) != 'null') {
$e_value = pht('Invalid');
$errors[] = pht(
'The given value must be valid JSON. This means, among '.
'other things, that you must wrap strings in double-quotes.');
} else {
$set_value = $json;
}
break;
}
if (!$errors) {
$xaction->setNewValue(
array(
'deleted' => false,
'value' => $set_value,
));
} else {
$xaction = null;
}
return array($e_value, $errors, $value, $xaction);
}
private function getDisplayValue(
PhabricatorConfigOption $option,
PhabricatorConfigEntry $entry) {
if ($entry->getIsDeleted()) {
return null;
}
$type = $option->getType();
$value = $entry->getValue();
switch ($type) {
case 'int':
case 'string':
+ case 'enum':
return $value;
case 'bool':
return $value ? 'true' : 'false';
case 'list':
return implode("\n", nonempty($value, array()));
case 'set':
return implode("\n", nonempty(array_keys($value), array()));
default:
return PhabricatorConfigJSON::prettyPrintJSON($value);
}
}
private function renderControl(
PhabricatorConfigOption $option,
$display_value,
$e_value) {
$type = $option->getType();
switch ($type) {
case 'int':
case 'string':
$control = id(new AphrontFormTextControl());
break;
case 'bool':
$control = id(new AphrontFormSelectControl())
->setOptions(
array(
'' => pht('(Use Default)'),
'true' => idx($option->getBoolOptions(), 0),
'false' => idx($option->getBoolOptions(), 1),
));
break;
+ case 'enum':
+ $options = array_mergev(
+ array(
+ array('' => pht('(Use Default)')),
+ $option->getEnumOptions(),
+ ));
+ $control = id(new AphrontFormSelectControl())
+ ->setOptions($options);
+ break;
case 'class':
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass($option->getBaseClass())
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
$names = ipull($symbols, 'name', 'name');
asort($names);
$names = array(
'' => pht('(Use Default)'),
) + $names;
$control = id(new AphrontFormSelectControl())
->setOptions($names);
break;
case 'list':
case 'set':
$control = id(new AphrontFormTextAreaControl())
->setCaption(pht('Separate values with newlines or commas.'));
break;
default:
$control = id(new AphrontFormTextAreaControl())
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setCustomClass('PhabricatorMonospaced')
->setCaption(pht('Enter value in JSON.'));
break;
}
$control
->setLabel('Value')
->setError($e_value)
->setValue($display_value)
->setName('value');
if ($option->getLocked()) {
$control->setDisabled(true);
}
return $control;
}
private function renderExamples(PhabricatorConfigOption $option) {
$examples = $option->getExamples();
if (!$examples) {
return null;
}
$table = array();
$table[] = '';
$table[] = '| '.pht('Example').' | ';
$table[] = ''.pht('Value').' | ';
$table[] = '
';
foreach ($examples as $example) {
list($value, $description) = $example;
if ($value === null) {
$value = ''.pht('(empty)').'';
} else {
$value = nl2br(phutil_escape_html($value));
}
$table[] = '';
$table[] = '| '.phutil_escape_html($description).' | ';
$table[] = ''.$value.' | ';
$table[] = '
';
}
require_celerity_resource('config-options-css');
return phutil_render_tag(
'table',
array(
'class' => 'config-option-table',
),
implode("\n", $table));
}
private function renderDefaults(PhabricatorConfigOption $option) {
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
/*
TODO: Once DatabaseSource lands, do this:
foreach ($stack as $key => $source) {
unset($stack[$key]);
if ($source instanceof PhabricatorConfigDatabaseSource) {
break;
}
}
*/
$table = array();
$table[] = '';
$table[] = '| '.pht('Source').' | ';
$table[] = ''.pht('Value').' | ';
$table[] = '
';
foreach ($stack as $key => $source) {
$value = $source->getKeys(
array(
$option->getKey(),
));
if (!array_key_exists($option->getKey(), $value)) {
$value = ''.pht('(empty)').'';
} else {
$value = PhabricatorConfigJSON::prettyPrintJSON(
$value[$option->getKey()]);
}
$table[] = '';
$table[] = '| '.phutil_escape_html($source->getName()).' | ';
$table[] = ''.$value.' | ';
$table[] = '
';
}
require_celerity_resource('config-options-css');
return phutil_render_tag(
'table',
array(
'class' => 'config-option-table',
),
implode("\n", $table));
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
index df678b79a1..d3becd3296 100644
--- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
@@ -1,99 +1,100 @@
setName('set')
->setExamples('**set** __key__ __value__')
->setSynopsis('Set a local configuration value.')
->setArguments(
array(
array(
'name' => 'args',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$argv = $args->getArg('args');
if (count($argv) == 0) {
throw new PhutilArgumentUsageException(
"Specify a configuration key and a value to set it to.");
}
$key = $argv[0];
if (count($argv) == 1) {
throw new PhutilArgumentUsageException(
"Specify a value to set the key '{$key}' to.");
}
$value = $argv[1];
if (count($argv) > 2) {
throw new PhutilArgumentUsageException(
"Too many arguments: expected one key and one value.");
}
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
if (empty($options[$key])) {
throw new PhutilArgumentUsageException(
"No such configuration key '{$key}'! Use `config list` to list all ".
"keys.");
}
$option = $options[$key];
$type = $option->getType();
switch ($type) {
case 'string':
case 'class':
+ case 'enum':
$value = (string)$value;
break;
case 'int':
if (!ctype_digit($value)) {
throw new PhutilArgumentUsageException(
"Config key '{$key}' is of type '{$type}'. Specify an integer.");
}
$value = (int)$value;
break;
case 'bool':
if ($value == 'true') {
$value = true;
} else if ($value == 'false') {
$value = false;
} else {
throw new PhutilArgumentUsageException(
"Config key '{$key}' is of type '{$type}'. ".
"Specify 'true' or 'false'.");
}
break;
default:
$value = json_decode($value, true);
if (!is_array($value)) {
throw new PhutilArgumentUsageException(
"Config key '{$key}' is of type '{$type}'. Specify it in JSON.");
}
break;
}
try {
$option->getGroup()->validateOption($option, $value);
} catch (PhabricatorConfigValidationException $validation) {
// Convert this into a usage exception so we don't dump a stack trace.
throw new PhutilArgumentUsageException($validation->getMessage());
}
$config = new PhabricatorConfigLocalSource();
$config->setKeys(array($key => $value));
$console->writeOut(
pht("Set '%s' in local configuration.", $key)."\n");
}
}
diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php
index df3a780ab4..5d0b5a286f 100644
--- a/src/applications/config/option/PhabricatorConfigOption.php
+++ b/src/applications/config/option/PhabricatorConfigOption.php
@@ -1,205 +1,220 @@
baseClass = $base_class;
return $this;
}
public function getBaseClass() {
return $this->baseClass;
}
public function setMasked($masked) {
$this->masked = $masked;
return $this;
}
public function getMasked() {
if ($this->masked) {
return true;
}
if ($this->getHidden()) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.mask'),
$this->getKey(),
false);
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
if ($this->hidden) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.hide'),
$this->getKey(),
false);
}
public function setLocked($locked) {
$this->locked = $locked;
return $this;
}
public function getLocked() {
if ($this->locked) {
return true;
}
if ($this->getHidden()) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.lock'),
$this->getKey(),
false);
}
public function addExample($value, $description) {
$this->examples[] = array($value, $description);
return $this;
}
public function getExamples() {
return $this->examples;
}
public function setGroup(PhabricatorApplicationConfigOptions $group) {
$this->group = $group;
return $this;
}
public function getGroup() {
return $this->group;
}
public function setBoolOptions(array $options) {
$this->boolOptions = $options;
return $this;
}
public function getBoolOptions() {
if ($this->boolOptions) {
return $this->boolOptions;
}
return array(
pht('True'),
pht('False'),
);
}
+ public function setEnumOptions(array $options) {
+ $this->enumOptions = $options;
+ return $this;
+ }
+
+ public function getEnumOptions() {
+ if ($this->enumOptions) {
+ return $this->enumOptions;
+ }
+
+ throw new Exception(
+ 'Call setEnumOptions() before trying to access them!');
+ }
+
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setDefault($default) {
$this->default = $default;
return $this;
}
public function getDefault() {
return $this->default;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
if (empty($this->summary)) {
return $this->getDescription();
}
return $this->summary;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
return $this->getKey().':'.$field;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
switch ($field) {
case 'description':
$text = $this->getDescription();
break;
case 'summary':
$text = $this->getSummary();
break;
}
// TODO: We should probably implement this as a real Markup rule, but
// markup rules are a bit of a mess right now and it doesn't hurt us to
// fake this.
$text = preg_replace(
'/{{([^}]+)}}/',
'[[/config/edit/\\1/ | \\1]]',
$text);
return $text;
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return false;
}
}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
index b3c188f335..19437ffffe 100644
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -1,327 +1,337 @@
deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<deformat(pht(<<'
- `real`: 'George Washington '
- `full`: 'gwashington (George Washington) '
The default is `full`.
EODOC
));
return array(
$this->newOption(
'metamta.default-address',
'string',
'noreply@phabricator.example.com')
->setDescription(pht('Default "From" address.')),
$this->newOption(
'metamta.domain',
'string',
'phabricator.example.com')
->setDescription(pht('Domain used to generate Message-IDs.')),
$this->newOption(
'metamta.mail-adapter',
'class',
'PhabricatorMailImplementationPHPMailerLiteAdapter')
->setBaseClass('PhabricatorMailImplementationAdapter')
->setSummary(pht('Control how mail is sent.'))
->setDescription($adapter_description),
$this->newOption(
'metamta.one-mail-per-recipient',
'bool',
true)
->setBoolOptions(
array(
pht("Send Mail To Each Recipient"),
pht("Send Mail To All Recipients"),
))
->setSummary(
pht(
'Controls whether Phabricator sends one email with multiple '.
'recipients in the "To:" line, or multiple emails, each with a '.
'single recipient in the "To:" line.'))
->setDescription($one_mail_per_recipient_desc),
$this->newOption('metamta.can-send-as-user', 'bool', false)
->setBoolOptions(
array(
pht("Send as User Taking Action"),
pht("Send as Phabricator"),
))
->setSummary(
pht(
'Controls whether Phabricator sends email "From" users.'))
->setDescription($send_as_user_desc),
$this->newOption('metamta.reply.show-hints', 'bool', true)
->setBoolOptions(
array(
pht("Show Reply Handler Hints"),
pht("No Reply Handler Hints"),
))
->setSummary(pht('Show hints about Herald rules in email.'))
->setDescription($herald_hints_description),
$this->newOption('metamta.herald.show-hints', 'bool', true)
->setBoolOptions(
array(
pht("Show Herald Hints"),
pht("No Herald Hints"),
))
->setSummary(pht('Show hints about reply handler actions in email.'))
->setDescription($reply_hints_description),
$this->newOption('metamta.recipients.show-hints', 'bool', true)
->setBoolOptions(
array(
pht("Show Recipient Hints"),
pht("No Recipient Hints"),
))
->setSummary(pht('Show "To:" and "Cc:" footer hints in email.'))
->setDescription($recipient_hints_description),
$this->newOption('metamta.precedence-bulk', 'bool', false)
->setBoolOptions(
array(
pht('Add "Precedence: bulk" Header'),
pht('No "Precedence: bulk" Header'),
))
->setSummary(pht('Control the "Precedence: bulk" header.'))
->setDescription($bulk_description),
$this->newOption('metamta.re-prefix', 'bool', false)
->setBoolOptions(
array(
pht('Force "Re:" Subject Prefix'),
pht('No "Re:" Subject Prefix'),
))
->setSummary(pht('Control "Re:" subject prefix, for Mail.app.'))
->setDescription($re_prefix_description),
$this->newOption('metamta.vary-subjects', 'bool', true)
->setBoolOptions(
array(
pht('Allow Varied Subjects'),
pht('Always Use the Same Thread Subject'),
))
->setSummary(pht('Control subject variance, for some mail clients.'))
->setDescription($vary_subjects_description),
$this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false)
->setBoolOptions(
array(
pht('Allow Insecure Reply-To Auth'),
pht('Disallow Reply-To Auth'),
))
->setSummary(pht('Trust "Reply-To" headers for authentication.'))
->setDescription($reply_to_description),
$this->newOption('metamta.send-immediately', 'bool', true)
->setBoolOptions(
array(
pht('Send Immediately (Slow)'),
pht('Send Via Daemons (Must Run Daemons)'),
))
->setSummary(pht('Improve performance by sending email via daemons.'))
->setDescription($immediately_description),
$this->newOption('metamta.placeholder-to-recipient', 'string', null)
->setSummary(pht('Placeholder for mail with only CCs.'))
->setDescription($placeholder_description),
$this->newOption('metamta.public-replies', 'bool', false)
->setBoolOptions(
array(
pht('Use Public Replies (Less Secure)'),
pht('Use Private Replies (More Secure)'),
))
->setSummary(
pht(
'Phabricator can use less-secure but mailing list friendly public '.
'reply addresses.'))
->setDescription($public_replies_description),
$this->newOption('metamta.single-reply-handler-prefix', 'string', null)
->setSummary(
pht('Allow Phabricator to use a single mailbox for all replies.'))
->setDescription($single_description),
- // TODO: 'enum'
- $this->newOption('metamta.user-address-format', 'string', 'full')
+ $this->newOption('metamta.user-address-format', 'enum', 'full')
+ ->setEnumOptions(
+ array(
+ 'short' => 'short',
+ 'real' => 'real',
+ 'full' => 'full',
+ ))
->setSummary(pht('Control how Phabricator renders user names in mail.'))
- ->setDescription($address_description),
+ ->setDescription($address_description)
+ ->addExample('gwashington ', 'short')
+ ->addExample('George Washington ', 'real')
+ ->addExample(
+ 'gwashington (George Washington) ',
+ 'full'),
);
}
}