1<?php
2/**
3 * set of functions with the insert/edit features in pma
4 */
5
6declare(strict_types=1);
7
8namespace PhpMyAdmin;
9
10use PhpMyAdmin\Controllers\Table\ChangeController;
11use PhpMyAdmin\Html\Generator;
12use PhpMyAdmin\Plugins\TransformationsPlugin;
13use const ENT_COMPAT;
14use const PASSWORD_DEFAULT;
15use function array_fill;
16use function array_flip;
17use function array_merge;
18use function array_values;
19use function bin2hex;
20use function class_exists;
21use function count;
22use function current;
23use function date;
24use function defined;
25use function explode;
26use function htmlspecialchars;
27use function implode;
28use function in_array;
29use function is_array;
30use function is_file;
31use function is_numeric;
32use function is_string;
33use function max;
34use function mb_stripos;
35use function mb_strlen;
36use function mb_strpos;
37use function mb_strstr;
38use function mb_substr;
39use function md5;
40use function method_exists;
41use function min;
42use function password_hash;
43use function preg_match;
44use function preg_replace;
45use function sprintf;
46use function str_replace;
47use function stripcslashes;
48use function stripslashes;
49use function strlen;
50use function strpos;
51use function substr;
52use function time;
53use function trim;
54
55/**
56 * PhpMyAdmin\InsertEdit class
57 */
58class InsertEdit
59{
60    /**
61     * DatabaseInterface instance
62     *
63     * @var DatabaseInterface
64     */
65    private $dbi;
66
67    /** @var Relation */
68    private $relation;
69
70    /** @var Transformations */
71    private $transformations;
72
73    /** @var FileListing */
74    private $fileListing;
75
76    /** @var Template */
77    public $template;
78
79    /**
80     * @param DatabaseInterface $dbi DatabaseInterface instance
81     */
82    public function __construct(DatabaseInterface $dbi)
83    {
84        $this->dbi = $dbi;
85        $this->relation = new Relation($this->dbi);
86        $this->transformations = new Transformations();
87        $this->fileListing = new FileListing();
88        $this->template = new Template();
89    }
90
91    /**
92     * Retrieve form parameters for insert/edit form
93     *
94     * @param string     $db                 name of the database
95     * @param string     $table              name of the table
96     * @param array|null $where_clauses      where clauses
97     * @param array      $where_clause_array array of where clauses
98     * @param string     $err_url            error url
99     *
100     * @return array array of insert/edit form parameters
101     */
102    public function getFormParametersForInsertForm(
103        $db,
104        $table,
105        ?array $where_clauses,
106        array $where_clause_array,
107        $err_url
108    ) {
109        $_form_params = [
110            'db'        => $db,
111            'table'     => $table,
112            'goto'      => $GLOBALS['goto'],
113            'err_url'   => $err_url,
114            'sql_query' => $_POST['sql_query'],
115        ];
116        if (isset($where_clauses)) {
117            foreach ($where_clause_array as $key_id => $where_clause) {
118                $_form_params['where_clause[' . $key_id . ']'] = trim($where_clause);
119            }
120        }
121        if (isset($_POST['clause_is_unique'])) {
122            $_form_params['clause_is_unique'] = $_POST['clause_is_unique'];
123        }
124
125        return $_form_params;
126    }
127
128    /**
129     * Creates array of where clauses
130     *
131     * @param array|string|null $where_clause where clause
132     *
133     * @return array whereClauseArray array of where clauses
134     */
135    private function getWhereClauseArray($where_clause)
136    {
137        if (! isset($where_clause)) {
138            return [];
139        }
140
141        if (is_array($where_clause)) {
142            return $where_clause;
143        }
144
145        return [0 => $where_clause];
146    }
147
148    /**
149     * Analysing where clauses array
150     *
151     * @param array  $where_clause_array array of where clauses
152     * @param string $table              name of the table
153     * @param string $db                 name of the database
154     *
155     * @return array $where_clauses, $result, $rows, $found_unique_key
156     */
157    private function analyzeWhereClauses(
158        array $where_clause_array,
159        $table,
160        $db
161    ) {
162        $rows               = [];
163        $result             = [];
164        $where_clauses      = [];
165        $found_unique_key   = false;
166        foreach ($where_clause_array as $key_id => $where_clause) {
167            $local_query     = 'SELECT * FROM '
168                . Util::backquote($db) . '.'
169                . Util::backquote($table)
170                . ' WHERE ' . $where_clause . ';';
171            $result[$key_id] = $this->dbi->query(
172                $local_query,
173                DatabaseInterface::CONNECT_USER,
174                DatabaseInterface::QUERY_STORE
175            );
176            $rows[$key_id] = $this->dbi->fetchAssoc($result[$key_id]);
177
178            $where_clauses[$key_id] = str_replace('\\', '\\\\', $where_clause);
179            $has_unique_condition = $this->showEmptyResultMessageOrSetUniqueCondition(
180                $rows,
181                $key_id,
182                $where_clause_array,
183                $local_query,
184                $result
185            );
186            if (! $has_unique_condition) {
187                continue;
188            }
189
190            $found_unique_key = true;
191        }
192
193        return [
194            $where_clauses,
195            $result,
196            $rows,
197            $found_unique_key,
198        ];
199    }
200
201    /**
202     * Show message for empty result or set the unique_condition
203     *
204     * @param array  $rows               MySQL returned rows
205     * @param string $key_id             ID in current key
206     * @param array  $where_clause_array array of where clauses
207     * @param string $local_query        query performed
208     * @param array  $result             MySQL result handle
209     *
210     * @return bool
211     */
212    private function showEmptyResultMessageOrSetUniqueCondition(
213        array $rows,
214        $key_id,
215        array $where_clause_array,
216        $local_query,
217        array $result
218    ) {
219        $has_unique_condition = false;
220
221        // No row returned
222        if (! $rows[$key_id]) {
223            unset($rows[$key_id], $where_clause_array[$key_id]);
224            Response::getInstance()->addHTML(
225                Generator::getMessage(
226                    __('MySQL returned an empty result set (i.e. zero rows).'),
227                    $local_query
228                )
229            );
230            /**
231             * @todo not sure what should be done at this point, but we must not
232             * exit if we want the message to be displayed
233             */
234        } else {// end if (no row returned)
235            $meta = $this->dbi->getFieldsMeta($result[$key_id]);
236
237            [$unique_condition, $tmp_clause_is_unique] = Util::getUniqueCondition(
238                $result[$key_id],
239                count($meta),
240                $meta,
241                $rows[$key_id],
242                true
243            );
244
245            if (! empty($unique_condition)) {
246                $has_unique_condition = true;
247            }
248            unset($unique_condition, $tmp_clause_is_unique);
249        }
250
251        return $has_unique_condition;
252    }
253
254    /**
255     * No primary key given, just load first row
256     *
257     * @param string $table name of the table
258     * @param string $db    name of the database
259     *
260     * @return array containing $result and $rows arrays
261     */
262    private function loadFirstRow($table, $db)
263    {
264        $result = $this->dbi->query(
265            'SELECT * FROM ' . Util::backquote($db)
266            . '.' . Util::backquote($table) . ' LIMIT 1;',
267            DatabaseInterface::CONNECT_USER,
268            DatabaseInterface::QUERY_STORE
269        );
270        // Can be a string on some old configuration storage settings
271        $rows = array_fill(0, (int) $GLOBALS['cfg']['InsertRows'], false);
272
273        return [
274            $result,
275            $rows,
276        ];
277    }
278
279    /**
280     * Add some url parameters
281     *
282     * @param array $url_params         containing $db and $table as url parameters
283     * @param array $where_clause_array where clauses array
284     *
285     * @return array Add some url parameters to $url_params array and return it
286     */
287    public function urlParamsInEditMode(
288        array $url_params,
289        array $where_clause_array
290    ): array {
291        foreach ($where_clause_array as $where_clause) {
292            $url_params['where_clause'] = trim($where_clause);
293        }
294        if (! empty($_POST['sql_query'])) {
295            $url_params['sql_query'] = $_POST['sql_query'];
296        }
297
298        return $url_params;
299    }
300
301    /**
302     * Show type information or function selectors in Insert/Edit
303     *
304     * @param string $which      function|type
305     * @param array  $url_params containing url parameters
306     * @param bool   $is_show    whether to show the element in $which
307     *
308     * @return string an HTML snippet
309     */
310    public function showTypeOrFunction($which, array $url_params, $is_show)
311    {
312        $params = [];
313
314        switch ($which) {
315            case 'function':
316                $params['ShowFunctionFields'] = ($is_show ? 0 : 1);
317                $params['ShowFieldTypesInDataEditView']
318                = $GLOBALS['cfg']['ShowFieldTypesInDataEditView'];
319                break;
320            case 'type':
321                $params['ShowFieldTypesInDataEditView'] = ($is_show ? 0 : 1);
322                $params['ShowFunctionFields']
323                = $GLOBALS['cfg']['ShowFunctionFields'];
324                break;
325        }
326
327        $params['goto'] = Url::getFromRoute('/sql');
328        $this_url_params = array_merge($url_params, $params);
329
330        if (! $is_show) {
331            return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
332                . Url::getCommon($this_url_params, '', false) . '">'
333                . $this->showTypeOrFunctionLabel($which)
334                . '</a>';
335        }
336
337        return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
338            . Url::getCommon($this_url_params, '', false)
339            . '" title="' . __('Hide') . '">'
340            . $this->showTypeOrFunctionLabel($which)
341            . '</a></th>';
342    }
343
344    /**
345     * Show type information or function selectors labels in Insert/Edit
346     *
347     * @param string $which function|type
348     *
349     * @return string|null an HTML snippet
350     */
351    private function showTypeOrFunctionLabel($which)
352    {
353        switch ($which) {
354            case 'function':
355                return __('Function');
356            case 'type':
357                return __('Type');
358        }
359
360        return null;
361    }
362
363     /**
364      * Analyze the table column array
365      *
366      * @param array $column         description of column in given table
367      * @param array $comments_map   comments for every column that has a comment
368      * @param bool  $timestamp_seen whether a timestamp has been seen
369      *
370      * @return array                   description of column in given table
371      */
372    private function analyzeTableColumnsArray(
373        array $column,
374        array $comments_map,
375        $timestamp_seen
376    ) {
377        $column['Field_html']    = htmlspecialchars($column['Field']);
378        $column['Field_md5']     = md5($column['Field']);
379        // True_Type contains only the type (stops at first bracket)
380        $column['True_Type']     = preg_replace('@\(.*@s', '', $column['Type']);
381        $column['len'] = preg_match('@float|double@', $column['Type']) ? 100 : -1;
382        $column['Field_title']   = $this->getColumnTitle($column, $comments_map);
383        $column['is_binary']     = $this->isColumn(
384            $column,
385            [
386                'binary',
387                'varbinary',
388            ]
389        );
390        $column['is_blob']       = $this->isColumn(
391            $column,
392            [
393                'blob',
394                'tinyblob',
395                'mediumblob',
396                'longblob',
397            ]
398        );
399        $column['is_char']       = $this->isColumn(
400            $column,
401            [
402                'char',
403                'varchar',
404            ]
405        );
406
407        [$column['pma_type'], $column['wrap'], $column['first_timestamp']]
408            = $this->getEnumSetAndTimestampColumns($column, $timestamp_seen);
409
410        return $column;
411    }
412
413     /**
414      * Retrieve the column title
415      *
416      * @param array $column       description of column in given table
417      * @param array $comments_map comments for every column that has a comment
418      *
419      * @return string              column title
420      */
421    private function getColumnTitle(array $column, array $comments_map)
422    {
423        if (isset($comments_map[$column['Field']])) {
424            return '<span style="border-bottom: 1px dashed black;" title="'
425                . htmlspecialchars($comments_map[$column['Field']]) . '">'
426                . $column['Field_html'] . '</span>';
427        }
428
429        return $column['Field_html'];
430    }
431
432     /**
433      * check whether the column is of a certain type
434      * the goal is to ensure that types such as "enum('one','two','binary',..)"
435      * or "enum('one','two','varbinary',..)" are not categorized as binary
436      *
437      * @param array $column description of column in given table
438      * @param array $types  the types to verify
439      *
440      * @return bool whether the column's type if one of the $types
441      */
442    public function isColumn(array $column, array $types)
443    {
444        foreach ($types as $one_type) {
445            if (mb_stripos($column['Type'], $one_type) === 0) {
446                return true;
447            }
448        }
449
450        return false;
451    }
452
453    /**
454     * Retrieve set, enum, timestamp table columns
455     *
456     * @param array $column         description of column in given table
457     * @param bool  $timestamp_seen whether a timestamp has been seen
458     *
459     * @return array $column['pma_type'], $column['wrap'], $column['first_timestamp']
460     */
461    private function getEnumSetAndTimestampColumns(array $column, $timestamp_seen)
462    {
463        $column['first_timestamp'] = false;
464        switch ($column['True_Type']) {
465            case 'set':
466                $column['pma_type'] = 'set';
467                $column['wrap']  = '';
468                break;
469            case 'enum':
470                $column['pma_type'] = 'enum';
471                $column['wrap']  = '';
472                break;
473            case 'timestamp':
474                if (! $timestamp_seen) {   // can only occur once per table
475                    $column['first_timestamp'] = true;
476                }
477                $column['pma_type'] = $column['Type'];
478                $column['wrap']  = ' nowrap';
479                break;
480
481            default:
482                $column['pma_type'] = $column['Type'];
483                $column['wrap']  = ' nowrap';
484                break;
485        }
486
487        return [
488            $column['pma_type'],
489            $column['wrap'],
490            $column['first_timestamp'],
491        ];
492    }
493
494    /**
495     * The function column
496     * We don't want binary data to be destroyed
497     * Note: from the MySQL manual: "BINARY doesn't affect how the column is
498     *       stored or retrieved" so it does not mean that the contents is binary
499     *
500     * @param array  $column                description of column in given table
501     * @param bool   $is_upload             upload or no
502     * @param string $column_name_appendix  the name attribute
503     * @param string $onChangeClause        onchange clause for fields
504     * @param array  $no_support_types      list of datatypes that are not (yet)
505     *                                      handled by PMA
506     * @param int    $tabindex_for_function +3000
507     * @param int    $tabindex              tab index
508     * @param int    $idindex               id index
509     * @param bool   $insert_mode           insert mode or edit mode
510     * @param bool   $readOnly              is column read only or not
511     * @param array  $foreignData           foreign key data
512     *
513     * @return string                           an html snippet
514     */
515    private function getFunctionColumn(
516        array $column,
517        $is_upload,
518        $column_name_appendix,
519        $onChangeClause,
520        array $no_support_types,
521        $tabindex_for_function,
522        $tabindex,
523        $idindex,
524        $insert_mode,
525        $readOnly,
526        array $foreignData
527    ): string {
528        $html_output = '';
529        if (($GLOBALS['cfg']['ProtectBinary'] === 'blob'
530            && $column['is_blob'] && ! $is_upload)
531            || ($GLOBALS['cfg']['ProtectBinary'] === 'all'
532            && $column['is_binary'])
533            || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob'
534            && $column['is_binary'])
535        ) {
536            $html_output .= '<td class="text-center">' . __('Binary') . '</td>' . "\n";
537        } elseif ($readOnly
538            || mb_strstr($column['True_Type'], 'enum')
539            || mb_strstr($column['True_Type'], 'set')
540            || in_array($column['pma_type'], $no_support_types)
541        ) {
542            $html_output .= '<td class="text-center">--</td>' . "\n";
543        } else {
544            $html_output .= '<td>' . "\n";
545
546            $html_output .= '<select name="funcs' . $column_name_appendix . '"'
547                . ' ' . $onChangeClause
548                . ' tabindex="' . ($tabindex + $tabindex_for_function) . '"'
549                . ' id="field_' . $idindex . '_1">';
550            $html_output .= Generator::getFunctionsForField(
551                $column,
552                $insert_mode,
553                $foreignData
554            ) . "\n";
555
556            $html_output .= '</select>' . "\n";
557            $html_output .= '</td>' . "\n";
558        }
559
560        return $html_output;
561    }
562
563    /**
564     * The null column
565     *
566     * @param array  $column               description of column in given table
567     * @param string $column_name_appendix the name attribute
568     * @param bool   $real_null_value      is column value null or not null
569     * @param int    $tabindex             tab index
570     * @param int    $tabindex_for_null    +6000
571     * @param int    $idindex              id index
572     * @param string $vkey                 [multi_edit]['row_id']
573     * @param array  $foreigners           keys into foreign fields
574     * @param array  $foreignData          data about the foreign keys
575     * @param bool   $readOnly             is column read only or not
576     *
577     * @return string                       an html snippet
578     */
579    private function getNullColumn(
580        array $column,
581        $column_name_appendix,
582        $real_null_value,
583        $tabindex,
584        $tabindex_for_null,
585        $idindex,
586        $vkey,
587        array $foreigners,
588        array $foreignData,
589        $readOnly
590    ) {
591        if ($column['Null'] !== 'YES' || $readOnly) {
592            return "<td></td>\n";
593        }
594        $html_output = '';
595        $html_output .= '<td>' . "\n";
596        $html_output .= '<input type="hidden" name="fields_null_prev'
597            . $column_name_appendix . '"';
598        if ($real_null_value && ! $column['first_timestamp']) {
599            $html_output .= ' value="on"';
600        }
601        $html_output .= '>' . "\n";
602
603        $html_output .= '<input type="checkbox" class="checkbox_null" tabindex="'
604            . ($tabindex + $tabindex_for_null) . '"'
605            . ' name="fields_null' . $column_name_appendix . '"';
606        if ($real_null_value) {
607            $html_output .= ' checked="checked"';
608        }
609        $html_output .= ' id="field_' . $idindex . '_2">';
610
611        // nullify_code is needed by the js nullify() function
612        $nullify_code = $this->getNullifyCodeForNullColumn(
613            $column,
614            $foreigners,
615            $foreignData
616        );
617        // to be able to generate calls to nullify() in jQuery
618        $html_output .= '<input type="hidden" class="nullify_code" name="nullify_code'
619            . $column_name_appendix . '" value="' . $nullify_code . '">';
620        $html_output .= '<input type="hidden" class="hashed_field" name="hashed_field'
621            . $column_name_appendix . '" value="' . $column['Field_md5'] . '">';
622        $html_output .= '<input type="hidden" class="multi_edit" name="multi_edit'
623            . $column_name_appendix . '" value="' . Sanitize::escapeJsString($vkey) . '">';
624        $html_output .= '</td>' . "\n";
625
626        return $html_output;
627    }
628
629    /**
630     * Retrieve the nullify code for the null column
631     *
632     * @param array $column      description of column in given table
633     * @param array $foreigners  keys into foreign fields
634     * @param array $foreignData data about the foreign keys
635     */
636    private function getNullifyCodeForNullColumn(
637        array $column,
638        array $foreigners,
639        array $foreignData
640    ): string {
641        $foreigner = $this->relation->searchColumnInForeigners($foreigners, $column['Field']);
642        if (mb_strstr($column['True_Type'], 'enum')) {
643            if (mb_strlen((string) $column['Type']) > 20) {
644                $nullify_code = '1';
645            } else {
646                $nullify_code = '2';
647            }
648        } elseif (mb_strstr($column['True_Type'], 'set')) {
649            $nullify_code = '3';
650        } elseif (! empty($foreigners)
651            && ! empty($foreigner)
652            && $foreignData['foreign_link'] == false
653        ) {
654            // foreign key in a drop-down
655            $nullify_code = '4';
656        } elseif (! empty($foreigners)
657            && ! empty($foreigner)
658            && $foreignData['foreign_link'] == true
659        ) {
660            // foreign key with a browsing icon
661            $nullify_code = '6';
662        } else {
663            $nullify_code = '5';
664        }
665
666        return $nullify_code;
667    }
668
669    /**
670     * Get the HTML elements for value column in insert form
671     * (here, "column" is used in the sense of HTML column in HTML table)
672     *
673     * @param array  $column                description of column in given table
674     * @param string $backup_field          hidden input field
675     * @param string $column_name_appendix  the name attribute
676     * @param string $onChangeClause        onchange clause for fields
677     * @param int    $tabindex              tab index
678     * @param int    $tabindex_for_value    offset for the values tabindex
679     * @param int    $idindex               id index
680     * @param string $data                  description of the column field
681     * @param string $special_chars         special characters
682     * @param array  $foreignData           data about the foreign keys
683     * @param array  $paramTableDbArray     array containing $table and $db
684     * @param int    $rownumber             the row number
685     * @param string $text_dir              text direction
686     * @param string $special_chars_encoded replaced char if the string starts
687     *                                      with a \r\n pair (0x0d0a) add an extra \n
688     * @param string $vkey                  [multi_edit]['row_id']
689     * @param bool   $is_upload             is upload or not
690     * @param int    $biggest_max_file_size 0 integer
691     * @param string $default_char_editing  default char editing mode which is stored
692     *                                      in the config.inc.php script
693     * @param array  $no_support_types      list of datatypes that are not (yet)
694     *                                      handled by PMA
695     * @param array  $gis_data_types        list of GIS data types
696     * @param array  $extracted_columnspec  associative array containing type,
697     *                                      spec_in_brackets and possibly
698     *                                      enum_set_values (another array)
699     * @param bool   $readOnly              is column read only or not
700     *
701     * @return string an html snippet
702     */
703    private function getValueColumn(
704        array $column,
705        $backup_field,
706        $column_name_appendix,
707        $onChangeClause,
708        $tabindex,
709        $tabindex_for_value,
710        $idindex,
711        $data,
712        $special_chars,
713        array $foreignData,
714        array $paramTableDbArray,
715        $rownumber,
716        $text_dir,
717        $special_chars_encoded,
718        $vkey,
719        $is_upload,
720        $biggest_max_file_size,
721        $default_char_editing,
722        array $no_support_types,
723        array $gis_data_types,
724        array $extracted_columnspec,
725        $readOnly
726    ) {
727        // HTML5 data-* attribute data-type
728        $data_type = $this->dbi->types->getTypeClass($column['True_Type']);
729        $html_output = '';
730
731        if ($foreignData['foreign_link'] == true) {
732            $html_output .= $this->getForeignLink(
733                $column,
734                $backup_field,
735                $column_name_appendix,
736                $onChangeClause,
737                $tabindex,
738                $tabindex_for_value,
739                $idindex,
740                $data,
741                $paramTableDbArray,
742                $rownumber,
743                $readOnly
744            );
745        } elseif (is_array($foreignData['disp_row'])) {
746            $html_output .= $this->dispRowForeignData(
747                $column,
748                $backup_field,
749                $column_name_appendix,
750                $onChangeClause,
751                $tabindex,
752                $tabindex_for_value,
753                $idindex,
754                $data,
755                $foreignData,
756                $readOnly
757            );
758        } elseif ((
759                $GLOBALS['cfg']['LongtextDoubleTextarea']
760                && mb_strstr($column['pma_type'], 'longtext'))
761            || mb_strstr($column['pma_type'], 'json')
762        ) {
763            $html_output .= $this->getTextarea(
764                $column,
765                $backup_field,
766                $column_name_appendix,
767                $onChangeClause,
768                $tabindex,
769                $tabindex_for_value,
770                $idindex,
771                $text_dir,
772                $special_chars_encoded,
773                $data_type,
774                $readOnly
775            );
776        } elseif (mb_strstr($column['pma_type'], 'text')) {
777            $html_output .= $this->getTextarea(
778                $column,
779                $backup_field,
780                $column_name_appendix,
781                $onChangeClause,
782                $tabindex,
783                $tabindex_for_value,
784                $idindex,
785                $text_dir,
786                $special_chars_encoded,
787                $data_type,
788                $readOnly
789            );
790            $html_output .= "\n";
791            if (mb_strlen($special_chars) > 32000) {
792                $html_output .= "</td>\n";
793                $html_output .= '<td>' . __(
794                    'Because of its length,<br> this column might not be editable.'
795                );
796            }
797        } elseif ($column['pma_type'] === 'enum') {
798            $html_output .= $this->getPmaTypeEnum(
799                $column,
800                $backup_field,
801                $column_name_appendix,
802                $extracted_columnspec,
803                $onChangeClause,
804                $tabindex,
805                $tabindex_for_value,
806                $idindex,
807                $data,
808                $readOnly
809            );
810        } elseif ($column['pma_type'] === 'set') {
811            $html_output .= $this->getPmaTypeSet(
812                $column,
813                $extracted_columnspec,
814                $backup_field,
815                $column_name_appendix,
816                $onChangeClause,
817                $tabindex,
818                $tabindex_for_value,
819                $idindex,
820                $data,
821                $readOnly
822            );
823        } elseif ($column['is_binary'] || $column['is_blob']) {
824            $html_output .= $this->getBinaryAndBlobColumn(
825                $column,
826                $data,
827                $special_chars,
828                $biggest_max_file_size,
829                $backup_field,
830                $column_name_appendix,
831                $onChangeClause,
832                $tabindex,
833                $tabindex_for_value,
834                $idindex,
835                $text_dir,
836                $special_chars_encoded,
837                $vkey,
838                $is_upload,
839                $readOnly
840            );
841        } elseif (! in_array($column['pma_type'], $no_support_types)) {
842            $html_output .= $this->getValueColumnForOtherDatatypes(
843                $column,
844                $default_char_editing,
845                $backup_field,
846                $column_name_appendix,
847                $onChangeClause,
848                $tabindex,
849                $special_chars,
850                $tabindex_for_value,
851                $idindex,
852                $text_dir,
853                $special_chars_encoded,
854                $data,
855                $extracted_columnspec,
856                $readOnly
857            );
858        }
859
860        if (in_array($column['pma_type'], $gis_data_types)) {
861            $html_output .= $this->getHtmlForGisDataTypes((int) $rownumber);
862        }
863
864        return $html_output;
865    }
866
867    /**
868     * Get HTML for foreign link in insert form
869     *
870     * @param array  $column               description of column in given table
871     * @param string $backup_field         hidden input field
872     * @param string $column_name_appendix the name attribute
873     * @param string $onChangeClause       onchange clause for fields
874     * @param int    $tabindex             tab index
875     * @param int    $tabindex_for_value   offset for the values tabindex
876     * @param int    $idindex              id index
877     * @param string $data                 data to edit
878     * @param array  $paramTableDbArray    array containing $table and $db
879     * @param int    $rownumber            the row number
880     * @param bool   $readOnly             is column read only or not
881     *
882     * @return string                       an html snippet
883     */
884    private function getForeignLink(
885        array $column,
886        $backup_field,
887        $column_name_appendix,
888        $onChangeClause,
889        $tabindex,
890        $tabindex_for_value,
891        $idindex,
892        $data,
893        array $paramTableDbArray,
894        $rownumber,
895        $readOnly
896    ) {
897        [$table, $db] = $paramTableDbArray;
898        $html_output = '';
899        $html_output .= $backup_field . "\n";
900
901        $html_output .= '<input type="hidden" name="fields_type'
902            . $column_name_appendix . '" value="foreign">';
903
904        $html_output .= '<input type="text" name="fields' . $column_name_appendix . '" '
905            . 'class="textfield" '
906            . $onChangeClause . ' '
907            . ($readOnly ? 'readonly="readonly" ' : '')
908            . 'tabindex="' . ($tabindex + $tabindex_for_value) . '" '
909            . 'id="field_' . $idindex . '_3" '
910            . 'value="' . htmlspecialchars($data) . '">';
911
912        $html_output .= '<a class="ajax browse_foreign" href="'
913            . Url::getFromRoute('/browse-foreigners')
914            . '" data-post="'
915            . Url::getCommon(
916                [
917                    'db' => $db,
918                    'table' => $table,
919                    'field' => $column['Field'],
920                    'rownumber' => $rownumber,
921                    'data'      => $data,
922                ],
923                '',
924                false
925            ) . '">'
926            . Generator::getIcon('b_browse', __('Browse foreign values')) . '</a>';
927
928        return $html_output;
929    }
930
931    /**
932     * Get HTML to display foreign data
933     *
934     * @param array  $column               description of column in given table
935     * @param string $backup_field         hidden input field
936     * @param string $column_name_appendix the name attribute
937     * @param string $onChangeClause       onchange clause for fields
938     * @param int    $tabindex             tab index
939     * @param int    $tabindex_for_value   offset for the values tabindex
940     * @param int    $idindex              id index
941     * @param string $data                 data to edit
942     * @param array  $foreignData          data about the foreign keys
943     * @param bool   $readOnly             is display read only or not
944     *
945     * @return string                       an html snippet
946     */
947    private function dispRowForeignData(
948        $column,
949        $backup_field,
950        $column_name_appendix,
951        $onChangeClause,
952        $tabindex,
953        $tabindex_for_value,
954        $idindex,
955        $data,
956        array $foreignData,
957        $readOnly
958    ) {
959        $html_output = '';
960        $html_output .= $backup_field . "\n";
961        $html_output .= '<input type="hidden"'
962            . ' name="fields_type' . $column_name_appendix . '"';
963        if ($column['is_binary']) {
964            $html_output .= ' value="hex">';
965        } else {
966            $html_output .= ' value="foreign">';
967        }
968
969        $html_output .= '<select name="fields' . $column_name_appendix . '"'
970            . ' ' . $onChangeClause
971            . ' class="textfield"'
972            . ($readOnly ? ' disabled' : '')
973            . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"'
974            . ' id="field_' . $idindex . '_3">';
975        $html_output .= $this->relation->foreignDropdown(
976            $foreignData['disp_row'],
977            $foreignData['foreign_field'],
978            $foreignData['foreign_display'],
979            $data,
980            $GLOBALS['cfg']['ForeignKeyMaxLimit']
981        );
982        $html_output .= '</select>';
983
984        //Add hidden input, as disabled <select> input does not included in POST.
985        if ($readOnly) {
986            $html_output .= '<input name="fields' . $column_name_appendix . '"'
987                . ' type="hidden" value="' . htmlspecialchars($data) . '">';
988        }
989
990        return $html_output;
991    }
992
993    /**
994     * Get HTML textarea for insert form
995     *
996     * @param array  $column                column information
997     * @param string $backup_field          hidden input field
998     * @param string $column_name_appendix  the name attribute
999     * @param string $onChangeClause        onchange clause for fields
1000     * @param int    $tabindex              tab index
1001     * @param int    $tabindex_for_value    offset for the values tabindex
1002     * @param int    $idindex               id index
1003     * @param string $text_dir              text direction
1004     * @param string $special_chars_encoded replaced char if the string starts
1005     *                                      with a \r\n pair (0x0d0a) add an extra \n
1006     * @param string $data_type             the html5 data-* attribute type
1007     * @param bool   $readOnly              is column read only or not
1008     *
1009     * @return string                       an html snippet
1010     */
1011    private function getTextarea(
1012        array $column,
1013        $backup_field,
1014        $column_name_appendix,
1015        $onChangeClause,
1016        $tabindex,
1017        $tabindex_for_value,
1018        $idindex,
1019        $text_dir,
1020        $special_chars_encoded,
1021        $data_type,
1022        $readOnly
1023    ) {
1024        $the_class = '';
1025        $textAreaRows = $GLOBALS['cfg']['TextareaRows'];
1026        $textareaCols = $GLOBALS['cfg']['TextareaCols'];
1027
1028        if ($column['is_char']) {
1029            /**
1030             * @todo clarify the meaning of the "textfield" class and explain
1031             *       why character columns have the "char" class instead
1032             */
1033            $the_class = 'char charField';
1034            $textAreaRows = $GLOBALS['cfg']['CharTextareaRows'];
1035            $textareaCols = $GLOBALS['cfg']['CharTextareaCols'];
1036            $extracted_columnspec = Util::extractColumnSpec(
1037                $column['Type']
1038            );
1039            $maxlength = $extracted_columnspec['spec_in_brackets'];
1040        } elseif ($GLOBALS['cfg']['LongtextDoubleTextarea']
1041            && mb_strstr($column['pma_type'], 'longtext')
1042        ) {
1043            $textAreaRows = $GLOBALS['cfg']['TextareaRows'] * 2;
1044            $textareaCols = $GLOBALS['cfg']['TextareaCols'] * 2;
1045        }
1046
1047        return $backup_field . "\n"
1048            . '<textarea name="fields' . $column_name_appendix . '"'
1049            . ' class="' . $the_class . '"'
1050            . ($readOnly ? ' readonly="readonly"' : '')
1051            . (isset($maxlength) ? ' data-maxlength="' . $maxlength . '"' : '')
1052            . ' rows="' . $textAreaRows . '"'
1053            . ' cols="' . $textareaCols . '"'
1054            . ' dir="' . $text_dir . '"'
1055            . ' id="field_' . $idindex . '_3"'
1056            . (! empty($onChangeClause) ? ' ' . $onChangeClause : '')
1057            . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"'
1058            . ' data-type="' . $data_type . '">'
1059            . $special_chars_encoded
1060            . '</textarea>';
1061    }
1062
1063    /**
1064     * Get HTML for enum type
1065     *
1066     * @param array  $column               description of column in given table
1067     * @param string $backup_field         hidden input field
1068     * @param string $column_name_appendix the name attribute
1069     * @param array  $extracted_columnspec associative array containing type,
1070     *                                     spec_in_brackets and possibly
1071     *                                     enum_set_values (another array)
1072     * @param string $onChangeClause       onchange clause for fields
1073     * @param int    $tabindex             tab index
1074     * @param int    $tabindex_for_value   offset for the values tabindex
1075     * @param int    $idindex              id index
1076     * @param mixed  $data                 data to edit
1077     * @param bool   $readOnly             is column read only or not
1078     *
1079     * @return string an html snippet
1080     */
1081    private function getPmaTypeEnum(
1082        array $column,
1083        $backup_field,
1084        $column_name_appendix,
1085        array $extracted_columnspec,
1086        $onChangeClause,
1087        $tabindex,
1088        $tabindex_for_value,
1089        $idindex,
1090        $data,
1091        $readOnly
1092    ) {
1093        $html_output = '';
1094        if (! isset($column['values'])) {
1095            $column['values'] = $this->getColumnEnumValues(
1096                $column,
1097                $extracted_columnspec
1098            );
1099        }
1100        $column_enum_values = $column['values'];
1101        $html_output .= '<input type="hidden" name="fields_type'
1102            . $column_name_appendix . '" value="enum">';
1103        $html_output .= "\n" . '            ' . $backup_field . "\n";
1104        if (mb_strlen($column['Type']) > 20) {
1105            $html_output .= $this->getDropDownDependingOnLength(
1106                $column,
1107                $column_name_appendix,
1108                $onChangeClause,
1109                $tabindex,
1110                $tabindex_for_value,
1111                $idindex,
1112                $data,
1113                $column_enum_values,
1114                $readOnly
1115            );
1116        } else {
1117            $html_output .= $this->getRadioButtonDependingOnLength(
1118                $column_name_appendix,
1119                $onChangeClause,
1120                $tabindex,
1121                $column,
1122                $tabindex_for_value,
1123                $idindex,
1124                $data,
1125                $column_enum_values,
1126                $readOnly
1127            );
1128        }
1129
1130        return $html_output;
1131    }
1132
1133    /**
1134     * Get column values
1135     *
1136     * @param array $column               description of column in given table
1137     * @param array $extracted_columnspec associative array containing type,
1138     *                                    spec_in_brackets and possibly enum_set_values
1139     *                                    (another array)
1140     *
1141     * @return array column values as an associative array
1142     */
1143    private function getColumnEnumValues(array $column, array $extracted_columnspec)
1144    {
1145        $column['values'] = [];
1146        foreach ($extracted_columnspec['enum_set_values'] as $val) {
1147            $column['values'][] = [
1148                'plain' => $val,
1149                'html'  => htmlspecialchars($val),
1150            ];
1151        }
1152
1153        return $column['values'];
1154    }
1155
1156    /**
1157     * Get HTML drop down for more than 20 string length
1158     *
1159     * @param array  $column               description of column in given table
1160     * @param string $column_name_appendix the name attribute
1161     * @param string $onChangeClause       onchange clause for fields
1162     * @param int    $tabindex             tab index
1163     * @param int    $tabindex_for_value   offset for the values tabindex
1164     * @param int    $idindex              id index
1165     * @param string $data                 data to edit
1166     * @param array  $column_enum_values   $column['values']
1167     * @param bool   $readOnly             is column read only or not
1168     *
1169     * @return string                       an html snippet
1170     */
1171    private function getDropDownDependingOnLength(
1172        array $column,
1173        $column_name_appendix,
1174        $onChangeClause,
1175        $tabindex,
1176        $tabindex_for_value,
1177        $idindex,
1178        $data,
1179        array $column_enum_values,
1180        $readOnly
1181    ) {
1182        $html_output = '<select name="fields' . $column_name_appendix . '"'
1183            . ' ' . $onChangeClause
1184            . ' class="textfield"'
1185            . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"'
1186            . ($readOnly ? ' disabled' : '')
1187            . ' id="field_' . $idindex . '_3">';
1188        $html_output .= '<option value="">&nbsp;</option>' . "\n";
1189
1190        $selected_html = '';
1191        foreach ($column_enum_values as $enum_value) {
1192            $html_output .= '<option value="' . $enum_value['html'] . '"';
1193            if ($data == $enum_value['plain']
1194                || ($data == ''
1195                && (! isset($_POST['where_clause']) || $column['Null'] !== 'YES')
1196                && isset($column['Default'])
1197                && $enum_value['plain'] == $column['Default'])
1198            ) {
1199                $html_output .= ' selected="selected"';
1200                $selected_html = $enum_value['html'];
1201            }
1202            $html_output .= '>' . $enum_value['html'] . '</option>' . "\n";
1203        }
1204        $html_output .= '</select>';
1205
1206        //Add hidden input, as disabled <select> input does not included in POST.
1207        if ($readOnly) {
1208            $html_output .= '<input name="fields' . $column_name_appendix . '"'
1209                . ' type="hidden" value="' . $selected_html . '">';
1210        }
1211
1212        return $html_output;
1213    }
1214
1215    /**
1216     * Get HTML radio button for less than 20 string length
1217     *
1218     * @param string $column_name_appendix the name attribute
1219     * @param string $onChangeClause       onchange clause for fields
1220     * @param int    $tabindex             tab index
1221     * @param array  $column               description of column in given table
1222     * @param int    $tabindex_for_value   offset for the values tabindex
1223     * @param int    $idindex              id index
1224     * @param string $data                 data to edit
1225     * @param array  $column_enum_values   $column['values']
1226     * @param bool   $readOnly             is column read only or not
1227     *
1228     * @return string                       an html snippet
1229     */
1230    private function getRadioButtonDependingOnLength(
1231        $column_name_appendix,
1232        $onChangeClause,
1233        $tabindex,
1234        array $column,
1235        $tabindex_for_value,
1236        $idindex,
1237        $data,
1238        array $column_enum_values,
1239        $readOnly
1240    ) {
1241        $j = 0;
1242        $html_output = '';
1243        foreach ($column_enum_values as $enum_value) {
1244            $html_output .= '            '
1245                . '<input type="radio" name="fields' . $column_name_appendix . '"'
1246                . ' class="textfield"'
1247                . ' value="' . $enum_value['html'] . '"'
1248                . ' id="field_' . $idindex . '_3_' . $j . '"'
1249                . ' ' . $onChangeClause;
1250            if ($data == $enum_value['plain']
1251                || ($data == ''
1252                && (! isset($_POST['where_clause']) || $column['Null'] !== 'YES')
1253                && isset($column['Default'])
1254                && $enum_value['plain'] == $column['Default'])
1255            ) {
1256                $html_output .= ' checked="checked"';
1257            } elseif ($readOnly) {
1258                $html_output .= ' disabled';
1259            }
1260            $html_output .= ' tabindex="' . ($tabindex + $tabindex_for_value) . '">';
1261            $html_output .= '<label for="field_' . $idindex . '_3_' . $j . '">'
1262                . $enum_value['html'] . '</label>' . "\n";
1263            $j++;
1264        }
1265
1266        return $html_output;
1267    }
1268
1269    /**
1270     * Get the HTML for 'set' pma type
1271     *
1272     * @param array  $column               description of column in given table
1273     * @param array  $extracted_columnspec associative array containing type,
1274     *                                     spec_in_brackets and possibly
1275     *                                     enum_set_values (another array)
1276     * @param string $backup_field         hidden input field
1277     * @param string $column_name_appendix the name attribute
1278     * @param string $onChangeClause       onchange clause for fields
1279     * @param int    $tabindex             tab index
1280     * @param int    $tabindex_for_value   offset for the values tabindex
1281     * @param int    $idindex              id index
1282     * @param string $data                 description of the column field
1283     * @param bool   $readOnly             is column read only or not
1284     *
1285     * @return string                       an html snippet
1286     */
1287    private function getPmaTypeSet(
1288        array $column,
1289        array $extracted_columnspec,
1290        $backup_field,
1291        $column_name_appendix,
1292        $onChangeClause,
1293        $tabindex,
1294        $tabindex_for_value,
1295        $idindex,
1296        $data,
1297        $readOnly
1298    ) {
1299        [$column_set_values, $select_size] = $this->getColumnSetValueAndSelectSize(
1300            $column,
1301            $extracted_columnspec
1302        );
1303        $vset = array_flip(explode(',', $data));
1304        $html_output = $backup_field . "\n";
1305        $html_output .= '<input type="hidden" name="fields_type'
1306            . $column_name_appendix . '" value="set">';
1307        $html_output .= '<select name="fields' . $column_name_appendix . '[]"'
1308            . ' class="textfield"'
1309            . ($readOnly ? ' disabled' : '')
1310            . ' size="' . $select_size . '"'
1311            . ' multiple="multiple"'
1312            . ' ' . $onChangeClause
1313            . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"'
1314            . ' id="field_' . $idindex . '_3">';
1315
1316        $selected_html = '';
1317        foreach ($column_set_values as $column_set_value) {
1318            $html_output .= '<option value="' . $column_set_value['html'] . '"';
1319            if (isset($vset[$column_set_value['plain']])) {
1320                $html_output .= ' selected="selected"';
1321                $selected_html = $column_set_value['html'];
1322            }
1323            $html_output .= '>' . $column_set_value['html'] . '</option>' . "\n";
1324        }
1325        $html_output .= '</select>';
1326
1327        //Add hidden input, as disabled <select> input does not included in POST.
1328        if ($readOnly) {
1329            $html_output .= '<input name="fields' . $column_name_appendix . '[]"'
1330                . ' type="hidden" value="' . $selected_html . '">';
1331        }
1332
1333        return $html_output;
1334    }
1335
1336    /**
1337     * Retrieve column 'set' value and select size
1338     *
1339     * @param array $column               description of column in given table
1340     * @param array $extracted_columnspec associative array containing type,
1341     *                                    spec_in_brackets and possibly enum_set_values
1342     *                                    (another array)
1343     *
1344     * @return array $column['values'], $column['select_size']
1345     */
1346    private function getColumnSetValueAndSelectSize(
1347        array $column,
1348        array $extracted_columnspec
1349    ) {
1350        if (! isset($column['values'])) {
1351            $column['values'] = [];
1352            foreach ($extracted_columnspec['enum_set_values'] as $val) {
1353                $column['values'][] = [
1354                    'plain' => $val,
1355                    'html'  => htmlspecialchars($val),
1356                ];
1357            }
1358            $column['select_size'] = min(4, count($column['values']));
1359        }
1360
1361        return [
1362            $column['values'],
1363            $column['select_size'],
1364        ];
1365    }
1366
1367    /**
1368     * Get HTML for binary and blob column
1369     *
1370     * @param array       $column                description of column in given table
1371     * @param string|null $data                  data to edit
1372     * @param string      $special_chars         special characters
1373     * @param int         $biggest_max_file_size biggest max file size for uploading
1374     * @param string      $backup_field          hidden input field
1375     * @param string      $column_name_appendix  the name attribute
1376     * @param string      $onChangeClause        onchange clause for fields
1377     * @param int         $tabindex              tab index
1378     * @param int         $tabindex_for_value    offset for the values tabindex
1379     * @param int         $idindex               id index
1380     * @param string      $text_dir              text direction
1381     * @param string      $special_chars_encoded replaced char if the string starts
1382     *                                           with a \r\n pair (0x0d0a) add an
1383     *                                           extra \n
1384     * @param string      $vkey                  [multi_edit]['row_id']
1385     * @param bool        $is_upload             is upload or not
1386     * @param bool        $readOnly              is column read only or not
1387     *
1388     * @return string                           an html snippet
1389     */
1390    private function getBinaryAndBlobColumn(
1391        array $column,
1392        ?string $data,
1393        $special_chars,
1394        $biggest_max_file_size,
1395        $backup_field,
1396        $column_name_appendix,
1397        $onChangeClause,
1398        $tabindex,
1399        $tabindex_for_value,
1400        $idindex,
1401        $text_dir,
1402        $special_chars_encoded,
1403        $vkey,
1404        $is_upload,
1405        $readOnly
1406    ) {
1407        $html_output = '';
1408        // Add field type : Protected or Hexadecimal
1409        $fields_type_html = '<input type="hidden" name="fields_type'
1410            . $column_name_appendix . '" value="%s">';
1411        // Default value : hex
1412        $fields_type_val = 'hex';
1413        if (($GLOBALS['cfg']['ProtectBinary'] === 'blob' && $column['is_blob'])
1414            || ($GLOBALS['cfg']['ProtectBinary'] === 'all')
1415            || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' && ! $column['is_blob'])
1416        ) {
1417            $html_output .= __('Binary - do not edit');
1418            if (isset($data)) {
1419                $data_size = Util::formatByteDown(
1420                    mb_strlen(stripslashes($data)),
1421                    3,
1422                    1
1423                );
1424                $html_output .= ' (' . $data_size[0] . ' ' . $data_size[1] . ')';
1425                unset($data_size);
1426            }
1427            $fields_type_val = 'protected';
1428            $html_output .= '<input type="hidden" name="fields'
1429                . $column_name_appendix . '" value="">';
1430        } elseif ($column['is_blob']
1431            || ($column['len'] > $GLOBALS['cfg']['LimitChars'])
1432        ) {
1433            $html_output .= "\n" . $this->getTextarea(
1434                $column,
1435                $backup_field,
1436                $column_name_appendix,
1437                $onChangeClause,
1438                $tabindex,
1439                $tabindex_for_value,
1440                $idindex,
1441                $text_dir,
1442                $special_chars_encoded,
1443                'HEX',
1444                $readOnly
1445            );
1446        } else {
1447            // field size should be at least 4 and max $GLOBALS['cfg']['LimitChars']
1448            $fieldsize = min(max($column['len'], 4), $GLOBALS['cfg']['LimitChars']);
1449            $html_output .= "\n" . $backup_field . "\n" . $this->getHtmlInput(
1450                $column,
1451                $column_name_appendix,
1452                $special_chars,
1453                $fieldsize,
1454                $onChangeClause,
1455                $tabindex,
1456                $tabindex_for_value,
1457                $idindex,
1458                'HEX',
1459                $readOnly
1460            );
1461        }
1462        $html_output .= sprintf($fields_type_html, $fields_type_val);
1463
1464        if ($is_upload && $column['is_blob'] && ! $readOnly) {
1465            // We don't want to prevent users from using
1466            // browser's default drag-drop feature on some page(s),
1467            // so we add noDragDrop class to the input
1468            $html_output .= '<br>'
1469                . '<input type="file"'
1470                . ' name="fields_upload' . $vkey . '[' . $column['Field_md5'] . ']"'
1471                . ' class="textfield noDragDrop" id="field_' . $idindex . '_3" size="10"'
1472                . ' ' . $onChangeClause . '>&nbsp;';
1473            [$html_out] = $this->getMaxUploadSize(
1474                $column,
1475                $biggest_max_file_size
1476            );
1477            $html_output .= $html_out;
1478        }
1479
1480        if (! empty($GLOBALS['cfg']['UploadDir']) && ! $readOnly) {
1481            $html_output .= $this->getSelectOptionForUpload($vkey, $column);
1482        }
1483
1484        return $html_output;
1485    }
1486
1487    /**
1488     * Get HTML input type
1489     *
1490     * @param array  $column               description of column in given table
1491     * @param string $column_name_appendix the name attribute
1492     * @param string $special_chars        special characters
1493     * @param int    $fieldsize            html field size
1494     * @param string $onChangeClause       onchange clause for fields
1495     * @param int    $tabindex             tab index
1496     * @param int    $tabindex_for_value   offset for the values tabindex
1497     * @param int    $idindex              id index
1498     * @param string $data_type            the html5 data-* attribute type
1499     * @param bool   $readOnly             is column read only or not
1500     *
1501     * @return string                       an html snippet
1502     */
1503    private function getHtmlInput(
1504        array $column,
1505        $column_name_appendix,
1506        $special_chars,
1507        $fieldsize,
1508        $onChangeClause,
1509        $tabindex,
1510        $tabindex_for_value,
1511        $idindex,
1512        $data_type,
1513        $readOnly
1514    ) {
1515        $input_type = 'text';
1516        // do not use the 'date' or 'time' types here; they have no effect on some
1517        // browsers and create side effects (see bug #4218)
1518
1519        $the_class = 'textfield';
1520        // verify True_Type which does not contain the parentheses and length
1521        if (! $readOnly) {
1522            if ($column['True_Type'] === 'date') {
1523                $the_class .= ' datefield';
1524            } elseif ($column['True_Type'] === 'time') {
1525                $the_class .= ' timefield';
1526            } elseif ($column['True_Type'] === 'datetime'
1527                || $column['True_Type'] === 'timestamp'
1528            ) {
1529                $the_class .= ' datetimefield';
1530            }
1531        }
1532        $input_min_max = false;
1533        if (in_array($column['True_Type'], $this->dbi->types->getIntegerTypes())) {
1534            $extracted_columnspec = Util::extractColumnSpec(
1535                $column['Type']
1536            );
1537            $is_unsigned = $extracted_columnspec['unsigned'];
1538            $min_max_values = $this->dbi->types->getIntegerRange(
1539                $column['True_Type'],
1540                ! $is_unsigned
1541            );
1542            $input_min_max = 'min="' . $min_max_values[0] . '" '
1543                . 'max="' . $min_max_values[1] . '"';
1544            $data_type = 'INT';
1545        }
1546
1547        return '<input type="' . $input_type . '"'
1548            . ' name="fields' . $column_name_appendix . '"'
1549            . ' value="' . $special_chars . '" size="' . $fieldsize . '"'
1550            . (isset($column['is_char']) && $column['is_char']
1551            ? ' data-maxlength="' . $fieldsize . '"'
1552            : '')
1553            . ($readOnly ? ' readonly="readonly"' : '')
1554            . ($input_min_max !== false ? ' ' . $input_min_max : '')
1555            . ' data-type="' . $data_type . '"'
1556            . ($input_type === 'time' ? ' step="1"' : '')
1557            . ' class="' . $the_class . '" ' . $onChangeClause
1558            . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"'
1559            . ' id="field_' . $idindex . '_3">';
1560    }
1561
1562    /**
1563     * Get HTML select option for upload
1564     *
1565     * @param string $vkey   [multi_edit]['row_id']
1566     * @param array  $column description of column in given table
1567     *
1568     * @return string|null an html snippet
1569     */
1570    private function getSelectOptionForUpload($vkey, array $column)
1571    {
1572        $files = $this->fileListing->getFileSelectOptions(
1573            Util::userDir((string) ($GLOBALS['cfg']['UploadDir'] ?? ''))
1574        );
1575
1576        if ($files === false) {
1577            return '<span style="color:red">' . __('Error') . '</span><br>' . "\n"
1578                . __('The directory you set for upload work cannot be reached.') . "\n";
1579        }
1580
1581        if (! empty($files)) {
1582            return "<br>\n"
1583                . '<i>' . __('Or') . '</i> '
1584                . __('web server upload directory:') . '<br>' . "\n"
1585                . '<select size="1" name="fields_uploadlocal'
1586                . $vkey . '[' . $column['Field_md5'] . ']">' . "\n"
1587                . '<option value="" selected="selected"></option>' . "\n"
1588                . $files
1589                . '</select>' . "\n";
1590        }
1591
1592        return null;
1593    }
1594
1595    /**
1596     * Retrieve the maximum upload file size
1597     *
1598     * @param array $column                description of column in given table
1599     * @param int   $biggest_max_file_size biggest max file size for uploading
1600     *
1601     * @return array an html snippet and $biggest_max_file_size
1602     */
1603    private function getMaxUploadSize(array $column, $biggest_max_file_size)
1604    {
1605        // find maximum upload size, based on field type
1606        /**
1607         * @todo with functions this is not so easy, as you can basically
1608         * process any data with function like MD5
1609         */
1610        global $max_upload_size;
1611        $max_field_sizes = [
1612            'tinyblob'   =>        '256',
1613            'blob'       =>      '65536',
1614            'mediumblob' =>   '16777216',
1615            'longblob'   => '4294967296',// yeah, really
1616        ];
1617
1618        $this_field_max_size = $max_upload_size; // from PHP max
1619        if ($this_field_max_size > $max_field_sizes[$column['pma_type']]) {
1620            $this_field_max_size = $max_field_sizes[$column['pma_type']];
1621        }
1622        $html_output
1623            = Util::getFormattedMaximumUploadSize(
1624                $this_field_max_size
1625            ) . "\n";
1626        // do not generate here the MAX_FILE_SIZE, because we should
1627        // put only one in the form to accommodate the biggest field
1628        if ($this_field_max_size > $biggest_max_file_size) {
1629            $biggest_max_file_size = $this_field_max_size;
1630        }
1631
1632        return [
1633            $html_output,
1634            $biggest_max_file_size,
1635        ];
1636    }
1637
1638    /**
1639     * Get HTML for the Value column of other datatypes
1640     * (here, "column" is used in the sense of HTML column in HTML table)
1641     *
1642     * @param array  $column                description of column in given table
1643     * @param string $default_char_editing  default char editing mode which is stored
1644     *                                      in the config.inc.php script
1645     * @param string $backup_field          hidden input field
1646     * @param string $column_name_appendix  the name attribute
1647     * @param string $onChangeClause        onchange clause for fields
1648     * @param int    $tabindex              tab index
1649     * @param string $special_chars         special characters
1650     * @param int    $tabindex_for_value    offset for the values tabindex
1651     * @param int    $idindex               id index
1652     * @param string $text_dir              text direction
1653     * @param string $special_chars_encoded replaced char if the string starts
1654     *                                      with a \r\n pair (0x0d0a) add an extra \n
1655     * @param string $data                  data to edit
1656     * @param array  $extracted_columnspec  associative array containing type,
1657     *                                      spec_in_brackets and possibly
1658     *                                      enum_set_values (another array)
1659     * @param bool   $readOnly              is column read only or not
1660     *
1661     * @return string an html snippet
1662     */
1663    private function getValueColumnForOtherDatatypes(
1664        array $column,
1665        $default_char_editing,
1666        $backup_field,
1667        $column_name_appendix,
1668        $onChangeClause,
1669        $tabindex,
1670        $special_chars,
1671        $tabindex_for_value,
1672        $idindex,
1673        $text_dir,
1674        $special_chars_encoded,
1675        $data,
1676        array $extracted_columnspec,
1677        $readOnly
1678    ) {
1679        // HTML5 data-* attribute data-type
1680        $data_type = $this->dbi->types->getTypeClass($column['True_Type']);
1681        $fieldsize = $this->getColumnSize($column, $extracted_columnspec);
1682        $html_output = $backup_field . "\n";
1683        if ($column['is_char']
1684            && ($GLOBALS['cfg']['CharEditing'] === 'textarea'
1685            || mb_strpos($data, "\n") !== false)
1686        ) {
1687            $html_output .= "\n";
1688            $GLOBALS['cfg']['CharEditing'] = $default_char_editing;
1689            $html_output .= $this->getTextarea(
1690                $column,
1691                $backup_field,
1692                $column_name_appendix,
1693                $onChangeClause,
1694                $tabindex,
1695                $tabindex_for_value,
1696                $idindex,
1697                $text_dir,
1698                $special_chars_encoded,
1699                $data_type,
1700                $readOnly
1701            );
1702        } else {
1703            $html_output .= $this->getHtmlInput(
1704                $column,
1705                $column_name_appendix,
1706                $special_chars,
1707                $fieldsize,
1708                $onChangeClause,
1709                $tabindex,
1710                $tabindex_for_value,
1711                $idindex,
1712                $data_type,
1713                $readOnly
1714            );
1715
1716            if (preg_match('/(VIRTUAL|PERSISTENT|GENERATED)/', $column['Extra'])
1717                && strpos($column['Extra'], 'DEFAULT_GENERATED') === false
1718            ) {
1719                $html_output .= '<input type="hidden" name="virtual'
1720                    . $column_name_appendix . '" value="1">';
1721            }
1722            if ($column['Extra'] === 'auto_increment') {
1723                $html_output .= '<input type="hidden" name="auto_increment'
1724                    . $column_name_appendix . '" value="1">';
1725            }
1726            if (substr($column['pma_type'], 0, 9) === 'timestamp') {
1727                $html_output .= '<input type="hidden" name="fields_type'
1728                    . $column_name_appendix . '" value="timestamp">';
1729            }
1730            if (substr($column['pma_type'], 0, 4) === 'date') {
1731                $type = substr($column['pma_type'], 0, 8) === 'datetime' ? 'datetime' : 'date';
1732                $html_output .= '<input type="hidden" name="fields_type'
1733                    . $column_name_appendix . '" value="' . $type . '">';
1734            }
1735            if ($column['True_Type'] === 'bit') {
1736                $html_output .= '<input type="hidden" name="fields_type'
1737                    . $column_name_appendix . '" value="bit">';
1738            }
1739        }
1740
1741        return $html_output;
1742    }
1743
1744    /**
1745     * Get the field size
1746     *
1747     * @param array $column               description of column in given table
1748     * @param array $extracted_columnspec associative array containing type,
1749     *                                    spec_in_brackets and possibly enum_set_values
1750     *                                    (another array)
1751     *
1752     * @return int field size
1753     */
1754    private function getColumnSize(array $column, array $extracted_columnspec)
1755    {
1756        if ($column['is_char']) {
1757            $fieldsize = $extracted_columnspec['spec_in_brackets'];
1758            if ($fieldsize > $GLOBALS['cfg']['MaxSizeForInputField']) {
1759                /**
1760                 * This case happens for CHAR or VARCHAR columns which have
1761                 * a size larger than the maximum size for input field.
1762                 */
1763                $GLOBALS['cfg']['CharEditing'] = 'textarea';
1764            }
1765        } else {
1766            /**
1767             * This case happens for example for INT or DATE columns;
1768             * in these situations, the value returned in $column['len']
1769             * seems appropriate.
1770             */
1771            $fieldsize = $column['len'];
1772        }
1773
1774        return min(
1775            max($fieldsize, $GLOBALS['cfg']['MinSizeForInputField']),
1776            $GLOBALS['cfg']['MaxSizeForInputField']
1777        );
1778    }
1779
1780    /**
1781     * Get HTML for gis data types
1782     *
1783     * @return string an html snippet
1784     */
1785    private function getHtmlForGisDataTypes(int $rowId): string
1786    {
1787        $edit_str = Generator::getIcon('b_edit', __('Edit/Insert'));
1788
1789        return '<span class="open_gis_editor" data-row-id="' . $rowId . '">'
1790            . Generator::linkOrButton(
1791                '#',
1792                null,
1793                $edit_str,
1794                [],
1795                '_blank'
1796            )
1797            . '</span>';
1798    }
1799
1800    /**
1801     * get html for continue insertion form
1802     *
1803     * @param string $table              name of the table
1804     * @param string $db                 name of the database
1805     * @param array  $where_clause_array array of where clauses
1806     * @param string $err_url            error url
1807     *
1808     * @return string                   an html snippet
1809     */
1810    public function getContinueInsertionForm(
1811        $table,
1812        $db,
1813        array $where_clause_array,
1814        $err_url
1815    ) {
1816        return $this->template->render('table/insert/continue_insertion_form', [
1817            'db' => $db,
1818            'table' => $table,
1819            'where_clause_array' => $where_clause_array,
1820            'err_url' => $err_url,
1821            'goto' => $GLOBALS['goto'],
1822            'sql_query' => $_POST['sql_query'] ?? null,
1823            'has_where_clause' => isset($_POST['where_clause']),
1824            'insert_rows_default' => $GLOBALS['cfg']['InsertRows'],
1825        ]);
1826    }
1827
1828    /**
1829     * Get action panel
1830     *
1831     * @param array|null $where_clause       where clause
1832     * @param string     $after_insert       insert mode, e.g. new_insert, same_insert
1833     * @param int        $tabindex           tab index
1834     * @param int        $tabindex_for_value offset for the values tabindex
1835     * @param bool       $found_unique_key   boolean variable for unique key
1836     *
1837     * @return string an html snippet
1838     */
1839    public function getActionsPanel(
1840        $where_clause,
1841        $after_insert,
1842        $tabindex,
1843        $tabindex_for_value,
1844        $found_unique_key
1845    ) {
1846        $html_output = '<fieldset id="actions_panel">'
1847            . '<table cellpadding="5" cellspacing="0" class="pma-table tdblock w-100">'
1848            . '<tr>'
1849            . '<td class="nowrap vmiddle">'
1850            . $this->getSubmitTypeDropDown($where_clause, $tabindex, $tabindex_for_value)
1851            . "\n";
1852
1853        $html_output .= '</td>'
1854            . '<td class="vmiddle">'
1855            . '&nbsp;&nbsp;&nbsp;<strong>'
1856            . __('and then') . '</strong>&nbsp;&nbsp;&nbsp;'
1857            . '</td>'
1858            . '<td class="nowrap vmiddle">'
1859            . $this->getAfterInsertDropDown(
1860                $where_clause,
1861                $after_insert,
1862                $found_unique_key
1863            )
1864            . '</td>'
1865            . '</tr>';
1866        $html_output .= '<tr>'
1867            . $this->getSubmitAndResetButtonForActionsPanel($tabindex, $tabindex_for_value)
1868            . '</tr>'
1869            . '</table>'
1870            . '</fieldset>';
1871
1872        return $html_output;
1873    }
1874
1875    /**
1876     * Get a HTML drop down for submit types
1877     *
1878     * @param array|null $where_clause       where clause
1879     * @param int        $tabindex           tab index
1880     * @param int        $tabindex_for_value offset for the values tabindex
1881     *
1882     * @return string                       an html snippet
1883     */
1884    private function getSubmitTypeDropDown(
1885        $where_clause,
1886        $tabindex,
1887        $tabindex_for_value
1888    ) {
1889        $html_output = '<select name="submit_type" class="control_at_footer" tabindex="'
1890            . ($tabindex + $tabindex_for_value + 1) . '">';
1891        if (isset($where_clause)) {
1892            $html_output .= '<option value="save">' . __('Save') . '</option>';
1893        }
1894        $html_output .= '<option value="insert">'
1895            . __('Insert as new row')
1896            . '</option>'
1897            . '<option value="insertignore">'
1898            . __('Insert as new row and ignore errors')
1899            . '</option>'
1900            . '<option value="showinsert">'
1901            . __('Show insert query')
1902            . '</option>'
1903            . '</select>';
1904
1905        return $html_output;
1906    }
1907
1908    /**
1909     * Get HTML drop down for after insert
1910     *
1911     * @param array|null $where_clause     where clause
1912     * @param string     $after_insert     insert mode, e.g. new_insert, same_insert
1913     * @param bool       $found_unique_key boolean variable for unique key
1914     *
1915     * @return string                   an html snippet
1916     */
1917    private function getAfterInsertDropDown($where_clause, $after_insert, $found_unique_key)
1918    {
1919        $html_output = '<select name="after_insert" class="control_at_footer">'
1920            . '<option value="back" '
1921            . ($after_insert === 'back' ? 'selected="selected"' : '') . '>'
1922            . __('Go back to previous page') . '</option>'
1923            . '<option value="new_insert" '
1924            . ($after_insert === 'new_insert' ? 'selected="selected"' : '') . '>'
1925            . __('Insert another new row') . '</option>';
1926
1927        if (isset($where_clause)) {
1928            $html_output .= '<option value="same_insert" '
1929                . ($after_insert === 'same_insert' ? 'selected="selected"' : '') . '>'
1930                . __('Go back to this page') . '</option>';
1931
1932            // If we have just numeric primary key, we can also edit next
1933            // in 2.8.2, we were looking for `field_name` = numeric_value
1934            //if (preg_match('@^[\s]*`[^`]*` = [0-9]+@', $where_clause)) {
1935            // in 2.9.0, we are looking for `table_name`.`field_name` = numeric_value
1936            $is_numeric = false;
1937            if (! is_array($where_clause)) {
1938                $where_clause = [$where_clause];
1939            }
1940            for ($i = 0, $nb = count($where_clause); $i < $nb; $i++) {
1941                // preg_match() returns 1 if there is a match
1942                $is_numeric = (preg_match(
1943                    '@^[\s]*`[^`]*`[\.]`[^`]*` = [0-9]+@',
1944                    $where_clause[$i]
1945                ) == 1);
1946                if ($is_numeric === true) {
1947                    break;
1948                }
1949            }
1950            if ($found_unique_key && $is_numeric) {
1951                $html_output .= '<option value="edit_next" '
1952                    . ($after_insert === 'edit_next' ? 'selected="selected"' : '') . '>'
1953                    . __('Edit next row') . '</option>';
1954            }
1955        }
1956
1957        return $html_output . '</select>';
1958    }
1959
1960    /**
1961     * get Submit button and Reset button for action panel
1962     *
1963     * @param int $tabindex           tab index
1964     * @param int $tabindex_for_value offset for the values tabindex
1965     *
1966     * @return string an html snippet
1967     */
1968    private function getSubmitAndResetButtonForActionsPanel($tabindex, $tabindex_for_value)
1969    {
1970        return '<td>'
1971            . Generator::showHint(
1972                __(
1973                    'Use TAB key to move from value to value,'
1974                    . ' or CTRL+arrows to move anywhere.'
1975                )
1976            )
1977            . '</td>'
1978            . '<td colspan="3" class="right vmiddle">'
1979            . '<input type="button" class="btn btn-secondary preview_sql" value="' . __('Preview SQL') . '"'
1980            . ' tabindex="' . ($tabindex + $tabindex_for_value + 6) . '">'
1981            . '<input type="reset" class="btn btn-secondary control_at_footer" value="' . __('Reset') . '"'
1982            . ' tabindex="' . ($tabindex + $tabindex_for_value + 7) . '">'
1983            . '<input type="submit" class="btn btn-primary control_at_footer" value="' . __('Go') . '"'
1984            . ' tabindex="' . ($tabindex + $tabindex_for_value + 8) . '" id="buttonYes">'
1985            . '</td>';
1986    }
1987
1988    /**
1989     * Get table head and table foot for insert row table
1990     *
1991     * @param array $url_params url parameters
1992     *
1993     * @return string           an html snippet
1994     */
1995    private function getHeadAndFootOfInsertRowTable(array $url_params)
1996    {
1997        $html_output = '<div class="responsivetable">'
1998            . '<table class="pma-table insertRowTable topmargin">'
1999            . '<thead>'
2000            . '<tr>'
2001            . '<th>' . __('Column') . '</th>';
2002
2003        if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) {
2004            $html_output .= $this->showTypeOrFunction('type', $url_params, true);
2005        }
2006        if ($GLOBALS['cfg']['ShowFunctionFields']) {
2007            $html_output .= $this->showTypeOrFunction('function', $url_params, true);
2008        }
2009
2010        $html_output .= '<th>' . __('Null') . '</th>'
2011            . '<th class="fillPage">' . __('Value') . '</th>'
2012            . '</tr>'
2013            . '</thead>'
2014            . ' <tfoot>'
2015            . '<tr>'
2016            . '<th colspan="5" class="tblFooters right">'
2017            . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">'
2018            . '</th>'
2019            . '</tr>'
2020            . '</tfoot>';
2021
2022        return $html_output;
2023    }
2024
2025    /**
2026     * Prepares the field value and retrieve special chars, backup field and data array
2027     *
2028     * @param array  $current_row          a row of the table
2029     * @param array  $column               description of column in given table
2030     * @param array  $extracted_columnspec associative array containing type,
2031     *                                     spec_in_brackets and possibly
2032     *                                     enum_set_values (another array)
2033     * @param bool   $real_null_value      whether column value null or not null
2034     * @param array  $gis_data_types       list of GIS data types
2035     * @param string $column_name_appendix string to append to column name in input
2036     * @param bool   $as_is                use the data as is, used in repopulating
2037     *
2038     * @return array $real_null_value, $data, $special_chars, $backup_field,
2039     *               $special_chars_encoded
2040     */
2041    private function getSpecialCharsAndBackupFieldForExistingRow(
2042        array $current_row,
2043        array $column,
2044        array $extracted_columnspec,
2045        $real_null_value,
2046        array $gis_data_types,
2047        $column_name_appendix,
2048        $as_is
2049    ) {
2050        $special_chars_encoded = '';
2051        $data = null;
2052        // (we are editing)
2053        if (! isset($current_row[$column['Field']])) {
2054            $real_null_value = true;
2055            $current_row[$column['Field']] = '';
2056            $special_chars = '';
2057            $data = $current_row[$column['Field']];
2058        } elseif ($column['True_Type'] === 'bit') {
2059            $special_chars = $as_is
2060                ? $current_row[$column['Field']]
2061                : Util::printableBitValue(
2062                    (int) $current_row[$column['Field']],
2063                    (int) $extracted_columnspec['spec_in_brackets']
2064                );
2065        } elseif ((substr($column['True_Type'], 0, 9) === 'timestamp'
2066            || $column['True_Type'] === 'datetime'
2067            || $column['True_Type'] === 'time')
2068            && (mb_strpos($current_row[$column['Field']], '.') !== false)
2069        ) {
2070            $current_row[$column['Field']] = $as_is
2071                ? $current_row[$column['Field']]
2072                : Util::addMicroseconds(
2073                    $current_row[$column['Field']]
2074                );
2075            $special_chars = htmlspecialchars($current_row[$column['Field']], ENT_COMPAT);
2076        } elseif (in_array($column['True_Type'], $gis_data_types)) {
2077            // Convert gis data to Well Know Text format
2078            $current_row[$column['Field']] = $as_is
2079                ? $current_row[$column['Field']]
2080                : Util::asWKT(
2081                    $current_row[$column['Field']],
2082                    true
2083                );
2084            $special_chars = htmlspecialchars($current_row[$column['Field']], ENT_COMPAT);
2085        } else {
2086            // special binary "characters"
2087            if ($column['is_binary']
2088                || ($column['is_blob'] && $GLOBALS['cfg']['ProtectBinary'] !== 'all')
2089            ) {
2090                $current_row[$column['Field']] = $as_is
2091                    ? $current_row[$column['Field']]
2092                    : bin2hex(
2093                        $current_row[$column['Field']]
2094                    );
2095            }
2096            $special_chars = htmlspecialchars($current_row[$column['Field']], ENT_COMPAT);
2097
2098            //We need to duplicate the first \n or otherwise we will lose
2099            //the first newline entered in a VARCHAR or TEXT column
2100            $special_chars_encoded
2101                = Util::duplicateFirstNewline($special_chars);
2102
2103            $data = $current_row[$column['Field']];
2104        }
2105
2106        //when copying row, it is useful to empty auto-increment column
2107        // to prevent duplicate key error
2108        if (isset($_POST['default_action'])
2109            && $_POST['default_action'] === 'insert'
2110        ) {
2111            if ($column['Key'] === 'PRI'
2112                && mb_strpos($column['Extra'], 'auto_increment') !== false
2113            ) {
2114                $data = $special_chars_encoded = $special_chars = null;
2115            }
2116        }
2117        // If a timestamp field value is not included in an update
2118        // statement MySQL auto-update it to the current timestamp;
2119        // however, things have changed since MySQL 4.1, so
2120        // it's better to set a fields_prev in this situation
2121        $backup_field = '<input type="hidden" name="fields_prev'
2122            . $column_name_appendix . '" value="'
2123            . htmlspecialchars($current_row[$column['Field']], ENT_COMPAT) . '">';
2124
2125        return [
2126            $real_null_value,
2127            $special_chars_encoded,
2128            $special_chars,
2129            $data,
2130            $backup_field,
2131        ];
2132    }
2133
2134    /**
2135     * display default values
2136     *
2137     * @param array $column          description of column in given table
2138     * @param bool  $real_null_value whether column value null or not null
2139     *
2140     * @return array $real_null_value, $data, $special_chars,
2141     *               $backup_field, $special_chars_encoded
2142     */
2143    private function getSpecialCharsAndBackupFieldForInsertingMode(
2144        array $column,
2145        $real_null_value
2146    ) {
2147        if (! isset($column['Default'])) {
2148            $column['Default']    = '';
2149            $real_null_value          = true;
2150            $data                     = '';
2151        } else {
2152            $data                     = $column['Default'];
2153        }
2154
2155        $trueType = $column['True_Type'];
2156
2157        if ($trueType === 'bit') {
2158            $special_chars = Util::convertBitDefaultValue(
2159                $column['Default']
2160            );
2161        } elseif (substr($trueType, 0, 9) === 'timestamp'
2162            || $trueType === 'datetime'
2163            || $trueType === 'time'
2164        ) {
2165            $special_chars = Util::addMicroseconds($column['Default']);
2166        } elseif ($trueType === 'binary' || $trueType === 'varbinary') {
2167            $special_chars = bin2hex($column['Default']);
2168        } elseif (substr($trueType, -4) === 'text') {
2169            $textDefault = substr($column['Default'], 1, -1);
2170            $special_chars = stripcslashes($textDefault !== false ? $textDefault : $column['Default']);
2171        } else {
2172            $special_chars = htmlspecialchars($column['Default']);
2173        }
2174        $backup_field = '';
2175        $special_chars_encoded = Util::duplicateFirstNewline(
2176            $special_chars
2177        );
2178
2179        return [
2180            $real_null_value,
2181            $data,
2182            $special_chars,
2183            $backup_field,
2184            $special_chars_encoded,
2185        ];
2186    }
2187
2188    /**
2189     * Prepares the update/insert of a row
2190     *
2191     * @return array $loop_array, $using_key, $is_insert, $is_insertignore
2192     */
2193    public function getParamsForUpdateOrInsert()
2194    {
2195        if (isset($_POST['where_clause'])) {
2196            // we were editing something => use the WHERE clause
2197            $loop_array = is_array($_POST['where_clause'])
2198                ? $_POST['where_clause']
2199                : [$_POST['where_clause']];
2200            $using_key  = true;
2201            $is_insert  = isset($_POST['submit_type'])
2202                          && ($_POST['submit_type'] === 'insert'
2203                          || $_POST['submit_type'] === 'showinsert'
2204                          || $_POST['submit_type'] === 'insertignore');
2205        } else {
2206            // new row => use indexes
2207            $loop_array = [];
2208            if (! empty($_POST['fields'])) {
2209                foreach ($_POST['fields']['multi_edit'] as $key => $dummy) {
2210                    $loop_array[] = $key;
2211                }
2212            }
2213            $using_key  = false;
2214            $is_insert  = true;
2215        }
2216        $is_insertignore  = isset($_POST['submit_type'])
2217            && $_POST['submit_type'] === 'insertignore';
2218
2219        return [
2220            $loop_array,
2221            $using_key,
2222            $is_insert,
2223            $is_insertignore,
2224        ];
2225    }
2226
2227    /**
2228     * Check wether insert row mode and if so include tbl_changen script and set
2229     * global variables.
2230     *
2231     * @return void
2232     */
2233    public function isInsertRow()
2234    {
2235        global $containerBuilder;
2236
2237        if (! isset($_POST['insert_rows'])
2238            || ! is_numeric($_POST['insert_rows'])
2239            || $_POST['insert_rows'] == $GLOBALS['cfg']['InsertRows']
2240        ) {
2241            return;
2242        }
2243
2244        $GLOBALS['cfg']['InsertRows'] = $_POST['insert_rows'];
2245        $response = Response::getInstance();
2246        $header = $response->getHeader();
2247        $scripts = $header->getScripts();
2248        $scripts->addFile('vendor/jquery/additional-methods.js');
2249        $scripts->addFile('table/change.js');
2250        if (! defined('TESTSUITE')) {
2251            /** @var ChangeController $controller */
2252            $controller = $containerBuilder->get(ChangeController::class);
2253            $controller->index();
2254            exit;
2255        }
2256    }
2257
2258    /**
2259     * set $_SESSION for edit_next
2260     *
2261     * @param string $one_where_clause one where clause from where clauses array
2262     *
2263     * @return void
2264     */
2265    public function setSessionForEditNext($one_where_clause)
2266    {
2267        $local_query = 'SELECT * FROM ' . Util::backquote($GLOBALS['db'])
2268            . '.' . Util::backquote($GLOBALS['table']) . ' WHERE '
2269            . str_replace('` =', '` >', $one_where_clause) . ' LIMIT 1;';
2270
2271        $res = $this->dbi->query($local_query);
2272        $row = $this->dbi->fetchRow($res);
2273        $meta = $this->dbi->getFieldsMeta($res);
2274        // must find a unique condition based on unique key,
2275        // not a combination of all fields
2276        [$unique_condition, $clause_is_unique] = Util::getUniqueCondition(
2277            $res,
2278            count($meta),
2279            $meta,
2280            $row ?? [],
2281            true
2282        );
2283        if (! empty($unique_condition)) {
2284            $_SESSION['edit_next'] = $unique_condition;
2285        }
2286        unset($unique_condition, $clause_is_unique);
2287    }
2288
2289    /**
2290     * set $goto_include variable for different cases and retrieve like,
2291     * if $GLOBALS['goto'] empty, if $goto_include previously not defined
2292     * and new_insert, same_insert, edit_next
2293     *
2294     * @param string|false $goto_include store some script for include, otherwise it is
2295     *                                   boolean false
2296     *
2297     * @return string|false
2298     */
2299    public function getGotoInclude($goto_include)
2300    {
2301        $valid_options = [
2302            'new_insert',
2303            'same_insert',
2304            'edit_next',
2305        ];
2306        if (isset($_POST['after_insert'])
2307            && in_array($_POST['after_insert'], $valid_options)
2308        ) {
2309            $goto_include = '/table/change';
2310        } elseif (! empty($GLOBALS['goto'])) {
2311            if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) {
2312                // this should NOT happen
2313                //$GLOBALS['goto'] = false;
2314                if ($GLOBALS['goto'] === 'index.php?route=/sql') {
2315                    $goto_include = '/sql';
2316                } else {
2317                    $goto_include = false;
2318                }
2319            } else {
2320                $goto_include = $GLOBALS['goto'];
2321            }
2322            if ($GLOBALS['goto'] === 'index.php?route=/database/sql' && strlen($GLOBALS['table']) > 0) {
2323                $GLOBALS['table'] = '';
2324            }
2325        }
2326        if (! $goto_include) {
2327            if (strlen($GLOBALS['table']) === 0) {
2328                $goto_include = '/database/sql';
2329            } else {
2330                $goto_include = '/table/sql';
2331            }
2332        }
2333
2334        return $goto_include;
2335    }
2336
2337    /**
2338     * Defines the url to return in case of failure of the query
2339     *
2340     * @param array $url_params url parameters
2341     *
2342     * @return string           error url for query failure
2343     */
2344    public function getErrorUrl(array $url_params)
2345    {
2346        if (isset($_POST['err_url'])) {
2347            return $_POST['err_url'];
2348        }
2349
2350        return Url::getFromRoute('/table/change', $url_params);
2351    }
2352
2353    /**
2354     * Builds the sql query
2355     *
2356     * @param bool  $is_insertignore $_POST['submit_type'] === 'insertignore'
2357     * @param array $query_fields    column names array
2358     * @param array $value_sets      array of query values
2359     *
2360     * @return array of query
2361     */
2362    public function buildSqlQuery($is_insertignore, array $query_fields, array $value_sets)
2363    {
2364        if ($is_insertignore) {
2365            $insert_command = 'INSERT IGNORE ';
2366        } else {
2367            $insert_command = 'INSERT ';
2368        }
2369
2370        return [
2371            $insert_command . 'INTO '
2372            . Util::backquote($GLOBALS['table'])
2373            . ' (' . implode(', ', $query_fields) . ') VALUES ('
2374            . implode('), (', $value_sets) . ')',
2375        ];
2376    }
2377
2378    /**
2379     * Executes the sql query and get the result, then move back to the calling page
2380     *
2381     * @param array $url_params url parameters array
2382     * @param array $query      built query from buildSqlQuery()
2383     *
2384     * @return array $url_params, $total_affected_rows, $last_messages
2385     *               $warning_messages, $error_messages, $return_to_sql_query
2386     */
2387    public function executeSqlQuery(array $url_params, array $query)
2388    {
2389        $return_to_sql_query = '';
2390        if (! empty($GLOBALS['sql_query'])) {
2391            $url_params['sql_query'] = $GLOBALS['sql_query'];
2392            $return_to_sql_query = $GLOBALS['sql_query'];
2393        }
2394        $GLOBALS['sql_query'] = implode('; ', $query) . ';';
2395        // to ensure that the query is displayed in case of
2396        // "insert as new row" and then "insert another new row"
2397        $GLOBALS['display_query'] = $GLOBALS['sql_query'];
2398
2399        $total_affected_rows = 0;
2400        $last_messages = [];
2401        $warning_messages = [];
2402        $error_messages = [];
2403
2404        foreach ($query as $single_query) {
2405            if (isset($_POST['submit_type']) && $_POST['submit_type'] === 'showinsert') {
2406                $last_messages[] = Message::notice(__('Showing SQL query'));
2407                continue;
2408            }
2409            if ($GLOBALS['cfg']['IgnoreMultiSubmitErrors']) {
2410                $result = $this->dbi->tryQuery($single_query);
2411            } else {
2412                $result = $this->dbi->query($single_query);
2413            }
2414            if (! $result) {
2415                $error_messages[] = $this->dbi->getError();
2416            } else {
2417                $tmp = @$this->dbi->affectedRows();
2418
2419                if ($tmp) {
2420                    $total_affected_rows += $tmp;
2421                }
2422                unset($tmp);
2423
2424                $insert_id = $this->dbi->insertId();
2425                if ($insert_id != 0) {
2426                    // insert_id is id of FIRST record inserted in one insert, so if we
2427                    // inserted multiple rows, we had to increment this
2428
2429                    if ($total_affected_rows > 0) {
2430                        $insert_id += $total_affected_rows - 1;
2431                    }
2432                    $last_message = Message::notice(__('Inserted row id: %1$d'));
2433                    $last_message->addParam($insert_id);
2434                    $last_messages[] = $last_message;
2435                }
2436                $this->dbi->freeResult($result);
2437            }
2438            $warning_messages = $this->getWarningMessages();
2439        }
2440
2441        return [
2442            $url_params,
2443            $total_affected_rows,
2444            $last_messages,
2445            $warning_messages,
2446            $error_messages,
2447            $return_to_sql_query,
2448        ];
2449    }
2450
2451    /**
2452     * get the warning messages array
2453     *
2454     * @return array
2455     */
2456    private function getWarningMessages()
2457    {
2458        $warning_essages = [];
2459        foreach ($this->dbi->getWarnings() as $warning) {
2460            $warning_essages[] = Message::sanitize(
2461                $warning['Level'] . ': #' . $warning['Code'] . ' ' . $warning['Message']
2462            );
2463        }
2464
2465        return $warning_essages;
2466    }
2467
2468    /**
2469     * Column to display from the foreign table?
2470     *
2471     * @param string $where_comparison string that contain relation field value
2472     * @param array  $map              all Relations to foreign tables for a given
2473     *                                 table or optionally a given column in a table
2474     * @param string $relation_field   relation field
2475     *
2476     * @return string display value from the foreign table
2477     */
2478    public function getDisplayValueForForeignTableColumn(
2479        $where_comparison,
2480        array $map,
2481        $relation_field
2482    ) {
2483        $foreigner = $this->relation->searchColumnInForeigners($map, $relation_field);
2484
2485        if (! is_array($foreigner)) {
2486            return '';
2487        }
2488
2489        $display_field = $this->relation->getDisplayField(
2490            $foreigner['foreign_db'],
2491            $foreigner['foreign_table']
2492        );
2493        // Field to display from the foreign table?
2494        if (is_string($display_field) && strlen($display_field) > 0) {
2495            $dispsql = 'SELECT ' . Util::backquote($display_field)
2496                . ' FROM ' . Util::backquote($foreigner['foreign_db'])
2497                . '.' . Util::backquote($foreigner['foreign_table'])
2498                . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
2499                . $where_comparison;
2500            $dispresult = $this->dbi->tryQuery(
2501                $dispsql,
2502                DatabaseInterface::CONNECT_USER,
2503                DatabaseInterface::QUERY_STORE
2504            );
2505            if ($dispresult && $this->dbi->numRows($dispresult) > 0) {
2506                [$dispval] = $this->dbi->fetchRow($dispresult);
2507            } else {
2508                $dispval = '';
2509            }
2510            if ($dispresult) {
2511                $this->dbi->freeResult($dispresult);
2512            }
2513
2514            return $dispval;
2515        }
2516
2517        return '';
2518    }
2519
2520    /**
2521     * Display option in the cell according to user choices
2522     *
2523     * @param array  $map                  all Relations to foreign tables for a given
2524     *                                     table or optionally a given column in a table
2525     * @param string $relation_field       relation field
2526     * @param string $where_comparison     string that contain relation field value
2527     * @param string $dispval              display value from the foreign table
2528     * @param string $relation_field_value relation field value
2529     *
2530     * @return string HTML <a> tag
2531     */
2532    public function getLinkForRelationalDisplayField(
2533        array $map,
2534        $relation_field,
2535        $where_comparison,
2536        $dispval,
2537        $relation_field_value
2538    ) {
2539        $foreigner = $this->relation->searchColumnInForeigners($map, $relation_field);
2540
2541        if (! is_array($foreigner)) {
2542            return '';
2543        }
2544
2545        if ($_SESSION['tmpval']['relational_display'] === 'K') {
2546            // user chose "relational key" in the display options, so
2547            // the title contains the display field
2548            $title = ! empty($dispval)
2549                ? ' title="' . htmlspecialchars($dispval) . '"'
2550                : '';
2551        } else {
2552            $title = ' title="' . htmlspecialchars($relation_field_value) . '"';
2553        }
2554        $sqlQuery = 'SELECT * FROM '
2555            . Util::backquote($foreigner['foreign_db'])
2556            . '.' . Util::backquote($foreigner['foreign_table'])
2557            . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
2558            . $where_comparison;
2559        $_url_params = [
2560            'db'    => $foreigner['foreign_db'],
2561            'table' => $foreigner['foreign_table'],
2562            'pos'   => '0',
2563            'sql_signature' => Core::signSqlQuery($sqlQuery),
2564            'sql_query' => $sqlQuery,
2565        ];
2566        $output = '<a href="' . Url::getFromRoute('/sql', $_url_params) . '"' . $title . '>';
2567
2568        if ($_SESSION['tmpval']['relational_display'] === 'D') {
2569            // user chose "relational display field" in the
2570            // display options, so show display field in the cell
2571            $output .= ! empty($dispval) ? htmlspecialchars($dispval) : '';
2572        } else {
2573            // otherwise display data in the cell
2574            $output .= htmlspecialchars($relation_field_value);
2575        }
2576        $output .= '</a>';
2577
2578        return $output;
2579    }
2580
2581    /**
2582     * Transform edited values
2583     *
2584     * @param string $db             db name
2585     * @param string $table          table name
2586     * @param array  $transformation mimetypes for all columns of a table
2587     *                               [field_name][field_key]
2588     * @param array  $edited_values  transform columns list and new values
2589     * @param string $file           file containing the transformation plugin
2590     * @param string $column_name    column name
2591     * @param array  $extra_data     extra data array
2592     * @param string $type           the type of transformation
2593     *
2594     * @return array
2595     */
2596    public function transformEditedValues(
2597        $db,
2598        $table,
2599        array $transformation,
2600        array &$edited_values,
2601        $file,
2602        $column_name,
2603        array $extra_data,
2604        $type
2605    ) {
2606        $include_file = 'libraries/classes/Plugins/Transformations/' . $file;
2607        if (is_file(ROOT_PATH . $include_file)) {
2608            // $cfg['SaveCellsAtOnce'] = true; JS code sends an array
2609            $whereClause = is_array($_POST['where_clause']) ? $_POST['where_clause'][0] : $_POST['where_clause'];
2610            $_url_params = [
2611                'db'            => $db,
2612                'table'         => $table,
2613                'where_clause_sign' => Core::signSqlQuery($whereClause),
2614                'where_clause'  => $whereClause,
2615                'transform_key' => $column_name,
2616            ];
2617            $transform_options = $this->transformations->getOptions(
2618                $transformation[$type . '_options'] ?? ''
2619            );
2620            $transform_options['wrapper_link'] = Url::getCommon($_url_params);
2621            $transform_options['wrapper_params'] = $_url_params;
2622            $class_name = $this->transformations->getClassName($include_file);
2623            if (class_exists($class_name)) {
2624                /** @var TransformationsPlugin $transformation_plugin */
2625                $transformation_plugin = new $class_name();
2626
2627                foreach ($edited_values as $cell_index => $curr_cell_edited_values) {
2628                    if (! isset($curr_cell_edited_values[$column_name])) {
2629                        continue;
2630                    }
2631
2632                    $edited_values[$cell_index][$column_name]
2633                        = $extra_data['transformations'][$cell_index]
2634                            = $transformation_plugin->applyTransformation(
2635                                $curr_cell_edited_values[$column_name],
2636                                $transform_options
2637                            );
2638                }
2639            }
2640        }
2641
2642        return $extra_data;
2643    }
2644
2645    /**
2646     * Get current value in multi edit mode
2647     *
2648     * @param array  $multi_edit_funcs        multiple edit functions array
2649     * @param array  $multi_edit_salt         multiple edit array with encryption salt
2650     * @param array  $gis_from_text_functions array that contains gis from text functions
2651     * @param string $current_value           current value in the column
2652     * @param array  $gis_from_wkb_functions  initially $val is $multi_edit_columns[$key]
2653     * @param array  $func_optional_param     array('RAND','UNIX_TIMESTAMP')
2654     * @param array  $func_no_param           array of set of string
2655     * @param string $key                     an md5 of the column name
2656     *
2657     * @return string
2658     */
2659    public function getCurrentValueAsAnArrayForMultipleEdit(
2660        $multi_edit_funcs,
2661        $multi_edit_salt,
2662        $gis_from_text_functions,
2663        $current_value,
2664        $gis_from_wkb_functions,
2665        $func_optional_param,
2666        $func_no_param,
2667        $key
2668    ) {
2669        if (empty($multi_edit_funcs[$key])) {
2670            return $current_value;
2671        }
2672
2673        if ($multi_edit_funcs[$key] === 'PHP_PASSWORD_HASH') {
2674            /**
2675             * @see https://github.com/vimeo/psalm/issues/3350
2676             *
2677             * @psalm-suppress InvalidArgument
2678             */
2679            $hash = password_hash($current_value, PASSWORD_DEFAULT);
2680
2681            return "'" . $hash . "'";
2682        }
2683
2684        if ($multi_edit_funcs[$key] === 'UUID') {
2685            /* This way user will know what UUID new row has */
2686            $uuid = $this->dbi->fetchValue('SELECT UUID()');
2687
2688            return "'" . $uuid . "'";
2689        }
2690
2691        if (
2692            in_array($multi_edit_funcs[$key], $gis_from_text_functions) ||
2693            in_array($multi_edit_funcs[$key], $gis_from_wkb_functions)
2694        ) {
2695            // Remove enclosing apostrophes
2696            $current_value = mb_substr($current_value, 1, -1);
2697            // Remove escaping apostrophes
2698            $current_value = str_replace("''", "'", $current_value);
2699            // Remove backslash-escaped apostrophes
2700            $current_value = str_replace("\'", "'", $current_value);
2701
2702            return $multi_edit_funcs[$key] . '(' . $current_value . ')';
2703        }
2704
2705        if (! in_array($multi_edit_funcs[$key], $func_no_param)
2706            || ($current_value != "''"
2707            && in_array($multi_edit_funcs[$key], $func_optional_param))
2708        ) {
2709            if ((isset($multi_edit_salt[$key])
2710                && ($multi_edit_funcs[$key] === 'AES_ENCRYPT'
2711                || $multi_edit_funcs[$key] === 'AES_DECRYPT'))
2712                || (! empty($multi_edit_salt[$key])
2713                && ($multi_edit_funcs[$key] === 'DES_ENCRYPT'
2714                || $multi_edit_funcs[$key] === 'DES_DECRYPT'
2715                || $multi_edit_funcs[$key] === 'ENCRYPT'))
2716            ) {
2717                return $multi_edit_funcs[$key] . '(' . $current_value . ",'"
2718                    . $this->dbi->escapeString($multi_edit_salt[$key]) . "')";
2719            }
2720
2721            return $multi_edit_funcs[$key] . '(' . $current_value . ')';
2722        }
2723
2724        return $multi_edit_funcs[$key] . '()';
2725    }
2726
2727    /**
2728     * Get query values array and query fields array for insert and update in multi edit
2729     *
2730     * @param array  $multi_edit_columns_name      multiple edit columns name array
2731     * @param array  $multi_edit_columns_null      multiple edit columns null array
2732     * @param string $current_value                current value in the column in loop
2733     * @param array  $multi_edit_columns_prev      multiple edit previous columns array
2734     * @param array  $multi_edit_funcs             multiple edit functions array
2735     * @param bool   $is_insert                    boolean value whether insert or not
2736     * @param array  $query_values                 SET part of the sql query
2737     * @param array  $query_fields                 array of query fields
2738     * @param string $current_value_as_an_array    current value in the column
2739     *                                             as an array
2740     * @param array  $value_sets                   array of valu sets
2741     * @param string $key                          an md5 of the column name
2742     * @param array  $multi_edit_columns_null_prev array of multiple edit columns
2743     *                                             null previous
2744     *
2745     * @return array ($query_values, $query_fields)
2746     */
2747    public function getQueryValuesForInsertAndUpdateInMultipleEdit(
2748        $multi_edit_columns_name,
2749        $multi_edit_columns_null,
2750        $current_value,
2751        $multi_edit_columns_prev,
2752        $multi_edit_funcs,
2753        $is_insert,
2754        $query_values,
2755        $query_fields,
2756        $current_value_as_an_array,
2757        $value_sets,
2758        $key,
2759        $multi_edit_columns_null_prev
2760    ) {
2761        //  i n s e r t
2762        if ($is_insert) {
2763            // no need to add column into the valuelist
2764            if (strlen($current_value_as_an_array) > 0) {
2765                $query_values[] = $current_value_as_an_array;
2766                // first inserted row so prepare the list of fields
2767                if (empty($value_sets)) {
2768                    $query_fields[] = Util::backquote(
2769                        $multi_edit_columns_name[$key]
2770                    );
2771                }
2772            }
2773        } elseif (! empty($multi_edit_columns_null_prev[$key])
2774            && ! isset($multi_edit_columns_null[$key])
2775        ) {
2776            //  u p d a t e
2777
2778            // field had the null checkbox before the update
2779            // field no longer has the null checkbox
2780            $query_values[]
2781                = Util::backquote($multi_edit_columns_name[$key])
2782                . ' = ' . $current_value_as_an_array;
2783        } elseif (! (empty($multi_edit_funcs[$key])
2784            && isset($multi_edit_columns_prev[$key])
2785            && (($current_value === "'" . $this->dbi->escapeString($multi_edit_columns_prev[$key]) . "'")
2786            || ($current_value === '0x' . $multi_edit_columns_prev[$key])))
2787            && ! empty($current_value)
2788        ) {
2789            // avoid setting a field to NULL when it's already NULL
2790            // (field had the null checkbox before the update
2791            //  field still has the null checkbox)
2792            if (empty($multi_edit_columns_null_prev[$key])
2793                || empty($multi_edit_columns_null[$key])
2794            ) {
2795                 $query_values[]
2796                     = Util::backquote($multi_edit_columns_name[$key])
2797                    . ' = ' . $current_value_as_an_array;
2798            }
2799        }
2800
2801        return [
2802            $query_values,
2803            $query_fields,
2804        ];
2805    }
2806
2807    /**
2808     * Get the current column value in the form for different data types
2809     *
2810     * @param string|false $possibly_uploaded_val        uploaded file content
2811     * @param string       $key                          an md5 of the column name
2812     * @param array|null   $multi_edit_columns_type      array of multi edit column types
2813     * @param string       $current_value                current column value in the form
2814     * @param array|null   $multi_edit_auto_increment    multi edit auto increment
2815     * @param int          $rownumber                    index of where clause array
2816     * @param array        $multi_edit_columns_name      multi edit column names array
2817     * @param array        $multi_edit_columns_null      multi edit columns null array
2818     * @param array        $multi_edit_columns_null_prev multi edit columns previous null
2819     * @param bool         $is_insert                    whether insert or not
2820     * @param bool         $using_key                    whether editing or new row
2821     * @param string       $where_clause                 where clause
2822     * @param string       $table                        table name
2823     * @param array        $multi_edit_funcs             multiple edit functions array
2824     *
2825     * @return string  current column value in the form
2826     */
2827    public function getCurrentValueForDifferentTypes(
2828        $possibly_uploaded_val,
2829        $key,
2830        ?array $multi_edit_columns_type,
2831        $current_value,
2832        ?array $multi_edit_auto_increment,
2833        $rownumber,
2834        $multi_edit_columns_name,
2835        $multi_edit_columns_null,
2836        $multi_edit_columns_null_prev,
2837        $is_insert,
2838        $using_key,
2839        $where_clause,
2840        $table,
2841        $multi_edit_funcs
2842    ) {
2843        // Fetch the current values of a row to use in case we have a protected field
2844        if ($is_insert
2845            && $using_key && isset($multi_edit_columns_type)
2846            && is_array($multi_edit_columns_type) && ! empty($where_clause)
2847        ) {
2848            $protected_row = $this->dbi->fetchSingleRow(
2849                'SELECT * FROM ' . Util::backquote($table)
2850                . ' WHERE ' . $where_clause . ';'
2851            );
2852        }
2853
2854        if ($possibly_uploaded_val !== false) {
2855            $current_value = $possibly_uploaded_val;
2856        } elseif (! empty($multi_edit_funcs[$key])) {
2857            $current_value = "'" . $this->dbi->escapeString($current_value)
2858                . "'";
2859        } else {
2860            // c o l u m n    v a l u e    i n    t h e    f o r m
2861            if (isset($multi_edit_columns_type[$key])) {
2862                $type = $multi_edit_columns_type[$key];
2863            } else {
2864                $type = '';
2865            }
2866
2867            if ($type !== 'protected' && $type !== 'set' && strlen($current_value) === 0) {
2868                // best way to avoid problems in strict mode
2869                // (works also in non-strict mode)
2870                if (isset($multi_edit_auto_increment, $multi_edit_auto_increment[$key])) {
2871                    $current_value = 'NULL';
2872                } else {
2873                    $current_value = "''";
2874                }
2875            } elseif ($type === 'set') {
2876                if (! empty($_POST['fields']['multi_edit'][$rownumber][$key])) {
2877                    $current_value = implode(
2878                        ',',
2879                        $_POST['fields']['multi_edit'][$rownumber][$key]
2880                    );
2881                    $current_value = "'"
2882                        . $this->dbi->escapeString($current_value) . "'";
2883                } else {
2884                     $current_value = "''";
2885                }
2886            } elseif ($type === 'protected') {
2887                // here we are in protected mode (asked in the config)
2888                // so tbl_change has put this special value in the
2889                // columns array, so we do not change the column value
2890                // but we can still handle column upload
2891
2892                // when in UPDATE mode, do not alter field's contents. When in INSERT
2893                // mode, insert empty field because no values were submitted.
2894                // If protected blobs where set, insert original fields content.
2895                if (! empty($protected_row[$multi_edit_columns_name[$key]])) {
2896                    $current_value = '0x'
2897                        . bin2hex($protected_row[$multi_edit_columns_name[$key]]);
2898                } else {
2899                    $current_value = '';
2900                }
2901            } elseif ($type === 'hex') {
2902                if (substr($current_value, 0, 2) != '0x') {
2903                    $current_value = '0x' . $current_value;
2904                }
2905            } elseif ($type === 'bit') {
2906                $current_value = preg_replace('/[^01]/', '0', $current_value);
2907                $current_value = "b'" . $this->dbi->escapeString($current_value)
2908                    . "'";
2909            } elseif (! ($type === 'datetime' || $type === 'timestamp' || $type === 'date')
2910                || ($current_value !== 'CURRENT_TIMESTAMP'
2911                    && $current_value !== 'current_timestamp()')
2912            ) {
2913                $current_value = "'" . $this->dbi->escapeString($current_value)
2914                    . "'";
2915            }
2916
2917            // Was the Null checkbox checked for this field?
2918            // (if there is a value, we ignore the Null checkbox: this could
2919            // be possible if Javascript is disabled in the browser)
2920            if (! empty($multi_edit_columns_null[$key])
2921                && ($current_value == "''" || $current_value == '')
2922            ) {
2923                $current_value = 'NULL';
2924            }
2925
2926            // The Null checkbox was unchecked for this field
2927            if (empty($current_value)
2928                && ! empty($multi_edit_columns_null_prev[$key])
2929                && ! isset($multi_edit_columns_null[$key])
2930            ) {
2931                $current_value = "''";
2932            }
2933        }
2934
2935        return $current_value;
2936    }
2937
2938    /**
2939     * Check whether inline edited value can be truncated or not,
2940     * and add additional parameters for extra_data array  if needed
2941     *
2942     * @param string $db          Database name
2943     * @param string $table       Table name
2944     * @param string $column_name Column name
2945     * @param array  $extra_data  Extra data for ajax response
2946     *
2947     * @return void
2948     */
2949    public function verifyWhetherValueCanBeTruncatedAndAppendExtraData(
2950        $db,
2951        $table,
2952        $column_name,
2953        array &$extra_data
2954    ) {
2955        $extra_data['isNeedToRecheck'] = false;
2956
2957        $sql_for_real_value = 'SELECT ' . Util::backquote($table) . '.'
2958            . Util::backquote($column_name)
2959            . ' FROM ' . Util::backquote($db) . '.'
2960            . Util::backquote($table)
2961            . ' WHERE ' . $_POST['where_clause'][0];
2962
2963        $result = $this->dbi->tryQuery($sql_for_real_value);
2964        $fields_meta = $this->dbi->getFieldsMeta($result);
2965        $meta = $fields_meta[0];
2966        $row = $this->dbi->fetchRow($result);
2967
2968        if ($row) {
2969            $new_value = $row[0];
2970            if ((substr($meta->type, 0, 9) === 'timestamp')
2971                || ($meta->type === 'datetime')
2972                || ($meta->type === 'time')
2973            ) {
2974                $new_value = Util::addMicroseconds($new_value);
2975            } elseif (mb_strpos($meta->flags, 'binary') !== false) {
2976                $new_value = '0x' . bin2hex($new_value);
2977            }
2978            $extra_data['isNeedToRecheck'] = true;
2979            $extra_data['truncatableFieldValue'] = $new_value;
2980        }
2981        $this->dbi->freeResult($result);
2982    }
2983
2984    /**
2985     * Function to get the columns of a table
2986     *
2987     * @param string $db    current db
2988     * @param string $table current table
2989     *
2990     * @return array
2991     */
2992    public function getTableColumns($db, $table)
2993    {
2994        $this->dbi->selectDb($db);
2995
2996        return array_values($this->dbi->getColumns($db, $table, null, true));
2997    }
2998
2999    /**
3000     * Function to determine Insert/Edit rows
3001     *
3002     * @param string $where_clause where clause
3003     * @param string $db           current database
3004     * @param string $table        current table
3005     *
3006     * @return array
3007     */
3008    public function determineInsertOrEdit($where_clause, $db, $table): array
3009    {
3010        if (isset($_POST['where_clause'])) {
3011            $where_clause = $_POST['where_clause'];
3012        }
3013        if (isset($_SESSION['edit_next'])) {
3014            $where_clause = $_SESSION['edit_next'];
3015            unset($_SESSION['edit_next']);
3016            $after_insert = 'edit_next';
3017        }
3018        if (isset($_POST['ShowFunctionFields'])) {
3019            $GLOBALS['cfg']['ShowFunctionFields'] = $_POST['ShowFunctionFields'];
3020        }
3021        if (isset($_POST['ShowFieldTypesInDataEditView'])) {
3022            $GLOBALS['cfg']['ShowFieldTypesInDataEditView']
3023                = $_POST['ShowFieldTypesInDataEditView'];
3024        }
3025        if (isset($_POST['after_insert'])) {
3026            $after_insert = $_POST['after_insert'];
3027        }
3028
3029        if (isset($where_clause)) {
3030            // we are editing
3031            $insert_mode = false;
3032            $where_clause_array = $this->getWhereClauseArray($where_clause);
3033            [$where_clauses, $result, $rows, $found_unique_key]
3034                = $this->analyzeWhereClauses(
3035                    $where_clause_array,
3036                    $table,
3037                    $db
3038                );
3039        } else {
3040            // we are inserting
3041            $insert_mode = true;
3042            $where_clause = null;
3043            [$result, $rows] = $this->loadFirstRow($table, $db);
3044            $where_clauses = null;
3045            $where_clause_array = [];
3046            $found_unique_key = false;
3047        }
3048
3049        // Copying a row - fetched data will be inserted as a new row,
3050        // therefore the where clause is needless.
3051        if (isset($_POST['default_action'])
3052            && $_POST['default_action'] === 'insert'
3053        ) {
3054            $where_clause = $where_clauses = null;
3055        }
3056
3057        return [
3058            $insert_mode,
3059            $where_clause,
3060            $where_clause_array,
3061            $where_clauses,
3062            $result,
3063            $rows,
3064            $found_unique_key,
3065            $after_insert ?? null,
3066        ];
3067    }
3068
3069    /**
3070     * Function to get comments for the table columns
3071     *
3072     * @param string $db    current database
3073     * @param string $table current table
3074     *
3075     * @return array comments for columns
3076     */
3077    public function getCommentsMap($db, $table)
3078    {
3079        $comments_map = [];
3080
3081        if ($GLOBALS['cfg']['ShowPropertyComments']) {
3082            $comments_map = $this->relation->getComments($db, $table);
3083        }
3084
3085        return $comments_map;
3086    }
3087
3088    /**
3089     * Function to get URL parameters
3090     *
3091     * @param string $db    current database
3092     * @param string $table current table
3093     *
3094     * @return array url parameters
3095     */
3096    public function getUrlParameters($db, $table)
3097    {
3098        global $goto;
3099        /**
3100         * @todo check if we could replace by "db_|tbl_" - please clarify!?
3101         */
3102        $url_params = [
3103            'db' => $db,
3104            'sql_query' => $_POST['sql_query'] ?? '',
3105        ];
3106
3107        if (strpos($goto, 'tbl_') === 0 || strpos($goto, 'index.php?route=/table') === 0) {
3108            $url_params['table'] = $table;
3109        }
3110
3111        return $url_params;
3112    }
3113
3114    /**
3115     * Function to get html for the gis editor div
3116     *
3117     * @return string
3118     */
3119    public function getHtmlForGisEditor()
3120    {
3121        return '<div id="gis_editor"></div>'
3122            . '<div id="popup_background"></div>'
3123            . '<br>';
3124    }
3125
3126    /**
3127     * Function to get html for the ignore option in insert mode
3128     *
3129     * @param int  $row_id  row id
3130     * @param bool $checked ignore option is checked or not
3131     *
3132     * @return string
3133     */
3134    public function getHtmlForIgnoreOption($row_id, $checked = true)
3135    {
3136        return '<input type="checkbox"'
3137                . ($checked ? ' checked="checked"' : '')
3138                . ' name="insert_ignore_' . $row_id . '"'
3139                . ' id="insert_ignore_' . $row_id . '">'
3140                . '<label for="insert_ignore_' . $row_id . '">'
3141                . __('Ignore')
3142                . '</label><br>' . "\n";
3143    }
3144
3145    /**
3146     * Function to get html for the function option
3147     *
3148     * @param array  $column               column
3149     * @param string $column_name_appendix column name appendix
3150     */
3151    private function getHtmlForFunctionOption(array $column, $column_name_appendix): string
3152    {
3153        return '<tr class="noclick">'
3154            . '<td '
3155            . 'class="text-center">'
3156            . $column['Field_title']
3157            . '<input type="hidden" name="fields_name' . $column_name_appendix
3158            . '" value="' . $column['Field_html'] . '">'
3159            . '</td>';
3160    }
3161
3162    /**
3163     * Function to get html for the column type
3164     *
3165     * @param array $column column
3166     *
3167     * @return string
3168     */
3169    private function getHtmlForInsertEditColumnType(array $column)
3170    {
3171        return '<td class="text-center' . $column['wrap'] . '">'
3172            . '<span class="column_type" dir="ltr">' . $column['pma_type'] . '</span>'
3173            . '</td>';
3174    }
3175
3176    /**
3177     * Function to get html for the insert edit form header
3178     *
3179     * @param bool $has_blob_field whether has blob field
3180     * @param bool $is_upload      whether is upload
3181     *
3182     * @return string
3183     */
3184    public function getHtmlForInsertEditFormHeader($has_blob_field, $is_upload)
3185    {
3186        $html_output = '<form id="insertForm" class="lock-page ';
3187        if ($has_blob_field && $is_upload) {
3188            $html_output .= 'disableAjax';
3189        }
3190        $html_output .= '" method="post" action="' . Url::getFromRoute('/table/replace') . '" name="insertForm" ';
3191        if ($is_upload) {
3192            $html_output .= ' enctype="multipart/form-data"';
3193        }
3194        $html_output .= '>';
3195
3196        return $html_output;
3197    }
3198
3199    /**
3200     * Function to get html for each insert/edit column
3201     *
3202     * @param array  $table_columns         table columns
3203     * @param int    $column_number         column index in table_columns
3204     * @param array  $comments_map          comments map
3205     * @param bool   $timestamp_seen        whether timestamp seen
3206     * @param array  $current_result        current result
3207     * @param string $chg_evt_handler       javascript change event handler
3208     * @param string $jsvkey                javascript validation key
3209     * @param string $vkey                  validation key
3210     * @param bool   $insert_mode           whether insert mode
3211     * @param array  $current_row           current row
3212     * @param int    $o_rows                row offset
3213     * @param int    $tabindex              tab index
3214     * @param int    $columns_cnt           columns count
3215     * @param bool   $is_upload             whether upload
3216     * @param int    $tabindex_for_function tab index offset for function
3217     * @param array  $foreigners            foreigners
3218     * @param int    $tabindex_for_null     tab index offset for null
3219     * @param int    $tabindex_for_value    tab index offset for value
3220     * @param string $table                 table
3221     * @param string $db                    database
3222     * @param int    $row_id                row id
3223     * @param int    $biggest_max_file_size biggest max file size
3224     * @param string $default_char_editing  default char editing mode which is stored
3225     *                                      in the config.inc.php script
3226     * @param string $text_dir              text direction
3227     * @param array  $repopulate            the data to be repopulated
3228     * @param array  $column_mime           the mime information of column
3229     * @param string $where_clause          the where clause
3230     *
3231     * @return string
3232     */
3233    private function getHtmlForInsertEditFormColumn(
3234        array $table_columns,
3235        $column_number,
3236        array $comments_map,
3237        $timestamp_seen,
3238        $current_result,
3239        $chg_evt_handler,
3240        $jsvkey,
3241        $vkey,
3242        $insert_mode,
3243        array $current_row,
3244        &$o_rows,
3245        &$tabindex,
3246        $columns_cnt,
3247        $is_upload,
3248        $tabindex_for_function,
3249        array $foreigners,
3250        $tabindex_for_null,
3251        $tabindex_for_value,
3252        $table,
3253        $db,
3254        $row_id,
3255        $biggest_max_file_size,
3256        $default_char_editing,
3257        $text_dir,
3258        array $repopulate,
3259        array $column_mime,
3260        $where_clause
3261    ) {
3262        $column = $table_columns[$column_number];
3263        $readOnly = false;
3264
3265        if (! isset($column['processed'])) {
3266            $column = $this->analyzeTableColumnsArray(
3267                $column,
3268                $comments_map,
3269                $timestamp_seen
3270            );
3271        }
3272        $as_is = false;
3273        if (! empty($repopulate) && ! empty($current_row)) {
3274            $current_row[$column['Field']] = $repopulate[$column['Field_md5']];
3275            $as_is = true;
3276        }
3277
3278        $extracted_columnspec
3279            = Util::extractColumnSpec($column['Type']);
3280
3281        if ($column['len'] === -1) {
3282            $column['len'] = $this->dbi->fieldLen(
3283                $current_result,
3284                $column_number
3285            );
3286            // length is unknown for geometry fields,
3287            // make enough space to edit very simple WKTs
3288            if ($column['len'] === -1) {
3289                $column['len'] = 30;
3290            }
3291        }
3292        //Call validation when the form submitted...
3293        $onChangeClause = $chg_evt_handler
3294            . "=\"return verificationsAfterFieldChange('"
3295            . Sanitize::escapeJsString($column['Field_md5']) . "', '"
3296            . Sanitize::escapeJsString($jsvkey) . "','" . $column['pma_type'] . "')\"";
3297
3298        // Use an MD5 as an array index to avoid having special characters
3299        // in the name attribute (see bug #1746964 )
3300        $column_name_appendix = $vkey . '[' . $column['Field_md5'] . ']';
3301
3302        if ($column['Type'] === 'datetime' && $column['Null'] !== 'YES'
3303            && ! isset($column['Default']) && $insert_mode) {
3304            $column['Default'] = date('Y-m-d H:i:s', time());
3305        }
3306
3307        $html_output = $this->getHtmlForFunctionOption(
3308            $column,
3309            $column_name_appendix
3310        );
3311
3312        if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) {
3313            $html_output .= $this->getHtmlForInsertEditColumnType($column);
3314        }
3315
3316        // Get a list of GIS data types.
3317        $gis_data_types = Util::getGISDatatypes();
3318
3319        // Prepares the field value
3320        $real_null_value = false;
3321        $special_chars_encoded = '';
3322        if (! empty($current_row)) {
3323            // (we are editing)
3324            [
3325                $real_null_value,
3326                $special_chars_encoded,
3327                $special_chars,
3328                $data,
3329                $backup_field,
3330            ]
3331                = $this->getSpecialCharsAndBackupFieldForExistingRow(
3332                    $current_row,
3333                    $column,
3334                    $extracted_columnspec,
3335                    $real_null_value,
3336                    $gis_data_types,
3337                    $column_name_appendix,
3338                    $as_is
3339                );
3340        } else {
3341            // (we are inserting)
3342            // display default values
3343            $tmp = $column;
3344            if (isset($repopulate[$column['Field_md5']])) {
3345                $tmp['Default'] = $repopulate[$column['Field_md5']];
3346            }
3347            [
3348                $real_null_value,
3349                $data,
3350                $special_chars,
3351                $backup_field,
3352                $special_chars_encoded,
3353            ]
3354                = $this->getSpecialCharsAndBackupFieldForInsertingMode(
3355                    $tmp,
3356                    $real_null_value
3357                );
3358            unset($tmp);
3359        }
3360
3361        $idindex = ($o_rows * $columns_cnt) + $column_number + 1;
3362        $tabindex = $idindex;
3363
3364        // Get a list of data types that are not yet supported.
3365        $no_support_types = Util::unsupportedDatatypes();
3366
3367        // The function column
3368        // -------------------
3369        $foreignData = $this->relation->getForeignData(
3370            $foreigners,
3371            $column['Field'],
3372            false,
3373            '',
3374            ''
3375        );
3376        if ($GLOBALS['cfg']['ShowFunctionFields']) {
3377            $html_output .= $this->getFunctionColumn(
3378                $column,
3379                $is_upload,
3380                $column_name_appendix,
3381                $onChangeClause,
3382                $no_support_types,
3383                $tabindex_for_function,
3384                $tabindex,
3385                $idindex,
3386                $insert_mode,
3387                $readOnly,
3388                $foreignData
3389            );
3390        }
3391
3392        // The null column
3393        // ---------------
3394        $html_output .= $this->getNullColumn(
3395            $column,
3396            $column_name_appendix,
3397            $real_null_value,
3398            $tabindex,
3399            $tabindex_for_null,
3400            $idindex,
3401            $vkey,
3402            $foreigners,
3403            $foreignData,
3404            $readOnly
3405        );
3406
3407        // The value column (depends on type)
3408        // ----------------
3409        // See bug #1667887 for the reason why we don't use the maxlength
3410        // HTML attribute
3411
3412        //add data attributes "no of decimals" and "data type"
3413        $no_decimals = 0;
3414        $type = current(explode('(', $column['pma_type']));
3415        if (preg_match('/\(([^()]+)\)/', $column['pma_type'], $match)) {
3416            $match[0] = trim($match[0], '()');
3417            $no_decimals = $match[0];
3418        }
3419        $html_output .= '<td data-type="' . $type . '" data-decimals="'
3420            . $no_decimals . '">' . "\n";
3421        // Will be used by js/table/change.js to set the default value
3422        // for the "Continue insertion" feature
3423        $html_output .= '<span class="default_value hide">'
3424            . $special_chars . '</span>';
3425
3426        // Check input transformation of column
3427        $transformed_html = '';
3428        if (! empty($column_mime['input_transformation'])) {
3429            $file = $column_mime['input_transformation'];
3430            $include_file = 'libraries/classes/Plugins/Transformations/' . $file;
3431            if (is_file(ROOT_PATH . $include_file)) {
3432                $class_name = $this->transformations->getClassName($include_file);
3433                if (class_exists($class_name)) {
3434                    $transformation_plugin = new $class_name();
3435                    $transformation_options = $this->transformations->getOptions(
3436                        $column_mime['input_transformation_options']
3437                    );
3438                    $_url_params = [
3439                        'db'            => $db,
3440                        'table'         => $table,
3441                        'transform_key' => $column['Field'],
3442                        'where_clause_sign' => Core::signSqlQuery($where_clause),
3443                        'where_clause'  => $where_clause,
3444                    ];
3445                    $transformation_options['wrapper_link'] = Url::getCommon($_url_params);
3446                    $transformation_options['wrapper_params'] = $_url_params;
3447                    $current_value = '';
3448                    if (isset($current_row[$column['Field']])) {
3449                        $current_value = $current_row[$column['Field']];
3450                    }
3451                    if (method_exists($transformation_plugin, 'getInputHtml')) {
3452                        $transformed_html = $transformation_plugin->getInputHtml(
3453                            $column,
3454                            $row_id,
3455                            $column_name_appendix,
3456                            $transformation_options,
3457                            $current_value,
3458                            $text_dir,
3459                            $tabindex,
3460                            $tabindex_for_value,
3461                            $idindex
3462                        );
3463                    }
3464                    if (method_exists($transformation_plugin, 'getScripts')) {
3465                        $GLOBALS['plugin_scripts'] = array_merge(
3466                            $GLOBALS['plugin_scripts'],
3467                            $transformation_plugin->getScripts()
3468                        );
3469                    }
3470                }
3471            }
3472        }
3473        if (! empty($transformed_html)) {
3474            $html_output .= $transformed_html;
3475        } else {
3476            $html_output .= $this->getValueColumn(
3477                $column,
3478                $backup_field,
3479                $column_name_appendix,
3480                $onChangeClause,
3481                $tabindex,
3482                $tabindex_for_value,
3483                $idindex,
3484                $data,
3485                $special_chars,
3486                $foreignData,
3487                [
3488                    $table,
3489                    $db,
3490                ],
3491                $row_id,
3492                $text_dir,
3493                $special_chars_encoded,
3494                $vkey,
3495                $is_upload,
3496                $biggest_max_file_size,
3497                $default_char_editing,
3498                $no_support_types,
3499                $gis_data_types,
3500                $extracted_columnspec,
3501                $readOnly
3502            );
3503        }
3504
3505        return $html_output;
3506    }
3507
3508    /**
3509     * Function to get html for each insert/edit row
3510     *
3511     * @param array  $url_params            url parameters
3512     * @param array  $table_columns         table columns
3513     * @param array  $comments_map          comments map
3514     * @param bool   $timestamp_seen        whether timestamp seen
3515     * @param array  $current_result        current result
3516     * @param string $chg_evt_handler       javascript change event handler
3517     * @param string $jsvkey                javascript validation key
3518     * @param string $vkey                  validation key
3519     * @param bool   $insert_mode           whether insert mode
3520     * @param array  $current_row           current row
3521     * @param int    $o_rows                row offset
3522     * @param int    $tabindex              tab index
3523     * @param int    $columns_cnt           columns count
3524     * @param bool   $is_upload             whether upload
3525     * @param int    $tabindex_for_function tab index offset for function
3526     * @param array  $foreigners            foreigners
3527     * @param int    $tabindex_for_null     tab index offset for null
3528     * @param int    $tabindex_for_value    tab index offset for value
3529     * @param string $table                 table
3530     * @param string $db                    database
3531     * @param int    $row_id                row id
3532     * @param int    $biggest_max_file_size biggest max file size
3533     * @param string $text_dir              text direction
3534     * @param array  $repopulate            the data to be repopulated
3535     * @param array  $where_clause_array    the array of where clauses
3536     *
3537     * @return string
3538     */
3539    public function getHtmlForInsertEditRow(
3540        array $url_params,
3541        array $table_columns,
3542        array $comments_map,
3543        $timestamp_seen,
3544        $current_result,
3545        $chg_evt_handler,
3546        $jsvkey,
3547        $vkey,
3548        $insert_mode,
3549        array $current_row,
3550        &$o_rows,
3551        &$tabindex,
3552        $columns_cnt,
3553        $is_upload,
3554        $tabindex_for_function,
3555        array $foreigners,
3556        $tabindex_for_null,
3557        $tabindex_for_value,
3558        $table,
3559        $db,
3560        $row_id,
3561        $biggest_max_file_size,
3562        $text_dir,
3563        array $repopulate,
3564        array $where_clause_array
3565    ) {
3566        $html_output = $this->getHeadAndFootOfInsertRowTable($url_params)
3567            . '<tbody>';
3568
3569        //store the default value for CharEditing
3570        $default_char_editing = $GLOBALS['cfg']['CharEditing'];
3571        $mime_map = $this->transformations->getMime($db, $table);
3572        $where_clause = '';
3573        if (isset($where_clause_array[$row_id])) {
3574            $where_clause = $where_clause_array[$row_id];
3575        }
3576        for ($column_number = 0; $column_number < $columns_cnt; $column_number++) {
3577            $table_column = $table_columns[$column_number];
3578            $column_mime = [];
3579            if (isset($mime_map[$table_column['Field']])) {
3580                $column_mime = $mime_map[$table_column['Field']];
3581            }
3582
3583            $virtual = [
3584                'VIRTUAL',
3585                'PERSISTENT',
3586                'VIRTUAL GENERATED',
3587                'STORED GENERATED',
3588            ];
3589            if (in_array($table_column['Extra'], $virtual)) {
3590                continue;
3591            }
3592
3593            $html_output .= $this->getHtmlForInsertEditFormColumn(
3594                $table_columns,
3595                $column_number,
3596                $comments_map,
3597                $timestamp_seen,
3598                $current_result,
3599                $chg_evt_handler,
3600                $jsvkey,
3601                $vkey,
3602                $insert_mode,
3603                $current_row,
3604                $o_rows,
3605                $tabindex,
3606                $columns_cnt,
3607                $is_upload,
3608                $tabindex_for_function,
3609                $foreigners,
3610                $tabindex_for_null,
3611                $tabindex_for_value,
3612                $table,
3613                $db,
3614                $row_id,
3615                $biggest_max_file_size,
3616                $default_char_editing,
3617                $text_dir,
3618                $repopulate,
3619                $column_mime,
3620                $where_clause
3621            );
3622        }
3623        $o_rows++;
3624
3625        return $html_output . '  </tbody>'
3626            . '</table></div><br>'
3627            . '<div class="clearfloat"></div>';
3628    }
3629}
3630