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