FormDisplay.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. <?php
  2. /**
  3. * Form management class, displays and processes forms
  4. *
  5. * Explanation of used terms:
  6. * o work_path - original field path, eg. Servers/4/verbose
  7. * o system_path - work_path modified so that it points to the first server,
  8. * eg. Servers/1/verbose
  9. * o translated_path - work_path modified for HTML field name, a path with
  10. * slashes changed to hyphens, eg. Servers-4-verbose
  11. */
  12. declare(strict_types=1);
  13. namespace PhpMyAdmin\Config;
  14. use PhpMyAdmin\Config\Forms\User\UserFormList;
  15. use PhpMyAdmin\Html\MySQLDocumentation;
  16. use PhpMyAdmin\Sanitize;
  17. use PhpMyAdmin\Util;
  18. use const E_USER_WARNING;
  19. use function array_flip;
  20. use function array_keys;
  21. use function array_search;
  22. use function count;
  23. use function explode;
  24. use function function_exists;
  25. use function gettype;
  26. use function implode;
  27. use function is_array;
  28. use function is_bool;
  29. use function is_numeric;
  30. use function mb_substr;
  31. use function preg_match;
  32. use function settype;
  33. use function sprintf;
  34. use function str_replace;
  35. use function trigger_error;
  36. use function trim;
  37. /**
  38. * Form management class, displays and processes forms
  39. */
  40. class FormDisplay
  41. {
  42. /**
  43. * ConfigFile instance
  44. *
  45. * @var ConfigFile
  46. */
  47. private $configFile;
  48. /**
  49. * Form list
  50. *
  51. * @var Form[]
  52. */
  53. private $forms = [];
  54. /**
  55. * Stores validation errors, indexed by paths
  56. * [ Form_name ] is an array of form errors
  57. * [path] is a string storing error associated with single field
  58. *
  59. * @var array
  60. */
  61. private $errors = [];
  62. /**
  63. * Paths changed so that they can be used as HTML ids, indexed by paths
  64. *
  65. * @var array
  66. */
  67. private $translatedPaths = [];
  68. /**
  69. * Server paths change indexes so we define maps from current server
  70. * path to the first one, indexed by work path
  71. *
  72. * @var array
  73. */
  74. private $systemPaths = [];
  75. /**
  76. * Language strings which will be sent to Messages JS variable
  77. * Will be looked up in $GLOBALS: str{value} or strSetup{value}
  78. *
  79. * @var array
  80. */
  81. private $jsLangStrings = [];
  82. /**
  83. * Tells whether forms have been validated
  84. *
  85. * @var bool
  86. */
  87. private $isValidated = true;
  88. /**
  89. * Dictionary with user preferences keys
  90. *
  91. * @var array|null
  92. */
  93. private $userprefsKeys;
  94. /**
  95. * Dictionary with disallowed user preferences keys
  96. *
  97. * @var array
  98. */
  99. private $userprefsDisallow;
  100. /** @var FormDisplayTemplate */
  101. private $formDisplayTemplate;
  102. /**
  103. * @param ConfigFile $cf Config file instance
  104. */
  105. public function __construct(ConfigFile $cf)
  106. {
  107. $this->formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']);
  108. $this->jsLangStrings = [
  109. 'error_nan_p' => __('Not a positive number!'),
  110. 'error_nan_nneg' => __('Not a non-negative number!'),
  111. 'error_incorrect_port' => __('Not a valid port number!'),
  112. 'error_invalid_value' => __('Incorrect value!'),
  113. 'error_value_lte' => __('Value must be less than or equal to %s!'),
  114. ];
  115. $this->configFile = $cf;
  116. // initialize validators
  117. Validator::getValidators($this->configFile);
  118. }
  119. /**
  120. * Returns {@link ConfigFile} associated with this instance
  121. *
  122. * @return ConfigFile
  123. */
  124. public function getConfigFile()
  125. {
  126. return $this->configFile;
  127. }
  128. /**
  129. * Registers form in form manager
  130. *
  131. * @param string $formName Form name
  132. * @param array $form Form data
  133. * @param int $serverId 0 if new server, validation; >= 1 if editing a server
  134. *
  135. * @return void
  136. */
  137. public function registerForm($formName, array $form, $serverId = null)
  138. {
  139. $this->forms[$formName] = new Form(
  140. $formName,
  141. $form,
  142. $this->configFile,
  143. $serverId
  144. );
  145. $this->isValidated = false;
  146. foreach ($this->forms[$formName]->fields as $path) {
  147. $workPath = $serverId === null
  148. ? $path
  149. : str_replace('Servers/1/', 'Servers/' . $serverId . '/', $path);
  150. $this->systemPaths[$workPath] = $path;
  151. $this->translatedPaths[$workPath] = str_replace('/', '-', $workPath);
  152. }
  153. }
  154. /**
  155. * Processes forms, returns true on successful save
  156. *
  157. * @param bool $allowPartialSave allows for partial form saving
  158. * on failed validation
  159. * @param bool $checkFormSubmit whether check for $_POST['submit_save']
  160. *
  161. * @return bool whether processing was successful
  162. */
  163. public function process($allowPartialSave = true, $checkFormSubmit = true)
  164. {
  165. if ($checkFormSubmit && ! isset($_POST['submit_save'])) {
  166. return false;
  167. }
  168. // save forms
  169. if (count($this->forms) > 0) {
  170. return $this->save(array_keys($this->forms), $allowPartialSave);
  171. }
  172. return false;
  173. }
  174. /**
  175. * Runs validation for all registered forms
  176. *
  177. * @return void
  178. */
  179. private function validate()
  180. {
  181. if ($this->isValidated) {
  182. return;
  183. }
  184. $paths = [];
  185. $values = [];
  186. foreach ($this->forms as $form) {
  187. /** @var Form $form */
  188. $paths[] = $form->name;
  189. // collect values and paths
  190. foreach ($form->fields as $path) {
  191. $workPath = array_search($path, $this->systemPaths);
  192. $values[$path] = $this->configFile->getValue($workPath);
  193. $paths[] = $path;
  194. }
  195. }
  196. // run validation
  197. $errors = Validator::validate(
  198. $this->configFile,
  199. $paths,
  200. $values,
  201. false
  202. );
  203. // change error keys from canonical paths to work paths
  204. if (is_array($errors) && count($errors) > 0) {
  205. $this->errors = [];
  206. foreach ($errors as $path => $errorList) {
  207. $workPath = array_search($path, $this->systemPaths);
  208. // field error
  209. if (! $workPath) {
  210. // form error, fix path
  211. $workPath = $path;
  212. }
  213. $this->errors[$workPath] = $errorList;
  214. }
  215. }
  216. $this->isValidated = true;
  217. }
  218. /**
  219. * Outputs HTML for the forms under the menu tab
  220. *
  221. * @param bool $showRestoreDefault whether to show "restore default"
  222. * button besides the input field
  223. * @param array $jsDefault stores JavaScript code
  224. * to be displayed
  225. * @param array $js will be updated with javascript code
  226. * @param bool $showButtons whether show submit and reset button
  227. *
  228. * @return string
  229. */
  230. private function displayForms(
  231. $showRestoreDefault,
  232. array &$jsDefault,
  233. array &$js,
  234. $showButtons
  235. ) {
  236. $htmlOutput = '';
  237. $validators = Validator::getValidators($this->configFile);
  238. foreach ($this->forms as $form) {
  239. /** @var Form $form */
  240. $formErrors = $this->errors[$form->name] ?? null;
  241. $htmlOutput .= $this->formDisplayTemplate->displayFieldsetTop(
  242. Descriptions::get('Form_' . $form->name),
  243. Descriptions::get('Form_' . $form->name, 'desc'),
  244. $formErrors,
  245. ['id' => $form->name]
  246. );
  247. foreach ($form->fields as $field => $path) {
  248. $workPath = array_search($path, $this->systemPaths);
  249. $translatedPath = $this->translatedPaths[$workPath];
  250. // always true/false for user preferences display
  251. // otherwise null
  252. $userPrefsAllow = isset($this->userprefsKeys[$path])
  253. ? ! isset($this->userprefsDisallow[$path])
  254. : null;
  255. // display input
  256. $htmlOutput .= $this->displayFieldInput(
  257. $form,
  258. $field,
  259. $path,
  260. $workPath,
  261. $translatedPath,
  262. $showRestoreDefault,
  263. $userPrefsAllow,
  264. $jsDefault
  265. );
  266. // register JS validators for this field
  267. if (! isset($validators[$path])) {
  268. continue;
  269. }
  270. $this->formDisplayTemplate->addJsValidate($translatedPath, $validators[$path], $js);
  271. }
  272. $htmlOutput .= $this->formDisplayTemplate->displayFieldsetBottom($showButtons);
  273. }
  274. return $htmlOutput;
  275. }
  276. /**
  277. * Outputs HTML for forms
  278. *
  279. * @param bool $tabbedForm if true, use a form with tabs
  280. * @param bool $showRestoreDefault whether show "restore default" button
  281. * besides the input field
  282. * @param bool $showButtons whether show submit and reset button
  283. * @param string $formAction action attribute for the form
  284. * @param array|null $hiddenFields array of form hidden fields (key: field
  285. * name)
  286. *
  287. * @return string HTML for forms
  288. */
  289. public function getDisplay(
  290. $tabbedForm = false,
  291. $showRestoreDefault = false,
  292. $showButtons = true,
  293. $formAction = null,
  294. $hiddenFields = null
  295. ) {
  296. static $jsLangSent = false;
  297. $htmlOutput = '';
  298. $js = [];
  299. $jsDefault = [];
  300. $htmlOutput .= $this->formDisplayTemplate->displayFormTop($formAction, 'post', $hiddenFields);
  301. if ($tabbedForm) {
  302. $tabs = [];
  303. foreach ($this->forms as $form) {
  304. $tabs[$form->name] = Descriptions::get('Form_' . $form->name);
  305. }
  306. $htmlOutput .= $this->formDisplayTemplate->displayTabsTop($tabs);
  307. }
  308. // validate only when we aren't displaying a "new server" form
  309. $isNewServer = false;
  310. foreach ($this->forms as $form) {
  311. /** @var Form $form */
  312. if ($form->index === 0) {
  313. $isNewServer = true;
  314. break;
  315. }
  316. }
  317. if (! $isNewServer) {
  318. $this->validate();
  319. }
  320. // user preferences
  321. $this->loadUserprefsInfo();
  322. // display forms
  323. $htmlOutput .= $this->displayForms(
  324. $showRestoreDefault,
  325. $jsDefault,
  326. $js,
  327. $showButtons
  328. );
  329. if ($tabbedForm) {
  330. $htmlOutput .= $this->formDisplayTemplate->displayTabsBottom();
  331. }
  332. $htmlOutput .= $this->formDisplayTemplate->displayFormBottom();
  333. // if not already done, send strings used for validation to JavaScript
  334. if (! $jsLangSent) {
  335. $jsLangSent = true;
  336. $jsLang = [];
  337. foreach ($this->jsLangStrings as $strName => $strValue) {
  338. $jsLang[] = "'" . $strName . "': '" . Sanitize::jsFormat($strValue, false) . '\'';
  339. }
  340. $js[] = "$.extend(Messages, {\n\t"
  341. . implode(",\n\t", $jsLang) . '})';
  342. }
  343. $js[] = "$.extend(defaultValues, {\n\t"
  344. . implode(",\n\t", $jsDefault) . '})';
  345. return $htmlOutput . $this->formDisplayTemplate->displayJavascript($js);
  346. }
  347. /**
  348. * Prepares data for input field display and outputs HTML code
  349. *
  350. * @param Form $form Form object
  351. * @param string $field field name as it appears in $form
  352. * @param string $systemPath field path, eg. Servers/1/verbose
  353. * @param string $workPath work path, eg. Servers/4/verbose
  354. * @param string $translatedPath work path changed so that it can be
  355. * used as XHTML id
  356. * @param bool $showRestoreDefault whether show "restore default" button
  357. * besides the input field
  358. * @param bool|null $userPrefsAllow whether user preferences are enabled
  359. * for this field (null - no support,
  360. * true/false - enabled/disabled)
  361. * @param array $jsDefault array which stores JavaScript code
  362. * to be displayed
  363. *
  364. * @return string|null HTML for input field
  365. */
  366. private function displayFieldInput(
  367. Form $form,
  368. $field,
  369. $systemPath,
  370. $workPath,
  371. $translatedPath,
  372. $showRestoreDefault,
  373. $userPrefsAllow,
  374. array &$jsDefault
  375. ) {
  376. $name = Descriptions::get($systemPath);
  377. $description = Descriptions::get($systemPath, 'desc');
  378. $value = $this->configFile->get($workPath);
  379. $valueDefault = $this->configFile->getDefault($systemPath);
  380. $valueIsDefault = false;
  381. if ($value === null || $value === $valueDefault) {
  382. $value = $valueDefault;
  383. $valueIsDefault = true;
  384. }
  385. $opts = [
  386. 'doc' => $this->getDocLink($systemPath),
  387. 'show_restore_default' => $showRestoreDefault,
  388. 'userprefs_allow' => $userPrefsAllow,
  389. 'userprefs_comment' => Descriptions::get($systemPath, 'cmt'),
  390. ];
  391. if (isset($form->default[$systemPath])) {
  392. $opts['setvalue'] = (string) $form->default[$systemPath];
  393. }
  394. if (isset($this->errors[$workPath])) {
  395. $opts['errors'] = $this->errors[$workPath];
  396. }
  397. $type = '';
  398. switch ($form->getOptionType($field)) {
  399. case 'string':
  400. $type = 'text';
  401. break;
  402. case 'short_string':
  403. $type = 'short_text';
  404. break;
  405. case 'double':
  406. case 'integer':
  407. $type = 'number_text';
  408. break;
  409. case 'boolean':
  410. $type = 'checkbox';
  411. break;
  412. case 'select':
  413. $type = 'select';
  414. $opts['values'] = $form->getOptionValueList($form->fields[$field]);
  415. break;
  416. case 'array':
  417. $type = 'list';
  418. $value = (array) $value;
  419. $valueDefault = (array) $valueDefault;
  420. break;
  421. case 'group':
  422. // :group:end is changed to :group:end:{unique id} in Form class
  423. $htmlOutput = '';
  424. if (mb_substr($field, 7, 4) !== 'end:') {
  425. $htmlOutput .= $this->formDisplayTemplate->displayGroupHeader(
  426. mb_substr($field, 7)
  427. );
  428. } else {
  429. $this->formDisplayTemplate->displayGroupFooter();
  430. }
  431. return $htmlOutput;
  432. case 'NULL':
  433. trigger_error('Field ' . $systemPath . ' has no type', E_USER_WARNING);
  434. return null;
  435. }
  436. // detect password fields
  437. if ($type === 'text'
  438. && (mb_substr($translatedPath, -9) === '-password'
  439. || mb_substr($translatedPath, -4) === 'pass'
  440. || mb_substr($translatedPath, -4) === 'Pass')
  441. ) {
  442. $type = 'password';
  443. }
  444. // TrustedProxies requires changes before displaying
  445. if ($systemPath === 'TrustedProxies') {
  446. foreach ($value as $ip => &$v) {
  447. if (preg_match('/^-\d+$/', $ip)) {
  448. continue;
  449. }
  450. $v = $ip . ': ' . $v;
  451. }
  452. }
  453. $this->setComments($systemPath, $opts);
  454. // send default value to form's JS
  455. $jsLine = '\'' . $translatedPath . '\': ';
  456. switch ($type) {
  457. case 'text':
  458. case 'short_text':
  459. case 'number_text':
  460. case 'password':
  461. $jsLine .= '\'' . Sanitize::escapeJsString($valueDefault) . '\'';
  462. break;
  463. case 'checkbox':
  464. $jsLine .= $valueDefault ? 'true' : 'false';
  465. break;
  466. case 'select':
  467. $valueDefaultJs = is_bool($valueDefault)
  468. ? (int) $valueDefault
  469. : $valueDefault;
  470. $jsLine .= '[\'' . Sanitize::escapeJsString($valueDefaultJs) . '\']';
  471. break;
  472. case 'list':
  473. $val = $valueDefault;
  474. if (isset($val['wrapper_params'])) {
  475. unset($val['wrapper_params']);
  476. }
  477. $jsLine .= '\'' . Sanitize::escapeJsString(implode("\n", $val))
  478. . '\'';
  479. break;
  480. }
  481. $jsDefault[] = $jsLine;
  482. return $this->formDisplayTemplate->displayInput(
  483. $translatedPath,
  484. $name,
  485. $type,
  486. $value,
  487. $description,
  488. $valueIsDefault,
  489. $opts
  490. );
  491. }
  492. /**
  493. * Displays errors
  494. *
  495. * @return string|null HTML for errors
  496. */
  497. public function displayErrors()
  498. {
  499. $this->validate();
  500. if (count($this->errors) === 0) {
  501. return null;
  502. }
  503. $htmlOutput = '';
  504. foreach ($this->errors as $systemPath => $errorList) {
  505. if (isset($this->systemPaths[$systemPath])) {
  506. $name = Descriptions::get($this->systemPaths[$systemPath]);
  507. } else {
  508. $name = Descriptions::get('Form_' . $systemPath);
  509. }
  510. $htmlOutput .= $this->formDisplayTemplate->displayErrors($name, $errorList);
  511. }
  512. return $htmlOutput;
  513. }
  514. /**
  515. * Reverts erroneous fields to their default values
  516. *
  517. * @return void
  518. */
  519. public function fixErrors()
  520. {
  521. $this->validate();
  522. if (count($this->errors) === 0) {
  523. return;
  524. }
  525. $cf = $this->configFile;
  526. foreach (array_keys($this->errors) as $workPath) {
  527. if (! isset($this->systemPaths[$workPath])) {
  528. continue;
  529. }
  530. $canonicalPath = $this->systemPaths[$workPath];
  531. $cf->set($workPath, $cf->getDefault($canonicalPath));
  532. }
  533. }
  534. /**
  535. * Validates select field and casts $value to correct type
  536. *
  537. * @param string|bool $value Current value
  538. * @param array $allowed List of allowed values
  539. */
  540. private function validateSelect(&$value, array $allowed): bool
  541. {
  542. $valueCmp = is_bool($value)
  543. ? (int) $value
  544. : $value;
  545. foreach ($allowed as $vk => $v) {
  546. // equality comparison only if both values are numeric or not numeric
  547. // (allows to skip 0 == 'string' equalling to true)
  548. // or identity (for string-string)
  549. if (! (($vk == $value && ! (is_numeric($valueCmp) xor is_numeric($vk)))
  550. || $vk === $value)
  551. ) {
  552. continue;
  553. }
  554. // keep boolean value as boolean
  555. if (! is_bool($value)) {
  556. // phpcs:ignore Generic.PHP.ForbiddenFunctions
  557. settype($value, gettype($vk));
  558. }
  559. return true;
  560. }
  561. return false;
  562. }
  563. /**
  564. * Validates and saves form data to session
  565. *
  566. * @param array|string $forms array of form names
  567. * @param bool $allowPartialSave allows for partial form saving on
  568. * failed validation
  569. *
  570. * @return bool true on success (no errors and all saved)
  571. */
  572. public function save($forms, $allowPartialSave = true)
  573. {
  574. $result = true;
  575. $forms = (array) $forms;
  576. $values = [];
  577. $toSave = [];
  578. $isSetupScript = $GLOBALS['PMA_Config']->get('is_setup');
  579. if ($isSetupScript) {
  580. $this->loadUserprefsInfo();
  581. }
  582. $this->errors = [];
  583. foreach ($forms as $formName) {
  584. if (! isset($this->forms[$formName])) {
  585. continue;
  586. }
  587. /** @var Form $form */
  588. $form = $this->forms[$formName];
  589. // get current server id
  590. $changeIndex = $form->index === 0
  591. ? $this->configFile->getServerCount() + 1
  592. : false;
  593. // grab POST values
  594. foreach ($form->fields as $field => $systemPath) {
  595. $workPath = array_search($systemPath, $this->systemPaths);
  596. $key = $this->translatedPaths[$workPath];
  597. $type = (string) $form->getOptionType($field);
  598. // skip groups
  599. if ($type === 'group') {
  600. continue;
  601. }
  602. // ensure the value is set
  603. if (! isset($_POST[$key])) {
  604. // checkboxes aren't set by browsers if they're off
  605. if ($type !== 'boolean') {
  606. $this->errors[$form->name][] = sprintf(
  607. __('Missing data for %s'),
  608. '<i>' . Descriptions::get($systemPath) . '</i>'
  609. );
  610. $result = false;
  611. continue;
  612. }
  613. $_POST[$key] = false;
  614. }
  615. // user preferences allow/disallow
  616. if ($isSetupScript
  617. && isset($this->userprefsKeys[$systemPath])
  618. ) {
  619. if (isset($this->userprefsDisallow[$systemPath], $_POST[$key . '-userprefs-allow'])
  620. ) {
  621. unset($this->userprefsDisallow[$systemPath]);
  622. } elseif (! isset($_POST[$key . '-userprefs-allow'])) {
  623. $this->userprefsDisallow[$systemPath] = true;
  624. }
  625. }
  626. // cast variables to correct type
  627. switch ($type) {
  628. case 'double':
  629. $_POST[$key] = Util::requestString($_POST[$key]);
  630. // phpcs:ignore Generic.PHP.ForbiddenFunctions
  631. settype($_POST[$key], 'float');
  632. break;
  633. case 'boolean':
  634. case 'integer':
  635. if ($_POST[$key] !== '') {
  636. $_POST[$key] = Util::requestString($_POST[$key]);
  637. // phpcs:ignore Generic.PHP.ForbiddenFunctions
  638. settype($_POST[$key], $type);
  639. }
  640. break;
  641. case 'select':
  642. $successfullyValidated = $this->validateSelect(
  643. $_POST[$key],
  644. $form->getOptionValueList($systemPath)
  645. );
  646. if (! $successfullyValidated) {
  647. $this->errors[$workPath][] = __('Incorrect value!');
  648. $result = false;
  649. // "continue" for the $form->fields foreach-loop
  650. continue 2;
  651. }
  652. break;
  653. case 'string':
  654. case 'short_string':
  655. $_POST[$key] = Util::requestString($_POST[$key]);
  656. break;
  657. case 'array':
  658. // eliminate empty values and ensure we have an array
  659. $postValues = is_array($_POST[$key])
  660. ? $_POST[$key]
  661. : explode("\n", $_POST[$key]);
  662. $_POST[$key] = [];
  663. $this->fillPostArrayParameters($postValues, $key);
  664. break;
  665. }
  666. // now we have value with proper type
  667. $values[$systemPath] = $_POST[$key];
  668. if ($changeIndex !== false) {
  669. $workPath = str_replace(
  670. 'Servers/' . $form->index . '/',
  671. 'Servers/' . $changeIndex . '/',
  672. $workPath
  673. );
  674. }
  675. $toSave[$workPath] = $systemPath;
  676. }
  677. }
  678. // save forms
  679. if (! $allowPartialSave && ! empty($this->errors)) {
  680. // don't look for non-critical errors
  681. $this->validate();
  682. return $result;
  683. }
  684. foreach ($toSave as $workPath => $path) {
  685. // TrustedProxies requires changes before saving
  686. if ($path === 'TrustedProxies') {
  687. $proxies = [];
  688. $i = 0;
  689. foreach ($values[$path] as $value) {
  690. $matches = [];
  691. $match = preg_match(
  692. '/^(.+):(?:[ ]?)(\\w+)$/',
  693. $value,
  694. $matches
  695. );
  696. if ($match) {
  697. // correct 'IP: HTTP header' pair
  698. $ip = trim($matches[1]);
  699. $proxies[$ip] = trim($matches[2]);
  700. } else {
  701. // save also incorrect values
  702. $proxies['-' . $i] = $value;
  703. $i++;
  704. }
  705. }
  706. $values[$path] = $proxies;
  707. }
  708. $this->configFile->set($workPath, $values[$path], $path);
  709. }
  710. if ($isSetupScript) {
  711. $this->configFile->set(
  712. 'UserprefsDisallow',
  713. array_keys($this->userprefsDisallow)
  714. );
  715. }
  716. // don't look for non-critical errors
  717. $this->validate();
  718. return $result;
  719. }
  720. /**
  721. * Tells whether form validation failed
  722. *
  723. * @return bool
  724. */
  725. public function hasErrors()
  726. {
  727. return count($this->errors) > 0;
  728. }
  729. /**
  730. * Returns link to documentation
  731. *
  732. * @param string $path Path to documentation
  733. *
  734. * @return string
  735. */
  736. public function getDocLink($path)
  737. {
  738. $test = mb_substr($path, 0, 6);
  739. if ($test === 'Import' || $test === 'Export') {
  740. return '';
  741. }
  742. return MySQLDocumentation::getDocumentationLink(
  743. 'config',
  744. 'cfg_' . $this->getOptName($path),
  745. Sanitize::isSetup() ? '../' : './'
  746. );
  747. }
  748. /**
  749. * Changes path so it can be used in URLs
  750. *
  751. * @param string $path Path
  752. *
  753. * @return string
  754. */
  755. private function getOptName($path)
  756. {
  757. return str_replace(['Servers/1/', '/'], ['Servers/', '_'], $path);
  758. }
  759. /**
  760. * Fills out {@link userprefs_keys} and {@link userprefs_disallow}
  761. *
  762. * @return void
  763. */
  764. private function loadUserprefsInfo()
  765. {
  766. if ($this->userprefsKeys !== null) {
  767. return;
  768. }
  769. $this->userprefsKeys = array_flip(UserFormList::getFields());
  770. // read real config for user preferences display
  771. $userPrefsDisallow = $GLOBALS['PMA_Config']->get('is_setup')
  772. ? $this->configFile->get('UserprefsDisallow', [])
  773. : $GLOBALS['cfg']['UserprefsDisallow'];
  774. $this->userprefsDisallow = array_flip($userPrefsDisallow ?? []);
  775. }
  776. /**
  777. * Sets field comments and warnings based on current environment
  778. *
  779. * @param string $systemPath Path to settings
  780. * @param array $opts Chosen options
  781. *
  782. * @return void
  783. */
  784. private function setComments($systemPath, array &$opts)
  785. {
  786. // RecodingEngine - mark unavailable types
  787. if ($systemPath === 'RecodingEngine') {
  788. $comment = '';
  789. if (! function_exists('iconv')) {
  790. $opts['values']['iconv'] .= ' (' . __('unavailable') . ')';
  791. $comment = sprintf(
  792. __('"%s" requires %s extension'),
  793. 'iconv',
  794. 'iconv'
  795. );
  796. }
  797. if (! function_exists('recode_string')) {
  798. $opts['values']['recode'] .= ' (' . __('unavailable') . ')';
  799. $comment .= ($comment ? ', ' : '') . sprintf(
  800. __('"%s" requires %s extension'),
  801. 'recode',
  802. 'recode'
  803. );
  804. }
  805. /* mbstring is always there thanks to polyfill */
  806. $opts['comment'] = $comment;
  807. $opts['comment_warning'] = true;
  808. }
  809. // ZipDump, GZipDump, BZipDump - check function availability
  810. if ($systemPath === 'ZipDump'
  811. || $systemPath === 'GZipDump'
  812. || $systemPath === 'BZipDump'
  813. ) {
  814. $comment = '';
  815. $funcs = [
  816. 'ZipDump' => [
  817. 'zip_open',
  818. 'gzcompress',
  819. ],
  820. 'GZipDump' => [
  821. 'gzopen',
  822. 'gzencode',
  823. ],
  824. 'BZipDump' => [
  825. 'bzopen',
  826. 'bzcompress',
  827. ],
  828. ];
  829. if (! function_exists($funcs[$systemPath][0])) {
  830. $comment = sprintf(
  831. __(
  832. 'Compressed import will not work due to missing function %s.'
  833. ),
  834. $funcs[$systemPath][0]
  835. );
  836. }
  837. if (! function_exists($funcs[$systemPath][1])) {
  838. $comment .= ($comment ? '; ' : '') . sprintf(
  839. __(
  840. 'Compressed export will not work due to missing function %s.'
  841. ),
  842. $funcs[$systemPath][1]
  843. );
  844. }
  845. $opts['comment'] = $comment;
  846. $opts['comment_warning'] = true;
  847. }
  848. if ($GLOBALS['PMA_Config']->get('is_setup')) {
  849. return;
  850. }
  851. if ($systemPath !== 'MaxDbList' && $systemPath !== 'MaxTableList'
  852. && $systemPath !== 'QueryHistoryMax'
  853. ) {
  854. return;
  855. }
  856. $opts['comment'] = sprintf(
  857. __('maximum %s'),
  858. $GLOBALS['cfg'][$systemPath]
  859. );
  860. }
  861. /**
  862. * Copy items of an array to $_POST variable
  863. *
  864. * @param array $postValues List of parameters
  865. * @param string $key Array key
  866. *
  867. * @return void
  868. */
  869. private function fillPostArrayParameters(array $postValues, $key)
  870. {
  871. foreach ($postValues as $v) {
  872. $v = Util::requestString($v);
  873. if ($v === '') {
  874. continue;
  875. }
  876. $_POST[$key][] = $v;
  877. }
  878. }
  879. }