PageRenderTime 104ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 1ms

/src/applications/config/controller/PhabricatorConfigEditController.php

https://github.com/navyuginfo/phabricator
PHP | 549 lines | 471 code | 72 blank | 6 comment | 58 complexity | 0b1a2a38d404f946dff2cfa4f51cdad9 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.0, LGPL-3.0, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  1. <?php
  2. final class PhabricatorConfigEditController
  3. extends PhabricatorConfigController {
  4. private $key;
  5. public function willProcessRequest(array $data) {
  6. $this->key = $data['key'];
  7. }
  8. public function processRequest() {
  9. $request = $this->getRequest();
  10. $user = $request->getUser();
  11. $options = PhabricatorApplicationConfigOptions::loadAllOptions();
  12. if (empty($options[$this->key])) {
  13. $ancient = PhabricatorSetupCheckExtraConfig::getAncientConfig();
  14. if (isset($ancient[$this->key])) {
  15. $desc = pht(
  16. "This configuration has been removed. You can safely delete ".
  17. "it.\n\n%s",
  18. $ancient[$this->key]);
  19. } else {
  20. $desc = pht(
  21. 'This configuration option is unknown. It may be misspelled, '.
  22. 'or have existed in a previous version of Phabricator.');
  23. }
  24. // This may be a dead config entry, which existed in the past but no
  25. // longer exists. Allow it to be edited so it can be reviewed and
  26. // deleted.
  27. $option = id(new PhabricatorConfigOption())
  28. ->setKey($this->key)
  29. ->setType('wild')
  30. ->setDefault(null)
  31. ->setDescription($desc);
  32. $group = null;
  33. $group_uri = $this->getApplicationURI();
  34. } else {
  35. $option = $options[$this->key];
  36. $group = $option->getGroup();
  37. $group_uri = $this->getApplicationURI('group/'.$group->getKey().'/');
  38. }
  39. $issue = $request->getStr('issue');
  40. if ($issue) {
  41. // If the user came here from an open setup issue, send them back.
  42. $done_uri = $this->getApplicationURI('issue/'.$issue.'/');
  43. } else {
  44. $done_uri = $group_uri;
  45. }
  46. // Check if the config key is already stored in the database.
  47. // Grab the value if it is.
  48. $config_entry = id(new PhabricatorConfigEntry())
  49. ->loadOneWhere(
  50. 'configKey = %s AND namespace = %s',
  51. $this->key,
  52. 'default');
  53. if (!$config_entry) {
  54. $config_entry = id(new PhabricatorConfigEntry())
  55. ->setConfigKey($this->key)
  56. ->setNamespace('default')
  57. ->setIsDeleted(true);
  58. $config_entry->setPHID($config_entry->generatePHID());
  59. }
  60. $e_value = null;
  61. $errors = array();
  62. if ($request->isFormPost() && !$option->getLocked()) {
  63. $result = $this->readRequest(
  64. $option,
  65. $request);
  66. list($e_value, $value_errors, $display_value, $xaction) = $result;
  67. $errors = array_merge($errors, $value_errors);
  68. if (!$errors) {
  69. $editor = id(new PhabricatorConfigEditor())
  70. ->setActor($user)
  71. ->setContinueOnNoEffect(true)
  72. ->setContentSourceFromRequest($request);
  73. try {
  74. $editor->applyTransactions($config_entry, array($xaction));
  75. return id(new AphrontRedirectResponse())->setURI($done_uri);
  76. } catch (PhabricatorConfigValidationException $ex) {
  77. $e_value = pht('Invalid');
  78. $errors[] = $ex->getMessage();
  79. }
  80. }
  81. } else {
  82. if ($config_entry->getIsDeleted()) {
  83. $display_value = null;
  84. } else {
  85. $display_value = $this->getDisplayValue(
  86. $option,
  87. $config_entry,
  88. $config_entry->getValue());
  89. }
  90. }
  91. $form = new AphrontFormView();
  92. $error_view = null;
  93. if ($errors) {
  94. $error_view = id(new AphrontErrorView())
  95. ->setErrors($errors);
  96. } else if ($option->getHidden()) {
  97. $msg = pht(
  98. 'This configuration is hidden and can not be edited or viewed from '.
  99. 'the web interface.');
  100. $error_view = id(new AphrontErrorView())
  101. ->setTitle(pht('Configuration Hidden'))
  102. ->setSeverity(AphrontErrorView::SEVERITY_WARNING)
  103. ->appendChild(phutil_tag('p', array(), $msg));
  104. } else if ($option->getLocked()) {
  105. $msg = pht(
  106. 'This configuration is locked and can not be edited from the web '.
  107. 'interface. Use `./bin/config` in `phabricator/` to edit it.');
  108. $error_view = id(new AphrontErrorView())
  109. ->setTitle(pht('Configuration Locked'))
  110. ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
  111. ->appendChild(phutil_tag('p', array(), $msg));
  112. }
  113. if ($option->getHidden()) {
  114. $control = null;
  115. } else {
  116. $control = $this->renderControl(
  117. $option,
  118. $display_value,
  119. $e_value);
  120. }
  121. $engine = new PhabricatorMarkupEngine();
  122. $engine->setViewer($user);
  123. $engine->addObject($option, 'description');
  124. $engine->process();
  125. $description = phutil_tag(
  126. 'div',
  127. array(
  128. 'class' => 'phabricator-remarkup',
  129. ),
  130. $engine->getOutput($option, 'description'));
  131. $form
  132. ->setUser($user)
  133. ->addHiddenInput('issue', $request->getStr('issue'))
  134. ->appendChild(
  135. id(new AphrontFormMarkupControl())
  136. ->setLabel(pht('Description'))
  137. ->setValue($description));
  138. if ($group) {
  139. $extra = $group->renderContextualDescription(
  140. $option,
  141. $request);
  142. if ($extra !== null) {
  143. $form->appendChild(
  144. id(new AphrontFormMarkupControl())
  145. ->setValue($extra));
  146. }
  147. }
  148. $form
  149. ->appendChild($control);
  150. $submit_control = id(new AphrontFormSubmitControl())
  151. ->addCancelButton($done_uri);
  152. if (!$option->getLocked()) {
  153. $submit_control->setValue(pht('Save Config Entry'));
  154. }
  155. $form->appendChild($submit_control);
  156. $examples = $this->renderExamples($option);
  157. if ($examples) {
  158. $form->appendChild(
  159. id(new AphrontFormMarkupControl())
  160. ->setLabel(pht('Examples'))
  161. ->setValue($examples));
  162. }
  163. if (!$option->getHidden()) {
  164. $form->appendChild(
  165. id(new AphrontFormMarkupControl())
  166. ->setLabel(pht('Default'))
  167. ->setValue($this->renderDefaults($option, $config_entry)));
  168. }
  169. $title = pht('Edit %s', $this->key);
  170. $short = pht('Edit');
  171. $form_box = id(new PHUIObjectBoxView())
  172. ->setHeaderText($title)
  173. ->setForm($form);
  174. if ($error_view) {
  175. $form_box->setErrorView($error_view);
  176. }
  177. $crumbs = $this->buildApplicationCrumbs();
  178. $crumbs->addTextCrumb(pht('Config'), $this->getApplicationURI());
  179. if ($group) {
  180. $crumbs->addTextCrumb($group->getName(), $group_uri);
  181. }
  182. $crumbs->addTextCrumb($this->key, '/config/edit/'.$this->key);
  183. $xactions = id(new PhabricatorConfigTransactionQuery())
  184. ->withObjectPHIDs(array($config_entry->getPHID()))
  185. ->setViewer($user)
  186. ->execute();
  187. $xaction_view = id(new PhabricatorApplicationTransactionView())
  188. ->setUser($user)
  189. ->setObjectPHID($config_entry->getPHID())
  190. ->setTransactions($xactions);
  191. return $this->buildApplicationPage(
  192. array(
  193. $crumbs,
  194. $form_box,
  195. $xaction_view,
  196. ),
  197. array(
  198. 'title' => $title,
  199. 'device' => true,
  200. ));
  201. }
  202. private function readRequest(
  203. PhabricatorConfigOption $option,
  204. AphrontRequest $request) {
  205. $xaction = new PhabricatorConfigTransaction();
  206. $xaction->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT);
  207. $e_value = null;
  208. $errors = array();
  209. $value = $request->getStr('value');
  210. if (!strlen($value)) {
  211. $value = null;
  212. $xaction->setNewValue(
  213. array(
  214. 'deleted' => true,
  215. 'value' => null,
  216. ));
  217. return array($e_value, $errors, $value, $xaction);
  218. }
  219. if ($option->isCustomType()) {
  220. $info = $option->getCustomObject()->readRequest($option, $request);
  221. list($e_value, $errors, $set_value, $value) = $info;
  222. } else {
  223. $type = $option->getType();
  224. $set_value = null;
  225. switch ($type) {
  226. case 'int':
  227. if (preg_match('/^-?[0-9]+$/', trim($value))) {
  228. $set_value = (int)$value;
  229. } else {
  230. $e_value = pht('Invalid');
  231. $errors[] = pht('Value must be an integer.');
  232. }
  233. break;
  234. case 'string':
  235. case 'enum':
  236. $set_value = (string)$value;
  237. break;
  238. case 'list<string>':
  239. case 'list<regex>':
  240. $set_value = phutil_split_lines(
  241. $request->getStr('value'),
  242. $retain_endings = false);
  243. foreach ($set_value as $key => $v) {
  244. if (!strlen($v)) {
  245. unset($set_value[$key]);
  246. }
  247. }
  248. $set_value = array_values($set_value);
  249. break;
  250. case 'set':
  251. $set_value = array_fill_keys($request->getStrList('value'), true);
  252. break;
  253. case 'bool':
  254. switch ($value) {
  255. case 'true':
  256. $set_value = true;
  257. break;
  258. case 'false':
  259. $set_value = false;
  260. break;
  261. default:
  262. $e_value = pht('Invalid');
  263. $errors[] = pht('Value must be boolean, "true" or "false".');
  264. break;
  265. }
  266. break;
  267. case 'class':
  268. if (!class_exists($value)) {
  269. $e_value = pht('Invalid');
  270. $errors[] = pht('Class does not exist.');
  271. } else {
  272. $base = $option->getBaseClass();
  273. if (!is_subclass_of($value, $base)) {
  274. $e_value = pht('Invalid');
  275. $errors[] = pht('Class is not of valid type.');
  276. } else {
  277. $set_value = $value;
  278. }
  279. }
  280. break;
  281. default:
  282. $json = json_decode($value, true);
  283. if ($json === null && strtolower($value) != 'null') {
  284. $e_value = pht('Invalid');
  285. $errors[] = pht(
  286. 'The given value must be valid JSON. This means, among '.
  287. 'other things, that you must wrap strings in double-quotes.');
  288. } else {
  289. $set_value = $json;
  290. }
  291. break;
  292. }
  293. }
  294. if (!$errors) {
  295. $xaction->setNewValue(
  296. array(
  297. 'deleted' => false,
  298. 'value' => $set_value,
  299. ));
  300. } else {
  301. $xaction = null;
  302. }
  303. return array($e_value, $errors, $value, $xaction);
  304. }
  305. private function getDisplayValue(
  306. PhabricatorConfigOption $option,
  307. PhabricatorConfigEntry $entry,
  308. $value) {
  309. if ($option->isCustomType()) {
  310. return $option->getCustomObject()->getDisplayValue(
  311. $option,
  312. $entry,
  313. $value);
  314. } else {
  315. $type = $option->getType();
  316. switch ($type) {
  317. case 'int':
  318. case 'string':
  319. case 'enum':
  320. case 'class':
  321. return $value;
  322. case 'bool':
  323. return $value ? 'true' : 'false';
  324. case 'list<string>':
  325. case 'list<regex>':
  326. return implode("\n", nonempty($value, array()));
  327. case 'set':
  328. return implode("\n", nonempty(array_keys($value), array()));
  329. default:
  330. return PhabricatorConfigJSON::prettyPrintJSON($value);
  331. }
  332. }
  333. }
  334. private function renderControl(
  335. PhabricatorConfigOption $option,
  336. $display_value,
  337. $e_value) {
  338. if ($option->isCustomType()) {
  339. $control = $option->getCustomObject()->renderControl(
  340. $option,
  341. $display_value,
  342. $e_value);
  343. } else {
  344. $type = $option->getType();
  345. switch ($type) {
  346. case 'int':
  347. case 'string':
  348. $control = id(new AphrontFormTextControl());
  349. break;
  350. case 'bool':
  351. $control = id(new AphrontFormSelectControl())
  352. ->setOptions(
  353. array(
  354. '' => pht('(Use Default)'),
  355. 'true' => idx($option->getBoolOptions(), 0),
  356. 'false' => idx($option->getBoolOptions(), 1),
  357. ));
  358. break;
  359. case 'enum':
  360. $options = array_mergev(
  361. array(
  362. array('' => pht('(Use Default)')),
  363. $option->getEnumOptions(),
  364. ));
  365. $control = id(new AphrontFormSelectControl())
  366. ->setOptions($options);
  367. break;
  368. case 'class':
  369. $symbols = id(new PhutilSymbolLoader())
  370. ->setType('class')
  371. ->setAncestorClass($option->getBaseClass())
  372. ->setConcreteOnly(true)
  373. ->selectSymbolsWithoutLoading();
  374. $names = ipull($symbols, 'name', 'name');
  375. asort($names);
  376. $names = array(
  377. '' => pht('(Use Default)'),
  378. ) + $names;
  379. $control = id(new AphrontFormSelectControl())
  380. ->setOptions($names);
  381. break;
  382. case 'list<string>':
  383. case 'list<regex>':
  384. $control = id(new AphrontFormTextAreaControl())
  385. ->setCaption(pht('Separate values with newlines.'));
  386. break;
  387. case 'set':
  388. $control = id(new AphrontFormTextAreaControl())
  389. ->setCaption(pht('Separate values with newlines or commas.'));
  390. break;
  391. default:
  392. $control = id(new AphrontFormTextAreaControl())
  393. ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
  394. ->setCustomClass('PhabricatorMonospaced')
  395. ->setCaption(pht('Enter value in JSON.'));
  396. break;
  397. }
  398. $control
  399. ->setLabel(pht('Value'))
  400. ->setError($e_value)
  401. ->setValue($display_value)
  402. ->setName('value');
  403. }
  404. if ($option->getLocked()) {
  405. $control->setDisabled(true);
  406. }
  407. return $control;
  408. }
  409. private function renderExamples(PhabricatorConfigOption $option) {
  410. $examples = $option->getExamples();
  411. if (!$examples) {
  412. return null;
  413. }
  414. $table = array();
  415. $table[] = phutil_tag('tr', array('class' => 'column-labels'), array(
  416. phutil_tag('th', array(), pht('Example')),
  417. phutil_tag('th', array(), pht('Value')),
  418. ));
  419. foreach ($examples as $example) {
  420. list($value, $description) = $example;
  421. if ($value === null) {
  422. $value = phutil_tag('em', array(), pht('(empty)'));
  423. } else {
  424. if (is_array($value)) {
  425. $value = implode("\n", $value);
  426. }
  427. }
  428. $table[] = phutil_tag('tr', array(), array(
  429. phutil_tag('th', array(), $description),
  430. phutil_tag('td', array(), $value),
  431. ));
  432. }
  433. require_celerity_resource('config-options-css');
  434. return phutil_tag(
  435. 'table',
  436. array(
  437. 'class' => 'config-option-table',
  438. ),
  439. $table);
  440. }
  441. private function renderDefaults(
  442. PhabricatorConfigOption $option,
  443. PhabricatorConfigEntry $entry) {
  444. $stack = PhabricatorEnv::getConfigSourceStack();
  445. $stack = $stack->getStack();
  446. $table = array();
  447. $table[] = phutil_tag('tr', array('class' => 'column-labels'), array(
  448. phutil_tag('th', array(), pht('Source')),
  449. phutil_tag('th', array(), pht('Value')),
  450. ));
  451. foreach ($stack as $key => $source) {
  452. $value = $source->getKeys(
  453. array(
  454. $option->getKey(),
  455. ));
  456. if (!array_key_exists($option->getKey(), $value)) {
  457. $value = phutil_tag('em', array(), pht('(empty)'));
  458. } else {
  459. $value = $this->getDisplayValue(
  460. $option,
  461. $entry,
  462. $value[$option->getKey()]);
  463. }
  464. $table[] = phutil_tag('tr', array('class' => 'column-labels'), array(
  465. phutil_tag('th', array(), $source->getName()),
  466. phutil_tag('td', array(), $value),
  467. ));
  468. }
  469. require_celerity_resource('config-options-css');
  470. return phutil_tag(
  471. 'table',
  472. array(
  473. 'class' => 'config-option-table',
  474. ),
  475. $table);
  476. }
  477. }