diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index 5983d4cb44..41d6ccc391 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -1,548 +1,546 @@ 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 { 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.'); + $msg = $option->getLockedMessage(); $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, $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, )); } 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, $value) { if ($option->isCustomType()) { return $option->getCustomObject()->getDisplayValue( $option, $entry, $value); } else { $type = $option->getType(); 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, 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 = $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/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php index 1c0c090b08..b2e6a0e85d 100644 --- a/src/applications/config/option/PhabricatorConfigOption.php +++ b/src/applications/config/option/PhabricatorConfigOption.php @@ -1,245 +1,260 @@ 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 setLockedMessage($message) { + $this->lockedMessage = $message; + return $this; + } + + public function getLockedMessage() { + if ($this->lockedMessage !== null) { + return $this->lockedMessage; + } + return pht( + 'This configuration is locked and can not be edited from the web '. + 'interface. Use `./bin/config` in `phabricator/` to edit it.'); + } + 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; } public function isCustomType() { return !strncmp($this->getType(), 'custom:', 7); } public function getCustomObject() { if (!$this->customObject) { if (!$this->isCustomType()) { throw new Exception('This option does not have a custom type!'); } $this->customObject = newv(substr($this->getType(), 7), array()); } return $this->customObject; } public function getCustomData() { return $this->customData; } public function setCustomData($data) { $this->customData = $data; return $this; } /* -( 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/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index d846155a99..068d1197d5 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -1,276 +1,285 @@ 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/\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-prototypes', 'bool', false) ->setBoolOptions( array( pht('Enable Prototypes'), pht('Disable Prototypes'), )) ->setSummary( pht( 'Install applications which are still under development.')) ->setDescription( pht( "IMPORTANT: The upstream does not provide support for prototype ". "applications.". "\n\n". "Phabricator includes prototype applications which are in an ". "**early stage of development**. By default, prototype ". "applications are not installed, because they are often not yet ". "developed enough to be generally usable. You can enable ". "this option to install them if you're developing Phabricator ". "or are interested in previewing upcoming features.". "\n\n". "To learn more about prototypes, see [[ %s | %s ]].". "\n\n". "After enabling prototypes, you can selectively uninstall them ". "(like normal applications).", $proto_doc_href, $proto_doc_name)), $this->newOption('phabricator.serious-business', 'bool', false) ->setBoolOptions( array( pht('Serious business'), pht('Shenanigans'), // That should be interesting to translate. :P )) ->setSummary( pht('Allows you to remove levity and jokes from the UI.')) ->setDescription( pht( 'By default, Phabricator includes some flavor text in the UI, '. 'like a prompt to "Weigh In" rather than "Add Comment" in '. 'Maniphest. If you\'d prefer more traditional UI strings like '. '"Add Comment", you can set this flag to disable most of the '. 'extra flavor.')), $this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/') ->setSummary( pht('Text values that match this regex and are also object names '. 'will not be linked.')) ->setDescription( pht( 'By default, Phabricator links object names in Remarkup fields '. 'to the corresponding object. This regex can be used to modify '. 'this behavior; object names that match this regex will not be '. 'linked.')), $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)) ->setLocked(true) ->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) + ->setLockedMessage(pht( + 'Use the %s to manage installed applications.', + phutil_tag( + 'a', + array( + 'href' => $applications_app_href, + ), + pht('Applications application')))) ->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')); } } } }