Translated using Weblate (Portuguese (Brazil))
[phpmyadmin.git] / src / InsertEdit.php
blobe2d1c6c38aa55eea24579fe74bad43ffb193cc19
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 substr;
46 use function trim;
48 use const ENT_COMPAT;
49 use const PASSWORD_DEFAULT;
51 class InsertEdit
53 private const FUNC_OPTIONAL_PARAM = ['RAND', 'UNIX_TIMESTAMP'];
55 private const FUNC_NO_PARAM = [
56 'CONNECTION_ID',
57 'CURRENT_USER',
58 'CURDATE',
59 'CURTIME',
60 'CURRENT_DATE',
61 'CURRENT_TIME',
62 'DATABASE',
63 'LAST_INSERT_ID',
64 'NOW',
65 'PI',
66 'RAND',
67 'SYSDATE',
68 'UNIX_TIMESTAMP',
69 'USER',
70 'UTC_DATE',
71 'UTC_TIME',
72 'UTC_TIMESTAMP',
73 'UUID',
74 'UUID_SHORT',
75 'VERSION',
78 private int $rowOffset = 0;
79 private int $fieldIndex = 0;
81 public function __construct(
82 private DatabaseInterface $dbi,
83 private Relation $relation,
84 private Transformations $transformations,
85 private FileListing $fileListing,
86 private Template $template,
87 ) {
90 /**
91 * Retrieve form parameters for insert/edit form
93 * @param string $db name of the database
94 * @param string $table name of the table
95 * @param mixed[] $whereClauseArray
97 * @return array<string, string> array of insert/edit form parameters
99 public function getFormParametersForInsertForm(
100 string $db,
101 string $table,
102 array|null $whereClauses,
103 array $whereClauseArray,
104 string $errorUrl,
105 ): array {
106 $formParams = [
107 'db' => $db,
108 'table' => $table,
109 'goto' => $GLOBALS['goto'],
110 'err_url' => $errorUrl,
111 'sql_query' => $_POST['sql_query'] ?? '',
114 if ($formParams['sql_query'] === '' && isset($_GET['sql_query'], $_GET['sql_signature'])) {
115 if (Core::checkSqlQuerySignature($_GET['sql_query'], $_GET['sql_signature'])) {
116 $formParams['sql_query'] = $_GET['sql_query'];
120 if (isset($whereClauses)) {
121 foreach ($whereClauseArray as $keyId => $whereClause) {
122 $formParams['where_clause[' . $keyId . ']'] = trim($whereClause);
126 if (isset($_POST['clause_is_unique'])) {
127 $formParams['clause_is_unique'] = $_POST['clause_is_unique'];
128 } elseif (isset($_GET['clause_is_unique'])) {
129 $formParams['clause_is_unique'] = $_GET['clause_is_unique'];
132 return $formParams;
136 * Analysing where clauses array
138 * @param string[] $whereClauseArray array of where clauses
139 * @param string $table name of the table
140 * @param string $db name of the database
142 * @return array<int, string[]|ResultInterface[]|array<string, string|null>[]|bool>
143 * @phpstan-return array{string[], ResultInterface[], array<string|null>[], bool}
145 private function analyzeWhereClauses(
146 array $whereClauseArray,
147 string $table,
148 string $db,
149 ): array {
150 $rows = [];
151 $result = [];
152 $whereClauses = [];
153 $foundUniqueKey = false;
154 foreach ($whereClauseArray as $keyId => $whereClause) {
155 $localQuery = 'SELECT * FROM '
156 . Util::backquote($db) . '.'
157 . Util::backquote($table)
158 . ' WHERE ' . $whereClause . ';';
159 $result[$keyId] = $this->dbi->query($localQuery);
160 $rows[$keyId] = $result[$keyId]->fetchAssoc();
162 $whereClauses[$keyId] = str_replace('\\', '\\\\', $whereClause);
163 $hasUniqueCondition = $this->showEmptyResultMessageOrSetUniqueCondition(
164 $rows,
165 $keyId,
166 $whereClauseArray,
167 $localQuery,
168 $result,
170 if (! $hasUniqueCondition) {
171 continue;
174 $foundUniqueKey = true;
177 return [$whereClauses, $result, $rows, $foundUniqueKey];
181 * Show message for empty result or set the unique_condition
183 * @param mixed[] $rows MySQL returned rows
184 * @param string|int $keyId ID in current key
185 * @param mixed[] $whereClauseArray array of where clauses
186 * @param string $localQuery query performed
187 * @param ResultInterface[] $result MySQL result handle
189 private function showEmptyResultMessageOrSetUniqueCondition(
190 array $rows,
191 string|int $keyId,
192 array $whereClauseArray,
193 string $localQuery,
194 array $result,
195 ): bool {
196 // No row returned
197 if (! $rows[$keyId]) {
198 unset($rows[$keyId], $whereClauseArray[$keyId]);
199 ResponseRenderer::getInstance()->addHTML(
200 Generator::getMessage(
201 __('MySQL returned an empty result set (i.e. zero rows).'),
202 $localQuery,
206 * @todo not sure what should be done at this point, but we must not
207 * exit if we want the message to be displayed
210 return false;
213 $meta = $this->dbi->getFieldsMeta($result[$keyId]);
215 $uniqueCondition = (new UniqueCondition($meta, $rows[$keyId], true))->getWhereClause();
217 return (bool) $uniqueCondition;
221 * No primary key given, just load first row
223 private function loadFirstRow(string $table, string $db): ResultInterface
225 return $this->dbi->query(
226 'SELECT * FROM ' . Util::backquote($db)
227 . '.' . Util::backquote($table) . ' LIMIT 1;',
231 /** @return false[] */
232 private function getInsertRows(): array
234 // Can be a string on some old configuration storage settings
235 return array_fill(0, Config::getInstance()->settings['InsertRows'], false);
239 * Show type information or function selectors in Insert/Edit
241 * @param string $which function|type
242 * @param mixed[] $urlParams containing url parameters
243 * @param bool $isShow whether to show the element in $which
245 * @return string an HTML snippet
247 public function showTypeOrFunction(string $which, array $urlParams, bool $isShow): string
249 $params = [];
251 $config = Config::getInstance();
252 switch ($which) {
253 case 'function':
254 $params['ShowFunctionFields'] = ($isShow ? 0 : 1);
255 $params['ShowFieldTypesInDataEditView'] = $config->settings['ShowFieldTypesInDataEditView'];
256 break;
257 case 'type':
258 $params['ShowFieldTypesInDataEditView'] = ($isShow ? 0 : 1);
259 $params['ShowFunctionFields'] = $config->settings['ShowFunctionFields'];
260 break;
263 $params['goto'] = Url::getFromRoute('/sql');
264 $thisUrlParams = array_merge($urlParams, $params);
266 if (! $isShow) {
267 return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
268 . Url::getCommon($thisUrlParams, '', false) . '">'
269 . $this->showTypeOrFunctionLabel($which)
270 . '</a>';
273 return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
274 . Url::getCommon($thisUrlParams, '', false)
275 . '" title="' . __('Hide') . '">'
276 . $this->showTypeOrFunctionLabel($which)
277 . '</a></th>';
281 * Show type information or function selectors labels in Insert/Edit
283 * @param string $which function|type
285 * @return string an HTML snippet
287 private function showTypeOrFunctionLabel(string $which): string
289 return match ($which) {
290 'function' => __('Function'),
291 'type' => __('Type'),
292 default => '',
297 * Retrieve the column title
299 * @param string $fieldName name of the column
300 * @param string[] $commentsMap comments for every column that has a comment
302 * @return string column title
304 private function getColumnTitle(string $fieldName, array $commentsMap): string
306 if (isset($commentsMap[$fieldName])) {
307 return '<span style="border-bottom: 1px dashed black;" title="'
308 . htmlspecialchars($commentsMap[$fieldName]) . '">'
309 . htmlspecialchars($fieldName) . '</span>';
312 return htmlspecialchars($fieldName);
316 * check whether the column is of a certain type
317 * the goal is to ensure that types such as "enum('one','two','binary',..)"
318 * or "enum('one','two','varbinary',..)" are not categorized as binary
320 * @param string $columnType column type as specified in the column definition
321 * @param string[] $types the types to verify
323 public function isColumn(string $columnType, array $types): bool
325 foreach ($types as $oneType) {
326 if (mb_stripos($columnType, $oneType) === 0) {
327 return true;
331 return false;
335 * Retrieve the nullify code for the null column
337 * @param InsertEditColumn $column description of column in given table
338 * @param mixed[] $foreigners keys into foreign fields
339 * @param mixed[] $foreignData data about the foreign keys
341 private function getNullifyCodeForNullColumn(
342 InsertEditColumn $column,
343 array $foreigners,
344 array $foreignData,
345 ): string {
346 $foreigner = $this->relation->searchColumnInForeigners($foreigners, $column->field);
347 if (mb_strstr($column->trueType, 'enum')) {
348 $nullifyCode = mb_strlen($column->type) > 20 ? '1' : '2';
349 } elseif (mb_strstr($column->trueType, 'set')) {
350 $nullifyCode = '3';
351 } elseif ($foreigner && $foreignData['foreign_link'] == false) {
352 // foreign key in a drop-down
353 $nullifyCode = '4';
354 } elseif ($foreigner && $foreignData['foreign_link'] == true) {
355 // foreign key with a browsing icon
356 $nullifyCode = '6';
357 } else {
358 $nullifyCode = '5';
361 return $nullifyCode;
365 * Get HTML textarea for insert form
367 * @param InsertEditColumn $column column information
368 * @param string $backupField hidden input field
369 * @param string $columnNameAppendix the name attribute
370 * @param string $onChangeClause onchange clause for fields
371 * @param string $textDir text direction
372 * @param string $specialCharsEncoded replaced char if the string starts
373 * with a \r\n pair (0x0d0a) add an extra \n
374 * @param string $dataType the html5 data-* attribute type
376 * @return string an html snippet
378 private function getTextarea(
379 InsertEditColumn $column,
380 string $backupField,
381 string $columnNameAppendix,
382 string $onChangeClause,
383 string $textDir,
384 string $specialCharsEncoded,
385 string $dataType,
386 ): string {
387 $theClass = '';
388 $config = Config::getInstance();
389 $textAreaRows = $config->settings['TextareaRows'];
390 $textareaCols = $config->settings['TextareaCols'];
392 if ($column->isChar) {
394 * @todo clarify the meaning of the "textfield" class and explain
395 * why character columns have the "char" class instead
397 $theClass = 'char charField';
398 $textAreaRows = $config->settings['CharTextareaRows'];
399 $textareaCols = $config->settings['CharTextareaCols'];
400 $extractedColumnspec = Util::extractColumnSpec($column->type);
401 $maxlength = $extractedColumnspec['spec_in_brackets'];
402 } elseif ($config->settings['LongtextDoubleTextarea'] && mb_strstr($column->pmaType, 'longtext')) {
403 $textAreaRows = $config->settings['TextareaRows'] * 2;
404 $textareaCols = $config->settings['TextareaCols'] * 2;
407 return $backupField . "\n"
408 . '<textarea name="fields' . $columnNameAppendix . '"'
409 . ' class="' . $theClass . '"'
410 . (isset($maxlength) ? ' data-maxlength="' . $maxlength . '"' : '')
411 . ' rows="' . $textAreaRows . '"'
412 . ' cols="' . $textareaCols . '"'
413 . ' dir="' . $textDir . '"'
414 . ' id="field_' . $this->fieldIndex . '_3"'
415 . ($onChangeClause !== '' ? ' onchange="' . htmlspecialchars($onChangeClause, ENT_COMPAT) . '"' : '')
416 . ' tabindex="' . $this->fieldIndex . '"'
417 . ' data-type="' . $dataType . '">'
418 . $specialCharsEncoded
419 . '</textarea>';
423 * Get HTML input type
425 * @param InsertEditColumn $column description of column in given table
426 * @param string $columnNameAppendix the name attribute
427 * @param string $specialChars special characters
428 * @param int $fieldsize html field size
429 * @param string $onChangeClause onchange clause for fields
430 * @param string $dataType the html5 data-* attribute type
432 * @return string an html snippet
434 private function getHtmlInput(
435 InsertEditColumn $column,
436 string $columnNameAppendix,
437 string $specialChars,
438 int $fieldsize,
439 string $onChangeClause,
440 string $dataType,
441 ): string {
442 $theClass = 'textfield';
443 // verify True_Type which does not contain the parentheses and length
444 if ($column->trueType === 'date') {
445 $theClass .= ' datefield';
446 } elseif ($column->trueType === 'time') {
447 $theClass .= ' timefield';
448 } elseif ($column->trueType === 'datetime' || $column->trueType === 'timestamp') {
449 $theClass .= ' datetimefield';
452 $inputMinMax = '';
453 $isInteger = in_array($column->trueType, $this->dbi->types->getIntegerTypes(), true);
454 if ($isInteger) {
455 $extractedColumnspec = Util::extractColumnSpec($column->type);
456 $isUnsigned = $extractedColumnspec['unsigned'];
457 $minMaxValues = $this->dbi->types->getIntegerRange($column->trueType, ! $isUnsigned);
458 $inputMinMax = 'min="' . $minMaxValues[0] . '" '
459 . 'max="' . $minMaxValues[1] . '"';
460 $dataType = 'INT';
463 // do not use the 'date' or 'time' types here; they have no effect on some
464 // browsers and create side effects (see bug #4218)
465 return '<input type="text"'
466 . ' name="fields' . $columnNameAppendix . '"'
467 . ' value="' . $specialChars . '" size="' . $fieldsize . '"'
468 . ($column->isChar
469 ? ' data-maxlength="' . $fieldsize . '"'
470 : '')
471 . ($inputMinMax !== '' ? ' ' . $inputMinMax : '')
472 . ' data-type="' . $dataType . '"'
473 . ' class="' . $theClass . '" onchange="' . htmlspecialchars($onChangeClause, ENT_COMPAT) . '"'
474 . ' tabindex="' . $this->fieldIndex . '"'
475 . ($isInteger ? ' inputmode="numeric"' : '')
476 . ' id="field_' . $this->fieldIndex . '_3">';
480 * Get HTML select option for upload
482 * @param string $vkey [multi_edit]['row_id']
483 * @param string $fieldHashMd5 array index as an MD5 to avoid having special characters
485 * @return string an HTML snippet
487 private function getSelectOptionForUpload(string $vkey, string $fieldHashMd5): string
489 $files = $this->fileListing->getFileSelectOptions(
490 Util::userDir((Config::getInstance()->settings['UploadDir'] ?? '')),
493 if ($files === false) {
494 return '<span style="color:red">' . __('Error') . '</span><br>' . "\n"
495 . __('The directory you set for upload work cannot be reached.') . "\n";
498 if ($files === '') {
499 return '';
502 return "<br>\n"
503 . '<i>' . __('Or') . '</i> '
504 . __('web server upload directory:') . '<br>' . "\n"
505 . '<select size="1" name="fields_uploadlocal'
506 . $vkey . '[' . $fieldHashMd5 . ']">' . "\n"
507 . '<option value="" selected="selected"></option>' . "\n"
508 . $files
509 . '</select>' . "\n";
513 * Retrieve the maximum upload file size
515 private function getMaxUploadSize(string $pmaType): string
517 // find maximum upload size, based on field type
519 * @todo with functions this is not so easy, as you can basically
520 * process any data with function like MD5
522 $maxFieldSize = match ($pmaType) {
523 'tinyblob' => 256,
524 'blob' => 65536,
525 'mediumblob' => 16777216,
526 'longblob' => 4294967296,// yeah, really
529 $thisFieldMaxSize = (int) Config::getInstance()->get('max_upload_size'); // from PHP max
531 return Util::getFormattedMaximumUploadSize(min($thisFieldMaxSize, $maxFieldSize)) . "\n";
535 * Get HTML for the Value column of other datatypes
536 * (here, "column" is used in the sense of HTML column in HTML table)
538 * @param InsertEditColumn $column description of column in given table
539 * @param string $defaultCharEditing default char editing mode which is stored
540 * in the config.inc.php script
541 * @param string $backupField hidden input field
542 * @param string $columnNameAppendix the name attribute
543 * @param string $onChangeClause onchange clause for fields
544 * @param string $specialChars special characters
545 * @param string $textDir text direction
546 * @param string $specialCharsEncoded replaced char if the string starts
547 * with a \r\n pair (0x0d0a) add an extra \n
548 * @param string $data data to edit
549 * @param mixed[] $extractedColumnspec associative array containing type,
550 * spec_in_brackets and possibly
551 * enum_set_values (another array)
553 * @return string an html snippet
555 private function getValueColumnForOtherDatatypes(
556 InsertEditColumn $column,
557 string $defaultCharEditing,
558 string $backupField,
559 string $columnNameAppendix,
560 string $onChangeClause,
561 string $specialChars,
562 string $textDir,
563 string $specialCharsEncoded,
564 string $data,
565 array $extractedColumnspec,
566 ): string {
567 // HTML5 data-* attribute data-type
568 $dataType = $this->dbi->types->getTypeClass($column->trueType);
569 $fieldsize = $this->getColumnSize($column, $extractedColumnspec['spec_in_brackets']);
571 $config = Config::getInstance();
572 $isTextareaRequired = $column->isChar
573 && ($config->settings['CharEditing'] === 'textarea' || str_contains($data, "\n"));
574 if ($isTextareaRequired) {
575 $config->settings['CharEditing'] = $defaultCharEditing;
576 $htmlField = $this->getTextarea(
577 $column,
578 $backupField,
579 $columnNameAppendix,
580 $onChangeClause,
581 $textDir,
582 $specialCharsEncoded,
583 $dataType,
585 } else {
586 $htmlField = $this->getHtmlInput(
587 $column,
588 $columnNameAppendix,
589 $specialChars,
590 $fieldsize,
591 $onChangeClause,
592 $dataType,
596 return $this->template->render('table/insert/value_column_for_other_datatype', [
597 'html_field' => $htmlField,
598 'backup_field' => $backupField,
599 'is_textarea' => $isTextareaRequired,
600 'columnNameAppendix' => $columnNameAppendix,
601 'column' => $column,
606 * Get the field size
608 * @param InsertEditColumn $column description of column in given table
609 * @param string $specInBrackets text in brackets inside column definition
611 * @return int field size
613 private function getColumnSize(InsertEditColumn $column, string $specInBrackets): int
615 $config = Config::getInstance();
616 if ($column->isChar) {
617 $fieldsize = (int) $specInBrackets;
618 if ($fieldsize > $config->settings['MaxSizeForInputField']) {
620 * This case happens for CHAR or VARCHAR columns which have
621 * a size larger than the maximum size for input field.
623 $config->settings['CharEditing'] = 'textarea';
625 } else {
627 * This case happens for example for INT or DATE columns;
628 * in these situations, the value returned in $column['len']
629 * seems appropriate.
631 $fieldsize = $column->length;
634 return min(
635 max($fieldsize, $config->settings['MinSizeForInputField']),
636 $config->settings['MaxSizeForInputField'],
641 * get html for continue insertion form
643 * @param string $table name of the table
644 * @param string $db name of the database
645 * @param mixed[] $whereClauseArray
647 * @return string an html snippet
649 public function getContinueInsertionForm(
650 string $table,
651 string $db,
652 array $whereClauseArray,
653 string $errorUrl,
654 ): string {
655 return $this->template->render('table/insert/continue_insertion_form', [
656 'db' => $db,
657 'table' => $table,
658 'where_clause_array' => $whereClauseArray,
659 'err_url' => $errorUrl,
660 'goto' => $GLOBALS['goto'],
661 'sql_query' => $_POST['sql_query'] ?? null,
662 'has_where_clause' => isset($_POST['where_clause']),
663 'insert_rows_default' => Config::getInstance()->settings['InsertRows'],
668 * @param string[]|string|null $whereClause
670 * @psalm-pure
672 public static function isWhereClauseNumeric(array|string|null $whereClause): bool
674 if ($whereClause === null) {
675 return false;
678 if (! is_array($whereClause)) {
679 $whereClause = [$whereClause];
682 // If we have just numeric primary key, we can also edit next
683 // we are looking for `table_name`.`field_name` = numeric_value
684 foreach ($whereClause as $clause) {
685 // preg_match() returns 1 if there is a match
686 $isNumeric = preg_match('@^[\s]*`[^`]*`[\.]`[^`]*` = [0-9]+@', $clause) === 1;
687 if ($isNumeric) {
688 return true;
692 return false;
696 * Get table head and table foot for insert row table
698 * @param mixed[] $urlParams url parameters
700 * @return string an html snippet
702 private function getHeadAndFootOfInsertRowTable(array $urlParams): string
704 $type = '';
705 $function = '';
707 $config = Config::getInstance();
708 if ($config->settings['ShowFieldTypesInDataEditView']) {
709 $type = $this->showTypeOrFunction('type', $urlParams, true);
712 if ($config->settings['ShowFunctionFields']) {
713 $function = $this->showTypeOrFunction('function', $urlParams, true);
716 $template = new Template();
718 return $template->render('table/insert/get_head_and_foot_of_insert_row_table', [
719 'type' => $type,
720 'function' => $function,
725 * Prepares the field value and retrieve special chars, backup field and data array
727 * @param mixed[] $currentRow a row of the table
728 * @param InsertEditColumn $column description of column in given table
729 * @param mixed[] $extractedColumnspec associative array containing type,
730 * spec_in_brackets and possibly
731 * enum_set_values (another array)
732 * @param string $columnNameAppendix string to append to column name in input
733 * @param bool $asIs use the data as is, used in repopulating
735 * @return mixed[] $real_null_value, $data, $special_chars, $backup_field,
736 * $special_chars_encoded
737 * @psalm-return array{bool, string, string, string, string}
739 private function getSpecialCharsAndBackupFieldForExistingRow(
740 array $currentRow,
741 InsertEditColumn $column,
742 array $extractedColumnspec,
743 string $columnNameAppendix,
744 bool $asIs,
745 ): array {
746 $specialCharsEncoded = '';
747 $data = null;
748 $realNullValue = false;
749 // (we are editing)
750 if (! isset($currentRow[$column->field])) {
751 $realNullValue = true;
752 $currentRow[$column->field] = '';
753 $specialChars = '';
754 $data = '';
755 } elseif ($column->trueType === 'bit') {
756 $specialChars = $asIs
757 ? $currentRow[$column->field]
758 : Util::printableBitValue(
759 (int) $currentRow[$column->field],
760 (int) $extractedColumnspec['spec_in_brackets'],
762 } elseif (
763 (str_starts_with($column->trueType, 'timestamp')
764 || $column->trueType === 'datetime'
765 || $column->trueType === 'time')
766 && (str_contains($currentRow[$column->field], '.'))
768 $currentRow[$column->field] = $asIs
769 ? $currentRow[$column->field]
770 : Util::addMicroseconds($currentRow[$column->field]);
771 $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
772 } elseif (in_array($column->trueType, Gis::getDataTypes(), true)) {
773 // Convert gis data to Well Know Text format
774 $currentRow[$column->field] = $asIs
775 ? $currentRow[$column->field]
776 : Gis::convertToWellKnownText($currentRow[$column->field], true);
777 $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
778 } else {
779 // special binary "characters"
780 if ($column->isBinary || ($column->isBlob && Config::getInstance()->settings['ProtectBinary'] !== 'all')) {
781 $currentRow[$column->field] = $asIs
782 ? $currentRow[$column->field]
783 : bin2hex($currentRow[$column->field]);
786 $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
788 //We need to duplicate the first \n or otherwise we will lose
789 //the first newline entered in a VARCHAR or TEXT column
790 $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
792 $data = $currentRow[$column->field];
795 /** @var string $defaultAction */
796 $defaultAction = $_POST['default_action'] ?? $_GET['default_action'] ?? '';
797 if (
798 $defaultAction === 'insert'
799 && $column->key === 'PRI'
800 && str_contains($column->extra, 'auto_increment')
802 // When copying row, it is useful to empty auto-increment column to prevent duplicate key error.
803 $data = $specialCharsEncoded = $specialChars = null;
806 // If a timestamp field value is not included in an update
807 // statement MySQL auto-update it to the current timestamp;
808 // however, things have changed since MySQL 4.1, so
809 // it's better to set a fields_prev in this situation
810 $backupField = '<input type="hidden" name="fields_prev'
811 . $columnNameAppendix . '" value="'
812 . htmlspecialchars($currentRow[$column->field], ENT_COMPAT) . '">';
814 return [$realNullValue, (string) $specialCharsEncoded, (string) $specialChars, (string) $data, $backupField];
818 * display default values
820 private function getSpecialCharsForInsertingMode(
821 string|null $defaultValue,
822 string $trueType,
823 ): string {
824 if ($defaultValue === null) {
825 $defaultValue = '';
828 if ($trueType === 'bit') {
829 $specialChars = Util::convertBitDefaultValue($defaultValue);
830 } elseif (str_starts_with($trueType, 'timestamp') || $trueType === 'datetime' || $trueType === 'time') {
831 $specialChars = Util::addMicroseconds($defaultValue);
832 } elseif ($trueType === 'binary' || $trueType === 'varbinary') {
833 $specialChars = bin2hex($defaultValue);
834 } elseif (str_ends_with($trueType, 'text')) {
835 $textDefault = substr($defaultValue, 1, -1);
836 $specialChars = stripcslashes($textDefault !== '' ? $textDefault : $defaultValue);
837 } else {
838 $specialChars = htmlspecialchars($defaultValue);
841 return $specialChars;
845 * set $_SESSION for edit_next
847 * @param string $oneWhereClause one where clause from where clauses array
849 public function setSessionForEditNext(string $oneWhereClause): void
851 $localQuery = 'SELECT * FROM ' . Util::backquote(Current::$database)
852 . '.' . Util::backquote(Current::$table) . ' WHERE '
853 . str_replace('` =', '` >', $oneWhereClause) . ' LIMIT 1;';
855 $res = $this->dbi->query($localQuery);
856 $row = $res->fetchRow();
857 $meta = $this->dbi->getFieldsMeta($res);
858 // must find a unique condition based on unique key,
859 // not a combination of all fields
860 $uniqueCondition = (new UniqueCondition($meta, $row, true))->getWhereClause();
861 if ($uniqueCondition === '') {
862 return;
865 $_SESSION['edit_next'] = $uniqueCondition;
869 * set $goto_include variable for different cases and retrieve like,
870 * if $GLOBALS['goto'] empty, if $goto_include previously not defined
871 * and new_insert, same_insert, edit_next
873 * @param string|false $gotoInclude store some script for include, otherwise it is
874 * boolean false
876 public function getGotoInclude(string|false $gotoInclude): string
878 $validOptions = ['new_insert', 'same_insert', 'edit_next'];
879 if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $validOptions, true)) {
880 return '/table/change';
883 if (! empty($GLOBALS['goto'])) {
884 if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) {
885 // this should NOT happen
886 //$GLOBALS['goto'] = false;
887 $gotoInclude = $GLOBALS['goto'] === 'index.php?route=/sql' ? '/sql' : false;
888 } else {
889 $gotoInclude = $GLOBALS['goto'];
892 if ($GLOBALS['goto'] === 'index.php?route=/database/sql' && Current::$table !== '') {
893 Current::$table = '';
897 if (! $gotoInclude) {
898 $gotoInclude = Current::$table === '' ? '/database/sql' : '/table/sql';
901 return $gotoInclude;
905 * Defines the url to return in case of failure of the query
907 * @param mixed[] $urlParams url parameters
909 * @return string error url for query failure
911 public function getErrorUrl(array $urlParams): string
913 return $_POST['err_url'] ?? Url::getFromRoute('/table/change', $urlParams);
917 * Executes the sql query and get the result, then move back to the calling page
919 * @param mixed[] $query built query from buildSqlQuery()
921 * @return mixed[] $total_affected_rows, $last_messages, $warning_messages, $error_messages
923 public function executeSqlQuery(array $query): array
925 $GLOBALS['sql_query'] = implode('; ', $query) . ';';
926 // to ensure that the query is displayed in case of
927 // "insert as new row" and then "insert another new row"
928 $GLOBALS['display_query'] = $GLOBALS['sql_query'];
930 $totalAffectedRows = 0;
931 $lastMessages = [];
932 $warningMessages = [];
933 $errorMessages = [];
935 foreach ($query as $singleQuery) {
936 if (isset($_POST['submit_type']) && $_POST['submit_type'] === 'showinsert') {
937 $lastMessages[] = Message::notice(__('Showing SQL query'));
938 continue;
941 if (Config::getInstance()->settings['IgnoreMultiSubmitErrors']) {
942 $result = $this->dbi->tryQuery($singleQuery);
943 } else {
944 $result = $this->dbi->query($singleQuery);
947 if (! $result) {
948 $errorMessages[] = $this->dbi->getError();
949 } else {
950 $totalAffectedRows += $this->dbi->affectedRows();
952 $insertId = $this->dbi->insertId();
953 if ($insertId !== 0) {
954 // insert_id is id of FIRST record inserted in one insert, so if we
955 // inserted multiple rows, we had to increment this
957 if ($totalAffectedRows > 0) {
958 $insertId += $totalAffectedRows - 1;
961 $lastMessage = Message::notice(__('Inserted row id: %1$d'));
962 $lastMessage->addParam($insertId);
963 $lastMessages[] = $lastMessage;
967 $warningMessages = $this->getWarningMessages();
970 return [$totalAffectedRows, $lastMessages, $warningMessages, $errorMessages];
974 * get the warning messages array
976 * @return string[]
978 private function getWarningMessages(): array
980 $warningMessages = [];
981 foreach ($this->dbi->getWarnings() as $warning) {
982 $warningMessages[] = htmlspecialchars((string) $warning);
985 return $warningMessages;
989 * Column to display from the foreign table?
991 * @param string $whereComparison string that contain relation field value
992 * @param mixed[] $map all Relations to foreign tables for a given
993 * table or optionally a given column in a table
994 * @param string $relationField relation field
996 * @return string display value from the foreign table
998 public function getDisplayValueForForeignTableColumn(
999 string $whereComparison,
1000 array $map,
1001 string $relationField,
1002 ): string {
1003 $foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
1005 if (! is_array($foreigner)) {
1006 return '';
1009 $displayField = $this->relation->getDisplayField($foreigner['foreign_db'], $foreigner['foreign_table']);
1010 // Field to display from the foreign table?
1011 if ($displayField !== '') {
1012 $dispsql = 'SELECT ' . Util::backquote($displayField)
1013 . ' FROM ' . Util::backquote($foreigner['foreign_db'])
1014 . '.' . Util::backquote($foreigner['foreign_table'])
1015 . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
1016 . $whereComparison;
1017 $dispresult = $this->dbi->tryQuery($dispsql);
1018 if ($dispresult && $dispresult->numRows() > 0) {
1019 return (string) $dispresult->fetchValue();
1023 return '';
1027 * Display option in the cell according to user choices
1029 * @param mixed[] $map all Relations to foreign tables for a given
1030 * table or optionally a given column in a table
1031 * @param string $relationField relation field
1032 * @param string $whereComparison string that contain relation field value
1033 * @param string $dispval display value from the foreign table
1034 * @param string $relationFieldValue relation field value
1036 * @return string HTML <a> tag
1038 public function getLinkForRelationalDisplayField(
1039 array $map,
1040 string $relationField,
1041 string $whereComparison,
1042 string $dispval,
1043 string $relationFieldValue,
1044 ): string {
1045 $foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
1047 if (! is_array($foreigner)) {
1048 return '';
1051 if ($_SESSION['tmpval']['relational_display'] === 'K') {
1052 // user chose "relational key" in the display options, so
1053 // the title contains the display field
1054 $title = $dispval !== ''
1055 ? ' title="' . htmlspecialchars($dispval) . '"'
1056 : '';
1057 } else {
1058 $title = ' title="' . htmlspecialchars($relationFieldValue) . '"';
1061 $sqlQuery = 'SELECT * FROM '
1062 . Util::backquote($foreigner['foreign_db'])
1063 . '.' . Util::backquote($foreigner['foreign_table'])
1064 . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
1065 . $whereComparison;
1066 $urlParams = [
1067 'db' => $foreigner['foreign_db'],
1068 'table' => $foreigner['foreign_table'],
1069 'pos' => '0',
1070 'sql_signature' => Core::signSqlQuery($sqlQuery),
1071 'sql_query' => $sqlQuery,
1073 $output = '<a href="' . Url::getFromRoute('/sql', $urlParams) . '"' . $title . '>';
1075 if ($_SESSION['tmpval']['relational_display'] === 'D') {
1076 // user chose "relational display field" in the
1077 // display options, so show display field in the cell
1078 $output .= htmlspecialchars($dispval);
1079 } else {
1080 // otherwise display data in the cell
1081 $output .= htmlspecialchars($relationFieldValue);
1084 $output .= '</a>';
1086 return $output;
1090 * Transform edited values
1092 * @param string $db db name
1093 * @param string $table table name
1094 * @param mixed[] $transformation mimetypes for all columns of a table
1095 * [field_name][field_key]
1096 * @param mixed[] $editedValues transform columns list and new values
1097 * @param string $file file containing the transformation plugin
1098 * @param string $columnName column name
1099 * @param mixed[] $extraData extra data array
1100 * @param string $type the type of transformation
1102 * @return mixed[]
1104 public function transformEditedValues(
1105 string $db,
1106 string $table,
1107 array $transformation,
1108 array &$editedValues,
1109 string $file,
1110 string $columnName,
1111 array $extraData,
1112 string $type,
1113 ): array {
1114 $includeFile = 'src/Plugins/Transformations/' . $file;
1115 if (is_file(ROOT_PATH . $includeFile)) {
1116 // $cfg['SaveCellsAtOnce'] = true; JS code sends an array
1117 $whereClause = is_array($_POST['where_clause']) ? $_POST['where_clause'][0] : $_POST['where_clause'];
1118 $urlParams = [
1119 'db' => $db,
1120 'table' => $table,
1121 'where_clause_sign' => Core::signSqlQuery($whereClause),
1122 'where_clause' => $whereClause,
1123 'transform_key' => $columnName,
1125 $transformOptions = $this->transformations->getOptions($transformation[$type . '_options'] ?? '');
1126 $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
1127 $transformOptions['wrapper_params'] = $urlParams;
1128 $className = $this->transformations->getClassName($includeFile);
1129 if (class_exists($className)) {
1130 /** @var TransformationsPlugin $transformationPlugin */
1131 $transformationPlugin = new $className();
1133 foreach ($editedValues as $cellIndex => $currCellEditedValues) {
1134 if (! isset($currCellEditedValues[$columnName])) {
1135 continue;
1138 $extraData['transformations'][$cellIndex] = $transformationPlugin->applyTransformation(
1139 $currCellEditedValues[$columnName],
1140 $transformOptions,
1142 $editedValues[$cellIndex][$columnName] = $extraData['transformations'][$cellIndex];
1147 return $extraData;
1151 * Get value part if a function was specified
1153 private function formatAsSqlFunction(
1154 EditField $editField,
1155 ): string {
1156 if ($editField->function === 'PHP_PASSWORD_HASH') {
1157 $hash = password_hash($editField->value, PASSWORD_DEFAULT);
1159 return $this->dbi->quoteString($hash);
1162 if ($editField->function === 'UUID') {
1163 /* This way user will know what UUID new row has */
1164 $uuid = (string) $this->dbi->fetchValue('SELECT UUID()');
1166 return $this->dbi->quoteString($uuid);
1169 if (
1170 in_array($editField->function, $this->getGisFromTextFunctions(), true)
1171 || in_array($editField->function, $this->getGisFromWKBFunctions(), true)
1173 preg_match('/^(\'?)(.*?)\1(?:,(\d+))?$/', $editField->value, $matches);
1174 $escapedParams = $this->dbi->quoteString($matches[2]) . (isset($matches[3]) ? ',' . $matches[3] : '');
1176 return $editField->function . '(' . $escapedParams . ')';
1179 if (
1180 ! in_array($editField->function, self::FUNC_NO_PARAM, true)
1181 || ($editField->value !== '' && in_array($editField->function, self::FUNC_OPTIONAL_PARAM, true))
1183 if (
1184 ($editField->salt !== null
1185 && ($editField->function === 'AES_ENCRYPT'
1186 || $editField->function === 'AES_DECRYPT'
1187 || $editField->function === 'SHA2'))
1188 || ($editField->salt
1189 && ($editField->function === 'DES_ENCRYPT'
1190 || $editField->function === 'DES_DECRYPT'
1191 || $editField->function === 'ENCRYPT'))
1193 return $editField->function . '(' . $this->dbi->quoteString($editField->value) . ','
1194 . $this->dbi->quoteString($editField->salt) . ')';
1197 return $editField->function . '(' . $this->dbi->quoteString($editField->value) . ')';
1200 return $editField->function . '()';
1204 * Get the field value formatted for use in a SQL statement.
1205 * Used in both INSERT and UPDATE statements.
1207 private function getValueFormattedAsSql(
1208 EditField $editField,
1209 string $protectedValue = '',
1210 ): string {
1211 if ($editField->isUploaded) {
1212 return $editField->value;
1215 if ($editField->function !== '') {
1216 return $this->formatAsSqlFunction($editField);
1219 return $this->formatAsSqlValueBasedOnType($editField, $protectedValue);
1223 * Get query values array and query fields array for insert and update in multi edit
1225 * @param string|int $whereClause Either a positional index or string representing selected row
1227 public function getQueryValueForInsert(
1228 EditField $editField,
1229 bool $usingKey,
1230 string|int $whereClause,
1231 ): string {
1232 $protectedValue = '';
1233 if ($editField->type === 'protected' && $usingKey && $whereClause !== '') {
1234 // Fetch the current values of a row to use in case we have a protected field
1235 $protectedValue = $this->dbi->fetchValue(
1236 'SELECT ' . Util::backquote($editField->columnName)
1237 . ' FROM ' . Util::backquote(Current::$table)
1238 . ' WHERE ' . $whereClause,
1240 $protectedValue = is_string($protectedValue) ? $protectedValue : '';
1243 return $this->getValueFormattedAsSql($editField, $protectedValue);
1247 * Get field-value pairs for update SQL.
1248 * During update, we build the SQL only with the fields that should be updated.
1250 public function getQueryValueForUpdate(EditField $editField): string
1252 $currentValueFormattedAsSql = $this->getValueFormattedAsSql($editField);
1254 // avoid setting a field to NULL when it's already NULL
1255 // (field had the null checkbox before the update; field still has the null checkbox)
1256 if ($editField->wasPreviouslyNull && $editField->isNull) {
1257 return '';
1260 // A blob field that hasn't been changed will have no value
1261 if ($currentValueFormattedAsSql === '') {
1262 return '';
1265 if (
1266 // Field had the null checkbox before the update; field no longer has the null checkbox
1267 $editField->wasPreviouslyNull ||
1268 // Field was marked as NULL (the value will be unchanged if it was an empty string)
1269 $editField->isNull ||
1270 // A function was applied to the field
1271 $editField->function !== '' ||
1272 // The value was changed
1273 $editField->value !== $editField->previousValue
1275 return Util::backquote($editField->columnName) . ' = ' . $currentValueFormattedAsSql;
1278 return '';
1282 * Get the current column value in the form for different data types
1284 private function formatAsSqlValueBasedOnType(
1285 EditField $editField,
1286 string $protectedValue,
1287 ): string {
1288 if ($editField->type === 'protected') {
1289 // here we are in protected mode (asked in the config)
1290 // so tbl_change has put this special value in the
1291 // columns array, so we do not change the column value
1292 // but we can still handle column upload
1294 // when in UPDATE mode, do not alter field's contents. When in INSERT
1295 // mode, insert empty field because no values were submitted.
1296 // If protected blobs were set, insert original field's content.
1297 if ($protectedValue !== '') {
1298 return '0x' . bin2hex($protectedValue);
1301 if ($editField->isNull) {
1302 return 'NULL';
1305 // The Null checkbox was unchecked for this field
1306 if ($editField->wasPreviouslyNull) {
1307 return "''";
1310 return '';
1313 if ($editField->value === '') {
1314 // When the field is autoIncrement, the best way to avoid problems
1315 // in strict mode is to set the value to null (works also in non-strict mode)
1317 // If the value is empty and the null checkbox is checked, set it to null
1318 return $editField->autoIncrement || $editField->isNull ? 'NULL' : "''";
1321 if ($editField->type === 'hex') {
1322 if (! str_starts_with($editField->value, '0x')) {
1323 return '0x' . $editField->value;
1326 return $editField->value;
1329 if ($editField->type === 'bit') {
1330 $currentValue = (string) preg_replace('/[^01]/', '0', $editField->value);
1332 return 'b' . $this->dbi->quoteString($currentValue);
1335 // For uuid type, generate uuid value
1336 // if empty value but not set null or value is uuid() function
1337 if (
1338 $editField->type === 'uuid'
1339 && ! $editField->isNull
1340 && in_array($editField->value, ["''", '', "'uuid()'", 'uuid()'], true)
1342 return 'uuid()';
1345 if (
1346 ($editField->type !== 'datetime' && $editField->type !== 'timestamp' && $editField->type !== 'date')
1347 || ($editField->value !== 'CURRENT_TIMESTAMP' && $editField->value !== 'current_timestamp()')
1349 return $this->dbi->quoteString($editField->value);
1352 // If there is a value, we ignore the Null checkbox;
1353 // this could be possible if Javascript is disabled in the browser
1354 return $editField->value;
1358 * Check whether inline edited value can be truncated or not,
1359 * and add additional parameters for extra_data array if needed
1361 * @param string $db Database name
1362 * @param string $table Table name
1363 * @param string $columnName Column name
1364 * @param mixed[] $extraData Extra data for ajax response
1366 public function verifyWhetherValueCanBeTruncatedAndAppendExtraData(
1367 string $db,
1368 string $table,
1369 string $columnName,
1370 array &$extraData,
1371 ): void {
1372 $extraData['isNeedToRecheck'] = false;
1374 $sqlForRealValue = 'SELECT ' . Util::backquote($table) . '.'
1375 . Util::backquote($columnName)
1376 . ' FROM ' . Util::backquote($db) . '.'
1377 . Util::backquote($table)
1378 . ' WHERE ' . $_POST['where_clause'][0];
1380 $result = $this->dbi->tryQuery($sqlForRealValue);
1382 if (! $result) {
1383 return;
1386 $fieldsMeta = $this->dbi->getFieldsMeta($result);
1387 $meta = $fieldsMeta[0];
1388 $newValue = $result->fetchValue();
1390 if ($newValue === false) {
1391 return;
1394 if ($newValue !== null) {
1395 if ($meta->isTimeType()) {
1396 $newValue = Util::addMicroseconds($newValue);
1397 } elseif ($meta->isBinary()) {
1398 $newValue = '0x' . bin2hex($newValue);
1402 $extraData['isNeedToRecheck'] = true;
1403 $extraData['truncatableFieldValue'] = $newValue;
1407 * Function to get the columns of a table
1409 * @param string $db current db
1410 * @param string $table current table
1412 * @return list<ColumnFull>
1414 public function getTableColumns(string $db, string $table): array
1416 $this->dbi->selectDb($db);
1418 return array_values($this->dbi->getColumns($db, $table, true));
1422 * Function to determine Insert/Edit rows
1424 * @param string[]|string|null $whereClause where clause
1425 * @param string $db current database
1426 * @param string $table current table
1428 * @return array<int, bool|string[]|string|ResultInterface|ResultInterface[]|null>
1429 * @phpstan-return array{
1430 * bool,
1431 * string[]|string|null,
1432 * string[],
1433 * string[]|null,
1434 * ResultInterface[]|ResultInterface,
1435 * array<string, string|null>[]|false[],
1436 * bool,
1437 * string|null
1440 public function determineInsertOrEdit(array|string|null $whereClause, string $db, string $table): array
1442 if (isset($_POST['where_clause'])) {
1443 $whereClause = $_POST['where_clause'];
1446 if (isset($_SESSION['edit_next'])) {
1447 $whereClause = $_SESSION['edit_next'];
1448 unset($_SESSION['edit_next']);
1449 $afterInsert = 'edit_next';
1452 $config = Config::getInstance();
1453 if (isset($_POST['ShowFunctionFields'])) {
1454 $config->settings['ShowFunctionFields'] = $_POST['ShowFunctionFields'];
1457 if (isset($_POST['ShowFieldTypesInDataEditView'])) {
1458 $config->settings['ShowFieldTypesInDataEditView'] = $_POST['ShowFieldTypesInDataEditView'];
1461 if (isset($_POST['after_insert'])) {
1462 $afterInsert = $_POST['after_insert'];
1465 if (isset($whereClause)) {
1466 // we are editing
1467 $insertMode = false;
1468 $whereClauseArray = (array) $whereClause;
1469 [$whereClauses, $result, $rows, $foundUniqueKey] = $this->analyzeWhereClauses(
1470 $whereClauseArray,
1471 $table,
1472 $db,
1474 } else {
1475 // we are inserting
1476 $insertMode = true;
1477 $whereClause = null;
1478 $result = $this->loadFirstRow($table, $db);
1479 $rows = $this->getInsertRows();
1480 $whereClauses = null;
1481 $whereClauseArray = [];
1482 $foundUniqueKey = false;
1485 /** @var string $defaultAction */
1486 $defaultAction = $_POST['default_action'] ?? $_GET['default_action'] ?? '';
1487 if ($defaultAction === 'insert') {
1488 // Copying a row - fetched data will be inserted as a new row, therefore the where clause is needless.
1489 $whereClause = $whereClauses = null;
1492 return [
1493 $insertMode,
1494 $whereClause,
1495 $whereClauseArray,
1496 $whereClauses,
1497 $result,
1498 $rows,
1499 $foundUniqueKey,
1500 $afterInsert ?? null,
1505 * Function to get comments for the table columns
1507 * @param string $db current database
1508 * @param string $table current table
1510 * @return string[] comments for columns
1512 public function getCommentsMap(string $db, string $table): array
1514 if (Config::getInstance()->settings['ShowPropertyComments']) {
1515 return $this->relation->getComments($db, $table);
1518 return [];
1522 * Function to get html for the gis editor div
1524 public function getHtmlForGisEditor(): string
1526 return '<div id="gis_editor"></div><div id="popup_background"></div><br>';
1530 * Function to get html for the ignore option in insert mode
1532 * @param int $rowId row id
1533 * @param bool $checked ignore option is checked or not
1535 public function getHtmlForIgnoreOption(int $rowId, bool $checked = true): string
1537 return '<input type="checkbox"'
1538 . ($checked ? ' checked="checked"' : '')
1539 . ' name="insert_ignore_' . $rowId . '"'
1540 . ' id="insert_ignore_' . $rowId . '">'
1541 . '<label for="insert_ignore_' . $rowId . '">'
1542 . __('Ignore')
1543 . '</label><br>' . "\n";
1547 * Function to get html for the insert edit form header
1549 * @param bool $hasBlobField whether has blob field
1550 * @param bool $isUpload whether is upload
1552 public function getHtmlForInsertEditFormHeader(bool $hasBlobField, bool $isUpload): string
1554 $template = new Template();
1556 return $template->render('table/insert/get_html_for_insert_edit_form_header', [
1557 'has_blob_field' => $hasBlobField,
1558 'is_upload' => $isUpload,
1563 * Function to get html for each insert/edit column
1565 * @param ColumnFull $tableColumn column
1566 * @param int $columnNumber column index in table_columns
1567 * @param string[] $commentsMap comments map
1568 * @param int $columnLength length of the current column taken from field metadata
1569 * @param bool $insertMode whether insert mode
1570 * @param mixed[] $currentRow current row
1571 * @param int $columnsCnt columns count
1572 * @param bool $isUpload whether upload
1573 * @param mixed[] $foreigners foreigners
1574 * @param string $table table
1575 * @param string $db database
1576 * @param int $rowId row id
1577 * @param string $defaultCharEditing default char editing mode which is stored in the config.inc.php script
1578 * @param string $textDir text direction
1579 * @param mixed[] $repopulate the data to be repopulated
1580 * @param mixed[] $columnMime the mime information of column
1581 * @param string $whereClause the where clause
1583 private function getHtmlForInsertEditFormColumn(
1584 ColumnFull $tableColumn,
1585 int $columnNumber,
1586 array $commentsMap,
1587 int $columnLength,
1588 bool $insertMode,
1589 array $currentRow,
1590 int $columnsCnt,
1591 bool $isUpload,
1592 array $foreigners,
1593 string $table,
1594 string $db,
1595 int $rowId,
1596 string $defaultCharEditing,
1597 string $textDir,
1598 array $repopulate,
1599 array $columnMime,
1600 string $whereClause,
1601 ): string {
1602 $column = new InsertEditColumn(
1603 $tableColumn->field,
1604 $tableColumn->type,
1605 $tableColumn->isNull,
1606 $tableColumn->key,
1607 $tableColumn->default,
1608 $tableColumn->extra,
1609 $columnLength,
1610 $this->isColumn($tableColumn->type, ['binary', 'varbinary']),
1611 $this->isColumn($tableColumn->type, ['blob', 'tinyblob', 'mediumblob', 'longblob']),
1612 $this->isColumn($tableColumn->type, ['char', 'varchar']),
1613 $insertMode,
1616 $asIs = false;
1617 $fieldHashMd5 = $column->md5;
1618 if ($repopulate !== [] && array_key_exists($fieldHashMd5, $currentRow)) {
1619 $currentRow[$column->field] = $repopulate[$fieldHashMd5];
1620 $asIs = true;
1623 $extractedColumnspec = Util::extractColumnSpec($column->type);
1625 //Call validation when the form submitted...
1626 $onChangeClause = 'return verificationsAfterFieldChange('
1627 . json_encode($fieldHashMd5) . ', '
1628 . json_encode((string) $rowId) . ',' . json_encode($column->pmaType) . ')';
1630 $vkey = '[multi_edit][' . $rowId . ']';
1631 // Use an MD5 as an array index to avoid having special characters
1632 // in the name attribute (see bug #1746964 )
1633 $columnNameAppendix = $vkey . '[' . $fieldHashMd5 . ']';
1635 // Prepares the field value
1636 if ($currentRow !== []) {
1637 // (we are editing)
1639 $realNullValue,
1640 $specialCharsEncoded,
1641 $specialChars,
1642 $data,
1643 $backupField,
1644 ] = $this->getSpecialCharsAndBackupFieldForExistingRow(
1645 $currentRow,
1646 $column,
1647 $extractedColumnspec,
1648 $columnNameAppendix,
1649 $asIs,
1651 } else {
1652 // (we are inserting)
1653 // display default values
1654 $defaultValue = $column->default ?? null;
1655 if (isset($repopulate[$fieldHashMd5])) {
1656 $defaultValue = $repopulate[$fieldHashMd5];
1659 $realNullValue = $defaultValue === null;
1660 $data = (string) $defaultValue;
1661 $specialChars = $this->getSpecialCharsForInsertingMode($defaultValue, $column->trueType);
1662 $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
1663 $backupField = '';
1666 $this->fieldIndex = ($this->rowOffset * $columnsCnt) + $columnNumber + 1;
1668 // The function column
1669 // -------------------
1670 $foreignData = $this->relation->getForeignData($foreigners, $column->field, false, '', '');
1671 $isColumnBinary = $this->isColumnBinary($column, $isUpload);
1672 $functionOptions = '';
1674 $config = Config::getInstance();
1675 if ($config->settings['ShowFunctionFields']) {
1676 $defaultFunction = Generator::getDefaultFunctionForField(
1677 $column->trueType,
1678 $column->firstTimestamp,
1679 $column->default,
1680 $column->extra,
1681 $column->isNull,
1682 $column->key,
1683 $column->type,
1684 $insertMode,
1686 $functionOptions = Generator::getFunctionsForField($defaultFunction, $foreignData);
1689 // nullify code is needed by the js nullify() function to be able to generate calls to nullify() in jQuery
1690 $nullifyCode = $this->getNullifyCodeForNullColumn($column, $foreigners, $foreignData);
1692 // The value column (depends on type)
1693 // ----------------
1694 // See bug #1667887 for the reason why we don't use the maxlength
1695 // HTML attribute
1697 //add data attributes "no of decimals" and "data type"
1698 $noDecimals = 0;
1699 $type = current(explode('(', $column->pmaType));
1700 if (preg_match('/\(([^()]+)\)/', $column->pmaType, $match)) {
1701 $match[0] = trim($match[0], '()');
1702 $noDecimals = $match[0];
1705 // Check input transformation of column
1706 $transformedHtml = '';
1707 if (! empty($columnMime['input_transformation'])) {
1708 $file = $columnMime['input_transformation'];
1709 $includeFile = 'src/Plugins/Transformations/' . $file;
1710 if (is_file(ROOT_PATH . $includeFile)) {
1711 $className = $this->transformations->getClassName($includeFile);
1712 if (class_exists($className)) {
1713 $transformationPlugin = new $className();
1714 $transformationOptions = $this->transformations->getOptions(
1715 $columnMime['input_transformation_options'],
1717 $urlParams = [
1718 'db' => $db,
1719 'table' => $table,
1720 'transform_key' => $column->field,
1721 'where_clause_sign' => Core::signSqlQuery($whereClause),
1722 'where_clause' => $whereClause,
1724 $transformationOptions['wrapper_link'] = Url::getCommon($urlParams);
1725 $transformationOptions['wrapper_params'] = $urlParams;
1726 $currentValue = '';
1727 if (isset($currentRow[$column->field])) {
1728 $currentValue = $currentRow[$column->field];
1731 if ($transformationPlugin instanceof IOTransformationsPlugin) {
1732 $transformedHtml = $transformationPlugin->getInputHtml(
1733 $columnNameAppendix,
1734 $transformationOptions,
1735 $currentValue,
1736 $textDir,
1737 $this->fieldIndex,
1740 $GLOBALS['plugin_scripts'] = array_merge(
1741 $GLOBALS['plugin_scripts'],
1742 $transformationPlugin->getScripts(),
1749 $columnValue = '';
1750 $foreignDropdown = '';
1751 $dataType = '';
1752 $textAreaRows = $config->settings['TextareaRows'];
1753 $textareaCols = $config->settings['TextareaCols'];
1754 $maxlength = '';
1755 $enumSelectedValue = '';
1756 $enumValues = [];
1757 $columnSetValues = [];
1758 $setSelectSize = 0;
1759 $isColumnProtectedBlob = false;
1760 $blobValue = '';
1761 $blobValueUnit = '';
1762 $maxUploadSize = 0;
1763 $selectOptionForUpload = '';
1764 $inputFieldHtml = '';
1765 if ($transformedHtml === '') {
1766 if (is_array($foreignData['disp_row'])) {
1767 $foreignDropdown = $this->relation->foreignDropdown(
1768 $foreignData['disp_row'],
1769 $foreignData['foreign_field'],
1770 $foreignData['foreign_display'],
1771 $data,
1772 $config->settings['ForeignKeyMaxLimit'],
1776 $dataType = $this->dbi->types->getTypeClass($column->trueType);
1778 if ($column->isChar) {
1779 $textAreaRows = max($config->settings['CharTextareaRows'], 7);
1780 $textareaCols = $config->settings['CharTextareaCols'];
1781 $maxlength = $extractedColumnspec['spec_in_brackets'];
1782 } elseif ($config->settings['LongtextDoubleTextarea'] && mb_strstr($column->pmaType, 'longtext')) {
1783 $textAreaRows = $config->settings['TextareaRows'] * 2;
1784 $textareaCols = $config->settings['TextareaCols'] * 2;
1787 if ($column->pmaType === 'enum') {
1788 $enumValues = $extractedColumnspec['enum_set_values'];
1790 foreach ($enumValues as $enumValue) {
1791 if (
1792 $data == $enumValue || ($data == ''
1793 && (! isset($_POST['where_clause']) || ! $column->isNull)
1794 && isset($column->default) && $enumValue == $column->default)
1796 $enumSelectedValue = $enumValue;
1797 break;
1800 } elseif ($column->pmaType === 'set') {
1801 $columnSetValues = $extractedColumnspec['enum_set_values'];
1802 $setSelectSize = min(4, count($extractedColumnspec['enum_set_values']));
1803 } elseif ($column->isBinary || $column->isBlob) {
1804 $isColumnProtectedBlob = ($config->settings['ProtectBinary'] === 'blob' && $column->isBlob)
1805 || ($config->settings['ProtectBinary'] === 'all')
1806 || ($config->settings['ProtectBinary'] === 'noblob' && ! $column->isBlob);
1807 if ($isColumnProtectedBlob) {
1808 [$blobValue, $blobValueUnit] = Util::formatByteDown(mb_strlen(stripslashes($data)), 3, 1);
1811 if ($isUpload && $column->isBlob) {
1812 $maxUploadSize = $this->getMaxUploadSize($column->pmaType);
1815 if (! empty($config->settings['UploadDir'])) {
1816 $selectOptionForUpload = $this->getSelectOptionForUpload($vkey, $fieldHashMd5);
1819 if (
1820 ! $isColumnProtectedBlob
1821 && ! ($column->isBlob || ($column->length > $config->settings['LimitChars']))
1823 $inputFieldHtml = $this->getHtmlInput(
1824 $column,
1825 $columnNameAppendix,
1826 $specialChars,
1827 min(max($column->length, 4), $config->settings['LimitChars']),
1828 $onChangeClause,
1829 'HEX',
1832 } else {
1833 $columnValue = $this->getValueColumnForOtherDatatypes(
1834 $column,
1835 $defaultCharEditing,
1836 $backupField,
1837 $columnNameAppendix,
1838 $onChangeClause,
1839 $specialChars,
1840 $textDir,
1841 $specialCharsEncoded,
1842 $data,
1843 $extractedColumnspec,
1848 return $this->template->render('table/insert/column_row', [
1849 'db' => $db,
1850 'table' => $table,
1851 'column' => $column,
1852 'row_id' => $rowId,
1853 'show_field_types_in_data_edit_view' => $config->settings['ShowFieldTypesInDataEditView'],
1854 'show_function_fields' => $config->settings['ShowFunctionFields'],
1855 'is_column_binary' => $isColumnBinary,
1856 'function_options' => $functionOptions,
1857 'nullify_code' => $nullifyCode,
1858 'real_null_value' => $realNullValue,
1859 'id_index' => $this->fieldIndex,
1860 'type' => $type,
1861 'decimals' => $noDecimals,
1862 'special_chars' => $specialChars,
1863 'transformed_value' => $transformedHtml,
1864 'value' => $columnValue,
1865 'is_value_foreign_link' => $foreignData['foreign_link'] === true,
1866 'backup_field' => $backupField,
1867 'data' => $data,
1868 'gis_data_types' => Gis::getDataTypes(),
1869 'foreign_dropdown' => $foreignDropdown,
1870 'data_type' => $dataType,
1871 'textarea_cols' => $textareaCols,
1872 'textarea_rows' => $textAreaRows,
1873 'text_dir' => $textDir,
1874 'max_length' => $maxlength,
1875 'longtext_double_textarea' => $config->settings['LongtextDoubleTextarea'],
1876 'enum_selected_value' => $enumSelectedValue,
1877 'enum_values' => $enumValues,
1878 'set_values' => $columnSetValues,
1879 'set_select_size' => $setSelectSize,
1880 'is_column_protected_blob' => $isColumnProtectedBlob,
1881 'blob_value' => $blobValue,
1882 'blob_value_unit' => $blobValueUnit,
1883 'is_upload' => $isUpload,
1884 'max_upload_size' => $maxUploadSize,
1885 'select_option_for_upload' => $selectOptionForUpload,
1886 'limit_chars' => $config->settings['LimitChars'],
1887 'input_field_html' => $inputFieldHtml,
1888 'field_title' => $this->getColumnTitle($column->field, $commentsMap),
1892 private function isColumnBinary(InsertEditColumn $column, bool $isUpload): bool
1894 $config = Config::getInstance();
1895 if (! $config->settings['ShowFunctionFields']) {
1896 return false;
1899 return ($config->settings['ProtectBinary'] === 'blob' && $column->isBlob && ! $isUpload)
1900 || ($config->settings['ProtectBinary'] === 'all' && $column->isBinary)
1901 || ($config->settings['ProtectBinary'] === 'noblob' && $column->isBinary);
1905 * Function to get html for each insert/edit row
1907 * @param mixed[] $urlParams url parameters
1908 * @param list<ColumnFull> $tableColumns table columns
1909 * @param string[] $commentsMap comments map
1910 * @param ResultInterface $currentResult current result
1911 * @param bool $insertMode whether insert mode
1912 * @param mixed[] $currentRow current row
1913 * @param bool $isUpload whether upload
1914 * @param mixed[] $foreigners foreigners
1915 * @param string $table table
1916 * @param string $db database
1917 * @param int $rowId row id
1918 * @param string $textDir text direction
1919 * @param mixed[] $repopulate the data to be repopulated
1920 * @param mixed[] $whereClauseArray the array of where clauses
1922 public function getHtmlForInsertEditRow(
1923 array $urlParams,
1924 array $tableColumns,
1925 array $commentsMap,
1926 ResultInterface $currentResult,
1927 bool $insertMode,
1928 array $currentRow,
1929 bool $isUpload,
1930 array $foreigners,
1931 string $table,
1932 string $db,
1933 int $rowId,
1934 string $textDir,
1935 array $repopulate,
1936 array $whereClauseArray,
1937 ): string {
1938 $htmlOutput = $this->getHeadAndFootOfInsertRowTable($urlParams)
1939 . '<tbody>';
1941 //store the default value for CharEditing
1942 $defaultCharEditing = Config::getInstance()->settings['CharEditing'];
1943 $mimeMap = $this->transformations->getMime($db, $table);
1944 $whereClause = '';
1945 if (isset($whereClauseArray[$rowId])) {
1946 $whereClause = $whereClauseArray[$rowId];
1949 $columnCount = count($tableColumns);
1950 for ($columnNumber = 0; $columnNumber < $columnCount; $columnNumber++) {
1951 $tableColumn = $tableColumns[$columnNumber];
1952 $columnMime = [];
1953 if (isset($mimeMap[$tableColumn->field])) {
1954 $columnMime = $mimeMap[$tableColumn->field];
1957 $virtual = ['VIRTUAL', 'PERSISTENT', 'VIRTUAL GENERATED', 'STORED GENERATED'];
1958 if (in_array($tableColumn->extra, $virtual, true)) {
1959 continue;
1962 $htmlOutput .= $this->getHtmlForInsertEditFormColumn(
1963 $tableColumn,
1964 $columnNumber,
1965 $commentsMap,
1966 $this->dbi->getFieldsMeta($currentResult)[$columnNumber]->length,
1967 $insertMode,
1968 $currentRow,
1969 $columnCount,
1970 $isUpload,
1971 $foreigners,
1972 $table,
1973 $db,
1974 $rowId,
1975 $defaultCharEditing,
1976 $textDir,
1977 $repopulate,
1978 $columnMime,
1979 $whereClause,
1983 $this->rowOffset++;
1985 return $htmlOutput . ' </tbody>'
1986 . '</table></div><br>'
1987 . '<div class="clearfloat"></div>';
1991 * Returns list of function names that accept WKB as text
1993 * @return string[]
1995 private function getGisFromTextFunctions(): array
1997 return $this->dbi->getVersion() >= 50600 ?
1999 'ST_GeomFromText',
2000 'ST_GeomCollFromText',
2001 'ST_LineFromText',
2002 'ST_MLineFromText',
2003 'ST_PointFromText',
2004 'ST_MPointFromText',
2005 'ST_PolyFromText',
2006 'ST_MPolyFromText',
2009 'GeomFromText',
2010 'GeomCollFromText',
2011 'LineFromText',
2012 'MLineFromText',
2013 'PointFromText',
2014 'MPointFromText',
2015 'PolyFromText',
2016 'MPolyFromText',
2021 * Returns list of function names that accept WKB as binary
2023 * @return string[]
2025 private function getGisFromWKBFunctions(): array
2027 return $this->dbi->getVersion() >= 50600 ?
2029 'ST_GeomFromWKB',
2030 'ST_GeomCollFromWKB',
2031 'ST_LineFromWKB',
2032 'ST_MLineFromWKB',
2033 'ST_PointFromWKB',
2034 'ST_MPointFromWKB',
2035 'ST_PolyFromWKB',
2036 'ST_MPolyFromWKB',
2039 'GeomFromWKB',
2040 'GeomCollFromWKB',
2041 'LineFromWKB',
2042 'MLineFromWKB',
2043 'PointFromWKB',
2044 'MPointFromWKB',
2045 'PolyFromWKB',
2046 'MPolyFromWKB',