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'), ); } }