Translated using Weblate (Russian)
[phpmyadmin.git] / src / InsertEdit.php
blobcc47e89bb0a5df937433b54542f9a54568cb4534
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\ConfigStorage\Relation;
8 use PhpMyAdmin\Dbal\ResultInterface;
9 use PhpMyAdmin\Html\Generator;
10 use PhpMyAdmin\Plugins\IOTransformationsPlugin;
11 use PhpMyAdmin\Plugins\TransformationsPlugin;
12 use PhpMyAdmin\Utils\Gis;
14 use function __;
15 use function array_fill;
16 use function array_key_exists;
17 use function array_merge;
18 use function array_values;
19 use function bin2hex;
20 use function class_exists;
21 use function count;
22 use function current;
23 use function explode;
24 use function htmlspecialchars;
25 use function implode;
26 use function in_array;
27 use function is_array;
28 use function is_file;
29 use function is_string;
30 use function json_encode;
31 use function max;
32 use function mb_stripos;
33 use function mb_strlen;
34 use function mb_strstr;
35 use function min;
36 use function password_hash;
37 use function preg_match;
38 use function preg_replace;
39 use function str_contains;
40 use function str_ends_with;
41 use function str_replace;
42 use function str_starts_with;
43 use function stripcslashes;
44 use function stripslashes;
45 use function strlen;
46 use function substr;
47 use function trim;
49 use const ENT_COMPAT;
50 use const PASSWORD_DEFAULT;
52 class InsertEdit
54 private const FUNC_OPTIONAL_PARAM = ['RAND', 'UNIX_TIMESTAMP'];
56 private const FUNC_NO_PARAM = [
57 'CONNECTION_ID',
58 'CURRENT_USER',
59 'CURDATE',
60 'CURTIME',
61 'CURRENT_DATE',
62 'CURRENT_TIME',
63 'DATABASE',
64 'LAST_INSERT_ID',
65 'NOW',
66 'PI',
67 'RAND',
68 'SYSDATE',
69 'UNIX_TIMESTAMP',
70 'USER',
71 'UTC_DATE',
72 'UTC_TIME',
73 'UTC_TIMESTAMP',
74 'UUID',
75 'UUID_SHORT',
76 'VERSION',
79 private int $rowOffset = 0;
80 private int $fieldIndex = 0;
82 public function __construct(
83 private DatabaseInterface $dbi,
84 private Relation $relation,
85 private Transformations $transformations,
86 private FileListing $fileListing,
87 private Template $template,
88 ) {
91 /**
92 * Retrieve form parameters for insert/edit form
94 * @param string $db name of the database
95 * @param string $table name of the table
96 * @param mixed[] $whereClauseArray
98 * @return array<string, string> array of insert/edit form parameters
100 public function getFormParametersForInsertForm(
101 string $db,
102 string $table,
103 array|null $whereClauses,
104 array $whereClauseArray,
105 string $errorUrl,
106 ): array {
107 $formParams = [
108 'db' => $db,
109 'table' => $table,
110 'goto' => $GLOBALS['goto'],
111 'err_url' => $errorUrl,
112 'sql_query' => $_POST['sql_query'] ?? '',
115 if ($formParams['sql_query'] === '' && isset($_GET['sql_query'], $_GET['sql_signature'])) {
116 if (Core::checkSqlQuerySignature($_GET['sql_query'], $_GET['sql_signature'])) {
117 $formParams['sql_query'] = $_GET['sql_query'];
121 if (isset($whereClauses)) {
122 foreach ($whereClauseArray as $keyId => $whereClause) {
123 $formParams['where_clause[' . $keyId . ']'] = trim($whereClause);
127 if (isset($_POST['clause_is_unique'])) {
128 $formParams['clause_is_unique'] = $_POST['clause_is_unique'];
129 } elseif (isset($_GET['clause_is_unique'])) {
130 $formParams['clause_is_unique'] = $_GET['clause_is_unique'];
133 return $formParams;
137 * Analysing where clauses array
139 * @param string[] $whereClauseArray array of where clauses
140 * @param string $table name of the table
141 * @param string $db name of the database
143 * @return array<int, string[]|ResultInterface[]|array<string, string|null>[]|bool>
144 * @phpstan-return array{string[], ResultInterface[], array<string|null>[], bool}
146 private function analyzeWhereClauses(
147 array $whereClauseArray,
148 string $table,
149 string $db,
150 ): array {
151 $rows = [];
152 $result = [];
153 $whereClauses = [];
154 $foundUniqueKey = false;
155 foreach ($whereClauseArray as $keyId => $whereClause) {
156 $localQuery = 'SELECT * FROM '
157 . Util::backquote($db) . '.'
158 . Util::backquote($table)
159 . ' WHERE ' . $whereClause . ';';
160 $result[$keyId] = $this->dbi->query($localQuery);
161 $rows[$keyId] = $result[$keyId]->fetchAssoc();
163 $whereClauses[$keyId] = str_replace('\\', '\\\\', $whereClause);
164 $hasUniqueCondition = $this->showEmptyResultMessageOrSetUniqueCondition(
165 $rows,
166 $keyId,
167 $whereClauseArray,
168 $localQuery,
169 $result,
171 if (! $hasUniqueCondition) {
172 continue;
175 $foundUniqueKey = true;
178 return [$whereClauses, $result, $rows, $foundUniqueKey];
182 * Show message for empty result or set the unique_condition
184 * @param mixed[] $rows MySQL returned rows
185 * @param string|int $keyId ID in current key
186 * @param mixed[] $whereClauseArray array of where clauses
187 * @param string $localQuery query performed
188 * @param ResultInterface[] $result MySQL result handle
190 private function showEmptyResultMessageOrSetUniqueCondition(
191 array $rows,
192 string|int $keyId,
193 array $whereClauseArray,
194 string $localQuery,
195 array $result,
196 ): bool {
197 // No row returned
198 if (! $rows[$keyId]) {
199 unset($rows[$keyId], $whereClauseArray[$keyId]);
200 ResponseRenderer::getInstance()->addHTML(
201 Generator::getMessage(
202 __('MySQL returned an empty result set (i.e. zero rows).'),
203 $localQuery,
207 * @todo not sure what should be done at this point, but we must not
208 * exit if we want the message to be displayed
211 return false;
214 $meta = $this->dbi->getFieldsMeta($result[$keyId]);
216 $uniqueCondition = (new UniqueCondition($meta, $rows[$keyId], true))->getWhereClause();
218 return (bool) $uniqueCondition;
222 * No primary key given, just load first row
224 private function loadFirstRow(string $table, string $db): ResultInterface
226 return $this->dbi->query(
227 'SELECT * FROM ' . Util::backquote($db)
228 . '.' . Util::backquote($table) . ' LIMIT 1;',
232 /** @return false[] */
233 private function getInsertRows(): array
235 // Can be a string on some old configuration storage settings
236 return array_fill(0, Config::getInstance()->settings['InsertRows'], false);
240 * Show type information or function selectors in Insert/Edit
242 * @param string $which function|type
243 * @param mixed[] $urlParams containing url parameters
244 * @param bool $isShow whether to show the element in $which
246 * @return string an HTML snippet
248 public function showTypeOrFunction(string $which, array $urlParams, bool $isShow): string
250 $params = [];
252 $config = Config::getInstance();
253 switch ($which) {
254 case 'function':
255 $params['ShowFunctionFields'] = ($isShow ? 0 : 1);
256 $params['ShowFieldTypesInDataEditView'] = $config->settings['ShowFieldTypesInDataEditView'];
257 break;
258 case 'type':
259 $params['ShowFieldTypesInDataEditView'] = ($isShow ? 0 : 1);
260 $params['ShowFunctionFields'] = $config->settings['ShowFunctionFields'];
261 break;
264 $params['goto'] = Url::getFromRoute('/sql');
265 $thisUrlParams = array_merge($urlParams, $params);
267 if (! $isShow) {
268 return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
269 . Url::getCommon($thisUrlParams, '', false) . '">'
270 . $this->showTypeOrFunctionLabel($which)
271 . '</a>';
274 return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
275 . Url::getCommon($thisUrlParams, '', false)
276 . '" title="' . __('Hide') . '">'
277 . $this->showTypeOrFunctionLabel($which)
278 . '</a></th>';
282 * Show type information or function selectors labels in Insert/Edit
284 * @param string $which function|type
286 * @return string an HTML snippet
288 private function showTypeOrFunctionLabel(string $which): string
290 return match ($which) {
291 'function' => __('Function'),
292 'type' => __('Type'),
293 default => '',
298 * Retrieve the column title
300 * @param string $fieldName name of the column
301 * @param string[] $commentsMap comments for every column that has a comment
303 * @return string column title
305 private function getColumnTitle(string $fieldName, array $commentsMap): string
307 if (isset($commentsMap[$fieldName])) {
308 return '<span style="border-bottom: 1px dashed black;" title="'
309 . htmlspecialchars($commentsMap[$fieldName]) . '">'
310 . htmlspecialchars($fieldName) . '</span>';
313 return htmlspecialchars($fieldName);
317 * check whether the column is of a certain type
318 * the goal is to ensure that types such as "enum('one','two','binary',..)"
319 * or "enum('one','two','varbinary',..)" are not categorized as binary
321 * @param string $columnType column type as specified in the column definition
322 * @param string[] $types the types to verify
324 public function isColumn(string $columnType, array $types): bool
326 foreach ($types as $oneType) {
327 if (mb_stripos($columnType, $oneType) === 0) {
328 return true;
332 return false;
336 * Retrieve the nullify code for the null column
338 * @param InsertEditColumn $column description of column in given table
339 * @param mixed[] $foreigners keys into foreign fields
340 * @param mixed[] $foreignData data about the foreign keys
342 private function getNullifyCodeForNullColumn(
343 InsertEditColumn $column,
344 array $foreigners,
345 array $foreignData,
346 ): string {
347 $foreigner = $this->relation->searchColumnInForeigners($foreigners, $column->field);
348 if (mb_strstr($column->trueType, 'enum')) {
349 $nullifyCode = mb_strlen($column->type) > 20 ? '1' : '2';
350 } elseif (mb_strstr($column->trueType, 'set')) {
351 $nullifyCode = '3';
352 } elseif ($foreigner && $foreignData['foreign_link'] == false) {
353 // foreign key in a drop-down
354 $nullifyCode = '4';
355 } elseif ($foreigner && $foreignData['foreign_link'] == true) {
356 // foreign key with a browsing icon
357 $nullifyCode = '6';
358 } else {
359 $nullifyCode = '5';
362 return $nullifyCode;
366 * Get HTML textarea for insert form
368 * @param InsertEditColumn $column column information
369 * @param string $backupField hidden input field
370 * @param string $columnNameAppendix the name attribute
371 * @param string $onChangeClause onchange clause for fields
372 * @param string $textDir text direction
373 * @param string $specialCharsEncoded replaced char if the string starts
374 * with a \r\n pair (0x0d0a) add an extra \n
375 * @param string $dataType the html5 data-* attribute type
377 * @return string an html snippet
379 private function getTextarea(
380 InsertEditColumn $column,
381 string $backupField,
382 string $columnNameAppendix,
383 string $onChangeClause,
384 string $textDir,
385 string $specialCharsEncoded,
386 string $dataType,
387 ): string {
388 $theClass = '';
389 $config = Config::getInstance();
390 $textAreaRows = $config->settings['TextareaRows'];
391 $textareaCols = $config->settings['TextareaCols'];
393 if ($column->isChar) {
395 * @todo clarify the meaning of the "textfield" class and explain
396 * why character columns have the "char" class instead
398 $theClass = 'char charField';
399 $textAreaRows = $config->settings['CharTextareaRows'];
400 $textareaCols = $config->settings['CharTextareaCols'];
401 $extractedColumnspec = Util::extractColumnSpec($column->type);
402 $maxlength = $extractedColumnspec['spec_in_brackets'];
403 } elseif ($config->settings['LongtextDoubleTextarea'] && mb_strstr($column->pmaType, 'longtext')) {
404 $textAreaRows = $config->settings['TextareaRows'] * 2;
405 $textareaCols = $config->settings['TextareaCols'] * 2;
408 return $backupField . "\n"
409 . '<textarea name="fields' . $columnNameAppendix . '"'
410 . ' class="' . $theClass . '"'
411 . (isset($maxlength) ? ' data-maxlength="' . $maxlength . '"' : '')
412 . ' rows="' . $textAreaRows . '"'
413 . ' cols="' . $textareaCols . '"'
414 . ' dir="' . $textDir . '"'
415 . ' id="field_' . $this->fieldIndex . '_3"'
416 . ($onChangeClause !== '' ? ' onchange="' . htmlspecialchars($onChangeClause, ENT_COMPAT) . '"' : '')
417 . ' tabindex="' . $this->fieldIndex . '"'
418 . ' data-type="' . $dataType . '">'
419 . $specialCharsEncoded
420 . '</textarea>';
424 * Get HTML input type
426 * @param InsertEditColumn $column description of column in given table
427 * @param string $columnNameAppendix the name attribute
428 * @param string $specialChars special characters
429 * @param int $fieldsize html field size
430 * @param string $onChangeClause onchange clause for fields
431 * @param string $dataType the html5 data-* attribute type
433 * @return string an html snippet
435 private function getHtmlInput(
436 InsertEditColumn $column,
437 string $columnNameAppendix,
438 string $specialChars,
439 int $fieldsize,
440 string $onChangeClause,
441 string $dataType,
442 ): string {
443 $theClass = 'textfield';
444 // verify True_Type which does not contain the parentheses and length
445 if ($column->trueType === 'date') {
446 $theClass .= ' datefield';
447 } elseif ($column->trueType === 'time') {
448 $theClass .= ' timefield';
449 } elseif ($column->trueType === 'datetime' || $column->trueType === 'timestamp') {
450 $theClass .= ' datetimefield';
453 $inputMinMax = '';
454 $isInteger = in_array($column->trueType, $this->dbi->types->getIntegerTypes());
455 if ($isInteger) {
456 $extractedColumnspec = Util::extractColumnSpec($column->type);
457 $isUnsigned = $extractedColumnspec['unsigned'];
458 $minMaxValues = $this->dbi->types->getIntegerRange($column->trueType, ! $isUnsigned);
459 $inputMinMax = 'min="' . $minMaxValues[0] . '" '
460 . 'max="' . $minMaxValues[1] . '"';
461 $dataType = 'INT';
464 // do not use the 'date' or 'time' types here; they have no effect on some
465 // browsers and create side effects (see bug #4218)
466 return '<input type="text"'
467 . ' name="fields' . $columnNameAppendix . '"'
468 . ' value="' . $specialChars . '" size="' . $fieldsize . '"'
469 . ($column->isChar
470 ? ' data-maxlength="' . $fieldsize . '"'
471 : '')
472 . ($inputMinMax !== '' ? ' ' . $inputMinMax : '')
473 . ' data-type="' . $dataType . '"'
474 . ' class="' . $theClass . '" onchange="' . htmlspecialchars($onChangeClause, ENT_COMPAT) . '"'
475 . ' tabindex="' . $this->fieldIndex . '"'
476 . ($isInteger ? ' inputmode="numeric"' : '')
477 . ' id="field_' . $this->fieldIndex . '_3">';
481 * Get HTML select option for upload
483 * @param string $vkey [multi_edit]['row_id']
484 * @param string $fieldHashMd5 array index as an MD5 to avoid having special characters
486 * @return string an HTML snippet
488 private function getSelectOptionForUpload(string $vkey, string $fieldHashMd5): string
490 $files = $this->fileListing->getFileSelectOptions(
491 Util::userDir((Config::getInstance()->settings['UploadDir'] ?? '')),
494 if ($files === false) {
495 return '<span style="color:red">' . __('Error') . '</span><br>' . "\n"
496 . __('The directory you set for upload work cannot be reached.') . "\n";
499 if ($files === '') {
500 return '';
503 return "<br>\n"
504 . '<i>' . __('Or') . '</i> '
505 . __('web server upload directory:') . '<br>' . "\n"
506 . '<select size="1" name="fields_uploadlocal'
507 . $vkey . '[' . $fieldHashMd5 . ']">' . "\n"
508 . '<option value="" selected="selected"></option>' . "\n"
509 . $files
510 . '</select>' . "\n";
514 * Retrieve the maximum upload file size
516 private function getMaxUploadSize(string $pmaType): string
518 // find maximum upload size, based on field type
520 * @todo with functions this is not so easy, as you can basically
521 * process any data with function like MD5
523 $maxFieldSize = match ($pmaType) {
524 'tinyblob' => 256,
525 'blob' => 65536,
526 'mediumblob' => 16777216,
527 'longblob' => 4294967296,// yeah, really
530 $thisFieldMaxSize = (int) Config::getInstance()->get('max_upload_size'); // from PHP max
532 return Util::getFormattedMaximumUploadSize(min($thisFieldMaxSize, $maxFieldSize)) . "\n";
536 * Get HTML for the Value column of other datatypes
537 * (here, "column" is used in the sense of HTML column in HTML table)
539 * @param InsertEditColumn $column description of column in given table
540 * @param string $defaultCharEditing default char editing mode which is stored
541 * in the config.inc.php script
542 * @param string $backupField hidden input field
543 * @param string $columnNameAppendix the name attribute
544 * @param string $onChangeClause onchange clause for fields
545 * @param string $specialChars special characters
546 * @param string $textDir text direction
547 * @param string $specialCharsEncoded replaced char if the string starts
548 * with a \r\n pair (0x0d0a) add an extra \n
549 * @param string $data data to edit
550 * @param mixed[] $extractedColumnspec associative array containing type,
551 * spec_in_brackets and possibly
552 * enum_set_values (another array)
554 * @return string an html snippet
556 private function getValueColumnForOtherDatatypes(
557 InsertEditColumn $column,
558 string $defaultCharEditing,
559 string $backupField,
560 string $columnNameAppendix,
561 string $onChangeClause,
562 string $specialChars,
563 string $textDir,
564 string $specialCharsEncoded,
565 string $data,
566 array $extractedColumnspec,
567 ): string {
568 // HTML5 data-* attribute data-type
569 $dataType = $this->dbi->types->getTypeClass($column->trueType);
570 $fieldsize = $this->getColumnSize($column, $extractedColumnspec['spec_in_brackets']);
572 $config = Config::getInstance();
573 $isTextareaRequired = $column->isChar
574 && ($config->settings['CharEditing'] === 'textarea' || str_contains($data, "\n"));
575 if ($isTextareaRequired) {
576 $config->settings['CharEditing'] = $defaultCharEditing;
577 $htmlField = $this->getTextarea(
578 $column,
579 $backupField,
580 $columnNameAppendix,
581 $onChangeClause,
582 $textDir,
583 $specialCharsEncoded,
584 $dataType,
586 } else {
587 $htmlField = $this->getHtmlInput(
588 $column,
589 $columnNameAppendix,
590 $specialChars,
591 $fieldsize,
592 $onChangeClause,
593 $dataType,
597 return $this->template->render('table/insert/value_column_for_other_datatype', [
598 'html_field' => $htmlField,
599 'backup_field' => $backupField,
600 'is_textarea' => $isTextareaRequired,
601 'columnNameAppendix' => $columnNameAppendix,
602 'column' => $column,
607 * Get the field size
609 * @param InsertEditColumn $column description of column in given table
610 * @param string $specInBrackets text in brackets inside column definition
612 * @return int field size
614 private function getColumnSize(InsertEditColumn $column, string $specInBrackets): int
616 $config = Config::getInstance();
617 if ($column->isChar) {
618 $fieldsize = (int) $specInBrackets;
619 if ($fieldsize > $config->settings['MaxSizeForInputField']) {
621 * This case happens for CHAR or VARCHAR columns which have
622 * a size larger than the maximum size for input field.
624 $config->settings['CharEditing'] = 'textarea';
626 } else {
628 * This case happens for example for INT or DATE columns;
629 * in these situations, the value returned in $column['len']
630 * seems appropriate.
632 $fieldsize = $column->length;
635 return min(
636 max($fieldsize, $config->settings['MinSizeForInputField']),
637 $config->settings['MaxSizeForInputField'],
642 * get html for continue insertion form
644 * @param string $table name of the table
645 * @param string $db name of the database
646 * @param mixed[] $whereClauseArray
648 * @return string an html snippet
650 public function getContinueInsertionForm(
651 string $table,
652 string $db,
653 array $whereClauseArray,
654 string $errorUrl,
655 ): string {
656 return $this->template->render('table/insert/continue_insertion_form', [
657 'db' => $db,
658 'table' => $table,
659 'where_clause_array' => $whereClauseArray,
660 'err_url' => $errorUrl,
661 'goto' => $GLOBALS['goto'],
662 'sql_query' => $_POST['sql_query'] ?? null,
663 'has_where_clause' => isset($_POST['where_clause']),
664 'insert_rows_default' => Config::getInstance()->settings['InsertRows'],
669 * @param string[]|string|null $whereClause
671 * @psalm-pure
673 public static function isWhereClauseNumeric(array|string|null $whereClause): bool
675 if ($whereClause === null) {
676 return false;
679 if (! is_array($whereClause)) {
680 $whereClause = [$whereClause];
683 // If we have just numeric primary key, we can also edit next
684 // we are looking for `table_name`.`field_name` = numeric_value
685 foreach ($whereClause as $clause) {
686 // preg_match() returns 1 if there is a match
687 $isNumeric = preg_match('@^[\s]*`[^`]*`[\.]`[^`]*` = [0-9]+@', $clause) === 1;
688 if ($isNumeric) {
689 return true;
693 return false;
697 * Get table head and table foot for insert row table
699 * @param mixed[] $urlParams url parameters
701 * @return string an html snippet
703 private function getHeadAndFootOfInsertRowTable(array $urlParams): string
705 $type = '';
706 $function = '';
708 $config = Config::getInstance();
709 if ($config->settings['ShowFieldTypesInDataEditView']) {
710 $type = $this->showTypeOrFunction('type', $urlParams, true);
713 if ($config->settings['ShowFunctionFields']) {
714 $function = $this->showTypeOrFunction('function', $urlParams, true);
717 $template = new Template();
719 return $template->render('table/insert/get_head_and_foot_of_insert_row_table', [
720 'type' => $type,
721 'function' => $function,
726 * Prepares the field value and retrieve special chars, backup field and data array
728 * @param mixed[] $currentRow a row of the table
729 * @param InsertEditColumn $column description of column in given table
730 * @param mixed[] $extractedColumnspec associative array containing type,
731 * spec_in_brackets and possibly
732 * enum_set_values (another array)
733 * @param mixed[] $gisDataTypes list of GIS data types
734 * @param string $columnNameAppendix string to append to column name in input
735 * @param bool $asIs use the data as is, used in repopulating
737 * @return mixed[] $real_null_value, $data, $special_chars, $backup_field,
738 * $special_chars_encoded
739 * @psalm-return array{bool, string, string, string, string}
741 private function getSpecialCharsAndBackupFieldForExistingRow(
742 array $currentRow,
743 InsertEditColumn $column,
744 array $extractedColumnspec,
745 array $gisDataTypes,
746 string $columnNameAppendix,
747 bool $asIs,
748 ): array {
749 $specialCharsEncoded = '';
750 $data = null;
751 $realNullValue = false;
752 // (we are editing)
753 if (! isset($currentRow[$column->field])) {
754 $realNullValue = true;
755 $currentRow[$column->field] = '';
756 $specialChars = '';
757 $data = '';
758 } elseif ($column->trueType === 'bit') {
759 $specialChars = $asIs
760 ? $currentRow[$column->field]
761 : Util::printableBitValue(
762 (int) $currentRow[$column->field],
763 (int) $extractedColumnspec['spec_in_brackets'],
765 } elseif (
766 (str_starts_with($column->trueType, 'timestamp')
767 || $column->trueType === 'datetime'
768 || $column->trueType === 'time')
769 && (str_contains($currentRow[$column->field], '.'))
771 $currentRow[$column->field] = $asIs
772 ? $currentRow[$column->field]
773 : Util::addMicroseconds($currentRow[$column->field]);
774 $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
775 } elseif (in_array($column->trueType, $gisDataTypes)) {
776 // Convert gis data to Well Know Text format
777 $currentRow[$column->field] = $asIs
778 ? $currentRow[$column->field]
779 : Gis::convertToWellKnownText($currentRow[$column->field], true);
780 $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
781 } else {
782 // special binary "characters"
783 if ($column->isBinary || ($column->isBlob && Config::getInstance()->settings['ProtectBinary'] !== 'all')) {
784 $currentRow[$column->field] = $asIs
785 ? $currentRow[$column->field]
786 : bin2hex($currentRow[$column->field]);
789 $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
791 //We need to duplicate the first \n or otherwise we will lose
792 //the first newline entered in a VARCHAR or TEXT column
793 $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
795 $data = $currentRow[$column->field];
798 /** @var string $defaultAction */
799 $defaultAction = $_POST['default_action'] ?? $_GET['default_action'] ?? '';
800 if (
801 $defaultAction === 'insert'
802 && $column->key === 'PRI'
803 && str_contains($column->extra, 'auto_increment')
805 // When copying row, it is useful to empty auto-increment column to prevent duplicate key error.
806 $data = $specialCharsEncoded = $specialChars = null;
809 // If a timestamp field value is not included in an update
810 // statement MySQL auto-update it to the current timestamp;
811 // however, things have changed since MySQL 4.1, so
812 // it's better to set a fields_prev in this situation
813 $backupField = '<input type="hidden" name="fields_prev'
814 . $columnNameAppendix . '" value="'
815 . htmlspecialchars($currentRow[$column->field], ENT_COMPAT) . '">';
817 return [$realNullValue, (string) $specialCharsEncoded, (string) $specialChars, (string) $data, $backupField];
821 * display default values
823 private function getSpecialCharsForInsertingMode(
824 string|null $defaultValue,
825 string $trueType,
826 ): string {
827 if ($defaultValue === null) {
828 $defaultValue = '';
831 if ($trueType === 'bit') {
832 $specialChars = Util::convertBitDefaultValue($defaultValue);
833 } elseif (str_starts_with($trueType, 'timestamp') || $trueType === 'datetime' || $trueType === 'time') {
834 $specialChars = Util::addMicroseconds($defaultValue);
835 } elseif ($trueType === 'binary' || $trueType === 'varbinary') {
836 $specialChars = bin2hex($defaultValue);
837 } elseif (str_ends_with($trueType, 'text')) {
838 $textDefault = substr($defaultValue, 1, -1);
839 $specialChars = stripcslashes($textDefault !== '' ? $textDefault : $defaultValue);
840 } else {
841 $specialChars = htmlspecialchars($defaultValue);
844 return $specialChars;
848 * set $_SESSION for edit_next
850 * @param string $oneWhereClause one where clause from where clauses array
852 public function setSessionForEditNext(string $oneWhereClause): void
854 $localQuery = 'SELECT * FROM ' . Util::backquote($GLOBALS['db'])
855 . '.' . Util::backquote($GLOBALS['table']) . ' WHERE '
856 . str_replace('` =', '` >', $oneWhereClause) . ' LIMIT 1;';
858 $res = $this->dbi->query($localQuery);
859 $row = $res->fetchRow();
860 $meta = $this->dbi->getFieldsMeta($res);
861 // must find a unique condition based on unique key,
862 // not a combination of all fields
863 $uniqueCondition = (new UniqueCondition($meta, $row, true))->getWhereClause();
864 if (! $uniqueCondition) {
865 return;
868 $_SESSION['edit_next'] = $uniqueCondition;
872 * set $goto_include variable for different cases and retrieve like,
873 * if $GLOBALS['goto'] empty, if $goto_include previously not defined
874 * and new_insert, same_insert, edit_next
876 * @param string|false $gotoInclude store some script for include, otherwise it is
877 * boolean false
879 public function getGotoInclude(string|false $gotoInclude): string
881 $validOptions = ['new_insert', 'same_insert', 'edit_next'];
882 if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $validOptions)) {
883 return '/table/change';
886 if (! empty($GLOBALS['goto'])) {
887 if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) {
888 // this should NOT happen
889 //$GLOBALS['goto'] = false;
890 $gotoInclude = $GLOBALS['goto'] === 'index.php?route=/sql' ? '/sql' : false;
891 } else {
892 $gotoInclude = $GLOBALS['goto'];
895 if ($GLOBALS['goto'] === 'index.php?route=/database/sql' && strlen($GLOBALS['table']) > 0) {
896 $GLOBALS['table'] = '';
900 if (! $gotoInclude) {
901 $gotoInclude = $GLOBALS['table'] === '' ? '/database/sql' : '/table/sql';
904 return $gotoInclude;
908 * Defines the url to return in case of failure of the query
910 * @param mixed[] $urlParams url parameters
912 * @return string error url for query failure
914 public function getErrorUrl(array $urlParams): string
916 return $_POST['err_url'] ?? Url::getFromRoute('/table/change', $urlParams);
920 * Executes the sql query and get the result, then move back to the calling page
922 * @param mixed[] $query built query from buildSqlQuery()
924 * @return mixed[] $total_affected_rows, $last_messages, $warning_messages, $error_messages
926 public function executeSqlQuery(array $query): array
928 $GLOBALS['sql_query'] = implode('; ', $query) . ';';
929 // to ensure that the query is displayed in case of
930 // "insert as new row" and then "insert another new row"
931 $GLOBALS['display_query'] = $GLOBALS['sql_query'];
933 $totalAffectedRows = 0;
934 $lastMessages = [];
935 $warningMessages = [];
936 $errorMessages = [];
938 foreach ($query as $singleQuery) {
939 if (isset($_POST['submit_type']) && $_POST['submit_type'] === 'showinsert') {
940 $lastMessages[] = Message::notice(__('Showing SQL query'));
941 continue;
944 if (Config::getInstance()->settings['IgnoreMultiSubmitErrors']) {
945 $result = $this->dbi->tryQuery($singleQuery);
946 } else {
947 $result = $this->dbi->query($singleQuery);
950 if (! $result) {
951 $errorMessages[] = $this->dbi->getError();
952 } else {
953 $totalAffectedRows += $this->dbi->affectedRows();
955 $insertId = $this->dbi->insertId();
956 if ($insertId !== 0) {
957 // insert_id is id of FIRST record inserted in one insert, so if we
958 // inserted multiple rows, we had to increment this
960 if ($totalAffectedRows > 0) {
961 $insertId += $totalAffectedRows - 1;
964 $lastMessage = Message::notice(__('Inserted row id: %1$d'));
965 $lastMessage->addParam($insertId);
966 $lastMessages[] = $lastMessage;
970 $warningMessages = $this->getWarningMessages();
973 return [$totalAffectedRows, $lastMessages, $warningMessages, $errorMessages];
977 * get the warning messages array
979 * @return string[]
981 private function getWarningMessages(): array
983 $warningMessages = [];
984 foreach ($this->dbi->getWarnings() as $warning) {
985 $warningMessages[] = htmlspecialchars((string) $warning);
988 return $warningMessages;
992 * Column to display from the foreign table?
994 * @param string $whereComparison string that contain relation field value
995 * @param mixed[] $map all Relations to foreign tables for a given
996 * table or optionally a given column in a table
997 * @param string $relationField relation field
999 * @return string display value from the foreign table
1001 public function getDisplayValueForForeignTableColumn(
1002 string $whereComparison,
1003 array $map,
1004 string $relationField,
1005 ): string {
1006 $foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
1008 if (! is_array($foreigner)) {
1009 return '';
1012 $displayField = $this->relation->getDisplayField($foreigner['foreign_db'], $foreigner['foreign_table']);
1013 // Field to display from the foreign table?
1014 if ($displayField !== '') {
1015 $dispsql = 'SELECT ' . Util::backquote($displayField)
1016 . ' FROM ' . Util::backquote($foreigner['foreign_db'])
1017 . '.' . Util::backquote($foreigner['foreign_table'])
1018 . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
1019 . $whereComparison;
1020 $dispresult = $this->dbi->tryQuery($dispsql);
1021 if ($dispresult && $dispresult->numRows() > 0) {
1022 return (string) $dispresult->fetchValue();
1026 return '';
1030 * Display option in the cell according to user choices
1032 * @param mixed[] $map all Relations to foreign tables for a given
1033 * table or optionally a given column in a table
1034 * @param string $relationField relation field
1035 * @param string $whereComparison string that contain relation field value
1036 * @param string $dispval display value from the foreign table
1037 * @param string $relationFieldValue relation field value
1039 * @return string HTML <a> tag
1041 public function getLinkForRelationalDisplayField(
1042 array $map,
1043 string $relationField,
1044 string $whereComparison,
1045 string $dispval,
1046 string $relationFieldValue,
1047 ): string {
1048 $foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
1050 if (! is_array($foreigner)) {
1051 return '';
1054 if ($_SESSION['tmpval']['relational_display'] === 'K') {
1055 // user chose "relational key" in the display options, so
1056 // the title contains the display field
1057 $title = $dispval !== ''
1058 ? ' title="' . htmlspecialchars($dispval) . '"'
1059 : '';
1060 } else {
1061 $title = ' title="' . htmlspecialchars($relationFieldValue) . '"';
1064 $sqlQuery = 'SELECT * FROM '
1065 . Util::backquote($foreigner['foreign_db'])
1066 . '.' . Util::backquote($foreigner['foreign_table'])
1067 . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
1068 . $whereComparison;
1069 $urlParams = [
1070 'db' => $foreigner['foreign_db'],
1071 'table' => $foreigner['foreign_table'],
1072 'pos' => '0',
1073 'sql_signature' => Core::signSqlQuery($sqlQuery),
1074 'sql_query' => $sqlQuery,
1076 $output = '<a href="' . Url::getFromRoute('/sql', $urlParams) . '"' . $title . '>';
1078 if ($_SESSION['tmpval']['relational_display'] === 'D') {
1079 // user chose "relational display field" in the
1080 // display options, so show display field in the cell
1081 $output .= htmlspecialchars($dispval);
1082 } else {
1083 // otherwise display data in the cell
1084 $output .= htmlspecialchars($relationFieldValue);
1087 $output .= '</a>';
1089 return $output;
1093 * Transform edited values
1095 * @param string $db db name
1096 * @param string $table table name
1097 * @param mixed[] $transformation mimetypes for all columns of a table
1098 * [field_name][field_key]
1099 * @param mixed[] $editedValues transform columns list and new values
1100 * @param string $file file containing the transformation plugin
1101 * @param string $columnName column name
1102 * @param mixed[] $extraData extra data array
1103 * @param string $type the type of transformation
1105 * @return mixed[]
1107 public function transformEditedValues(
1108 string $db,
1109 string $table,
1110 array $transformation,
1111 array &$editedValues,
1112 string $file,
1113 string $columnName,
1114 array $extraData,
1115 string $type,
1116 ): array {
1117 $includeFile = 'src/Plugins/Transformations/' . $file;
1118 if (is_file(ROOT_PATH . $includeFile)) {
1119 // $cfg['SaveCellsAtOnce'] = true; JS code sends an array
1120 $whereClause = is_array($_POST['where_clause']) ? $_POST['where_clause'][0] : $_POST['where_clause'];
1121 $urlParams = [
1122 'db' => $db,
1123 'table' => $table,
1124 'where_clause_sign' => Core::signSqlQuery($whereClause),
1125 'where_clause' => $whereClause,
1126 'transform_key' => $columnName,
1128 $transformOptions = $this->transformations->getOptions($transformation[$type . '_options'] ?? '');
1129 $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
1130 $transformOptions['wrapper_params'] = $urlParams;
1131 $className = $this->transformations->getClassName($includeFile);
1132 if (class_exists($className)) {
1133 /** @var TransformationsPlugin $transformationPlugin */
1134 $transformationPlugin = new $className();
1136 foreach ($editedValues as $cellIndex => $currCellEditedValues) {
1137 if (! isset($currCellEditedValues[$columnName])) {
1138 continue;
1141 $extraData['transformations'][$cellIndex] = $transformationPlugin->applyTransformation(
1142 $currCellEditedValues[$columnName],
1143 $transformOptions,
1145 $editedValues[$cellIndex][$columnName] = $extraData['transformations'][$cellIndex];
1150 return $extraData;
1154 * Get value part if a function was specified
1156 private function formatAsSqlFunction(
1157 EditField $editField,
1158 ): string {
1159 if ($editField->function === 'PHP_PASSWORD_HASH') {
1160 $hash = password_hash($editField->value, PASSWORD_DEFAULT);
1162 return $this->dbi->quoteString($hash);
1165 if ($editField->function === 'UUID') {
1166 /* This way user will know what UUID new row has */
1167 $uuid = (string) $this->dbi->fetchValue('SELECT UUID()');
1169 return $this->dbi->quoteString($uuid);
1172 if (
1173 in_array($editField->function, $this->getGisFromTextFunctions())
1174 || in_array($editField->function, $this->getGisFromWKBFunctions())
1176 preg_match('/^(\'?)(.*?)\1(?:,(\d+))?$/', $editField->value, $matches);
1177 $escapedParams = $this->dbi->quoteString($matches[2]) . (isset($matches[3]) ? ',' . $matches[3] : '');
1179 return $editField->function . '(' . $escapedParams . ')';
1182 if (
1183 ! in_array($editField->function, self::FUNC_NO_PARAM)
1184 || ($editField->value !== '' && in_array($editField->function, self::FUNC_OPTIONAL_PARAM))
1186 if (
1187 ($editField->salt !== null
1188 && ($editField->function === 'AES_ENCRYPT'
1189 || $editField->function === 'AES_DECRYPT'
1190 || $editField->function === 'SHA2'))
1191 || ($editField->salt
1192 && ($editField->function === 'DES_ENCRYPT'
1193 || $editField->function === 'DES_DECRYPT'
1194 || $editField->function === 'ENCRYPT'))
1196 return $editField->function . '(' . $this->dbi->quoteString($editField->value) . ','
1197 . $this->dbi->quoteString($editField->salt) . ')';
1200 return $editField->function . '(' . $this->dbi->quoteString($editField->value) . ')';
1203 return $editField->function . '()';
1207 * Get the field value formatted for use in a SQL statement.
1208 * Used in both INSERT and UPDATE statements.
1210 private function getValueFormattedAsSql(
1211 EditField $editField,
1212 string $protectedValue = '',
1213 ): string {
1214 if ($editField->isUploaded) {
1215 return $editField->value;
1218 if ($editField->function !== '') {
1219 return $this->formatAsSqlFunction($editField);
1222 return $this->formatAsSqlValueBasedOnType($editField, $protectedValue);
1226 * Get query values array and query fields array for insert and update in multi edit
1228 * @param string|int $whereClause Either a positional index or string representing selected row
1230 public function getQueryValueForInsert(
1231 EditField $editField,
1232 bool $usingKey,
1233 string|int $whereClause,
1234 ): string {
1235 $protectedValue = '';
1236 if ($editField->type === 'protected' && $usingKey && $whereClause !== '') {
1237 // Fetch the current values of a row to use in case we have a protected field
1238 $protectedValue = $this->dbi->fetchValue(
1239 'SELECT ' . Util::backquote($editField->columnName)
1240 . ' FROM ' . Util::backquote($GLOBALS['table'])
1241 . ' WHERE ' . $whereClause,
1243 $protectedValue = is_string($protectedValue) ? $protectedValue : '';
1246 return $this->getValueFormattedAsSql($editField, $protectedValue);
1250 * Get field-value pairs for update SQL.
1251 * During update, we build the SQL only with the fields that should be updated.
1253 public function getQueryValueForUpdate(EditField $editField): string
1255 $currentValueFormattedAsSql = $this->getValueFormattedAsSql($editField);
1257 // avoid setting a field to NULL when it's already NULL
1258 // (field had the null checkbox before the update; field still has the null checkbox)
1259 if ($editField->wasPreviouslyNull && $editField->isNull) {
1260 return '';
1263 // A blob field that hasn't been changed will have no value
1264 if ($currentValueFormattedAsSql === '') {
1265 return '';
1268 if (
1269 // Field had the null checkbox before the update; field no longer has the null checkbox
1270 $editField->wasPreviouslyNull ||
1271 // Field was marked as NULL (the value will be unchanged if it was an empty string)
1272 $editField->isNull ||
1273 // A function was applied to the field
1274 $editField->function !== '' ||
1275 // The value was changed
1276 $editField->value !== $editField->previousValue
1278 return Util::backquote($editField->columnName) . ' = ' . $currentValueFormattedAsSql;
1281 return '';
1285 * Get the current column value in the form for different data types
1287 private function formatAsSqlValueBasedOnType(
1288 EditField $editField,
1289 string $protectedValue,
1290 ): string {
1291 if ($editField->type === 'protected') {
1292 // here we are in protected mode (asked in the config)
1293 // so tbl_change has put this special value in the
1294 // columns array, so we do not change the column value
1295 // but we can still handle column upload
1297 // when in UPDATE mode, do not alter field's contents. When in INSERT
1298 // mode, insert empty field because no values were submitted.
1299 // If protected blobs were set, insert original field's content.
1300 if ($protectedValue !== '') {
1301 return '0x' . bin2hex($protectedValue);
1304 if ($editField->isNull) {
1305 return 'NULL';
1308 // The Null checkbox was unchecked for this field
1309 if ($editField->wasPreviouslyNull) {
1310 return "''";
1313 return '';
1316 if ($editField->value === '') {
1317 // When the field is autoIncrement, the best way to avoid problems
1318 // in strict mode is to set the value to null (works also in non-strict mode)
1320 // If the value is empty and the null checkbox is checked, set it to null
1321 return $editField->autoIncrement || $editField->isNull ? 'NULL' : "''";
1324 if ($editField->type === 'hex') {
1325 if (! str_starts_with($editField->value, '0x')) {
1326 return '0x' . $editField->value;
1329 return $editField->value;
1332 if ($editField->type === 'bit') {
1333 $currentValue = (string) preg_replace('/[^01]/', '0', $editField->value);
1335 return 'b' . $this->dbi->quoteString($currentValue);
1338 // For uuid type, generate uuid value
1339 // if empty value but not set null or value is uuid() function
1340 if (
1341 $editField->type === 'uuid'
1342 && ! $editField->isNull
1343 && in_array($editField->value, ["''", '', "'uuid()'", 'uuid()'], true)
1345 return 'uuid()';
1348 if (
1349 ($editField->type !== 'datetime' && $editField->type !== 'timestamp' && $editField->type !== 'date')
1350 || ($editField->value !== 'CURRENT_TIMESTAMP' && $editField->value !== 'current_timestamp()')
1352 return $this->dbi->quoteString($editField->value);
1355 // If there is a value, we ignore the Null checkbox;
1356 // this could be possible if Javascript is disabled in the browser
1357 return $editField->value;
1361 * Check whether inline edited value can be truncated or not,
1362 * and add additional parameters for extra_data array if needed
1364 * @param string $db Database name
1365 * @param string $table Table name
1366 * @param string $columnName Column name
1367 * @param mixed[] $extraData Extra data for ajax response
1369 public function verifyWhetherValueCanBeTruncatedAndAppendExtraData(
1370 string $db,
1371 string $table,
1372 string $columnName,
1373 array &$extraData,
1374 ): void {
1375 $extraData['isNeedToRecheck'] = false;
1377 $sqlForRealValue = 'SELECT ' . Util::backquote($table) . '.'
1378 . Util::backquote($columnName)
1379 . ' FROM ' . Util::backquote($db) . '.'
1380 . Util::backquote($table)
1381 . ' WHERE ' . $_POST['where_clause'][0];
1383 $result = $this->dbi->tryQuery($sqlForRealValue);
1385 if (! $result) {
1386 return;
1389 $fieldsMeta = $this->dbi->getFieldsMeta($result);
1390 $meta = $fieldsMeta[0];
1391 $newValue = $result->fetchValue();
1393 if ($newValue === false) {
1394 return;
1397 if ($newValue !== null) {
1398 if ($meta->isTimeType()) {
1399 $newValue = Util::addMicroseconds($newValue);
1400 } elseif ($meta->isBinary()) {
1401 $newValue = '0x' . bin2hex($newValue);
1405 $extraData['isNeedToRecheck'] = true;
1406 $extraData['truncatableFieldValue'] = $newValue;
1410 * Function to get the columns of a table
1412 * @param string $db current db
1413 * @param string $table current table
1415 * @return list<ColumnFull>
1417 public function getTableColumns(string $db, string $table): array
1419 $this->dbi->selectDb($db);
1421 return array_values($this->dbi->getColumns($db, $table, true));
1425 * Function to determine Insert/Edit rows
1427 * @param string[]|string|null $whereClause where clause
1428 * @param string $db current database
1429 * @param string $table current table
1431 * @return array<int, bool|string[]|string|ResultInterface|ResultInterface[]|null>
1432 * @phpstan-return array{
1433 * bool,
1434 * string[]|string|null,
1435 * string[],
1436 * string[]|null,
1437 * ResultInterface[]|ResultInterface,
1438 * array<string, string|null>[]|false[],
1439 * bool,
1440 * string|null
1443 public function determineInsertOrEdit(array|string|null $whereClause, string $db, string $table): array
1445 if (isset($_POST['where_clause'])) {
1446 $whereClause = $_POST['where_clause'];
1449 if (isset($_SESSION['edit_next'])) {
1450 $whereClause = $_SESSION['edit_next'];
1451 unset($_SESSION['edit_next']);
1452 $afterInsert = 'edit_next';
1455 $config = Config::getInstance();
1456 if (isset($_POST['ShowFunctionFields'])) {
1457 $config->settings['ShowFunctionFields'] = $_POST['ShowFunctionFields'];
1460 if (isset($_POST['ShowFieldTypesInDataEditView'])) {
1461 $config->settings['ShowFieldTypesInDataEditView'] = $_POST['ShowFieldTypesInDataEditView'];
1464 if (isset($_POST['after_insert'])) {
1465 $afterInsert = $_POST['after_insert'];
1468 if (isset($whereClause)) {
1469 // we are editing
1470 $insertMode = false;
1471 $whereClauseArray = (array) $whereClause;
1472 [$whereClauses, $result, $rows, $foundUniqueKey] = $this->analyzeWhereClauses(
1473 $whereClauseArray,
1474 $table,
1475 $db,
1477 } else {
1478 // we are inserting
1479 $insertMode = true;
1480 $whereClause = null;
1481 $result = $this->loadFirstRow($table, $db);
1482 $rows = $this->getInsertRows();
1483 $whereClauses = null;
1484 $whereClauseArray = [];
1485 $foundUniqueKey = false;
1488 /** @var string $defaultAction */
1489 $defaultAction = $_POST['default_action'] ?? $_GET['default_action'] ?? '';
1490 if ($defaultAction === 'insert') {
1491 // Copying a row - fetched data will be inserted as a new row, therefore the where clause is needless.
1492 $whereClause = $whereClauses = null;
1495 return [
1496 $insertMode,
1497 $whereClause,
1498 $whereClauseArray,
1499 $whereClauses,
1500 $result,
1501 $rows,
1502 $foundUniqueKey,
1503 $afterInsert ?? null,
1508 * Function to get comments for the table columns
1510 * @param string $db current database
1511 * @param string $table current table
1513 * @return string[] comments for columns
1515 public function getCommentsMap(string $db, string $table): array
1517 if (Config::getInstance()->settings['ShowPropertyComments']) {
1518 return $this->relation->getComments($db, $table);
1521 return [];
1525 * Function to get html for the gis editor div
1527 public function getHtmlForGisEditor(): string
1529 return '<div id="gis_editor"></div><div id="popup_background"></div><br>';
1533 * Function to get html for the ignore option in insert mode
1535 * @param int $rowId row id
1536 * @param bool $checked ignore option is checked or not
1538 public function getHtmlForIgnoreOption(int $rowId, bool $checked = true): string
1540 return '<input type="checkbox"'
1541 . ($checked ? ' checked="checked"' : '')
1542 . ' name="insert_ignore_' . $rowId . '"'
1543 . ' id="insert_ignore_' . $rowId . '">'
1544 . '<label for="insert_ignore_' . $rowId . '">'
1545 . __('Ignore')
1546 . '</label><br>' . "\n";
1550 * Function to get html for the insert edit form header
1552 * @param bool $hasBlobField whether has blob field
1553 * @param bool $isUpload whether is upload
1555 public function getHtmlForInsertEditFormHeader(bool $hasBlobField, bool $isUpload): string
1557 $template = new Template();
1559 return $template->render('table/insert/get_html_for_insert_edit_form_header', [
1560 'has_blob_field' => $hasBlobField,
1561 'is_upload' => $isUpload,
1566 * Function to get html for each insert/edit column
1568 * @param ColumnFull $tableColumn column
1569 * @param int $columnNumber column index in table_columns
1570 * @param string[] $commentsMap comments map
1571 * @param int $columnLength length of the current column taken from field metadata
1572 * @param bool $insertMode whether insert mode
1573 * @param mixed[] $currentRow current row
1574 * @param int $columnsCnt columns count
1575 * @param bool $isUpload whether upload
1576 * @param mixed[] $foreigners foreigners
1577 * @param string $table table
1578 * @param string $db database
1579 * @param int $rowId row id
1580 * @param string $defaultCharEditing default char editing mode which is stored in the config.inc.php script
1581 * @param string $textDir text direction
1582 * @param mixed[] $repopulate the data to be repopulated
1583 * @param mixed[] $columnMime the mime information of column
1584 * @param string $whereClause the where clause
1586 private function getHtmlForInsertEditFormColumn(
1587 ColumnFull $tableColumn,
1588 int $columnNumber,
1589 array $commentsMap,
1590 int $columnLength,
1591 bool $insertMode,
1592 array $currentRow,
1593 int $columnsCnt,
1594 bool $isUpload,
1595 array $foreigners,
1596 string $table,
1597 string $db,
1598 int $rowId,
1599 string $defaultCharEditing,
1600 string $textDir,
1601 array $repopulate,
1602 array $columnMime,
1603 string $whereClause,
1604 ): string {
1605 $column = new InsertEditColumn(
1606 $tableColumn->field,
1607 $tableColumn->type,
1608 $tableColumn->isNull,
1609 $tableColumn->key,
1610 $tableColumn->default,
1611 $tableColumn->extra,
1612 $columnLength,
1613 $this->isColumn($tableColumn->type, ['binary', 'varbinary']),
1614 $this->isColumn($tableColumn->type, ['blob', 'tinyblob', 'mediumblob', 'longblob']),
1615 $this->isColumn($tableColumn->type, ['char', 'varchar']),
1616 $insertMode,
1619 $asIs = false;
1620 $fieldHashMd5 = $column->md5;
1621 if ($repopulate !== [] && array_key_exists($fieldHashMd5, $currentRow)) {
1622 $currentRow[$column->field] = $repopulate[$fieldHashMd5];
1623 $asIs = true;
1626 $extractedColumnspec = Util::extractColumnSpec($column->type);
1628 //Call validation when the form submitted...
1629 $onChangeClause = 'return verificationsAfterFieldChange('
1630 . json_encode($fieldHashMd5) . ', '
1631 . json_encode((string) $rowId) . ',' . json_encode($column->pmaType) . ')';
1633 $vkey = '[multi_edit][' . $rowId . ']';
1634 // Use an MD5 as an array index to avoid having special characters
1635 // in the name attribute (see bug #1746964 )
1636 $columnNameAppendix = $vkey . '[' . $fieldHashMd5 . ']';
1638 // Get a list of GIS data types.
1639 $gisDataTypes = Gis::getDataTypes();
1641 // Prepares the field value
1642 if ($currentRow !== []) {
1643 // (we are editing)
1645 $realNullValue,
1646 $specialCharsEncoded,
1647 $specialChars,
1648 $data,
1649 $backupField,
1650 ] = $this->getSpecialCharsAndBackupFieldForExistingRow(
1651 $currentRow,
1652 $column,
1653 $extractedColumnspec,
1654 $gisDataTypes,
1655 $columnNameAppendix,
1656 $asIs,
1658 } else {
1659 // (we are inserting)
1660 // display default values
1661 $defaultValue = $column->default ?? null;
1662 if (isset($repopulate[$fieldHashMd5])) {
1663 $defaultValue = $repopulate[$fieldHashMd5];
1666 $realNullValue = $defaultValue === null;
1667 $data = (string) $defaultValue;
1668 $specialChars = $this->getSpecialCharsForInsertingMode($defaultValue, $column->trueType);
1669 $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
1670 $backupField = '';
1673 $this->fieldIndex = ($this->rowOffset * $columnsCnt) + $columnNumber + 1;
1675 // The function column
1676 // -------------------
1677 $foreignData = $this->relation->getForeignData($foreigners, $column->field, false, '', '');
1678 $isColumnBinary = $this->isColumnBinary($column, $isUpload);
1679 $functionOptions = '';
1681 $config = Config::getInstance();
1682 if ($config->settings['ShowFunctionFields']) {
1683 $defaultFunction = Generator::getDefaultFunctionForField(
1684 $column->trueType,
1685 $column->firstTimestamp,
1686 $column->default,
1687 $column->extra,
1688 $column->isNull,
1689 $column->key,
1690 $column->type,
1691 $insertMode,
1693 $functionOptions = Generator::getFunctionsForField($defaultFunction, $foreignData);
1696 // nullify code is needed by the js nullify() function to be able to generate calls to nullify() in jQuery
1697 $nullifyCode = $this->getNullifyCodeForNullColumn($column, $foreigners, $foreignData);
1699 // The value column (depends on type)
1700 // ----------------
1701 // See bug #1667887 for the reason why we don't use the maxlength
1702 // HTML attribute
1704 //add data attributes "no of decimals" and "data type"
1705 $noDecimals = 0;
1706 $type = current(explode('(', $column->pmaType));
1707 if (preg_match('/\(([^()]+)\)/', $column->pmaType, $match)) {
1708 $match[0] = trim($match[0], '()');
1709 $noDecimals = $match[0];
1712 // Check input transformation of column
1713 $transformedHtml = '';
1714 if (! empty($columnMime['input_transformation'])) {
1715 $file = $columnMime['input_transformation'];
1716 $includeFile = 'src/Plugins/Transformations/' . $file;
1717 if (is_file(ROOT_PATH . $includeFile)) {
1718 $className = $this->transformations->getClassName($includeFile);
1719 if (class_exists($className)) {
1720 $transformationPlugin = new $className();
1721 $transformationOptions = $this->transformations->getOptions(
1722 $columnMime['input_transformation_options'],
1724 $urlParams = [
1725 'db' => $db,
1726 'table' => $table,
1727 'transform_key' => $column->field,
1728 'where_clause_sign' => Core::signSqlQuery($whereClause),
1729 'where_clause' => $whereClause,
1731 $transformationOptions['wrapper_link'] = Url::getCommon($urlParams);
1732 $transformationOptions['wrapper_params'] = $urlParams;
1733 $currentValue = '';
1734 if (isset($currentRow[$column->field])) {
1735 $currentValue = $currentRow[$column->field];
1738 if ($transformationPlugin instanceof IOTransformationsPlugin) {
1739 $transformedHtml = $transformationPlugin->getInputHtml(
1740 $columnNameAppendix,
1741 $transformationOptions,
1742 $currentValue,
1743 $textDir,
1744 $this->fieldIndex,
1747 $GLOBALS['plugin_scripts'] = array_merge(
1748 $GLOBALS['plugin_scripts'],
1749 $transformationPlugin->getScripts(),
1756 $columnValue = '';
1757 $foreignDropdown = '';
1758 $dataType = '';
1759 $textAreaRows = $config->settings['TextareaRows'];
1760 $textareaCols = $config->settings['TextareaCols'];
1761 $maxlength = '';
1762 $enumSelectedValue = '';
1763 $enumValues = [];
1764 $columnSetValues = [];
1765 $setSelectSize = 0;
1766 $isColumnProtectedBlob = false;
1767 $blobValue = '';
1768 $blobValueUnit = '';
1769 $maxUploadSize = 0;
1770 $selectOptionForUpload = '';
1771 $inputFieldHtml = '';
1772 if ($transformedHtml === '') {
1773 if (is_array($foreignData['disp_row'])) {
1774 $foreignDropdown = $this->relation->foreignDropdown(
1775 $foreignData['disp_row'],
1776 $foreignData['foreign_field'],
1777 $foreignData['foreign_display'],
1778 $data,
1779 $config->settings['ForeignKeyMaxLimit'],
1783 $dataType = $this->dbi->types->getTypeClass($column->trueType);
1785 if ($column->isChar) {
1786 $textAreaRows = max($config->settings['CharTextareaRows'], 7);
1787 $textareaCols = $config->settings['CharTextareaCols'];
1788 $maxlength = $extractedColumnspec['spec_in_brackets'];
1789 } elseif ($config->settings['LongtextDoubleTextarea'] && mb_strstr($column->pmaType, 'longtext')) {
1790 $textAreaRows = $config->settings['TextareaRows'] * 2;
1791 $textareaCols = $config->settings['TextareaCols'] * 2;
1794 if ($column->pmaType === 'enum') {
1795 $enumValues = $extractedColumnspec['enum_set_values'];
1797 foreach ($enumValues as $enumValue) {
1798 if (
1799 $data == $enumValue || ($data == ''
1800 && (! isset($_POST['where_clause']) || ! $column->isNull)
1801 && isset($column->default) && $enumValue == $column->default)
1803 $enumSelectedValue = $enumValue;
1804 break;
1807 } elseif ($column->pmaType === 'set') {
1808 $columnSetValues = $extractedColumnspec['enum_set_values'];
1809 $setSelectSize = min(4, count($extractedColumnspec['enum_set_values']));
1810 } elseif ($column->isBinary || $column->isBlob) {
1811 $isColumnProtectedBlob = ($config->settings['ProtectBinary'] === 'blob' && $column->isBlob)
1812 || ($config->settings['ProtectBinary'] === 'all')
1813 || ($config->settings['ProtectBinary'] === 'noblob' && ! $column->isBlob);
1814 if ($isColumnProtectedBlob) {
1815 [$blobValue, $blobValueUnit] = Util::formatByteDown(mb_strlen(stripslashes($data)), 3, 1);
1818 if ($isUpload && $column->isBlob) {
1819 $maxUploadSize = $this->getMaxUploadSize($column->pmaType);
1822 if (! empty($config->settings['UploadDir'])) {
1823 $selectOptionForUpload = $this->getSelectOptionForUpload($vkey, $fieldHashMd5);
1826 if (
1827 ! $isColumnProtectedBlob
1828 && ! ($column->isBlob || ($column->length > $config->settings['LimitChars']))
1830 $inputFieldHtml = $this->getHtmlInput(
1831 $column,
1832 $columnNameAppendix,
1833 $specialChars,
1834 min(max($column->length, 4), $config->settings['LimitChars']),
1835 $onChangeClause,
1836 'HEX',
1839 } else {
1840 $columnValue = $this->getValueColumnForOtherDatatypes(
1841 $column,
1842 $defaultCharEditing,
1843 $backupField,
1844 $columnNameAppendix,
1845 $onChangeClause,
1846 $specialChars,
1847 $textDir,
1848 $specialCharsEncoded,
1849 $data,
1850 $extractedColumnspec,
1855 return $this->template->render('table/insert/column_row', [
1856 'db' => $db,
1857 'table' => $table,
1858 'column' => $column,
1859 'row_id' => $rowId,
1860 'show_field_types_in_data_edit_view' => $config->settings['ShowFieldTypesInDataEditView'],
1861 'show_function_fields' => $config->settings['ShowFunctionFields'],
1862 'is_column_binary' => $isColumnBinary,
1863 'function_options' => $functionOptions,
1864 'nullify_code' => $nullifyCode,
1865 'real_null_value' => $realNullValue,
1866 'id_index' => $this->fieldIndex,
1867 'type' => $type,
1868 'decimals' => $noDecimals,
1869 'special_chars' => $specialChars,
1870 'transformed_value' => $transformedHtml,
1871 'value' => $columnValue,
1872 'is_value_foreign_link' => $foreignData['foreign_link'] === true,
1873 'backup_field' => $backupField,
1874 'data' => $data,
1875 'gis_data_types' => $gisDataTypes,
1876 'foreign_dropdown' => $foreignDropdown,
1877 'data_type' => $dataType,
1878 'textarea_cols' => $textareaCols,
1879 'textarea_rows' => $textAreaRows,
1880 'text_dir' => $textDir,
1881 'max_length' => $maxlength,
1882 'longtext_double_textarea' => $config->settings['LongtextDoubleTextarea'],
1883 'enum_selected_value' => $enumSelectedValue,
1884 'enum_values' => $enumValues,
1885 'set_values' => $columnSetValues,
1886 'set_select_size' => $setSelectSize,
1887 'is_column_protected_blob' => $isColumnProtectedBlob,
1888 'blob_value' => $blobValue,
1889 'blob_value_unit' => $blobValueUnit,
1890 'is_upload' => $isUpload,
1891 'max_upload_size' => $maxUploadSize,
1892 'select_option_for_upload' => $selectOptionForUpload,
1893 'limit_chars' => $config->settings['LimitChars'],
1894 'input_field_html' => $inputFieldHtml,
1895 'field_title' => $this->getColumnTitle($column->field, $commentsMap),
1899 private function isColumnBinary(InsertEditColumn $column, bool $isUpload): bool
1901 $config = Config::getInstance();
1902 if (! $config->settings['ShowFunctionFields']) {
1903 return false;
1906 return ($config->settings['ProtectBinary'] === 'blob' && $column->isBlob && ! $isUpload)
1907 || ($config->settings['ProtectBinary'] === 'all' && $column->isBinary)
1908 || ($config->settings['ProtectBinary'] === 'noblob' && $column->isBinary);
1912 * Function to get html for each insert/edit row
1914 * @param mixed[] $urlParams url parameters
1915 * @param list<ColumnFull> $tableColumns table columns
1916 * @param string[] $commentsMap comments map
1917 * @param ResultInterface $currentResult current result
1918 * @param bool $insertMode whether insert mode
1919 * @param mixed[] $currentRow current row
1920 * @param bool $isUpload whether upload
1921 * @param mixed[] $foreigners foreigners
1922 * @param string $table table
1923 * @param string $db database
1924 * @param int $rowId row id
1925 * @param string $textDir text direction
1926 * @param mixed[] $repopulate the data to be repopulated
1927 * @param mixed[] $whereClauseArray the array of where clauses
1929 public function getHtmlForInsertEditRow(
1930 array $urlParams,
1931 array $tableColumns,
1932 array $commentsMap,
1933 ResultInterface $currentResult,
1934 bool $insertMode,
1935 array $currentRow,
1936 bool $isUpload,
1937 array $foreigners,
1938 string $table,
1939 string $db,
1940 int $rowId,
1941 string $textDir,
1942 array $repopulate,
1943 array $whereClauseArray,
1944 ): string {
1945 $htmlOutput = $this->getHeadAndFootOfInsertRowTable($urlParams)
1946 . '<tbody>';
1948 //store the default value for CharEditing
1949 $defaultCharEditing = Config::getInstance()->settings['CharEditing'];
1950 $mimeMap = $this->transformations->getMime($db, $table);
1951 $whereClause = '';
1952 if (isset($whereClauseArray[$rowId])) {
1953 $whereClause = $whereClauseArray[$rowId];
1956 $columnCount = count($tableColumns);
1957 for ($columnNumber = 0; $columnNumber < $columnCount; $columnNumber++) {
1958 $tableColumn = $tableColumns[$columnNumber];
1959 $columnMime = [];
1960 if (isset($mimeMap[$tableColumn->field])) {
1961 $columnMime = $mimeMap[$tableColumn->field];
1964 $virtual = ['VIRTUAL', 'PERSISTENT', 'VIRTUAL GENERATED', 'STORED GENERATED'];
1965 if (in_array($tableColumn->extra, $virtual)) {
1966 continue;
1969 $htmlOutput .= $this->getHtmlForInsertEditFormColumn(
1970 $tableColumn,
1971 $columnNumber,
1972 $commentsMap,
1973 $this->dbi->getFieldsMeta($currentResult)[$columnNumber]->length,
1974 $insertMode,
1975 $currentRow,
1976 $columnCount,
1977 $isUpload,
1978 $foreigners,
1979 $table,
1980 $db,
1981 $rowId,
1982 $defaultCharEditing,
1983 $textDir,
1984 $repopulate,
1985 $columnMime,
1986 $whereClause,
1990 $this->rowOffset++;
1992 return $htmlOutput . ' </tbody>'
1993 . '</table></div><br>'
1994 . '<div class="clearfloat"></div>';
1998 * Returns list of function names that accept WKB as text
2000 * @return string[]
2002 private function getGisFromTextFunctions(): array
2004 return $this->dbi->getVersion() >= 50600 ?
2006 'ST_GeomFromText',
2007 'ST_GeomCollFromText',
2008 'ST_LineFromText',
2009 'ST_MLineFromText',
2010 'ST_PointFromText',
2011 'ST_MPointFromText',
2012 'ST_PolyFromText',
2013 'ST_MPolyFromText',
2016 'GeomFromText',
2017 'GeomCollFromText',
2018 'LineFromText',
2019 'MLineFromText',
2020 'PointFromText',
2021 'MPointFromText',
2022 'PolyFromText',
2023 'MPolyFromText',
2028 * Returns list of function names that accept WKB as binary
2030 * @return string[]
2032 private function getGisFromWKBFunctions(): array
2034 return $this->dbi->getVersion() >= 50600 ?
2036 'ST_GeomFromWKB',
2037 'ST_GeomCollFromWKB',
2038 'ST_LineFromWKB',
2039 'ST_MLineFromWKB',
2040 'ST_PointFromWKB',
2041 'ST_MPointFromWKB',
2042 'ST_PolyFromWKB',
2043 'ST_MPolyFromWKB',
2046 'GeomFromWKB',
2047 'GeomCollFromWKB',
2048 'LineFromWKB',
2049 'MLineFromWKB',
2050 'PointFromWKB',
2051 'MPointFromWKB',
2052 'PolyFromWKB',
2053 'MPolyFromWKB',