InsertEdit   F
last analyzed

Complexity

Total Complexity 260

Size/Duplication

Total Lines 1965
Duplicated Lines 0 %

Test Coverage

Coverage 87.88%

Importance

Changes 0
Metric Value
wmc 260
eloc 874
c 0
b 0
f 0
dl 0
loc 1965
ccs 805
cts 916
cp 0.8788
rs 1.726

47 Methods

Rating   Name   Duplication   Size   Complexity  
B executeSqlQuery() 0 48 8
A getContinueInsertionForm() 0 15 1
A __construct() 0 8 1
A getInsertRows() 0 4 1
A isColumn() 0 9 3
A getValueColumnForOtherDatatypes() 0 46 4
B getQueryValueForUpdate() 0 29 8
D formatAsSqlValueBasedOnType() 0 71 19
B getNullifyCodeForNullColumn() 0 21 8
A isWhereClauseNumeric() 0 21 5
B isColumnBinary() 0 9 8
A transformEditedValues() 0 39 5
C formatAsSqlFunction() 0 48 17
A getCommentsMap() 0 7 2
D getSpecialCharsAndBackupFieldForExistingRow() 0 76 18
A setSessionForEditNext() 0 17 2
A getErrorUrl() 0 3 1
A getSelectOptionForUpload() 0 23 3
B determineInsertOrEdit() 0 60 8
A getHeadAndFootOfInsertRowTable() 0 18 3
A getMaxUploadSize() 0 17 1
A showEmptyResultMessageOrSetUniqueCondition() 0 29 2
A getHtmlForInsertEditFormHeader() 0 7 1
A loadFirstRow() 0 5 1
A getColumnSize() 0 23 3
B getFormParametersForInsertForm() 0 34 8
B getGotoInclude() 0 26 10
B verifyWhetherValueCanBeTruncatedAndAppendExtraData() 0 38 6
A getQueryValueForInsert() 0 17 5
A getWarningMessages() 0 8 2
B getHtmlInput() 0 43 9
A getHtmlForInsertEditRow() 0 60 3
A getDisplayValueForForeignTableColumn() 0 26 5
B getSpecialCharsForInsertingMode() 0 22 10
A getTableColumns() 0 5 1
A getValueFormattedAsSql() 0 13 3
B getTextarea() 0 41 6
A getGisFromWKBFunctions() 0 22 2
A analyzeWhereClauses() 0 33 3
A getGisFromTextFunctions() 0 22 2
A getLinkForRelationalDisplayField() 0 49 5
A getColumnTitle() 0 9 2
A getHtmlForIgnoreOption() 0 9 2
A getHtmlForGisEditor() 0 3 1
F getHtmlForInsertEditFormColumn() 0 292 35
A showTypeOrFunctionLabel() 0 6 1
B showTypeOrFunction() 0 30 6

How to fix   Complexity   

Complex Class

Complex classes like InsertEdit often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use InsertEdit, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin;
6
7
use PhpMyAdmin\ConfigStorage\Relation;
8
use PhpMyAdmin\Dbal\ResultInterface;
9
use PhpMyAdmin\Html\Generator;
10
use PhpMyAdmin\Plugins\IOTransformationsPlugin;
11
use PhpMyAdmin\Plugins\TransformationsInterface;
12
use PhpMyAdmin\Utils\Gis;
13
14
use function __;
15
use function array_fill;
16
use function array_key_exists;
17
use function array_merge;
18
use function array_values;
19
use function bin2hex;
20
use function count;
21
use function current;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PhpMyAdmin\current. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
22
use function explode;
23
use function htmlspecialchars;
24
use function implode;
25
use function in_array;
26
use function is_array;
27
use function is_string;
28
use function json_encode;
29
use function max;
30
use function mb_stripos;
31
use function mb_strlen;
32
use function min;
33
use function password_hash;
34
use function preg_match;
35
use function preg_replace;
36
use function str_contains;
37
use function str_ends_with;
38
use function str_replace;
39
use function str_starts_with;
40
use function stripcslashes;
41
use function stripslashes;
42
use function substr;
43
use function trim;
44
45
use const ENT_COMPAT;
46
use const PASSWORD_DEFAULT;
47
48
class InsertEdit
49
{
50
    private const FUNC_OPTIONAL_PARAM = ['RAND', 'UNIX_TIMESTAMP'];
51
52
    private const FUNC_NO_PARAM = [
53
        'CONNECTION_ID',
54
        'CURRENT_USER',
55
        'CURDATE',
56
        'CURTIME',
57
        'CURRENT_DATE',
58
        'CURRENT_TIME',
59
        'DATABASE',
60
        'LAST_INSERT_ID',
61
        'NOW',
62
        'PI',
63
        'RAND',
64
        'SYSDATE',
65
        'UNIX_TIMESTAMP',
66
        'USER',
67
        'UTC_DATE',
68
        'UTC_TIME',
69
        'UTC_TIMESTAMP',
70
        'UUID',
71
        'UUID_SHORT',
72
        'VERSION',
73
    ];
74
75
    private int $rowOffset = 0;
76
    private int $fieldIndex = 0;
77
78 188
    public function __construct(
79
        private readonly DatabaseInterface $dbi,
80
        private readonly Relation $relation,
81
        private readonly Transformations $transformations,
82
        private readonly FileListing $fileListing,
83
        private readonly Template $template,
84
        private readonly Config $config,
85
    ) {
86 188
    }
87
88
    /**
89
     * Retrieve form parameters for insert/edit form
90
     *
91
     * @param string  $db               name of the database
92
     * @param string  $table            name of the table
93
     * @param mixed[] $whereClauseArray
94
     *
95
     * @return array<string, string> array of insert/edit form parameters
96
     */
97 8
    public function getFormParametersForInsertForm(
98
        string $db,
99
        string $table,
100
        array|null $whereClauses,
101
        array $whereClauseArray,
102
        string $errorUrl,
103
    ): array {
104 8
        $formParams = [
105 8
            'db' => $db,
106 8
            'table' => $table,
107 8
            'goto' => $GLOBALS['goto'],
108 8
            'err_url' => $errorUrl,
109 8
            'sql_query' => $_POST['sql_query'] ?? '',
110 8
        ];
111
112 8
        if ($formParams['sql_query'] === '' && isset($_GET['sql_query'], $_GET['sql_signature'])) {
113 4
            if (Core::checkSqlQuerySignature($_GET['sql_query'], $_GET['sql_signature'])) {
114 4
                $formParams['sql_query'] = $_GET['sql_query'];
115
            }
116
        }
117
118 8
        if (isset($whereClauses)) {
119 8
            foreach ($whereClauseArray as $keyId => $whereClause) {
120 8
                $formParams['where_clause[' . $keyId . ']'] = trim($whereClause);
121
            }
122
        }
123
124 8
        if (isset($_POST['clause_is_unique'])) {
125 4
            $formParams['clause_is_unique'] = $_POST['clause_is_unique'];
126 4
        } elseif (isset($_GET['clause_is_unique'])) {
127 4
            $formParams['clause_is_unique'] = $_GET['clause_is_unique'];
128
        }
129
130 8
        return $formParams;
131
    }
132
133
    /**
134
     * Analysing where clauses array
135
     *
136
     * @param string[] $whereClauseArray array of where clauses
137
     * @param string   $table            name of the table
138
     * @param string   $db               name of the database
139
     *
140
     * @return array<int, string[]|ResultInterface[]|array<string, string|null>[]|bool>
141
     * @phpstan-return array{string[], ResultInterface[], array<string|null>[], bool}
142
     */
143 8
    private function analyzeWhereClauses(
144
        array $whereClauseArray,
145
        string $table,
146
        string $db,
147
    ): array {
148 8
        $rows = [];
149 8
        $result = [];
150 8
        $whereClauses = [];
151 8
        $foundUniqueKey = false;
152 8
        foreach ($whereClauseArray as $keyId => $whereClause) {
153 8
            $localQuery = 'SELECT * FROM '
154 8
                . Util::backquote($db) . '.'
155 8
                . Util::backquote($table)
156 8
                . ' WHERE ' . $whereClause . ';';
157 8
            $result[$keyId] = $this->dbi->query($localQuery);
158 8
            $rows[$keyId] = $result[$keyId]->fetchAssoc();
159
160 8
            $whereClauses[$keyId] = str_replace('\\', '\\\\', $whereClause);
161 8
            $hasUniqueCondition = $this->showEmptyResultMessageOrSetUniqueCondition(
162 8
                $rows,
163 8
                $keyId,
164 8
                $whereClauseArray,
165 8
                $localQuery,
166 8
                $result,
167 8
            );
168 8
            if (! $hasUniqueCondition) {
169 8
                continue;
170
            }
171
172
            $foundUniqueKey = true;
173
        }
174
175 8
        return [$whereClauses, $result, $rows, $foundUniqueKey];
176
    }
177
178
    /**
179
     * Show message for empty result or set the unique_condition
180
     *
181
     * @param mixed[]           $rows             MySQL returned rows
182
     * @param string|int        $keyId            ID in current key
183
     * @param mixed[]           $whereClauseArray array of where clauses
184
     * @param string            $localQuery       query performed
185
     * @param ResultInterface[] $result           MySQL result handle
186
     */
187 12
    private function showEmptyResultMessageOrSetUniqueCondition(
188
        array $rows,
189
        string|int $keyId,
190
        array $whereClauseArray,
191
        string $localQuery,
192
        array $result,
193
    ): bool {
194
        // No row returned
195 12
        if (! $rows[$keyId]) {
196 8
            unset($rows[$keyId], $whereClauseArray[$keyId]);
197 8
            ResponseRenderer::getInstance()->addHTML(
198 8
                Generator::getMessage(
199 8
                    __('MySQL returned an empty result set (i.e. zero rows).'),
200 8
                    $localQuery,
201 8
                ),
202 8
            );
203
            /**
204
             * @todo not sure what should be done at this point, but we must not
205
             * exit if we want the message to be displayed
206
             */
207
208 8
            return false;
209
        }
210
211 8
        $meta = $this->dbi->getFieldsMeta($result[$keyId]);
212
213 8
        $uniqueCondition = (new UniqueCondition($meta, $rows[$keyId], true))->getWhereClause();
214
215 8
        return (bool) $uniqueCondition;
216
    }
217
218
    /**
219
     * No primary key given, just load first row
220
     */
221 8
    private function loadFirstRow(string $table, string $db): ResultInterface
222
    {
223 8
        return $this->dbi->query(
224 8
            'SELECT * FROM ' . Util::backquote($db)
225 8
            . '.' . Util::backquote($table) . ' LIMIT 1;',
226 8
        );
227
    }
228
229
    /** @return false[] */
230 12
    private function getInsertRows(): array
231
    {
232
        // Can be a string on some old configuration storage settings
233 12
        return array_fill(0, $this->config->settings['InsertRows'], false);
234
    }
235
236
    /**
237
     * Show type information or function selectors in Insert/Edit
238
     *
239
     * @param string  $which     function|type
240
     * @param mixed[] $urlParams containing url parameters
241
     * @param bool    $isShow    whether to show the element in $which
242
     *
243
     * @return string an HTML snippet
244
     */
245 16
    public function showTypeOrFunction(string $which, array $urlParams, bool $isShow): string
246
    {
247 16
        $params = [];
248
249
        switch ($which) {
250 16
            case 'function':
251 16
                $params['ShowFunctionFields'] = $isShow ? 0 : 1;
252 16
                $params['ShowFieldTypesInDataEditView'] = $this->config->settings['ShowFieldTypesInDataEditView'];
253 16
                break;
254 16
            case 'type':
255 16
                $params['ShowFieldTypesInDataEditView'] = $isShow ? 0 : 1;
256 16
                $params['ShowFunctionFields'] = $this->config->settings['ShowFunctionFields'];
257 16
                break;
258
        }
259
260 16
        $params['goto'] = Url::getFromRoute('/sql');
261 16
        $thisUrlParams = array_merge($urlParams, $params);
262
263 16
        if (! $isShow) {
264 4
            return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
265 4
                . Url::getCommon($thisUrlParams, '', false) . '">'
266 4
                . $this->showTypeOrFunctionLabel($which)
267 4
                . '</a>';
268
        }
269
270 16
        return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
271 16
            . Url::getCommon($thisUrlParams, '', false)
272 16
            . '" title="' . __('Hide') . '">'
273 16
            . $this->showTypeOrFunctionLabel($which)
274 16
            . '</a></th>';
275
    }
276
277
    /**
278
     * Show type information or function selectors labels in Insert/Edit
279
     *
280
     * @param string $which function|type
281
     *
282
     * @return string an HTML snippet
283
     */
284 16
    private function showTypeOrFunctionLabel(string $which): string
285
    {
286 16
        return match ($which) {
287 16
            'function' => __('Function'),
288 16
            'type' => __('Type'),
289 16
            default => '',
290 16
        };
291
    }
292
293
    /**
294
     * Retrieve the column title
295
     *
296
     * @param string   $fieldName   name of the column
297
     * @param string[] $commentsMap comments for every column that has a comment
298
     *
299
     * @return string              column title
300
     */
301 16
    private function getColumnTitle(string $fieldName, array $commentsMap): string
302
    {
303 16
        if (isset($commentsMap[$fieldName])) {
304 4
            return '<span style="border-bottom: 1px dashed black;" title="'
305 4
                . htmlspecialchars($commentsMap[$fieldName]) . '">'
306 4
                . htmlspecialchars($fieldName) . '</span>';
307
        }
308
309 16
        return htmlspecialchars($fieldName);
310
    }
311
312
    /**
313
     * check whether the column is of a certain type
314
     * the goal is to ensure that types such as "enum('one','two','binary',..)"
315
     * or "enum('one','two','varbinary',..)" are not categorized as binary
316
     *
317
     * @param string   $columnType column type as specified in the column definition
318
     * @param string[] $types      the types to verify
319
     */
320 16
    public function isColumn(string $columnType, array $types): bool
321
    {
322 16
        foreach ($types as $oneType) {
323 16
            if (mb_stripos($columnType, $oneType) === 0) {
324 8
                return true;
325
            }
326
        }
327
328 16
        return false;
329
    }
330
331
    /**
332
     * Retrieve the nullify code for the null column
333
     *
334
     * @param InsertEditColumn $column      description of column in given table
335
     * @param mixed[]          $foreigners  keys into foreign fields
336
     * @param mixed[]          $foreignData data about the foreign keys
337
     */
338 16
    private function getNullifyCodeForNullColumn(
339
        InsertEditColumn $column,
340
        array $foreigners,
341
        array $foreignData,
342
    ): string {
343 16
        $foreigner = $this->relation->searchColumnInForeigners($foreigners, $column->field);
344 16
        if (str_contains($column->trueType, 'enum')) {
345 4
            $nullifyCode = mb_strlen($column->type) > 20 ? '1' : '2';
346 16
        } elseif (str_contains($column->trueType, 'set')) {
347 4
            $nullifyCode = '3';
348 16
        } elseif ($foreigner && $foreignData['foreign_link'] == false) {
349
            // foreign key in a drop-down
350 4
            $nullifyCode = '4';
351 12
        } elseif ($foreigner && $foreignData['foreign_link'] == true) {
352
            // foreign key with a browsing icon
353
            $nullifyCode = '6';
354
        } else {
355 12
            $nullifyCode = '5';
356
        }
357
358 16
        return $nullifyCode;
359
    }
360
361
    /**
362
     * Get HTML textarea for insert form
363
     *
364
     * @param InsertEditColumn $column              column information
365
     * @param string           $backupField         hidden input field
366
     * @param string           $columnNameAppendix  the name attribute
367
     * @param string           $onChangeClause      onchange clause for fields
368
     * @param string           $textDir             text direction
369
     * @param string           $specialCharsEncoded replaced char if the string starts
370
     *                                                with a \r\n pair (0x0d0a) add an extra \n
371
     * @param string           $dataType            the html5 data-* attribute type
372
     *
373
     * @return string                       an html snippet
374
     */
375 8
    private function getTextarea(
376
        InsertEditColumn $column,
377
        string $backupField,
378
        string $columnNameAppendix,
379
        string $onChangeClause,
380
        string $textDir,
381
        string $specialCharsEncoded,
382
        string $dataType,
383
    ): string {
384 8
        $theClass = '';
385 8
        $textAreaRows = $this->config->settings['TextareaRows'];
386 8
        $textareaCols = $this->config->settings['TextareaCols'];
387
388 8
        if ($column->isChar) {
389
            /**
390
             * @todo clarify the meaning of the "textfield" class and explain
391
             *       why character columns have the "char" class instead
392
             */
393 8
            $theClass = 'char charField';
394 8
            $textAreaRows = $this->config->settings['CharTextareaRows'];
395 8
            $textareaCols = $this->config->settings['CharTextareaCols'];
396 8
            $extractedColumnspec = Util::extractColumnSpec($column->type);
397 8
            $maxlength = $extractedColumnspec['spec_in_brackets'];
398
        } elseif ($this->config->settings['LongtextDoubleTextarea'] && str_contains($column->pmaType, 'longtext')) {
399
            $textAreaRows = $this->config->settings['TextareaRows'] * 2;
400
            $textareaCols = $this->config->settings['TextareaCols'] * 2;
401
        }
402
403 8
        return $backupField . "\n"
404 8
            . '<textarea name="fields' . $columnNameAppendix . '"'
405 8
            . ' class="' . $theClass . '"'
406 8
            . (isset($maxlength) ? ' data-maxlength="' . $maxlength . '"' : '')
407 8
            . ' rows="' . $textAreaRows . '"'
408 8
            . ' cols="' . $textareaCols . '"'
409 8
            . ' dir="' . $textDir . '"'
410 8
            . ' id="field_' . $this->fieldIndex . '_3"'
411 8
            . ($onChangeClause !== '' ? ' onchange="' . htmlspecialchars($onChangeClause, ENT_COMPAT) . '"' : '')
412 8
            . ' tabindex="' . $this->fieldIndex . '"'
413 8
            . ' data-type="' . $dataType . '">'
414 8
            . $specialCharsEncoded
415 8
            . '</textarea>';
416
    }
417
418
    /**
419
     * Get HTML input type
420
     *
421
     * @param InsertEditColumn $column             description of column in given table
422
     * @param string           $columnNameAppendix the name attribute
423
     * @param string           $specialChars       special characters
424
     * @param int              $fieldsize          html field size
425
     * @param string           $onChangeClause     onchange clause for fields
426
     * @param string           $dataType           the html5 data-* attribute type
427
     *
428
     * @return string                       an html snippet
429
     */
430 20
    private function getHtmlInput(
431
        InsertEditColumn $column,
432
        string $columnNameAppendix,
433
        string $specialChars,
434
        int $fieldsize,
435
        string $onChangeClause,
436
        string $dataType,
437
    ): string {
438 20
        $theClass = 'textfield';
439
        // verify True_Type which does not contain the parentheses and length
440 20
        if ($column->trueType === 'date') {
441 8
            $theClass .= ' datefield';
442 20
        } elseif ($column->trueType === 'time') {
443
            $theClass .= ' timefield';
444 20
        } elseif ($column->trueType === 'datetime' || $column->trueType === 'timestamp') {
445 12
            $theClass .= ' datetimefield';
446
        }
447
448 20
        $inputMinMax = '';
449 20
        $isInteger = in_array($column->trueType, $this->dbi->types->getIntegerTypes(), true);
450 20
        if ($isInteger) {
451 4
            $extractedColumnspec = Util::extractColumnSpec($column->type);
452 4
            $isUnsigned = $extractedColumnspec['unsigned'];
453 4
            $minMaxValues = $this->dbi->types->getIntegerRange($column->trueType, ! $isUnsigned);
454 4
            $inputMinMax = 'min="' . $minMaxValues[0] . '" '
455 4
                . 'max="' . $minMaxValues[1] . '"';
456 4
            $dataType = 'INT';
457
        }
458
459
        // do not use the 'date' or 'time' types here; they have no effect on some
460
        // browsers and create side effects (see bug #4218)
461 20
        return '<input type="text"'
462 20
            . ' name="fields' . $columnNameAppendix . '"'
463 20
            . ' value="' . $specialChars . '" size="' . $fieldsize . '"'
464 20
            . ($column->isChar
465
                ? ' data-maxlength="' . $fieldsize . '"'
466 20
                : '')
467 20
            . ($inputMinMax !== '' ? ' ' . $inputMinMax : '')
468 20
            . ' data-type="' . $dataType . '"'
469 20
            . ' class="' . $theClass . '" onchange="' . htmlspecialchars($onChangeClause, ENT_COMPAT) . '"'
470 20
            . ' tabindex="' . $this->fieldIndex . '"'
471 20
            . ($isInteger ? ' inputmode="numeric"' : '')
472 20
            . ' id="field_' . $this->fieldIndex . '_3">';
473
    }
474
475
    /**
476
     * Get HTML select option for upload
477
     *
478
     * @param string $vkey         [multi_edit]['row_id']
479
     * @param string $fieldHashMd5 array index as an MD5 to avoid having special characters
480
     *
481
     * @return string an HTML snippet
482
     */
483
    private function getSelectOptionForUpload(string $vkey, string $fieldHashMd5): string
484
    {
485
        $files = $this->fileListing->getFileSelectOptions(
486
            Util::userDir($this->config->settings['UploadDir'] ?? ''),
487
        );
488
489
        if ($files === false) {
0 ignored issues
show
introduced by
The condition $files === false is always true.
Loading history...
490
            return '<span style="color:red">' . __('Error') . '</span><br>' . "\n"
491
                . __('The directory you set for upload work cannot be reached.') . "\n";
492
        }
493
494
        if ($files === '') {
495
            return '';
496
        }
497
498
        return "<br>\n"
499
            . '<i>' . __('Or') . '</i> '
500
            . __('web server upload directory:') . '<br>' . "\n"
501
            . '<select size="1" name="fields_uploadlocal'
502
            . $vkey . '[' . $fieldHashMd5 . ']">' . "\n"
503
            . '<option value="" selected="selected"></option>' . "\n"
504
            . $files
505
            . '</select>' . "\n";
506
    }
507
508
    /**
509
     * Retrieve the maximum upload file size
510
     */
511 4
    private function getMaxUploadSize(string $pmaType): string
512
    {
513
        // find maximum upload size, based on field type
514
        /**
515
         * @todo with functions this is not so easy, as you can basically
516
         * process any data with function like MD5
517
         */
518 4
        $maxFieldSize = match ($pmaType) {
519 4
            'tinyblob' => 256,
520 4
            'blob' => 65536,
521 4
            'mediumblob' => 16777216,
522 4
            'longblob' => 4294967296,// yeah, really
523 4
        };
524
525 4
        $thisFieldMaxSize = (int) $this->config->get('max_upload_size'); // from PHP max
526
527 4
        return Util::getFormattedMaximumUploadSize(min($thisFieldMaxSize, $maxFieldSize)) . "\n";
528
    }
529
530
    /**
531
     * Get HTML for the Value column of other datatypes
532
     * (here, "column" is used in the sense of HTML column in HTML table)
533
     *
534
     * @param InsertEditColumn $column              description of column in given table
535
     * @param string           $defaultCharEditing  default char editing mode which is stored
536
     *                                                 in the config.inc.php script
537
     * @param string           $backupField         hidden input field
538
     * @param string           $columnNameAppendix  the name attribute
539
     * @param string           $onChangeClause      onchange clause for fields
540
     * @param string           $specialChars        special characters
541
     * @param string           $textDir             text direction
542
     * @param string           $specialCharsEncoded replaced char if the string starts
543
     *                                                with a \r\n pair (0x0d0a) add an extra \n
544
     * @param string           $data                data to edit
545
     * @param mixed[]          $extractedColumnspec associative array containing type,
546
     *                                              spec_in_brackets and possibly
547
     *                                              enum_set_values (another array)
548
     *
549
     * @return string an html snippet
550
     */
551 16
    private function getValueColumnForOtherDatatypes(
552
        InsertEditColumn $column,
553
        string $defaultCharEditing,
554
        string $backupField,
555
        string $columnNameAppendix,
556
        string $onChangeClause,
557
        string $specialChars,
558
        string $textDir,
559
        string $specialCharsEncoded,
560
        string $data,
561
        array $extractedColumnspec,
562
    ): string {
563
        // HTML5 data-* attribute data-type
564 16
        $dataType = $this->dbi->types->getTypeClass($column->trueType);
565 16
        $fieldsize = $this->getColumnSize($column, $extractedColumnspec['spec_in_brackets']);
566
567 16
        $isTextareaRequired = $column->isChar
568 16
            && ($this->config->settings['CharEditing'] === 'textarea' || str_contains($data, "\n"));
569 16
        if ($isTextareaRequired) {
570 4
            $this->config->settings['CharEditing'] = $defaultCharEditing;
571 4
            $htmlField = $this->getTextarea(
572 4
                $column,
573 4
                $backupField,
574 4
                $columnNameAppendix,
575 4
                $onChangeClause,
576 4
                $textDir,
577 4
                $specialCharsEncoded,
578 4
                $dataType,
579 4
            );
580
        } else {
581 16
            $htmlField = $this->getHtmlInput(
582 16
                $column,
583 16
                $columnNameAppendix,
584 16
                $specialChars,
585 16
                $fieldsize,
586 16
                $onChangeClause,
587 16
                $dataType,
588 16
            );
589
        }
590
591 16
        return $this->template->render('table/insert/value_column_for_other_datatype', [
592 16
            'html_field' => $htmlField,
593 16
            'backup_field' => $backupField,
594 16
            'is_textarea' => $isTextareaRequired,
595 16
            'columnNameAppendix' => $columnNameAppendix,
596 16
            'column' => $column,
597 16
        ]);
598
    }
599
600
    /**
601
     * Get the field size
602
     *
603
     * @param InsertEditColumn $column         description of column in given table
604
     * @param string           $specInBrackets text in brackets inside column definition
605
     *
606
     * @return int field size
607
     */
608 20
    private function getColumnSize(InsertEditColumn $column, string $specInBrackets): int
609
    {
610 20
        if ($column->isChar) {
611 8
            $fieldsize = (int) $specInBrackets;
612 8
            if ($fieldsize > $this->config->settings['MaxSizeForInputField']) {
613
                /**
614
                 * This case happens for CHAR or VARCHAR columns which have
615
                 * a size larger than the maximum size for input field.
616
                 */
617 6
                $this->config->settings['CharEditing'] = 'textarea';
618
            }
619
        } else {
620
            /**
621
             * This case happens for example for INT or DATE columns;
622
             * in these situations, the value returned in $column['len']
623
             * seems appropriate.
624
             */
625 20
            $fieldsize = $column->length;
626
        }
627
628 20
        return min(
629 20
            max($fieldsize, $this->config->settings['MinSizeForInputField']),
630 20
            $this->config->settings['MaxSizeForInputField'],
631 20
        );
632
    }
633
634
    /**
635
     * get html for continue insertion form
636
     *
637
     * @param string  $table            name of the table
638
     * @param string  $db               name of the database
639
     * @param mixed[] $whereClauseArray
640
     *
641
     * @return string                   an html snippet
642
     */
643 4
    public function getContinueInsertionForm(
644
        string $table,
645
        string $db,
646
        array $whereClauseArray,
647
        string $errorUrl,
648
    ): string {
649 4
        return $this->template->render('table/insert/continue_insertion_form', [
650 4
            'db' => $db,
651 4
            'table' => $table,
652 4
            'where_clause_array' => $whereClauseArray,
653 4
            'err_url' => $errorUrl,
654 4
            'goto' => $GLOBALS['goto'],
655 4
            'sql_query' => $_POST['sql_query'] ?? null,
656 4
            'has_where_clause' => isset($_POST['where_clause']),
657 4
            'insert_rows_default' => $this->config->settings['InsertRows'],
658 4
        ]);
659
    }
660
661
    /**
662
     * @param string[]|string|null $whereClause
663
     *
664
     * @psalm-pure
665
     */
666 4
    public static function isWhereClauseNumeric(array|string|null $whereClause): bool
667
    {
668 4
        if ($whereClause === null) {
0 ignored issues
show
introduced by
The condition $whereClause === null is always false.
Loading history...
669 4
            return false;
670
        }
671
672 4
        if (! is_array($whereClause)) {
0 ignored issues
show
introduced by
The condition is_array($whereClause) is always true.
Loading history...
673 4
            $whereClause = [$whereClause];
674
        }
675
676
        // If we have just numeric primary key, we can also edit next
677
        // we are looking for `table_name`.`field_name` = numeric_value
678 4
        foreach ($whereClause as $clause) {
679
            // preg_match() returns 1 if there is a match
680 4
            $isNumeric = preg_match('@^[\s]*`[^`]*`[\.]`[^`]*` = [0-9]+@', $clause) === 1;
681 4
            if ($isNumeric) {
682 4
                return true;
683
            }
684
        }
685
686 4
        return false;
687
    }
688
689
    /**
690
     * Get table head and table foot for insert row table
691
     *
692
     * @param mixed[] $urlParams url parameters
693
     *
694
     * @return string           an html snippet
695
     */
696 12
    private function getHeadAndFootOfInsertRowTable(array $urlParams): string
697
    {
698 12
        $type = '';
699 12
        $function = '';
700
701 12
        if ($this->config->settings['ShowFieldTypesInDataEditView']) {
702 12
            $type = $this->showTypeOrFunction('type', $urlParams, true);
703
        }
704
705 12
        if ($this->config->settings['ShowFunctionFields']) {
706 12
            $function = $this->showTypeOrFunction('function', $urlParams, true);
707
        }
708
709 12
        $template = new Template();
710
711 12
        return $template->render('table/insert/get_head_and_foot_of_insert_row_table', [
712 12
            'type' => $type,
713 12
            'function' => $function,
714 12
        ]);
715
    }
716
717
    /**
718
     * Prepares the field value and retrieve special chars, backup field and data array
719
     *
720
     * @param mixed[]          $currentRow          a row of the table
721
     * @param InsertEditColumn $column              description of column in given table
722
     * @param mixed[]          $extractedColumnspec associative array containing type,
723
     *                                              spec_in_brackets and possibly
724
     *                                              enum_set_values (another array)
725
     * @param string           $columnNameAppendix  string to append to column name in input
726
     * @param bool             $asIs                use the data as is, used in repopulating
727
     *
728
     * @return mixed[] $real_null_value, $data, $special_chars, $backup_field,
729
     *               $special_chars_encoded
730
     * @psalm-return array{bool, string, string, string, string}
731
     */
732 4
    private function getSpecialCharsAndBackupFieldForExistingRow(
733
        array $currentRow,
734
        InsertEditColumn $column,
735
        array $extractedColumnspec,
736
        string $columnNameAppendix,
737
        bool $asIs,
738
    ): array {
739 4
        $specialCharsEncoded = '';
740 4
        $data = null;
741 4
        $realNullValue = false;
742
        // (we are editing)
743 4
        if (! isset($currentRow[$column->field])) {
744 4
            $realNullValue = true;
745 4
            $currentRow[$column->field] = '';
746 4
            $specialChars = '';
747 4
            $data = '';
748 4
        } elseif ($column->trueType === 'bit') {
749 4
            $specialChars = $asIs
750 4
                ? $currentRow[$column->field]
751 4
                : Util::printableBitValue(
752 4
                    (int) $currentRow[$column->field],
753 4
                    (int) $extractedColumnspec['spec_in_brackets'],
754 4
                );
755
        } elseif (
756 4
            (str_starts_with($column->trueType, 'timestamp')
757 4
                || $column->trueType === 'datetime'
758 4
                || $column->trueType === 'time')
759 4
            && (str_contains($currentRow[$column->field], '.'))
760
        ) {
761
            $currentRow[$column->field] = $asIs
762
                ? $currentRow[$column->field]
763
                : Util::addMicroseconds($currentRow[$column->field]);
764
            $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
765 4
        } elseif (in_array($column->trueType, Gis::getDataTypes(), true)) {
766
            // Convert gis data to Well Know Text format
767 4
            $currentRow[$column->field] = $asIs
768
                ? $currentRow[$column->field]
769 4
                : Gis::convertToWellKnownText($currentRow[$column->field], true);
770 4
            $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
771
        } else {
772
            // special binary "characters"
773 4
            if ($column->isBinary || ($column->isBlob && $this->config->settings['ProtectBinary'] !== 'all')) {
774 4
                $currentRow[$column->field] = $asIs
775
                    ? $currentRow[$column->field]
776 4
                    : bin2hex($currentRow[$column->field]);
777
            }
778
779 4
            $specialChars = htmlspecialchars($currentRow[$column->field], ENT_COMPAT);
780
781
            //We need to duplicate the first \n or otherwise we will lose
782
            //the first newline entered in a VARCHAR or TEXT column
783 4
            $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
784
785 4
            $data = $currentRow[$column->field];
786
        }
787
788
        /** @var string $defaultAction */
789 4
        $defaultAction = $_POST['default_action'] ?? $_GET['default_action'] ?? '';
790
        if (
791 4
            $defaultAction === 'insert'
792 4
            && $column->key === 'PRI'
793 4
            && str_contains($column->extra, 'auto_increment')
794
        ) {
795
            // When copying row, it is useful to empty auto-increment column to prevent duplicate key error.
796 4
            $data = $specialCharsEncoded = $specialChars = null;
797
        }
798
799
        // If a timestamp field value is not included in an update
800
        // statement MySQL auto-update it to the current timestamp;
801
        // however, things have changed since MySQL 4.1, so
802
        // it's better to set a fields_prev in this situation
803 4
        $backupField = '<input type="hidden" name="fields_prev'
804 4
            . $columnNameAppendix . '" value="'
805 4
            . htmlspecialchars($currentRow[$column->field], ENT_COMPAT) . '">';
806
807 4
        return [$realNullValue, (string) $specialCharsEncoded, (string) $specialChars, (string) $data, $backupField];
808
    }
809
810
    /**
811
     * display default values
812
     */
813 44
    private function getSpecialCharsForInsertingMode(
814
        string|null $defaultValue,
815
        string $trueType,
816
    ): string {
817 44
        if ($defaultValue === null) {
818 12
            $defaultValue = '';
819
        }
820
821 44
        if ($trueType === 'bit') {
822 4
            $specialChars = Util::convertBitDefaultValue($defaultValue);
823 40
        } elseif (str_starts_with($trueType, 'timestamp') || $trueType === 'datetime' || $trueType === 'time') {
824 20
            $specialChars = Util::addMicroseconds($defaultValue);
825 24
        } elseif ($trueType === 'binary' || $trueType === 'varbinary') {
826
            $specialChars = bin2hex($defaultValue);
827 24
        } elseif (str_ends_with($trueType, 'text')) {
828 12
            $textDefault = substr($defaultValue, 1, -1);
829 12
            $specialChars = stripcslashes($textDefault !== '' ? $textDefault : $defaultValue);
830
        } else {
831 16
            $specialChars = htmlspecialchars($defaultValue);
832
        }
833
834 44
        return $specialChars;
835
    }
836
837
    /**
838
     * set $_SESSION for edit_next
839
     *
840
     * @param string $oneWhereClause one where clause from where clauses array
841
     */
842 4
    public function setSessionForEditNext(string $oneWhereClause): void
843
    {
844 4
        $localQuery = 'SELECT * FROM ' . Util::backquote(Current::$database)
845 4
            . '.' . Util::backquote(Current::$table) . ' WHERE '
846 4
            . str_replace('` =', '` >', $oneWhereClause) . ' LIMIT 1;';
847
848 4
        $res = $this->dbi->query($localQuery);
849 4
        $row = $res->fetchRow();
850 4
        $meta = $this->dbi->getFieldsMeta($res);
851
        // must find a unique condition based on unique key,
852
        // not a combination of all fields
853 4
        $uniqueCondition = (new UniqueCondition($meta, $row, true))->getWhereClause();
854 4
        if ($uniqueCondition === '') {
855
            return;
856
        }
857
858 4
        $_SESSION['edit_next'] = $uniqueCondition;
859
    }
860
861
    /**
862
     * set $goto_include variable for different cases and retrieve like,
863
     * if $GLOBALS['goto'] empty, if $goto_include previously not defined
864
     * and new_insert, same_insert, edit_next
865
     *
866
     * @param string|false $gotoInclude store some script for include, otherwise it is
867
     *                                   boolean false
868
     */
869 4
    public function getGotoInclude(string|false $gotoInclude): string
870
    {
871 4
        $validOptions = ['new_insert', 'same_insert', 'edit_next'];
872 4
        if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $validOptions, true)) {
873 4
            return '/table/change';
874
        }
875
876 4
        if (! empty($GLOBALS['goto'])) {
877 4
            if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) {
878
                // this should NOT happen
879
                //$GLOBALS['goto'] = false;
880 4
                $gotoInclude = str_contains($GLOBALS['goto'], 'index.php?route=/sql') ? '/sql' : false;
881
            } else {
882
                $gotoInclude = $GLOBALS['goto'];
883
            }
884
885 4
            if ($GLOBALS['goto'] === 'index.php?route=/database/sql' && Current::$table !== '') {
886 4
                Current::$table = '';
887
            }
888
        }
889
890 4
        if (! $gotoInclude) {
891 4
            $gotoInclude = Current::$table === '' ? '/database/sql' : '/table/sql';
892
        }
893
894 4
        return $gotoInclude;
895
    }
896
897
    /**
898
     * Defines the url to return in case of failure of the query
899
     *
900
     * @param mixed[] $urlParams url parameters
901
     *
902
     * @return string           error url for query failure
903
     */
904 4
    public function getErrorUrl(array $urlParams): string
905
    {
906 4
        return $_POST['err_url'] ?? Url::getFromRoute('/table/change', $urlParams);
907
    }
908
909
    /**
910
     * Executes the sql query and get the result, then move back to the calling page
911
     *
912
     * @param mixed[] $query built query from buildSqlQuery()
913
     *
914
     * @return array{int, Message[], string[], string[]}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{int, Message[], string[], string[]} at position 2 could not be parsed: Expected ':' at position 2, but found 'int'.
Loading history...
915
     */
916 8
    public function executeSqlQuery(array $query): array
917
    {
918 8
        $GLOBALS['sql_query'] = implode('; ', $query) . ';';
919
        // to ensure that the query is displayed in case of
920
        // "insert as new row" and then "insert another new row"
921 8
        $GLOBALS['display_query'] = $GLOBALS['sql_query'];
922
923 8
        $totalAffectedRows = 0;
924 8
        $lastMessages = [];
925 8
        $warningMessages = [];
926 8
        $errorMessages = [];
927
928 8
        foreach ($query as $singleQuery) {
929 8
            if (isset($_POST['submit_type']) && $_POST['submit_type'] === 'showinsert') {
930
                $lastMessages[] = Message::notice(__('Showing SQL query'));
931
                continue;
932
            }
933
934 8
            if ($this->config->settings['IgnoreMultiSubmitErrors']) {
935 4
                $result = $this->dbi->tryQuery($singleQuery);
936
            } else {
937 4
                $result = $this->dbi->query($singleQuery);
938
            }
939
940 8
            if (! $result) {
941
                $errorMessages[] = $this->dbi->getError();
942
            } else {
943 8
                $totalAffectedRows += (int) $this->dbi->affectedRows();
944
945 8
                $insertId = $this->dbi->insertId();
946 8
                if ($insertId !== 0) {
947
                    // insert_id is id of FIRST record inserted in one insert, so if we
948
                    // inserted multiple rows, we had to increment this
949
950
                    if ($totalAffectedRows > 0) {
951
                        $insertId += $totalAffectedRows - 1;
952
                    }
953
954
                    $lastMessage = Message::notice(__('Inserted row id: %1$d'));
955
                    $lastMessage->addParam($insertId);
956
                    $lastMessages[] = $lastMessage;
957
                }
958
            }
959
960 8
            $warningMessages = $this->getWarningMessages();
961
        }
962
963 8
        return [$totalAffectedRows, $lastMessages, $warningMessages, $errorMessages];
964
    }
965
966
    /**
967
     * get the warning messages array
968
     *
969
     * @return string[]
970
     */
971 12
    private function getWarningMessages(): array
972
    {
973 12
        $warningMessages = [];
974 12
        foreach ($this->dbi->getWarnings() as $warning) {
975 4
            $warningMessages[] = htmlspecialchars((string) $warning);
976
        }
977
978 12
        return $warningMessages;
979
    }
980
981
    /**
982
     * Column to display from the foreign table?
983
     *
984
     * @param string  $whereComparison string that contain relation field value
985
     * @param mixed[] $map             all Relations to foreign tables for a given
986
     *                                            table or optionally a given column in a table
987
     * @param string  $relationField   relation field
988
     *
989
     * @return string display value from the foreign table
990
     */
991 4
    public function getDisplayValueForForeignTableColumn(
992
        string $whereComparison,
993
        array $map,
994
        string $relationField,
995
    ): string {
996 4
        $foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
997
998 4
        if (! is_array($foreigner)) {
999
            return '';
1000
        }
1001
1002 4
        $displayField = $this->relation->getDisplayField($foreigner['foreign_db'], $foreigner['foreign_table']);
1003
        // Field to display from the foreign table?
1004 4
        if ($displayField !== '') {
1005 4
            $dispsql = 'SELECT ' . Util::backquote($displayField)
1006 4
                . ' FROM ' . Util::backquote($foreigner['foreign_db'])
1007 4
                . '.' . Util::backquote($foreigner['foreign_table'])
1008 4
                . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
1009 4
                . $whereComparison;
1010 4
            $dispresult = $this->dbi->tryQuery($dispsql);
1011 4
            if ($dispresult && $dispresult->numRows() > 0) {
1012 4
                return (string) $dispresult->fetchValue();
1013
            }
1014
        }
1015
1016
        return '';
1017
    }
1018
1019
    /**
1020
     * Display option in the cell according to user choices
1021
     *
1022
     * @param mixed[] $map                all Relations to foreign tables for a given
1023
     *                                                  table or optionally a given column in a table
1024
     * @param string  $relationField      relation field
1025
     * @param string  $whereComparison    string that contain relation field value
1026
     * @param string  $dispval            display value from the foreign table
1027
     * @param string  $relationFieldValue relation field value
1028
     *
1029
     * @return string HTML <a> tag
1030
     */
1031 4
    public function getLinkForRelationalDisplayField(
1032
        array $map,
1033
        string $relationField,
1034
        string $whereComparison,
1035
        string $dispval,
1036
        string $relationFieldValue,
1037
    ): string {
1038 4
        $foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
1039
1040 4
        if (! is_array($foreigner)) {
1041
            return '';
1042
        }
1043
1044 4
        if ($_SESSION['tmpval']['relational_display'] === 'K') {
1045
            // user chose "relational key" in the display options, so
1046
            // the title contains the display field
1047 4
            $title = $dispval !== ''
1048 4
                ? ' title="' . htmlspecialchars($dispval) . '"'
1049
                : '';
1050
        } else {
1051 4
            $title = ' title="' . htmlspecialchars($relationFieldValue) . '"';
1052
        }
1053
1054 4
        $sqlQuery = 'SELECT * FROM '
1055 4
            . Util::backquote($foreigner['foreign_db'])
1056 4
            . '.' . Util::backquote($foreigner['foreign_table'])
1057 4
            . ' WHERE ' . Util::backquote($foreigner['foreign_field'])
1058 4
            . $whereComparison;
1059 4
        $urlParams = [
1060 4
            'db' => $foreigner['foreign_db'],
1061 4
            'table' => $foreigner['foreign_table'],
1062 4
            'pos' => '0',
1063 4
            'sql_signature' => Core::signSqlQuery($sqlQuery),
1064 4
            'sql_query' => $sqlQuery,
1065 4
        ];
1066 4
        $output = '<a href="' . Url::getFromRoute('/sql', $urlParams) . '"' . $title . '>';
1067
1068 4
        if ($_SESSION['tmpval']['relational_display'] === 'D') {
1069
            // user chose "relational display field" in the
1070
            // display options, so show display field in the cell
1071 4
            $output .= htmlspecialchars($dispval);
1072
        } else {
1073
            // otherwise display data in the cell
1074 4
            $output .= htmlspecialchars($relationFieldValue);
1075
        }
1076
1077 4
        $output .= '</a>';
1078
1079 4
        return $output;
1080
    }
1081
1082
    /**
1083
     * Transform edited values
1084
     *
1085
     * @param string  $db             db name
1086
     * @param string  $table          table name
1087
     * @param mixed[] $transformation mimetypes for all columns of a table
1088
     *                               [field_name][field_key]
1089
     * @param mixed[] $editedValues   transform columns list and new values
1090
     * @param string  $file           file containing the transformation plugin
1091
     * @param string  $columnName     column name
1092
     * @param mixed[] $extraData      extra data array
1093
     * @param string  $type           the type of transformation
1094
     *
1095
     * @return mixed[]
1096
     */
1097 4
    public function transformEditedValues(
1098
        string $db,
1099
        string $table,
1100
        array $transformation,
1101
        array &$editedValues,
1102
        string $file,
1103
        string $columnName,
1104
        array $extraData,
1105
        string $type,
1106
    ): array {
1107
        // $cfg['SaveCellsAtOnce'] = true; JS code sends an array
1108 4
        $whereClause = is_array($_POST['where_clause']) ? $_POST['where_clause'][0] : $_POST['where_clause'];
1109 4
        $urlParams = [
1110 4
            'db' => $db,
1111 4
            'table' => $table,
1112 4
            'where_clause_sign' => Core::signSqlQuery($whereClause),
1113 4
            'where_clause' => $whereClause,
1114 4
            'transform_key' => $columnName,
1115 4
        ];
1116 4
        $transformOptions = $this->transformations->getOptions($transformation[$type . '_options'] ?? '');
1117 4
        $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
1118 4
        $transformOptions['wrapper_params'] = $urlParams;
1119
1120 4
        $transformationPlugin = $this->transformations->getPluginInstance($file);
1121 4
        if ($transformationPlugin instanceof TransformationsInterface) {
1122 4
            foreach ($editedValues as $cellIndex => $currCellEditedValues) {
1123 4
                if (! isset($currCellEditedValues[$columnName])) {
1124
                    continue;
1125
                }
1126
1127 4
                $extraData['transformations'][$cellIndex] = $transformationPlugin->applyTransformation(
1128 4
                    $currCellEditedValues[$columnName],
1129 4
                    $transformOptions,
1130 4
                );
1131 4
                $editedValues[$cellIndex][$columnName] = $extraData['transformations'][$cellIndex];
1132
            }
1133
        }
1134
1135 4
        return $extraData;
1136
    }
1137
1138
    /**
1139
     * Get value part if a function was specified
1140
     */
1141 4
    private function formatAsSqlFunction(
1142
        EditField $editField,
1143
    ): string {
1144 4
        if ($editField->function === 'PHP_PASSWORD_HASH') {
1145 4
            $hash = password_hash($editField->value, PASSWORD_DEFAULT);
1146
1147 4
            return $this->dbi->quoteString($hash);
1148
        }
1149
1150 4
        if ($editField->function === 'UUID') {
1151
            /* This way user will know what UUID new row has */
1152 4
            $uuid = (string) $this->dbi->fetchValue('SELECT UUID()');
1153
1154 4
            return $this->dbi->quoteString($uuid);
1155
        }
1156
1157
        if (
1158 4
            in_array($editField->function, $this->getGisFromTextFunctions(), true)
1159 4
            || in_array($editField->function, $this->getGisFromWKBFunctions(), true)
1160
        ) {
1161 4
            preg_match('/^(\'?)(.*?)\1(?:,(\d+))?$/', $editField->value, $matches);
1162 4
            $escapedParams = $this->dbi->quoteString($matches[2]) . (isset($matches[3]) ? ',' . $matches[3] : '');
1163
1164 4
            return $editField->function . '(' . $escapedParams . ')';
1165
        }
1166
1167
        if (
1168 4
            ! in_array($editField->function, self::FUNC_NO_PARAM, true)
1169 4
            || ($editField->value !== '' && in_array($editField->function, self::FUNC_OPTIONAL_PARAM, true))
1170
        ) {
1171
            if (
1172 4
                ($editField->salt !== null
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($editField->salt !== nu...>function === 'ENCRYPT', Probably Intended Meaning: $editField->salt !== nul...function === 'ENCRYPT')
Loading history...
1173 4
                    && ($editField->function === 'AES_ENCRYPT'
1174 4
                        || $editField->function === 'AES_DECRYPT'
1175 4
                        || $editField->function === 'SHA2'))
1176 4
                || ($editField->salt
1177 4
                    && ($editField->function === 'DES_ENCRYPT'
1178 4
                        || $editField->function === 'DES_DECRYPT'
1179 4
                        || $editField->function === 'ENCRYPT'))
1180
            ) {
1181 4
                return $editField->function . '(' . $this->dbi->quoteString($editField->value) . ','
1182 4
                    . $this->dbi->quoteString($editField->salt) . ')';
1183
            }
1184
1185 4
            return $editField->function . '(' . $this->dbi->quoteString($editField->value) . ')';
1186
        }
1187
1188 4
        return $editField->function . '()';
1189
    }
1190
1191
    /**
1192
     * Get the field value formatted for use in a SQL statement.
1193
     * Used in both INSERT and UPDATE statements.
1194
     */
1195 8
    private function getValueFormattedAsSql(
1196
        EditField $editField,
1197
        string $protectedValue = '',
1198
    ): string {
1199 8
        if ($editField->isUploaded) {
1200 4
            return $editField->value;
1201
        }
1202
1203 8
        if ($editField->function !== '') {
1204 4
            return $this->formatAsSqlFunction($editField);
1205
        }
1206
1207 8
        return $this->formatAsSqlValueBasedOnType($editField, $protectedValue);
1208
    }
1209
1210
    /**
1211
     * Get query values array and query fields array for insert and update in multi edit
1212
     *
1213
     * @param string|int $whereClause Either a positional index or string representing selected row
1214
     */
1215 4
    public function getQueryValueForInsert(
1216
        EditField $editField,
1217
        bool $usingKey,
1218
        string|int $whereClause,
1219
    ): string {
1220 4
        $protectedValue = '';
1221 4
        if ($editField->type === 'protected' && $usingKey && $whereClause !== '') {
1222
            // Fetch the current values of a row to use in case we have a protected field
1223 4
            $protectedValue = $this->dbi->fetchValue(
1224 4
                'SELECT ' . Util::backquote($editField->columnName)
1225 4
                . ' FROM ' . Util::backquote(Current::$table)
1226 4
                . ' WHERE ' . $whereClause,
1227 4
            );
1228 4
            $protectedValue = is_string($protectedValue) ? $protectedValue : '';
1229
        }
1230
1231 4
        return $this->getValueFormattedAsSql($editField, $protectedValue);
1232
    }
1233
1234
    /**
1235
     * Get field-value pairs for update SQL.
1236
     * During update, we build the SQL only with the fields that should be updated.
1237
     */
1238 4
    public function getQueryValueForUpdate(EditField $editField): string
1239
    {
1240 4
        $currentValueFormattedAsSql = $this->getValueFormattedAsSql($editField);
1241
1242
        // avoid setting a field to NULL when it's already NULL
1243
        // (field had the null checkbox before the update; field still has the null checkbox)
1244 4
        if ($editField->wasPreviouslyNull && $editField->isNull) {
1245 4
            return '';
1246
        }
1247
1248
        // A blob field that hasn't been changed will have no value
1249 4
        if ($currentValueFormattedAsSql === '') {
1250 4
            return '';
1251
        }
1252
1253
        if (
1254
            // Field had the null checkbox before the update; field no longer has the null checkbox
1255 4
            $editField->wasPreviouslyNull ||
1256
            // Field was marked as NULL (the value will be unchanged if it was an empty string)
1257 4
            $editField->isNull ||
1258
            // A function was applied to the field
1259 4
            $editField->function !== '' ||
1260
            // The value was changed
1261 4
            $editField->value !== $editField->previousValue
1262
        ) {
1263 4
            return Util::backquote($editField->columnName) . ' = ' . $currentValueFormattedAsSql;
1264
        }
1265
1266 4
        return '';
1267
    }
1268
1269
    /**
1270
     * Get the current column value in the form for different data types
1271
     */
1272 8
    private function formatAsSqlValueBasedOnType(
1273
        EditField $editField,
1274
        string $protectedValue,
1275
    ): string {
1276 8
        if ($editField->type === 'protected') {
1277
            // here we are in protected mode (asked in the config)
1278
            // so tbl_change has put this special value in the
1279
            // columns array, so we do not change the column value
1280
            // but we can still handle column upload
1281
1282
            // when in UPDATE mode, do not alter field's contents. When in INSERT
1283
            // mode, insert empty field because no values were submitted.
1284
            // If protected blobs were set, insert original field's content.
1285 8
            if ($protectedValue !== '') {
1286 4
                return '0x' . bin2hex($protectedValue);
1287
            }
1288
1289 8
            if ($editField->isNull) {
1290 4
                return 'NULL';
1291
            }
1292
1293
            // The Null checkbox was unchecked for this field
1294 8
            if ($editField->wasPreviouslyNull) {
1295 4
                return "''";
1296
            }
1297
1298 8
            return '';
1299
        }
1300
1301 8
        if ($editField->value === '') {
1302
            // When the field is autoIncrement, the best way to avoid problems
1303
            // in strict mode is to set the value to null (works also in non-strict mode)
1304
1305
            // If the value is empty and the null checkbox is checked, set it to null
1306 8
            return $editField->autoIncrement || $editField->isNull ? 'NULL' : "''";
1307
        }
1308
1309 8
        if ($editField->type === 'hex') {
1310 4
            if (! str_starts_with($editField->value, '0x')) {
1311 4
                return '0x' . $editField->value;
1312
            }
1313
1314 4
            return $editField->value;
1315
        }
1316
1317 8
        if ($editField->type === 'bit') {
1318 4
            $currentValue = (string) preg_replace('/[^01]/', '0', $editField->value);
1319
1320 4
            return 'b' . $this->dbi->quoteString($currentValue);
1321
        }
1322
1323
        // For uuid type, generate uuid value
1324
        // if empty value but not set null or value is uuid() function
1325
        if (
1326 8
            $editField->type === 'uuid'
1327 8
                && ! $editField->isNull
1328 8
                && in_array($editField->value, ["''", '', "'uuid()'", 'uuid()'], true)
1329
        ) {
1330 4
            return 'uuid()';
1331
        }
1332
1333
        if (
1334 8
            ($editField->type !== 'datetime' && $editField->type !== 'timestamp' && $editField->type !== 'date')
1335 8
            || ($editField->value !== 'CURRENT_TIMESTAMP' && $editField->value !== 'current_timestamp()')
1336
        ) {
1337 8
            return $this->dbi->quoteString($editField->value);
1338
        }
1339
1340
        // If there is a value, we ignore the Null checkbox;
1341
        // this could be possible if Javascript is disabled in the browser
1342 4
        return $editField->value;
1343
    }
1344
1345
    /**
1346
     * Check whether inline edited value can be truncated or not,
1347
     * and add additional parameters for extra_data array  if needed
1348
     *
1349
     * @param string  $db         Database name
1350
     * @param string  $table      Table name
1351
     * @param string  $columnName Column name
1352
     * @param mixed[] $extraData  Extra data for ajax response
1353
     */
1354 4
    public function verifyWhetherValueCanBeTruncatedAndAppendExtraData(
1355
        string $db,
1356
        string $table,
1357
        string $columnName,
1358
        array &$extraData,
1359
    ): void {
1360 4
        $extraData['isNeedToRecheck'] = false;
1361
1362 4
        $sqlForRealValue = 'SELECT ' . Util::backquote($table) . '.'
1363 4
            . Util::backquote($columnName)
1364 4
            . ' FROM ' . Util::backquote($db) . '.'
1365 4
            . Util::backquote($table)
1366 4
            . ' WHERE ' . $_POST['where_clause'][0];
1367
1368 4
        $result = $this->dbi->tryQuery($sqlForRealValue);
1369
1370 4
        if (! $result) {
1371
            return;
1372
        }
1373
1374 4
        $fieldsMeta = $this->dbi->getFieldsMeta($result);
1375 4
        $meta = $fieldsMeta[0];
1376 4
        $newValue = $result->fetchValue();
1377
1378 4
        if ($newValue === false) {
1379 4
            return;
1380
        }
1381
1382 4
        if ($newValue !== null) {
0 ignored issues
show
introduced by
The condition $newValue !== null is always true.
Loading history...
1383 4
            if ($meta->isTimeType()) {
1384 4
                $newValue = Util::addMicroseconds($newValue);
1385 4
            } elseif ($meta->isBinary()) {
1386
                $newValue = '0x' . bin2hex($newValue);
1387
            }
1388
        }
1389
1390 4
        $extraData['isNeedToRecheck'] = true;
1391 4
        $extraData['truncatableFieldValue'] = $newValue;
1392
    }
1393
1394
    /**
1395
     * Function to get the columns of a table
1396
     *
1397
     * @param string $db    current db
1398
     * @param string $table current table
1399
     *
1400
     * @return list<ColumnFull>
1401
     */
1402 4
    public function getTableColumns(string $db, string $table): array
1403
    {
1404 4
        $this->dbi->selectDb($db);
1405
1406 4
        return array_values($this->dbi->getColumns($db, $table, true));
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_values($thi...mns($db, $table, true)) returns the type array which is incompatible with the documented return type PhpMyAdmin\list.
Loading history...
1407
    }
1408
1409
    /**
1410
     * Function to determine Insert/Edit rows
1411
     *
1412
     * @param string[]|string|null $whereClause where clause
1413
     * @param string               $db          current database
1414
     * @param string               $table       current table
1415
     *
1416
     * @return array<int, bool|string[]|string|ResultInterface|ResultInterface[]|null>
1417
     * @phpstan-return array{
1418
     *     bool,
1419
     *     string[]|string|null,
1420
     *     string[],
1421
     *     string[]|null,
1422
     *     ResultInterface[]|ResultInterface,
1423
     *     array<string, string|null>[]|false[],
1424
     *     bool,
1425
     *     string|null
1426
     * }
1427
     */
1428 4
    public function determineInsertOrEdit(array|string|null $whereClause, string $db, string $table): array
1429
    {
1430 4
        if (isset($_POST['where_clause'])) {
1431 4
            $whereClause = $_POST['where_clause'];
1432
        }
1433
1434 4
        if (isset($_SESSION['edit_next'])) {
1435 4
            $whereClause = $_SESSION['edit_next'];
1436 4
            unset($_SESSION['edit_next']);
1437 4
            $afterInsert = 'edit_next';
1438
        }
1439
1440 4
        if (isset($_POST['ShowFunctionFields'])) {
1441 4
            $this->config->settings['ShowFunctionFields'] = $_POST['ShowFunctionFields'];
1442
        }
1443
1444 4
        if (isset($_POST['ShowFieldTypesInDataEditView'])) {
1445 4
            $this->config->settings['ShowFieldTypesInDataEditView'] = $_POST['ShowFieldTypesInDataEditView'];
1446
        }
1447
1448 4
        if (isset($_POST['after_insert'])) {
1449 4
            $afterInsert = $_POST['after_insert'];
1450
        }
1451
1452 4
        if (isset($whereClause)) {
1453
            // we are editing
1454 4
            $insertMode = false;
1455 4
            $whereClauseArray = (array) $whereClause;
1456 4
            [$whereClauses, $result, $rows, $foundUniqueKey] = $this->analyzeWhereClauses(
1457 4
                $whereClauseArray,
1458 4
                $table,
1459 4
                $db,
1460 4
            );
1461
        } else {
1462
            // we are inserting
1463 4
            $insertMode = true;
1464 4
            $whereClause = null;
1465 4
            $result = $this->loadFirstRow($table, $db);
1466 4
            $rows = $this->getInsertRows();
1467 4
            $whereClauses = null;
1468 4
            $whereClauseArray = [];
1469 4
            $foundUniqueKey = false;
1470
        }
1471
1472
        /** @var string $defaultAction */
1473 4
        $defaultAction = $_POST['default_action'] ?? $_GET['default_action'] ?? '';
1474 4
        if ($defaultAction === 'insert') {
1475
            // Copying a row - fetched data will be inserted as a new row, therefore the where clause is needless.
1476 4
            $whereClause = $whereClauses = null;
1477
        }
1478
1479 4
        return [
1480 4
            $insertMode,
1481 4
            $whereClause,
1482 4
            $whereClauseArray,
1483 4
            $whereClauses,
1484 4
            $result,
1485 4
            $rows,
1486 4
            $foundUniqueKey,
1487 4
            $afterInsert ?? null,
1488 4
        ];
1489
    }
1490
1491
    /**
1492
     * Function to get comments for the table columns
1493
     *
1494
     * @param string $db    current database
1495
     * @param string $table current table
1496
     *
1497
     * @return string[] comments for columns
1498
     */
1499 4
    public function getCommentsMap(string $db, string $table): array
1500
    {
1501 4
        if ($this->config->settings['ShowPropertyComments']) {
1502 4
            return $this->relation->getComments($db, $table);
1503
        }
1504
1505 4
        return [];
1506
    }
1507
1508
    /**
1509
     * Function to get html for the gis editor div
1510
     */
1511
    public function getHtmlForGisEditor(): string
1512
    {
1513
        return '<div id="gis_editor"></div><div id="popup_background"></div><br>';
1514
    }
1515
1516
    /**
1517
     * Function to get html for the ignore option in insert mode
1518
     *
1519
     * @param int  $rowId   row id
1520
     * @param bool $checked ignore option is checked or not
1521
     */
1522 4
    public function getHtmlForIgnoreOption(int $rowId, bool $checked = true): string
1523
    {
1524 4
        return '<input type="checkbox"'
1525 4
            . ($checked ? ' checked="checked"' : '')
1526 4
            . ' name="insert_ignore_' . $rowId . '"'
1527 4
            . ' id="insert_ignore_' . $rowId . '">'
1528 4
            . '<label for="insert_ignore_' . $rowId . '">'
1529 4
            . __('Ignore')
1530 4
            . '</label><br>' . "\n";
1531
    }
1532
1533
    /**
1534
     * Function to get html for the insert edit form header
1535
     *
1536
     * @param bool $hasBlobField whether has blob field
1537
     * @param bool $isUpload     whether is upload
1538
     */
1539
    public function getHtmlForInsertEditFormHeader(bool $hasBlobField, bool $isUpload): string
1540
    {
1541
        $template = new Template();
1542
1543
        return $template->render('table/insert/get_html_for_insert_edit_form_header', [
1544
            'has_blob_field' => $hasBlobField,
1545
            'is_upload' => $isUpload,
1546
        ]);
1547
    }
1548
1549
    /**
1550
     * Function to get html for each insert/edit column
1551
     *
1552
     * @param ColumnFull $tableColumn        column
1553
     * @param int        $columnNumber       column index in table_columns
1554
     * @param string[]   $commentsMap        comments map
1555
     * @param int        $columnLength       length of the current column taken from field metadata
1556
     * @param bool       $insertMode         whether insert mode
1557
     * @param mixed[]    $currentRow         current row
1558
     * @param int        $columnsCnt         columns count
1559
     * @param bool       $isUpload           whether upload
1560
     * @param mixed[]    $foreigners         foreigners
1561
     * @param string     $table              table
1562
     * @param string     $db                 database
1563
     * @param int        $rowId              row id
1564
     * @param string     $defaultCharEditing default char editing mode which is stored in the config.inc.php script
1565
     * @param string     $textDir            text direction
1566
     * @param mixed[]    $repopulate         the data to be repopulated
1567
     * @param mixed[]    $columnMime         the mime information of column
1568
     * @param string     $whereClause        the where clause
1569
     */
1570 12
    private function getHtmlForInsertEditFormColumn(
1571
        ColumnFull $tableColumn,
1572
        int $columnNumber,
1573
        array $commentsMap,
1574
        int $columnLength,
1575
        bool $insertMode,
1576
        array $currentRow,
1577
        int $columnsCnt,
1578
        bool $isUpload,
1579
        array $foreigners,
1580
        string $table,
1581
        string $db,
1582
        int $rowId,
1583
        string $defaultCharEditing,
1584
        string $textDir,
1585
        array $repopulate,
1586
        array $columnMime,
1587
        string $whereClause,
1588
    ): string {
1589 12
        $column = new InsertEditColumn(
1590 12
            $tableColumn->field,
1591 12
            $tableColumn->type,
1592 12
            $tableColumn->isNull,
1593 12
            $tableColumn->key,
1594 12
            $tableColumn->default,
1595 12
            $tableColumn->extra,
1596 12
            $columnLength,
1597 12
            $this->isColumn($tableColumn->type, ['binary', 'varbinary']),
1598 12
            $this->isColumn($tableColumn->type, ['blob', 'tinyblob', 'mediumblob', 'longblob']),
1599 12
            $this->isColumn($tableColumn->type, ['char', 'varchar']),
1600 12
            $insertMode,
1601 12
        );
1602
1603 12
        $asIs = false;
1604 12
        $fieldHashMd5 = $column->md5;
1605 12
        if ($repopulate !== [] && array_key_exists($fieldHashMd5, $currentRow)) {
1606
            $currentRow[$column->field] = $repopulate[$fieldHashMd5];
1607
            $asIs = true;
1608
        }
1609
1610 12
        $extractedColumnspec = Util::extractColumnSpec($column->type);
1611
1612
        //Call validation when the form submitted...
1613 12
        $onChangeClause = 'return verificationsAfterFieldChange('
1614 12
            . json_encode($fieldHashMd5) . ', '
1615 12
            . json_encode((string) $rowId) . ',' . json_encode($column->pmaType) . ')';
1616
1617 12
        $vkey = '[multi_edit][' . $rowId . ']';
1618
        // Use an MD5 as an array index to avoid having special characters
1619
        // in the name attribute (see bug #1746964 )
1620 12
        $columnNameAppendix = $vkey . '[' . $fieldHashMd5 . ']';
1621
1622
        // Prepares the field value
1623 12
        if ($currentRow !== []) {
1624
            // (we are editing)
1625
            [
1626
                $realNullValue,
1627
                $specialCharsEncoded,
1628
                $specialChars,
1629
                $data,
1630
                $backupField,
1631
            ] = $this->getSpecialCharsAndBackupFieldForExistingRow(
1632
                $currentRow,
1633
                $column,
1634
                $extractedColumnspec,
1635
                $columnNameAppendix,
1636
                $asIs,
1637
            );
1638
        } else {
1639
            // (we are inserting)
1640
            // display default values
1641 12
            $defaultValue = $repopulate[$fieldHashMd5] ?? $column->default ?? null;
1642
1643 12
            $realNullValue = $defaultValue === null;
1644 12
            $data = (string) $defaultValue;
1645 12
            $specialChars = $this->getSpecialCharsForInsertingMode($defaultValue, $column->trueType);
1646 12
            $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
1647 12
            $backupField = '';
1648
        }
1649
1650 12
        $this->fieldIndex = ($this->rowOffset * $columnsCnt) + $columnNumber + 1;
1651
1652
        // The function column
1653
        // -------------------
1654 12
        $foreignData = $this->relation->getForeignData($foreigners, $column->field, false, '', '');
1655 12
        $isColumnBinary = $this->isColumnBinary($column, $isUpload);
1656 12
        $functionOptions = '';
1657
1658 12
        if ($this->config->settings['ShowFunctionFields']) {
1659 12
            $defaultFunction = Generator::getDefaultFunctionForField(
1660 12
                $column->trueType,
1661 12
                $column->firstTimestamp,
1662 12
                $column->default,
1663 12
                $column->extra,
1664 12
                $column->isNull,
1665 12
                $column->key,
1666 12
                $column->type,
1667 12
                $insertMode,
1668 12
            );
1669 12
            $functionOptions = Generator::getFunctionsForField($defaultFunction, $foreignData);
1670
        }
1671
1672
        // nullify code is needed by the js nullify() function to be able to generate calls to nullify() in jQuery
1673 12
        $nullifyCode = $this->getNullifyCodeForNullColumn($column, $foreigners, $foreignData);
1674
1675
        // The value column (depends on type)
1676
        // ----------------
1677
        // See bug #1667887 for the reason why we don't use the maxlength
1678
        // HTML attribute
1679
1680
        //add data attributes "no of decimals" and "data type"
1681 12
        $noDecimals = 0;
1682 12
        $type = current(explode('(', $column->pmaType));
1683 12
        if (preg_match('/\(([^()]+)\)/', $column->pmaType, $match)) {
1684 4
            $match[0] = trim($match[0], '()');
1685 4
            $noDecimals = $match[0];
1686
        }
1687
1688
        // Check input transformation of column
1689 12
        $transformedHtml = '';
1690 12
        if (! empty($columnMime['input_transformation'])) {
1691 4
            $file = $columnMime['input_transformation'];
1692 4
            $transformationPlugin = $this->transformations->getPluginInstance((string) $file);
1693 4
            if ($transformationPlugin instanceof IOTransformationsPlugin) {
1694 4
                $transformationOptions = $this->transformations->getOptions(
1695 4
                    $columnMime['input_transformation_options'],
1696 4
                );
1697 4
                $urlParams = [
1698 4
                    'db' => $db,
1699 4
                    'table' => $table,
1700 4
                    'transform_key' => $column->field,
1701 4
                    'where_clause_sign' => Core::signSqlQuery($whereClause),
1702 4
                    'where_clause' => $whereClause,
1703 4
                ];
1704 4
                $transformationOptions['wrapper_link'] = Url::getCommon($urlParams);
1705 4
                $transformationOptions['wrapper_params'] = $urlParams;
1706
1707 4
                $transformedHtml = $transformationPlugin->getInputHtml(
1708 4
                    $columnNameAppendix,
1709 4
                    $transformationOptions,
1710 4
                    $currentRow[$column->field] ?? '',
1711 4
                    $textDir,
1712 4
                    $this->fieldIndex,
1713 4
                );
1714
1715 4
                $GLOBALS['plugin_scripts'] = array_merge(
1716 4
                    $GLOBALS['plugin_scripts'],
1717 4
                    $transformationPlugin->getScripts(),
1718 4
                );
1719
            }
1720
        }
1721
1722 12
        $columnValue = '';
1723 12
        $foreignDropdown = '';
1724 12
        $dataType = '';
1725 12
        $textAreaRows = $this->config->settings['TextareaRows'];
1726 12
        $textareaCols = $this->config->settings['TextareaCols'];
1727 12
        $maxlength = '';
1728 12
        $enumSelectedValue = '';
1729 12
        $enumValues = [];
1730 12
        $columnSetValues = [];
1731 12
        $setSelectSize = 0;
1732 12
        $isColumnProtectedBlob = false;
1733 12
        $blobValue = '';
1734 12
        $blobValueUnit = '';
1735 12
        $maxUploadSize = 0;
1736 12
        $selectOptionForUpload = '';
1737 12
        $inputFieldHtml = '';
1738 12
        if ($transformedHtml === '') {
1739 12
            if (is_array($foreignData['disp_row'])) {
1740
                $foreignDropdown = $this->relation->foreignDropdown(
1741
                    $foreignData['disp_row'],
1742
                    $foreignData['foreign_field'],
1743
                    $foreignData['foreign_display'],
1744
                    $data,
1745
                    $this->config->settings['ForeignKeyMaxLimit'],
1746
                );
1747
            }
1748
1749 12
            $dataType = $this->dbi->types->getTypeClass($column->trueType);
1750
1751 12
            if ($column->isChar) {
1752
                $textAreaRows = max($this->config->settings['CharTextareaRows'], 7);
1753
                $textareaCols = $this->config->settings['CharTextareaCols'];
1754
                $maxlength = $extractedColumnspec['spec_in_brackets'];
1755 12
            } elseif ($this->config->settings['LongtextDoubleTextarea'] && str_contains($column->pmaType, 'longtext')) {
1756 8
                $textAreaRows = $this->config->settings['TextareaRows'] * 2;
1757 8
                $textareaCols = $this->config->settings['TextareaCols'] * 2;
1758
            }
1759
1760 12
            if ($column->pmaType === 'enum') {
1761
                $enumValues = $extractedColumnspec['enum_set_values'];
1762
1763
                foreach ($enumValues as $enumValue) {
1764
                    if (
1765
                        $data == $enumValue || ($data == ''
1766
                            && (! isset($_POST['where_clause']) || ! $column->isNull)
1767
                            && isset($column->default) && $enumValue == $column->default)
1768
                    ) {
1769
                        $enumSelectedValue = $enumValue;
1770
                        break;
1771
                    }
1772
                }
1773 12
            } elseif ($column->pmaType === 'set') {
1774
                $columnSetValues = $extractedColumnspec['enum_set_values'];
1775
                $setSelectSize = min(4, count($extractedColumnspec['enum_set_values']));
1776 12
            } elseif ($column->isBinary || $column->isBlob) {
1777
                $isColumnProtectedBlob = ($this->config->settings['ProtectBinary'] === 'blob' && $column->isBlob)
1778
                    || ($this->config->settings['ProtectBinary'] === 'all')
1779
                    || ($this->config->settings['ProtectBinary'] === 'noblob' && ! $column->isBlob);
1780
                if ($isColumnProtectedBlob) {
1781
                    [$blobValue, $blobValueUnit] = Util::formatByteDown(mb_strlen(stripslashes($data)), 3, 1);
1782
                }
1783
1784
                if ($isUpload && $column->isBlob) {
1785
                    $maxUploadSize = $this->getMaxUploadSize($column->pmaType);
1786
                }
1787
1788
                if (! empty($this->config->settings['UploadDir'])) {
1789
                    $selectOptionForUpload = $this->getSelectOptionForUpload($vkey, $fieldHashMd5);
1790
                }
1791
1792
                if (
1793
                    ! $isColumnProtectedBlob
1794
                    && ! ($column->isBlob || ($column->length > $this->config->settings['LimitChars']))
1795
                ) {
1796
                    $inputFieldHtml = $this->getHtmlInput(
1797
                        $column,
1798
                        $columnNameAppendix,
1799
                        $specialChars,
1800
                        min(max($column->length, 4), $this->config->settings['LimitChars']),
1801
                        $onChangeClause,
1802
                        'HEX',
1803
                    );
1804
                }
1805
            } else {
1806 12
                $columnValue = $this->getValueColumnForOtherDatatypes(
1807 12
                    $column,
1808 12
                    $defaultCharEditing,
1809 12
                    $backupField,
1810 12
                    $columnNameAppendix,
1811 12
                    $onChangeClause,
1812 12
                    $specialChars,
1813 12
                    $textDir,
1814 12
                    $specialCharsEncoded,
1815 12
                    $data,
1816 12
                    $extractedColumnspec,
1817 12
                );
1818
            }
1819
        }
1820
1821 12
        return $this->template->render('table/insert/column_row', [
1822 12
            'db' => $db,
1823 12
            'table' => $table,
1824 12
            'column' => $column,
1825 12
            'row_id' => $rowId,
1826 12
            'show_field_types_in_data_edit_view' => $this->config->settings['ShowFieldTypesInDataEditView'],
1827 12
            'show_function_fields' => $this->config->settings['ShowFunctionFields'],
1828 12
            'is_column_binary' => $isColumnBinary,
1829 12
            'function_options' => $functionOptions,
1830 12
            'nullify_code' => $nullifyCode,
1831 12
            'real_null_value' => $realNullValue,
1832 12
            'id_index' => $this->fieldIndex,
1833 12
            'type' => $type,
1834 12
            'decimals' => $noDecimals,
1835 12
            'special_chars' => $specialChars,
1836 12
            'transformed_value' => $transformedHtml,
1837 12
            'value' => $columnValue,
1838 12
            'is_value_foreign_link' => $foreignData['foreign_link'] === true,
1839 12
            'backup_field' => $backupField,
1840 12
            'data' => $data,
1841 12
            'gis_data_types' => Gis::getDataTypes(),
1842 12
            'foreign_dropdown' => $foreignDropdown,
1843 12
            'data_type' => $dataType,
1844 12
            'textarea_cols' => $textareaCols,
1845 12
            'textarea_rows' => $textAreaRows,
1846 12
            'text_dir' => $textDir,
1847 12
            'max_length' => $maxlength,
1848 12
            'longtext_double_textarea' => $this->config->settings['LongtextDoubleTextarea'],
1849 12
            'enum_selected_value' => $enumSelectedValue,
1850 12
            'enum_values' => $enumValues,
1851 12
            'set_values' => $columnSetValues,
1852 12
            'set_select_size' => $setSelectSize,
1853 12
            'is_column_protected_blob' => $isColumnProtectedBlob,
1854 12
            'blob_value' => $blobValue,
1855 12
            'blob_value_unit' => $blobValueUnit,
1856 12
            'is_upload' => $isUpload,
1857 12
            'max_upload_size' => $maxUploadSize,
1858 12
            'select_option_for_upload' => $selectOptionForUpload,
1859 12
            'limit_chars' => $this->config->settings['LimitChars'],
1860 12
            'input_field_html' => $inputFieldHtml,
1861 12
            'field_title' => $this->getColumnTitle($column->field, $commentsMap),
1862 12
        ]);
1863
    }
1864
1865 12
    private function isColumnBinary(InsertEditColumn $column, bool $isUpload): bool
1866
    {
1867 12
        if (! $this->config->settings['ShowFunctionFields']) {
1868
            return false;
1869
        }
1870
1871 12
        return ($this->config->settings['ProtectBinary'] === 'blob' && $column->isBlob && ! $isUpload)
1872 12
            || ($this->config->settings['ProtectBinary'] === 'all' && $column->isBinary)
1873 12
            || ($this->config->settings['ProtectBinary'] === 'noblob' && $column->isBinary);
1874
    }
1875
1876
    /**
1877
     * Function to get html for each insert/edit row
1878
     *
1879
     * @param mixed[]          $urlParams        url parameters
1880
     * @param list<ColumnFull> $tableColumns     table columns
1881
     * @param string[]         $commentsMap      comments map
1882
     * @param ResultInterface  $currentResult    current result
1883
     * @param bool             $insertMode       whether insert mode
1884
     * @param mixed[]          $currentRow       current row
1885
     * @param bool             $isUpload         whether upload
1886
     * @param mixed[]          $foreigners       foreigners
1887
     * @param string           $table            table
1888
     * @param string           $db               database
1889
     * @param int              $rowId            row id
1890
     * @param string           $textDir          text direction
1891
     * @param mixed[]          $repopulate       the data to be repopulated
1892
     * @param mixed[]          $whereClauseArray the array of where clauses
1893
     */
1894 8
    public function getHtmlForInsertEditRow(
1895
        array $urlParams,
1896
        array $tableColumns,
1897
        array $commentsMap,
1898
        ResultInterface $currentResult,
1899
        bool $insertMode,
1900
        array $currentRow,
1901
        bool $isUpload,
1902
        array $foreigners,
1903
        string $table,
1904
        string $db,
1905
        int $rowId,
1906
        string $textDir,
1907
        array $repopulate,
1908
        array $whereClauseArray,
1909
    ): string {
1910 8
        $htmlOutput = $this->getHeadAndFootOfInsertRowTable($urlParams)
1911 8
            . '<tbody>';
1912
1913
        //store the default value for CharEditing
1914 8
        $defaultCharEditing = $this->config->settings['CharEditing'];
1915 8
        $mimeMap = $this->transformations->getMime($db, $table);
1916 8
        $whereClause = $whereClauseArray[$rowId] ?? '';
1917
1918 8
        $columnCount = count($tableColumns);
1919 8
        for ($columnNumber = 0; $columnNumber < $columnCount; $columnNumber++) {
1920 8
            $tableColumn = $tableColumns[$columnNumber];
1921 8
            $columnMime = $mimeMap[$tableColumn->field] ?? [];
1922
1923 8
            $virtual = ['VIRTUAL', 'PERSISTENT', 'VIRTUAL GENERATED', 'STORED GENERATED'];
1924 8
            if (in_array($tableColumn->extra, $virtual, true)) {
1925
                continue;
1926
            }
1927
1928 8
            $htmlOutput .= $this->getHtmlForInsertEditFormColumn(
1929 8
                $tableColumn,
1930 8
                $columnNumber,
1931 8
                $commentsMap,
1932 8
                $this->dbi->getFieldsMeta($currentResult)[$columnNumber]->length,
1933 8
                $insertMode,
1934 8
                $currentRow,
1935 8
                $columnCount,
1936 8
                $isUpload,
1937 8
                $foreigners,
1938 8
                $table,
1939 8
                $db,
1940 8
                $rowId,
1941 8
                $defaultCharEditing,
1942 8
                $textDir,
1943 8
                $repopulate,
1944 8
                $columnMime,
1945 8
                $whereClause,
1946 8
            );
1947
        }
1948
1949 8
        $this->rowOffset++;
1950
1951 8
        return $htmlOutput . '  </tbody>'
1952 8
            . '</table></div><br>'
1953 8
            . '<div class="clearfloat"></div>';
1954
    }
1955
1956
    /**
1957
     * Returns list of function names that accept WKB as text
1958
     *
1959
     * @return string[]
1960
     */
1961 4
    private function getGisFromTextFunctions(): array
1962
    {
1963 4
        return $this->dbi->getVersion() >= 50600 ?
1964 4
        [
1965 4
            'ST_GeomFromText',
1966 4
            'ST_GeomCollFromText',
1967 4
            'ST_LineFromText',
1968 4
            'ST_MLineFromText',
1969 4
            'ST_PointFromText',
1970 4
            'ST_MPointFromText',
1971 4
            'ST_PolyFromText',
1972 4
            'ST_MPolyFromText',
1973 4
        ] :
1974 4
        [
1975 4
            'GeomFromText',
1976 4
            'GeomCollFromText',
1977 4
            'LineFromText',
1978 4
            'MLineFromText',
1979 4
            'PointFromText',
1980 4
            'MPointFromText',
1981 4
            'PolyFromText',
1982 4
            'MPolyFromText',
1983 4
        ];
1984
    }
1985
1986
    /**
1987
     * Returns list of function names that accept WKB as binary
1988
     *
1989
     * @return string[]
1990
     */
1991 4
    private function getGisFromWKBFunctions(): array
1992
    {
1993 4
        return $this->dbi->getVersion() >= 50600 ?
1994 4
        [
1995 4
            'ST_GeomFromWKB',
1996 4
            'ST_GeomCollFromWKB',
1997 4
            'ST_LineFromWKB',
1998 4
            'ST_MLineFromWKB',
1999 4
            'ST_PointFromWKB',
2000 4
            'ST_MPointFromWKB',
2001 4
            'ST_PolyFromWKB',
2002 4
            'ST_MPolyFromWKB',
2003 4
        ] :
2004 4
        [
2005 4
            'GeomFromWKB',
2006 4
            'GeomCollFromWKB',
2007 4
            'LineFromWKB',
2008 4
            'MLineFromWKB',
2009 4
            'PointFromWKB',
2010 4
            'MPointFromWKB',
2011 4
            'PolyFromWKB',
2012 4
            'MPolyFromWKB',
2013 4
        ];
2014
    }
2015
}
2016