diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index bd0079750f..18815ab83b 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -1,538 +1,549 @@ key = $data['key']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $options = PhabricatorApplicationConfigOptions::loadAllOptions(); if (empty($options[$this->key])) { $ancient = PhabricatorSetupCheckExtraConfig::getAncientConfig(); if (isset($ancient[$this->key])) { $desc = pht( "This configuration has been removed. You can safely delete ". "it.\n\n%s", $ancient[$this->key]); } else { $desc = pht( "This configuration option is unknown. It may be misspelled, ". "or have existed in a previous version of Phabricator."); } // 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($desc); $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); $config_entry->setPHID($config_entry->generatePHID()); } $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) ->setContentSourceFromRequest($request); 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); + if ($config_entry->getIsDeleted()) { + $display_value = null; + } else { + $display_value = $this->getDisplayValue( + $option, + $config_entry, + $config_entry->getValue()); + } } $form = new AphrontFormView(); $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->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_tag('p', array(), $msg)); } else if ($option->getLocked()) { $msg = pht( "This configuration is locked and can not be edited from the web ". "interface. Use `./bin/config` in `phabricator/` to edit it."); $error_view = id(new AphrontErrorView()) ->setTitle(pht('Configuration Locked')) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->appendChild(phutil_tag('p', array(), $msg)); } if ($option->getHidden()) { $control = null; } else { $control = $this->renderControl( $option, $display_value, $e_value); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->addObject($option, 'description'); $engine->process(); $description = phutil_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)); if ($group) { $extra = $group->renderContextualDescription( $option, $request); if ($extra !== null) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setValue($extra)); } } $form ->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))); + ->setValue($this->renderDefaults($option, $config_entry))); } $title = pht('Edit %s', $this->key); $short = pht('Edit'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); if ($error_view) { $form_box->setErrorView($error_view); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Config'), $this->getApplicationURI()); if ($group) { $crumbs->addTextCrumb($group->getName(), $group_uri); } $crumbs->addTextCrumb($this->key, '/config/edit/'.$this->key); $xactions = id(new PhabricatorConfigTransactionQuery()) ->withObjectPHIDs(array($config_entry->getPHID())) ->setViewer($user) ->execute(); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($config_entry->getPHID()) ->setTransactions($xactions); return $this->buildApplicationPage( array( $crumbs, $form_box, $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); } if ($option->isCustomType()) { $info = $option->getCustomObject()->readRequest($option, $request); list($e_value, $errors, $set_value, $value) = $info; } else { $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': case 'list': $set_value = phutil_split_lines( $request->getStr('value'), $retain_endings = false); foreach ($set_value as $key => $v) { if (!strlen($v)) { unset($set_value[$key]); } } $set_value = array_values($set_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; - } + PhabricatorConfigEntry $entry, + $value) { if ($option->isCustomType()) { - return $option->getCustomObject()->getDisplayValue($option, $entry); + return $option->getCustomObject()->getDisplayValue( + $option, + $entry, + $value); } else { $type = $option->getType(); - $value = $entry->getValue(); switch ($type) { case 'int': case 'string': case 'enum': case 'class': return $value; case 'bool': return $value ? 'true' : 'false'; case 'list': 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) { if ($option->isCustomType()) { $control = $option->getCustomObject()->renderControl( $option, $display_value, $e_value); } else { $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 'list': $control = id(new AphrontFormTextAreaControl()) ->setCaption(pht('Separate values with newlines.')); break; 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(pht('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[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), pht('Example')), phutil_tag('th', array(), pht('Value')), )); foreach ($examples as $example) { list($value, $description) = $example; if ($value === null) { $value = phutil_tag('em', array(), pht('(empty)')); } else { if (is_array($value)) { $value = implode("\n", $value); } } $table[] = phutil_tag('tr', array(), array( phutil_tag('th', array(), $description), phutil_tag('td', array(), $value), )); } require_celerity_resource('config-options-css'); return phutil_tag( 'table', array( 'class' => 'config-option-table', ), $table); } - private function renderDefaults(PhabricatorConfigOption $option) { + private function renderDefaults( + PhabricatorConfigOption $option, + PhabricatorConfigEntry $entry) { + $stack = PhabricatorEnv::getConfigSourceStack(); $stack = $stack->getStack(); $table = array(); $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), pht('Source')), phutil_tag('th', array(), pht('Value')), )); foreach ($stack as $key => $source) { $value = $source->getKeys( array( $option->getKey(), )); if (!array_key_exists($option->getKey(), $value)) { $value = phutil_tag('em', array(), pht('(empty)')); } else { - $value = PhabricatorConfigJSON::prettyPrintJSON( + $value = $this->getDisplayValue( + $option, + $entry, $value[$option->getKey()]); } $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), $source->getName()), phutil_tag('td', array(), $value), )); } require_celerity_resource('config-options-css'); return phutil_tag( 'table', array( 'class' => 'config-option-table', ), $table); } } diff --git a/src/applications/config/custom/PhabricatorConfigJSONOptionType.php b/src/applications/config/custom/PhabricatorConfigJSONOptionType.php index 91250e05c4..84d4ad2441 100644 --- a/src/applications/config/custom/PhabricatorConfigJSONOptionType.php +++ b/src/applications/config/custom/PhabricatorConfigJSONOptionType.php @@ -1,62 +1,50 @@ getStr('value'); $display_value = $request->getStr('value'); if (strlen($display_value)) { $storage_value = phutil_json_decode($display_value); if ($storage_value === null) { $e_value = pht('Invalid'); $errors[] = pht( 'Configuration value should be specified in JSON. The provided '. 'value is not valid JSON.'); } else { try { $this->validateOption($option, $storage_value); } catch (Exception $ex) { $e_value = pht('Invalid'); $errors[] = $ex->getMessage(); } } } else { $storage_value = null; } return array($e_value, $errors, $storage_value, $display_value); } - public function getDisplayValue( - PhabricatorConfigOption $option, - PhabricatorConfigEntry $entry) { - $value = $entry->getValue(); - if (!$value) { - return ''; - } - - $json = new PhutilJSON(); - return $json->encodeFormatted($value); - } - public function renderControl( PhabricatorConfigOption $option, $display_value, $e_value) { return id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('value') ->setLabel(pht('Value')) ->setValue($display_value) ->setError($e_value); } } diff --git a/src/applications/config/custom/PhabricatorConfigOptionType.php b/src/applications/config/custom/PhabricatorConfigOptionType.php index 6cd4877b48..c50315a140 100644 --- a/src/applications/config/custom/PhabricatorConfigOptionType.php +++ b/src/applications/config/custom/PhabricatorConfigOptionType.php @@ -1,39 +1,47 @@ getStr('value'); $display_value = $request->getStr('value'); return array($e_value, $errors, $storage_value, $display_value); } public function getDisplayValue( PhabricatorConfigOption $option, - PhabricatorConfigEntry $entry) { - return $entry->getValue(); + PhabricatorConfigEntry $entry, + $value) { + + if (is_array($value)) { + $json = new PhutilJSON(); + return $json->encodeFormatted($value); + } else { + return $value; + } + } public function renderControl( PhabricatorConfigOption $option, $display_value, $e_value) { return id(new AphrontFormTextControl()) ->setName('value') ->setLabel(pht('Value')) ->setValue($display_value) ->setError($e_value); } } diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index c99abe0419..dda46f82c6 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -1,250 +1,250 @@ newOption('phabricator.base-uri', 'string', null) ->setLocked(true) ->setSummary(pht("URI where Phabricator is installed.")) ->setDescription( pht( "Set the URI where Phabricator is installed. Setting this ". "improves security by preventing cookies from being set on other ". "domains, and allows daemons to send emails with links that have ". "the correct domain.")) ->addExample('http://phabricator.example.com/', pht('Valid Setting')), $this->newOption('phabricator.production-uri', 'string', null) ->setSummary( pht("Primary install URI, for multi-environment installs.")) ->setDescription( pht( "If you have multiple Phabricator environments (like a ". "development/staging environment for working on testing ". "Phabricator, and a production environment for deploying it), ". "set the production environment URI here so that emails and other ". "durable URIs will always generate with links pointing at the ". "production environment. If unset, defaults to ". "{{phabricator.base-uri}}. Most installs do not need to set ". "this option.")) ->addExample('http://phabricator.example.com/', pht('Valid Setting')), $this->newOption('phabricator.allowed-uris', 'list', array()) ->setLocked(true) ->setSummary(pht("Alternative URIs that can access Phabricator.")) ->setDescription( pht( "These alternative URIs will be able to access 'normal' pages ". "on your Phabricator install. Other features such as OAuth ". "won't work. The major use case for this is moving installs ". "across domains.")) ->addExample( - '["http://phabricator2.example.com/", '. - '"http://phabricator3.example.com/"]', + "http://phabricator2.example.com/\n". + "http://phabricator3.example.com/", pht('Valid Setting')), $this->newOption('phabricator.timezone', 'string', null) ->setSummary( pht("The timezone Phabricator should use.")) ->setDescription( pht( "PHP requires that you set a timezone in your php.ini before ". "using date functions, or it will emit a warning. If this isn't ". "possible (for instance, because you are using HPHP) you can set ". "some valid constant for date_default_timezone_set() here and ". "Phabricator will set it on your behalf, silencing the warning.")) ->addExample('America/New_York', pht('US East (EDT)')) ->addExample('America/Chicago', pht('US Central (CDT)')) ->addExample('America/Boise', pht('US Mountain (MDT)')) ->addExample('America/Los_Angeles', pht('US West (PDT)')), $this->newOption('phabricator.cookie-prefix', 'string', null) ->setSummary( pht("Set a string Phabricator should use to prefix ". "cookie names")) ->setDescription( pht( "Cookies set for x.com are also sent for y.x.com. Assuming ". "Phabricator instances are running on both domains, this will ". "create a collision preventing you from logging in.")) ->addExample('dev', pht('Prefix cookie with "dev"')), $this->newOption('phabricator.show-beta-applications', 'bool', false) ->setBoolOptions( array( pht('Install Beta Applications'), pht('Uninstall Beta Applications') )) ->setDescription( pht( "Phabricator includes 'Beta' applications which are in an early ". "stage of development. They range from very rough prototypes to ". "relatively complete (but unpolished) applications.\n\n". "By default, Beta applications are not installed. You can enable ". "this option to install them if you're interested in previewing ". "upcoming features.\n\n". "After enabling Beta applications, you can selectively uninstall ". "them (like normal applications).")), $this->newOption('phabricator.serious-business', 'bool', false) ->setBoolOptions( array( pht('Serious business'), pht('Shenanigans'), // That should be interesting to translate. :P )) ->setSummary( pht("Should Phabricator be serious?")) ->setDescription( pht( "By default, Phabricator includes some silly nonsense in the UI, ". "such as a submit button called 'Clowncopterize' in Differential ". "and a call to 'Leap Into Action'. If you'd prefer more ". "traditional UI strings like 'Submit', you can set this flag to ". "disable most of the jokes and easter eggs.")), $this->newOption('environment.append-paths', 'list', $paths) ->setSummary( pht("These paths get appended to your \$PATH envrionment variable.")) ->setDescription( pht( "Phabricator occasionally shells out to other binaries on the ". "server. An example of this is the `pygmentize` command, used ". "to syntax-highlight code written in languages other than PHP. ". "By default, it is assumed that these binaries are in the \$PATH ". "of the user running Phabricator (normally 'apache', 'httpd', or ". "'nobody'). Here you can add extra directories to the \$PATH ". "environment variable, for when these binaries are in ". "non-standard locations.\n\n". "Note that you can also put binaries in ". "`phabricator/support/bin/` (for example, by symlinking them).\n\n". "The current value of PATH after configuration is applied is:\n\n". " lang=text\n". " %s", $path)) ->addExample('/usr/local/bin', pht('Add One Path')) ->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')), $this->newOption('config.lock', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to lock.')), $this->newOption('config.hide', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to hide.')), $this->newOption('config.mask', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to mask.')), $this->newOption('config.ignore-issues', 'set', array()) ->setLocked(true) ->setDescription(pht('Setup issues to ignore.')), $this->newOption('phabricator.env', 'string', null) ->setLocked(true) ->setDescription(pht('Internal.')), $this->newOption('test.value', 'wild', null) ->setLocked(true) ->setDescription(pht('Unit test value.')), $this->newOption('phabricator.uninstalled-applications', 'set', array()) ->setLocked(true) ->setDescription( pht('Array containing list of Uninstalled applications.')), $this->newOption('phabricator.application-settings', 'wild', array()) ->setLocked(true) ->setDescription( pht('Customized settings for Phabricator applications.')), $this->newOption('welcome.html', 'string', null) ->setLocked(true) ->setDescription( pht('Custom HTML to show on the main Phabricator dashboard.')), $this->newOption('phabricator.cache-namespace', 'string', null) ->setLocked(true) ->setDescription(pht('Cache namespace.')), $this->newOption('phabricator.allow-email-users', 'bool', false) ->setBoolOptions( array( pht('Allow'), pht('Disallow'), ))->setDescription( pht( 'Allow non-members to interact with tasks over email.')), ); } protected function didValidateOption( PhabricatorConfigOption $option, $value) { $key = $option->getKey(); if ($key == 'phabricator.base-uri' || $key == 'phabricator.production-uri') { $uri = new PhutilURI($value); $protocol = $uri->getProtocol(); if ($protocol !== 'http' && $protocol !== 'https') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must start with ". "'http://' or 'https://'.", $key)); } $domain = $uri->getDomain(); if (strpos($domain, '.') === false) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must contain a dot ('.'), ". "like 'http://example.com/', not just a bare name like ". "'http://example/'. Some web browsers will not set cookies on ". "domains with no TLD.", $key)); } $path = $uri->getPath(); if ($path !== '' && $path !== '/') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must NOT have a path, ". "e.g. 'http://phabricator.example.com/' is OK, but ". "'http://example.com/phabricator/' is not. Phabricator must be ". "installed on an entire domain; it can not be installed on a ". "path.", $key)); } } if ($key === 'phabricator.timezone') { $old = date_default_timezone_get(); $ok = @date_default_timezone_set($value); @date_default_timezone_set($old); if (!$ok) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The timezone identifier must ". "be a valid timezone identifier recognized by PHP, like ". "'America/Los_Angeles'. You can find a list of valid identifiers ". "here: %s", $key, 'http://php.net/manual/timezones.php')); } } } } diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php index 556d213df6..5c5235074e 100644 --- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php +++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php @@ -1,252 +1,251 @@ newOption('security.alternate-file-domain', 'string', null) ->setSummary(pht("Alternate domain to serve files from.")) ->setDescription( pht( "IMPORTANT: By default, Phabricator serves files from the same ". "domain the application lives on. This is convenient but not ". "secure: it creates a large class of vulnerabilities which can ". "not be generally mitigated.\n\n". "To avoid this, you should configure a second domain in the same ". "way you have the primary domain configured (i.e., point it at ". "the same machine and set up the same vhost rules) and provide ". "it here. For instance, if your primary install is on ". "'http://www.phabricator-example.com/', you could configure ". "'http://www.phabricator-files.com/' and specify the entire ". "domain (with protocol) here. This will enforce that files are ". "served only from the alternate domain. Ideally, you should use ". "a completely separate domain name rather than just a different ". "subdomain.\n\n". "It is **STRONGLY RECOMMENDED** that you configure this. Your ". "install is **NOT SECURE** unless you do so.")) ->addExample('http://www.phabricator-files.com/', pht('Valid Setting')), $this->newOption( 'security.hmac-key', 'string', '[D\t~Y7eNmnQGJ;rnH6aF;m2!vJ8@v8C=Cs:aQS\.Qw') ->setMasked(true) ->setSummary( pht("Key for HMAC digests.")) ->setDescription( pht( "Default key for HMAC digests where the key is not important ". "(i.e., the hash itself is secret). You can change this if you ". "want (to any other string), but doing so will break existing ". "sessions and CSRF tokens.")), $this->newOption('security.require-https', 'bool', false) ->setLocked(true) ->setSummary( pht("Force users to connect via HTTPS instead of HTTP.")) ->setDescription( pht( "If the web server responds to both HTTP and HTTPS requests but ". "you want users to connect with only HTTPS, you can set this ". "to true to make Phabricator redirect HTTP requests to HTTPS.\n\n". "Normally, you should just configure your server not to accept ". "HTTP traffic, but this setting may be useful if you originally ". "used HTTP and have now switched to HTTPS but don't want to ". "break old links, or if your webserver sits behind a load ". "balancer which terminates HTTPS connections and you can not ". "reasonably configure more granular behavior there.\n\n". "IMPORTANT: Phabricator determines if a request is HTTPS or not ". "by examining the PHP \$_SERVER['HTTPS'] variable. If you run ". "Apache/mod_php this will probably be set correctly for you ". "automatically, but if you run Phabricator as CGI/FCGI (e.g., ". "through nginx or lighttpd), you need to configure your web ". "server so that it passes the value correctly based on the ". "connection type.")) ->setBoolOptions( array( pht('Force HTTPS'), pht('Allow HTTP'), )), $this->newOption( 'phabricator.csrf-key', 'string', '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3') ->setMasked(true) ->setSummary( pht("Hashed with other inputs to generate CSRF tokens.")) ->setDescription( pht( "This is hashed with other inputs to generate CSRF tokens. If ". "you want, you can change it to some other string which is ". "unique to your install. This will make your install more secure ". "in a vague, mostly theoretical way. But it will take you like 3 ". "seconds of mashing on your keyboard to set it up so you might ". "as well.")), $this->newOption( 'phabricator.mail-key', 'string', '5ce3e7e8787f6e40dfae861da315a5cdf1018f12') ->setMasked(true) ->setSummary( pht("Hashed with other inputs to generate mail tokens.")) ->setDescription( pht( "This is hashed with other inputs to generate mail tokens. If ". "you want, you can change it to some other string which is ". "unique to your install. In particular, you will want to do ". "this if you accidentally send a bunch of mail somewhere you ". "shouldn't have, to invalidate all old reply-to addresses.")), $this->newOption( 'uri.allowed-protocols', 'set', array( 'http' => true, 'https' => true, )) ->setSummary( pht("Determines which URI protocols are auto-linked.")) ->setDescription( pht( "When users write comments which have URIs, they'll be ". "automatically linked if the protocol appears in this set. This ". "whitelist is primarily to prevent security issues like ". "javascript:// URIs.")) - ->addExample( - '{"http": true, "https": true"}', pht('Valid Setting')) + ->addExample("http\nhttps", pht('Valid Setting')) ->setLocked(true), $this->newOption( 'uri.allowed-editor-protocols', 'set', array( 'http' => true, 'https' => true, // This handler is installed by Textmate. 'txmt' => true, // This handler is for MacVim. 'mvim' => true, // Unofficial handler for Vim. 'vim' => true, // Unofficial handler for Sublime. 'subl' => true, // Unofficial handler for Emacs. 'emacs' => true, // This isn't a standard handler installed by an application, but // is a reasonable name for a user-installed handler. 'editor' => true, )) ->setSummary(pht('Whitelists editor protocols for "Open in Editor".')) ->setDescription( pht( "Users can configure a URI pattern to open files in a text ". "editor. The URI must use a protocol on this whitelist.\n\n". "(If you use an editor which defines a protocol not on this ". "list, [[ %s | let us know ]] and we'll update the defaults.)", $support_href)) ->setLocked(true), $this->newOption( 'celerity.resource-hash', 'string', 'd9455ea150622ee044f7931dabfa52aa') ->setSummary( pht("An input to the hash function when building resource hashes.")) ->setDescription( pht( "This value is an input to the hash function when building ". "resource hashes. It has no security value, but if you ". "accidentally poison user caches (by pushing a bad patch or ". "having something go wrong with a CDN, e.g.) you can change this ". "to something else and rebuild the Celerity map to break user ". "caches. Unless you are doing Celerity development, it is ". "exceptionally unlikely that you need to modify this.")), $this->newOption('remarkup.enable-embedded-youtube', 'bool', false) ->setBoolOptions( array( pht("Embed YouTube videos"), pht("Don't embed YouTube videos"), )) ->setSummary( pht("Determines whether or not YouTube videos get embedded.")) ->setDescription( pht( "If you enable this, linked YouTube videos will be embeded ". "inline. This has mild security implications (you'll leak ". "referrers to YouTube) and is pretty silly (but sort of ". "awesome).")), $this->newOption('security.allow-outbound-http', 'bool', true) ->setBoolOptions( array( pht("Allow"), pht("Disallow"), )) ->setSummary( pht("Allow outbound HTTP requests")) ->setDescription( pht( "If you enable this, you are allowing Phabricator to potentially ". "make requests to external servers.")), ); } protected function didValidateOption( PhabricatorConfigOption $option, $value) { $key = $option->getKey(); if ($key == 'security.alternate-file-domain') { $uri = new PhutilURI($value); $protocol = $uri->getProtocol(); if ($protocol !== 'http' && $protocol !== 'https') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must start with ". "'http://' or 'https://'.", $key)); } $domain = $uri->getDomain(); if (strpos($domain, '.') === false) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must contain a dot ('.'), ". "like 'http://example.com/', not just a bare name like ". "'http://example/'. Some web browsers will not set cookies on ". "domains with no TLD.", $key)); } $path = $uri->getPath(); if ($path !== '' && $path !== '/') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must NOT have a path, ". "e.g. 'http://phabricator.example.com/' is OK, but ". "'http://example.com/phabricator/' is not. Phabricator must be ". "installed on an entire domain; it can not be installed on a ". "path.", $key)); } } } } diff --git a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php b/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php index 0795ac5467..a904c4fea2 100644 --- a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php +++ b/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php @@ -1,372 +1,372 @@ setName('map') ->setExamples('**map** [options]') ->setSynopsis(pht('Rebuild static resource maps.')) ->setArguments( array()); } public function execute(PhutilArgumentParser $args) { $resources_map = CelerityPhysicalResources::getAll(); $this->log( pht( "Rebuilding %d resource source(s).", new PhutilNumber(count($resources_map)))); foreach ($resources_map as $name => $resources) { $this->rebuildResources($resources); } $this->log(pht("Done.")); return 0; } /** * Rebuild the resource map for a resource source. * * @param CelerityPhysicalResources Resource source to rebuild. * @return void */ private function rebuildResources(CelerityPhysicalResources $resources) { $this->log( pht( 'Rebuilding resource source "%s" (%s)...', $resources->getName(), get_class($resources))); $binary_map = $this->rebuildBinaryResources($resources); $this->log( pht( 'Found %d binary resources.', new PhutilNumber(count($binary_map)))); $xformer = id(new CelerityResourceTransformer()) ->setMinify(false) ->setRawURIMap(ipull($binary_map, 'uri')); $text_map = $this->rebuildTextResources($resources, $xformer); $this->log( pht( 'Found %d text resources.', new PhutilNumber(count($text_map)))); $resource_graph = array(); $requires_map = array(); $symbol_map = array(); foreach ($text_map as $name => $info) { if (isset($info['provides'])) { $symbol_map[$info['provides']] = $info['hash']; // We only need to check for cycles and add this to the requires map // if it actually requires anything. if (!empty($info['requires'])) { $resource_graph[$info['provides']] = $info['requires']; $requires_map[$info['hash']] = $info['requires']; } } } $this->detectGraphCycles($resource_graph); $name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash'); $hash_map = array_flip($name_map); $package_map = $this->rebuildPackages( $resources, $symbol_map, $hash_map); $this->log( pht( 'Found %d packages.', new PhutilNumber(count($package_map)))); $component_map = array(); foreach ($package_map as $package_name => $package_info) { foreach ($package_info['symbols'] as $symbol) { $component_map[$symbol] = $package_name; } } $name_map = $this->mergeNameMaps( array( array(pht('Binary'), ipull($binary_map, 'hash')), array(pht('Text'), ipull($text_map, 'hash')), array(pht('Package'), ipull($package_map, 'hash')), )); $package_map = ipull($package_map, 'symbols'); ksort($name_map); ksort($symbol_map); ksort($requires_map); ksort($package_map); $map_content = $this->formatMapContent(array( 'names' => $name_map, 'symbols' => $symbol_map, 'requires' => $requires_map, 'packages' => $package_map, )); $map_path = $resources->getPathToMap(); $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path))); Filesystem::writeFile($map_path, $map_content); } /** * Find binary resources (like PNG and SWF) and return information about * them. * * @param CelerityPhysicalResources Resource map to find binary resources for. * @return map> Resource information map. */ private function rebuildBinaryResources( CelerityPhysicalResources $resources) { $binary_map = $resources->findBinaryResources(); $result_map = array(); foreach ($binary_map as $name => $data_hash) { $hash = $resources->getCelerityHash($data_hash.$name); $result_map[$name] = array( 'hash' => $hash, 'uri' => $resources->getResourceURI($hash, $name), ); } return $result_map; } /** * Find text resources (like JS and CSS) and return information about them. * * @param CelerityPhysicalResources Resource map to find text resources for. * @param CelerityResourceTransformer Configured resource transformer. * @return map> Resource information map. */ private function rebuildTextResources( CelerityPhysicalResources $resources, CelerityResourceTransformer $xformer) { $text_map = $resources->findTextResources(); $result_map = array(); foreach ($text_map as $name => $data_hash) { $raw_data = $resources->getResourceData($name); $xformed_data = $xformer->transformResource($name, $raw_data); $data_hash = $resources->getCelerityHash($xformed_data); $hash = $resources->getCelerityHash($data_hash.$name); list($provides, $requires) = $this->getProvidesAndRequires( $name, $raw_data); $result_map[$name] = array( 'hash' => $hash, ); if ($provides !== null) { $result_map[$name] += array( 'provides' => $provides, 'requires' => $requires, ); } } return $result_map; } /** * Parse the `@provides` and `@requires` symbols out of a text resource, like * JS or CSS. * * @param string Resource name. * @param string Resource data. - * @return pair|nul> The `@provides` symbol and the + * @return pair|null> The `@provides` symbol and the * list of `@requires` symbols. If the resource is not part of the * dependency graph, both are null. */ private function getProvidesAndRequires($name, $data) { $parser = new PhutilDocblockParser(); $matches = array(); $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); if (!$ok) { throw new Exception( pht( 'Resource "%s" does not have a header doc comment. Encode '. 'dependency data in a header docblock.', $name)); } list($description, $metadata) = $parser->parse($matches[0]); $provides = preg_split('/\s+/', trim(idx($metadata, 'provides'))); $requires = preg_split('/\s+/', trim(idx($metadata, 'requires'))); $provides = array_filter($provides); $requires = array_filter($requires); if (!$provides) { // Tests and documentation-only JS is permitted to @provide no targets. return array(null, null); } if (count($provides) > 1) { throw new Exception( pht( 'Resource "%s" must @provide at most one Celerity target.', $name)); } return array(head($provides), $requires); } /** * Check for dependency cycles in the resource graph. Raises an exception if * a cycle is detected. * * @param map> Map of `@provides` symbols to their * `@requires` symbols. * @return void */ private function detectGraphCycles(array $nodes) { $graph = id(new CelerityResourceGraph()) ->addNodes($nodes) ->setResourceGraph($nodes) ->loadGraph(); foreach ($nodes as $provides => $requires) { $cycle = $graph->detectCycles($provides); if ($cycle) { throw new Exception( pht( 'Cycle detected in resource graph: %s', implode(' > ', $cycle))); } } } /** * Build package specifications for a given resource source. * * @param CelerityPhysicalResources Resource source to rebuild. * @param list Map of `@provides` to hashes. * @param list Map of hashes to resource names. * @return map> Package information maps. */ private function rebuildPackages( CelerityPhysicalResources $resources, array $symbol_map, array $reverse_map) { $package_map = array(); $package_spec = $resources->getResourcePackages(); foreach ($package_spec as $package_name => $package_symbols) { $type = null; $hashes = array(); foreach ($package_symbols as $symbol) { $symbol_hash = idx($symbol_map, $symbol); if ($symbol_hash === null) { throw new Exception( pht( 'Package specification for "%s" includes "%s", but that symbol '. 'is not @provided by any resource.', $package_name, $symbol)); } $resource_name = $reverse_map[$symbol_hash]; $resource_type = $resources->getResourceType($resource_name); if ($type === null) { $type = $resource_type; } else if ($type !== $resource_type) { throw new Exception( pht( 'Package specification for "%s" includes resources of multiple '. 'types (%s, %s). Each package may only contain one type of '. 'resource.', $package_name, $type, $resource_type)); } $hashes[] = $symbol.':'.$symbol_hash; } $hash = $resources->getCelerityHash(implode("\n", $hashes)); $package_map[$package_name] = array( 'hash' => $hash, 'symbols' => $package_symbols, ); } return $package_map; } private function mergeNameMaps(array $maps) { $result = array(); $origin = array(); foreach ($maps as $map) { list($map_name, $data) = $map; foreach ($data as $name => $hash) { if (empty($result[$name])) { $result[$name] = $hash; $origin[$name] = $map_name; } else { $old = $origin[$name]; $new = $map_name; throw new Exception( pht( 'Resource source defines two resources with the same name, '. '"%s". One is defined in the "%s" map; the other in the "%s" '. 'map. Each resource must have a unique name.', $name, $old, $new)); } } } return $result; } private function log($message) { $console = PhutilConsole::getConsole(); $console->writeErr("%s\n", $message); } private function formatMapContent(array $data) { $content = var_export($data, true); $content = preg_replace('/\s+$/m', '', $content); $content = preg_replace('/array \(/', 'array(', $content); $generated = '@'.'generated'; return <<