InsertEdit::getQueryValueForUpdate()   B
last analyzed

Complexity

Conditions 8
Paths 4

Size

Total Lines 29
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8

Importance

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