mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-21 04:50:55 +01:00
7e0558612b
Summary: See IRC. We're supposed to repair configuration, but if custom validators throw a generic `Exception` or use `PhutilTypeSpec` to do a check, we may explode way harder than we intend to. Instead, soften these exceptions into validation exceptions so we repair configuration, raise a setup issue, and continue. Test Plan: {F1059609} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D14998
256 lines
7.6 KiB
PHP
256 lines
7.6 KiB
PHP
<?php
|
|
|
|
abstract class PhabricatorApplicationConfigOptions extends Phobject {
|
|
|
|
abstract public function getName();
|
|
abstract public function getDescription();
|
|
abstract public function getGroup();
|
|
abstract public function getOptions();
|
|
|
|
public function getFontIcon() {
|
|
return 'fa-sliders';
|
|
}
|
|
|
|
public function validateOption(PhabricatorConfigOption $option, $value) {
|
|
if ($value === $option->getDefault()) {
|
|
return;
|
|
}
|
|
|
|
if ($value === null) {
|
|
return;
|
|
}
|
|
|
|
if ($option->isCustomType()) {
|
|
try {
|
|
return $option->getCustomObject()->validateOption($option, $value);
|
|
} catch (Exception $ex) {
|
|
// If custom validators threw exceptions, convert them to configuation
|
|
// validation exceptions so we repair the configuration and raise
|
|
// an error.
|
|
throw new PhabricatorConfigValidationException($ex->getMessage());
|
|
}
|
|
}
|
|
|
|
switch ($option->getType()) {
|
|
case 'bool':
|
|
if ($value !== true &&
|
|
$value !== false) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' is of type bool, but value is not true or false.",
|
|
$option->getKey()));
|
|
}
|
|
break;
|
|
case 'int':
|
|
if (!is_int($value)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' is of type int, but value is not an integer.",
|
|
$option->getKey()));
|
|
}
|
|
break;
|
|
case 'string':
|
|
if (!is_string($value)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' is of type string, but value is not a string.",
|
|
$option->getKey()));
|
|
}
|
|
break;
|
|
case 'class':
|
|
$symbols = id(new PhutilSymbolLoader())
|
|
->setType('class')
|
|
->setAncestorClass($option->getBaseClass())
|
|
->setConcreteOnly(true)
|
|
->selectSymbolsWithoutLoading();
|
|
$names = ipull($symbols, 'name', 'name');
|
|
if (empty($names[$value])) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' value must name a class extending '%s'.",
|
|
$option->getKey(),
|
|
$option->getBaseClass()));
|
|
}
|
|
break;
|
|
case 'set':
|
|
$valid = true;
|
|
if (!is_array($value)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a set, but value is not an array.",
|
|
$option->getKey()));
|
|
}
|
|
foreach ($value as $v) {
|
|
if ($v !== true) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a set, but array contains values other ".
|
|
"than 'true'.",
|
|
$option->getKey()));
|
|
}
|
|
}
|
|
break;
|
|
case 'list<regex>':
|
|
$valid = true;
|
|
if (!is_array($value)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a list of regular expressions, but value ".
|
|
"is not an array.",
|
|
$option->getKey()));
|
|
}
|
|
if ($value && array_keys($value) != range(0, count($value) - 1)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a list of regular expressions, but the ".
|
|
"value is a map with unnatural keys.",
|
|
$option->getKey()));
|
|
}
|
|
foreach ($value as $v) {
|
|
$ok = @preg_match($v, '');
|
|
if ($ok === false) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a list of regular expressions, but the ".
|
|
"value '%s' is not a valid regular expression.",
|
|
$option->getKey(),
|
|
$v));
|
|
}
|
|
}
|
|
break;
|
|
case 'list<string>':
|
|
$valid = true;
|
|
if (!is_array($value)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a list of strings, but value is not ".
|
|
"an array.",
|
|
$option->getKey()));
|
|
}
|
|
if ($value && array_keys($value) != range(0, count($value) - 1)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a list of strings, but the value is a ".
|
|
"map with unnatural keys.",
|
|
$option->getKey()));
|
|
}
|
|
foreach ($value as $v) {
|
|
if (!is_string($v)) {
|
|
throw new PhabricatorConfigValidationException(
|
|
pht(
|
|
"Option '%s' must be a list of strings, but it contains one ".
|
|
"or more non-strings.",
|
|
$option->getKey()));
|
|
}
|
|
}
|
|
break;
|
|
case 'wild':
|
|
default:
|
|
break;
|
|
}
|
|
|
|
$this->didValidateOption($option, $value);
|
|
}
|
|
|
|
protected function didValidateOption(
|
|
PhabricatorConfigOption $option,
|
|
$value) {
|
|
// Hook for subclasses to do complex validation.
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Hook to render additional hints based on, e.g., the viewing user, request,
|
|
* or other context. For example, this is used to show workspace IDs when
|
|
* configuring `asana.workspace-id`.
|
|
*
|
|
* @param PhabricatorConfigOption Option being rendered.
|
|
* @param AphrontRequest Active request.
|
|
* @return wild Additional contextual description
|
|
* information.
|
|
*/
|
|
public function renderContextualDescription(
|
|
PhabricatorConfigOption $option,
|
|
AphrontRequest $request) {
|
|
return null;
|
|
}
|
|
|
|
public function getKey() {
|
|
$class = get_class($this);
|
|
$matches = null;
|
|
if (preg_match('/^Phabricator(.*)ConfigOptions$/', $class, $matches)) {
|
|
return strtolower($matches[1]);
|
|
}
|
|
return strtolower(get_class($this));
|
|
}
|
|
|
|
final protected function newOption($key, $type, $default) {
|
|
return id(new PhabricatorConfigOption())
|
|
->setKey($key)
|
|
->setType($type)
|
|
->setDefault($default)
|
|
->setGroup($this);
|
|
}
|
|
|
|
final public static function loadAll($external_only = false) {
|
|
$symbols = id(new PhutilSymbolLoader())
|
|
->setAncestorClass(__CLASS__)
|
|
->setConcreteOnly(true)
|
|
->selectAndLoadSymbols();
|
|
|
|
$groups = array();
|
|
foreach ($symbols as $symbol) {
|
|
if ($external_only && $symbol['library'] == 'phabricator') {
|
|
continue;
|
|
}
|
|
|
|
$obj = newv($symbol['name'], array());
|
|
$key = $obj->getKey();
|
|
if (isset($groups[$key])) {
|
|
$pclass = get_class($groups[$key]);
|
|
$nclass = $symbol['name'];
|
|
|
|
throw new Exception(
|
|
pht(
|
|
"Multiple %s subclasses have the same key ('%s'): %s, %s.",
|
|
__CLASS__,
|
|
$key,
|
|
$pclass,
|
|
$nclass));
|
|
}
|
|
$groups[$key] = $obj;
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
final public static function loadAllOptions($external_only = false) {
|
|
$groups = self::loadAll($external_only);
|
|
|
|
$options = array();
|
|
foreach ($groups as $group) {
|
|
foreach ($group->getOptions() as $option) {
|
|
$key = $option->getKey();
|
|
if (isset($options[$key])) {
|
|
throw new Exception(
|
|
pht(
|
|
"Mulitple %s subclasses contain an option named '%s'!",
|
|
__CLASS__,
|
|
$key));
|
|
}
|
|
$options[$key] = $option;
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Deformat a HEREDOC for use in remarkup by converting line breaks to
|
|
* spaces.
|
|
*/
|
|
final protected function deformat($string) {
|
|
return preg_replace('/(?<=\S)\n(?=\S)/', ' ', $string);
|
|
}
|
|
|
|
}
|