* Adding inputmode numeric to integer fields To easier use phpMyAdmin on touch devices with virtual keyboards, adding the inputmode ensures that the correct virtual keyboard is shown per default when focusing the field. See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode for more information. Related to #17745. Signed-off-by: Jesper Skytte <jesper@skytte.it>
2513 lines
86 KiB
PHP
2513 lines
86 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace PhpMyAdmin;
|
|
|
|
use PhpMyAdmin\ConfigStorage\Relation;
|
|
use PhpMyAdmin\Dbal\ResultInterface;
|
|
use PhpMyAdmin\Html\Generator;
|
|
use PhpMyAdmin\Plugins\TransformationsPlugin;
|
|
use PhpMyAdmin\Utils\Gis;
|
|
|
|
use function __;
|
|
use function array_fill;
|
|
use function array_key_exists;
|
|
use function array_keys;
|
|
use function array_merge;
|
|
use function array_values;
|
|
use function bin2hex;
|
|
use function class_exists;
|
|
use function count;
|
|
use function current;
|
|
use function date;
|
|
use function explode;
|
|
use function htmlspecialchars;
|
|
use function implode;
|
|
use function in_array;
|
|
use function is_array;
|
|
use function is_file;
|
|
use function is_string;
|
|
use function max;
|
|
use function mb_stripos;
|
|
use function mb_strlen;
|
|
use function mb_strstr;
|
|
use function md5;
|
|
use function method_exists;
|
|
use function min;
|
|
use function password_hash;
|
|
use function preg_match;
|
|
use function preg_replace;
|
|
use function str_contains;
|
|
use function str_replace;
|
|
use function stripcslashes;
|
|
use function stripslashes;
|
|
use function strlen;
|
|
use function substr;
|
|
use function time;
|
|
use function trim;
|
|
|
|
use const ENT_COMPAT;
|
|
use const PASSWORD_DEFAULT;
|
|
|
|
class InsertEdit
|
|
{
|
|
/** @var DatabaseInterface */
|
|
private $dbi;
|
|
|
|
/** @var Relation */
|
|
private $relation;
|
|
|
|
/** @var Transformations */
|
|
private $transformations;
|
|
|
|
/** @var FileListing */
|
|
private $fileListing;
|
|
|
|
/** @var Template */
|
|
private $template;
|
|
|
|
private const FUNC_OPTIONAL_PARAM = [
|
|
'RAND',
|
|
'UNIX_TIMESTAMP',
|
|
];
|
|
|
|
private const FUNC_NO_PARAM = [
|
|
'CONNECTION_ID',
|
|
'CURRENT_USER',
|
|
'CURDATE',
|
|
'CURTIME',
|
|
'CURRENT_DATE',
|
|
'CURRENT_TIME',
|
|
'DATABASE',
|
|
'LAST_INSERT_ID',
|
|
'NOW',
|
|
'PI',
|
|
'RAND',
|
|
'SYSDATE',
|
|
'UNIX_TIMESTAMP',
|
|
'USER',
|
|
'UTC_DATE',
|
|
'UTC_TIME',
|
|
'UTC_TIMESTAMP',
|
|
'UUID',
|
|
'UUID_SHORT',
|
|
'VERSION',
|
|
];
|
|
|
|
public function __construct(
|
|
DatabaseInterface $dbi,
|
|
Relation $relation,
|
|
Transformations $transformations,
|
|
FileListing $fileListing,
|
|
Template $template
|
|
) {
|
|
$this->dbi = $dbi;
|
|
$this->relation = $relation;
|
|
$this->transformations = $transformations;
|
|
$this->fileListing = $fileListing;
|
|
$this->template = $template;
|
|
}
|
|
|
|
/**
|
|
* Retrieve form parameters for insert/edit form
|
|
*
|
|
* @param string $db name of the database
|
|
* @param string $table name of the table
|
|
* @param array|null $whereClauses where clauses
|
|
* @param array $whereClauseArray array of where clauses
|
|
* @param string $errorUrl error url
|
|
*
|
|
* @return array<string, string> array of insert/edit form parameters
|
|
*/
|
|
public function getFormParametersForInsertForm(
|
|
$db,
|
|
$table,
|
|
?array $whereClauses,
|
|
array $whereClauseArray,
|
|
$errorUrl
|
|
): array {
|
|
$formParams = [
|
|
'db' => $db,
|
|
'table' => $table,
|
|
'goto' => $GLOBALS['goto'],
|
|
'err_url' => $errorUrl,
|
|
'sql_query' => $_POST['sql_query'] ?? '',
|
|
];
|
|
if (isset($whereClauses)) {
|
|
foreach ($whereClauseArray as $keyId => $whereClause) {
|
|
$formParams['where_clause[' . $keyId . ']'] = trim($whereClause);
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['clause_is_unique'])) {
|
|
$formParams['clause_is_unique'] = $_POST['clause_is_unique'];
|
|
}
|
|
|
|
return $formParams;
|
|
}
|
|
|
|
/**
|
|
* Creates array of where clauses
|
|
*
|
|
* @param string[]|string|null $whereClause where clause
|
|
*
|
|
* @return string[] whereClauseArray array of where clauses
|
|
*/
|
|
private function getWhereClauseArray($whereClause): array
|
|
{
|
|
if ($whereClause === null) {
|
|
return [];
|
|
}
|
|
|
|
if (is_array($whereClause)) {
|
|
return $whereClause;
|
|
}
|
|
|
|
return [$whereClause];
|
|
}
|
|
|
|
/**
|
|
* Analysing where clauses array
|
|
*
|
|
* @param string[] $whereClauseArray array of where clauses
|
|
* @param string $table name of the table
|
|
* @param string $db name of the database
|
|
*
|
|
* @return array<int, string[]|ResultInterface[]|array<string, string|null>[]|bool>
|
|
* @phpstan-return array{string[], ResultInterface[], array<string, string|null>[], bool}
|
|
*/
|
|
private function analyzeWhereClauses(
|
|
array $whereClauseArray,
|
|
$table,
|
|
$db
|
|
): array {
|
|
$rows = [];
|
|
$result = [];
|
|
$whereClauses = [];
|
|
$foundUniqueKey = false;
|
|
foreach ($whereClauseArray as $keyId => $whereClause) {
|
|
$localQuery = 'SELECT * FROM '
|
|
. Util::backquote($db) . '.'
|
|
. Util::backquote($table)
|
|
. ' WHERE ' . $whereClause . ';';
|
|
$result[$keyId] = $this->dbi->query($localQuery);
|
|
$rows[$keyId] = $result[$keyId]->fetchAssoc();
|
|
|
|
$whereClauses[$keyId] = str_replace('\\', '\\\\', $whereClause);
|
|
$hasUniqueCondition = $this->showEmptyResultMessageOrSetUniqueCondition(
|
|
$rows,
|
|
$keyId,
|
|
$whereClauseArray,
|
|
$localQuery,
|
|
$result
|
|
);
|
|
if (! $hasUniqueCondition) {
|
|
continue;
|
|
}
|
|
|
|
$foundUniqueKey = true;
|
|
}
|
|
|
|
return [
|
|
$whereClauses,
|
|
$result,
|
|
$rows,
|
|
$foundUniqueKey,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Show message for empty result or set the unique_condition
|
|
*
|
|
* @param array $rows MySQL returned rows
|
|
* @param string $keyId ID in current key
|
|
* @param array $whereClauseArray array of where clauses
|
|
* @param string $localQuery query performed
|
|
* @param ResultInterface[] $result MySQL result handle
|
|
*/
|
|
private function showEmptyResultMessageOrSetUniqueCondition(
|
|
array $rows,
|
|
$keyId,
|
|
array $whereClauseArray,
|
|
$localQuery,
|
|
array $result
|
|
): bool {
|
|
// No row returned
|
|
if (! $rows[$keyId]) {
|
|
unset($rows[$keyId], $whereClauseArray[$keyId]);
|
|
ResponseRenderer::getInstance()->addHTML(
|
|
Generator::getMessage(
|
|
__('MySQL returned an empty result set (i.e. zero rows).'),
|
|
$localQuery
|
|
)
|
|
);
|
|
/**
|
|
* @todo not sure what should be done at this point, but we must not
|
|
* exit if we want the message to be displayed
|
|
*/
|
|
|
|
return false;
|
|
}
|
|
|
|
$meta = $this->dbi->getFieldsMeta($result[$keyId]);
|
|
|
|
[$uniqueCondition] = Util::getUniqueCondition(
|
|
count($meta),
|
|
$meta,
|
|
$rows[$keyId],
|
|
true
|
|
);
|
|
|
|
return (bool) $uniqueCondition;
|
|
}
|
|
|
|
/**
|
|
* No primary key given, just load first row
|
|
*
|
|
* @param string $table name of the table
|
|
* @param string $db name of the database
|
|
*
|
|
* @return array<int, ResultInterface|false[]>
|
|
* @phpstan-return array{ResultInterface, false[]}
|
|
*/
|
|
private function loadFirstRow($table, $db): array
|
|
{
|
|
$result = $this->dbi->query(
|
|
'SELECT * FROM ' . Util::backquote($db)
|
|
. '.' . Util::backquote($table) . ' LIMIT 1;'
|
|
);
|
|
// Can be a string on some old configuration storage settings
|
|
$rows = array_fill(0, (int) $GLOBALS['cfg']['InsertRows'], false);
|
|
|
|
return [
|
|
$result,
|
|
$rows,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Add some url parameters
|
|
*
|
|
* @param array $urlParams containing $db and $table as url parameters
|
|
* @param array $whereClauseArray where clauses array
|
|
*
|
|
* @return array Add some url parameters to $url_params array and return it
|
|
*/
|
|
public function urlParamsInEditMode(
|
|
array $urlParams,
|
|
array $whereClauseArray
|
|
): array {
|
|
foreach ($whereClauseArray as $whereClause) {
|
|
$urlParams['where_clause'] = trim($whereClause);
|
|
}
|
|
|
|
if (! empty($_POST['sql_query'])) {
|
|
$urlParams['sql_query'] = $_POST['sql_query'];
|
|
}
|
|
|
|
return $urlParams;
|
|
}
|
|
|
|
/**
|
|
* Show type information or function selectors in Insert/Edit
|
|
*
|
|
* @param string $which function|type
|
|
* @param array $urlParams containing url parameters
|
|
* @param bool $isShow whether to show the element in $which
|
|
*
|
|
* @return string an HTML snippet
|
|
*/
|
|
public function showTypeOrFunction($which, array $urlParams, $isShow): string
|
|
{
|
|
$params = [];
|
|
|
|
switch ($which) {
|
|
case 'function':
|
|
$params['ShowFunctionFields'] = ($isShow ? 0 : 1);
|
|
$params['ShowFieldTypesInDataEditView'] = $GLOBALS['cfg']['ShowFieldTypesInDataEditView'];
|
|
break;
|
|
case 'type':
|
|
$params['ShowFieldTypesInDataEditView'] = ($isShow ? 0 : 1);
|
|
$params['ShowFunctionFields'] = $GLOBALS['cfg']['ShowFunctionFields'];
|
|
break;
|
|
}
|
|
|
|
$params['goto'] = Url::getFromRoute('/sql');
|
|
$thisUrlParams = array_merge($urlParams, $params);
|
|
|
|
if (! $isShow) {
|
|
return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
|
|
. Url::getCommon($thisUrlParams, '', false) . '">'
|
|
. $this->showTypeOrFunctionLabel($which)
|
|
. '</a>';
|
|
}
|
|
|
|
return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
|
|
. Url::getCommon($thisUrlParams, '', false)
|
|
. '" title="' . __('Hide') . '">'
|
|
. $this->showTypeOrFunctionLabel($which)
|
|
. '</a></th>';
|
|
}
|
|
|
|
/**
|
|
* Show type information or function selectors labels in Insert/Edit
|
|
*
|
|
* @param string $which function|type
|
|
*
|
|
* @return string an HTML snippet
|
|
*/
|
|
private function showTypeOrFunctionLabel($which): string
|
|
{
|
|
switch ($which) {
|
|
case 'function':
|
|
return __('Function');
|
|
|
|
case 'type':
|
|
return __('Type');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Analyze the table column array
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param array $commentsMap comments for every column that has a comment
|
|
* @param bool $timestampSeen whether a timestamp has been seen
|
|
*
|
|
* @return array description of column in given table
|
|
*/
|
|
private function analyzeTableColumnsArray(
|
|
array $column,
|
|
array $commentsMap,
|
|
$timestampSeen
|
|
) {
|
|
$column['Field_md5'] = md5($column['Field']);
|
|
// True_Type contains only the type (stops at first bracket)
|
|
$column['True_Type'] = preg_replace('@\(.*@s', '', $column['Type']);
|
|
$column['len'] = preg_match('@float|double@', $column['Type']) ? 100 : -1;
|
|
$column['Field_title'] = $this->getColumnTitle($column, $commentsMap);
|
|
$column['is_binary'] = $this->isColumn(
|
|
$column,
|
|
[
|
|
'binary',
|
|
'varbinary',
|
|
]
|
|
);
|
|
$column['is_blob'] = $this->isColumn(
|
|
$column,
|
|
[
|
|
'blob',
|
|
'tinyblob',
|
|
'mediumblob',
|
|
'longblob',
|
|
]
|
|
);
|
|
$column['is_char'] = $this->isColumn(
|
|
$column,
|
|
[
|
|
'char',
|
|
'varchar',
|
|
]
|
|
);
|
|
|
|
[
|
|
$column['pma_type'],
|
|
$column['wrap'],
|
|
$column['first_timestamp'],
|
|
] = $this->getEnumSetAndTimestampColumns($column, $timestampSeen);
|
|
|
|
return $column;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the column title
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param array $commentsMap comments for every column that has a comment
|
|
*
|
|
* @return string column title
|
|
*/
|
|
private function getColumnTitle(array $column, array $commentsMap): string
|
|
{
|
|
if (isset($commentsMap[$column['Field']])) {
|
|
return '<span style="border-bottom: 1px dashed black;" title="'
|
|
. htmlspecialchars($commentsMap[$column['Field']]) . '">'
|
|
. htmlspecialchars($column['Field']) . '</span>';
|
|
}
|
|
|
|
return htmlspecialchars($column['Field']);
|
|
}
|
|
|
|
/**
|
|
* check whether the column is of a certain type
|
|
* the goal is to ensure that types such as "enum('one','two','binary',..)"
|
|
* or "enum('one','two','varbinary',..)" are not categorized as binary
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param string[] $types the types to verify
|
|
*/
|
|
public function isColumn(array $column, array $types): bool
|
|
{
|
|
foreach ($types as $oneType) {
|
|
if (mb_stripos($column['Type'], $oneType) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieve set, enum, timestamp table columns
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param bool $timestampSeen whether a timestamp has been seen
|
|
*
|
|
* @return array $column['pma_type'], $column['wrap'], $column['first_timestamp']
|
|
* @psalm-return array{0: mixed, 1: string, 2: bool}
|
|
*/
|
|
private function getEnumSetAndTimestampColumns(array $column, $timestampSeen)
|
|
{
|
|
switch ($column['True_Type']) {
|
|
case 'set':
|
|
return [
|
|
'set',
|
|
'',
|
|
false,
|
|
];
|
|
|
|
case 'enum':
|
|
return [
|
|
'enum',
|
|
'',
|
|
false,
|
|
];
|
|
|
|
case 'timestamp':
|
|
return [
|
|
$column['Type'],
|
|
' text-nowrap',
|
|
! $timestampSeen, // can only occur once per table
|
|
];
|
|
|
|
default:
|
|
return [
|
|
$column['Type'],
|
|
' text-nowrap',
|
|
false,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the nullify code for the null column
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param array $foreigners keys into foreign fields
|
|
* @param array $foreignData data about the foreign keys
|
|
*/
|
|
private function getNullifyCodeForNullColumn(
|
|
array $column,
|
|
array $foreigners,
|
|
array $foreignData
|
|
): string {
|
|
$foreigner = $this->relation->searchColumnInForeigners($foreigners, $column['Field']);
|
|
if (mb_strstr($column['True_Type'], 'enum')) {
|
|
if (mb_strlen((string) $column['Type']) > 20) {
|
|
$nullifyCode = '1';
|
|
} else {
|
|
$nullifyCode = '2';
|
|
}
|
|
} elseif (mb_strstr($column['True_Type'], 'set')) {
|
|
$nullifyCode = '3';
|
|
} elseif ($foreigner && $foreignData['foreign_link'] == false) {
|
|
// foreign key in a drop-down
|
|
$nullifyCode = '4';
|
|
} elseif ($foreigner && $foreignData['foreign_link'] == true) {
|
|
// foreign key with a browsing icon
|
|
$nullifyCode = '6';
|
|
} else {
|
|
$nullifyCode = '5';
|
|
}
|
|
|
|
return $nullifyCode;
|
|
}
|
|
|
|
/**
|
|
* Get HTML textarea for insert form
|
|
*
|
|
* @param array $column column information
|
|
* @param string $backupField hidden input field
|
|
* @param string $columnNameAppendix the name attribute
|
|
* @param string $onChangeClause onchange clause for fields
|
|
* @param int $tabindex tab index
|
|
* @param int $tabindexForValue offset for the values tabindex
|
|
* @param int $idindex id index
|
|
* @param string $textDir text direction
|
|
* @param string $specialCharsEncoded replaced char if the string starts
|
|
* with a \r\n pair (0x0d0a) add an extra \n
|
|
* @param string $dataType the html5 data-* attribute type
|
|
* @param bool $readOnly is column read only or not
|
|
*
|
|
* @return string an html snippet
|
|
*/
|
|
private function getTextarea(
|
|
array $column,
|
|
$backupField,
|
|
$columnNameAppendix,
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
$textDir,
|
|
$specialCharsEncoded,
|
|
$dataType,
|
|
$readOnly
|
|
): string {
|
|
$theClass = '';
|
|
$textAreaRows = $GLOBALS['cfg']['TextareaRows'];
|
|
$textareaCols = $GLOBALS['cfg']['TextareaCols'];
|
|
|
|
if ($column['is_char']) {
|
|
/**
|
|
* @todo clarify the meaning of the "textfield" class and explain
|
|
* why character columns have the "char" class instead
|
|
*/
|
|
$theClass = 'char charField';
|
|
$textAreaRows = $GLOBALS['cfg']['CharTextareaRows'];
|
|
$textareaCols = $GLOBALS['cfg']['CharTextareaCols'];
|
|
$extractedColumnspec = Util::extractColumnSpec($column['Type']);
|
|
$maxlength = $extractedColumnspec['spec_in_brackets'];
|
|
} elseif ($GLOBALS['cfg']['LongtextDoubleTextarea'] && mb_strstr($column['pma_type'], 'longtext')) {
|
|
$textAreaRows = $GLOBALS['cfg']['TextareaRows'] * 2;
|
|
$textareaCols = $GLOBALS['cfg']['TextareaCols'] * 2;
|
|
}
|
|
|
|
return $backupField . "\n"
|
|
. '<textarea name="fields' . $columnNameAppendix . '"'
|
|
. ' class="' . $theClass . '"'
|
|
. ($readOnly ? ' readonly="readonly"' : '')
|
|
. (isset($maxlength) ? ' data-maxlength="' . $maxlength . '"' : '')
|
|
. ' rows="' . $textAreaRows . '"'
|
|
. ' cols="' . $textareaCols . '"'
|
|
. ' dir="' . $textDir . '"'
|
|
. ' id="field_' . $idindex . '_3"'
|
|
. ($onChangeClause ? ' ' . $onChangeClause : '')
|
|
. ' tabindex="' . ($tabindex + $tabindexForValue) . '"'
|
|
. ' data-type="' . $dataType . '">'
|
|
. $specialCharsEncoded
|
|
. '</textarea>';
|
|
}
|
|
|
|
/**
|
|
* Get column values
|
|
*
|
|
* @param string[] $enum_set_values
|
|
*
|
|
* @return array column values as an associative array
|
|
* @psalm-return list<array{html: string, plain: string}>
|
|
*/
|
|
private function getColumnEnumValues(array $enum_set_values): array
|
|
{
|
|
$values = [];
|
|
foreach ($enum_set_values as $val) {
|
|
$values[] = [
|
|
'plain' => $val,
|
|
'html' => htmlspecialchars($val),
|
|
];
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Retrieve column 'set' value and select size
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param string[] $enum_set_values
|
|
*
|
|
* @return array $column['values'], $column['select_size']
|
|
*/
|
|
private function getColumnSetValueAndSelectSize(
|
|
array $column,
|
|
array $enum_set_values
|
|
): array {
|
|
if (! isset($column['values'])) {
|
|
$column['values'] = [];
|
|
foreach ($enum_set_values as $val) {
|
|
$column['values'][] = [
|
|
'plain' => $val,
|
|
'html' => htmlspecialchars($val),
|
|
];
|
|
}
|
|
|
|
$column['select_size'] = min(4, count($column['values']));
|
|
}
|
|
|
|
return [
|
|
$column['values'],
|
|
$column['select_size'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get HTML input type
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param string $columnNameAppendix the name attribute
|
|
* @param string $specialChars special characters
|
|
* @param int $fieldsize html field size
|
|
* @param string $onChangeClause onchange clause for fields
|
|
* @param int $tabindex tab index
|
|
* @param int $tabindexForValue offset for the values tabindex
|
|
* @param int $idindex id index
|
|
* @param string $dataType the html5 data-* attribute type
|
|
* @param bool $readOnly is column read only or not
|
|
*
|
|
* @return string an html snippet
|
|
*/
|
|
private function getHtmlInput(
|
|
array $column,
|
|
$columnNameAppendix,
|
|
$specialChars,
|
|
$fieldsize,
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
$dataType,
|
|
$readOnly
|
|
): string {
|
|
$theClass = 'textfield';
|
|
// verify True_Type which does not contain the parentheses and length
|
|
if (! $readOnly) {
|
|
if ($column['True_Type'] === 'date') {
|
|
$theClass .= ' datefield';
|
|
} elseif ($column['True_Type'] === 'time') {
|
|
$theClass .= ' timefield';
|
|
} elseif ($column['True_Type'] === 'datetime' || $column['True_Type'] === 'timestamp') {
|
|
$theClass .= ' datetimefield';
|
|
}
|
|
}
|
|
|
|
$inputMinMax = '';
|
|
$isInteger = in_array($column['True_Type'], $this->dbi->types->getIntegerTypes());
|
|
if ($isInteger) {
|
|
$extractedColumnspec = Util::extractColumnSpec($column['Type']);
|
|
$isUnsigned = $extractedColumnspec['unsigned'];
|
|
$minMaxValues = $this->dbi->types->getIntegerRange($column['True_Type'], ! $isUnsigned);
|
|
$inputMinMax = 'min="' . $minMaxValues[0] . '" '
|
|
. 'max="' . $minMaxValues[1] . '"';
|
|
$dataType = 'INT';
|
|
}
|
|
|
|
// do not use the 'date' or 'time' types here; they have no effect on some
|
|
// browsers and create side effects (see bug #4218)
|
|
return '<input type="text"'
|
|
. ' name="fields' . $columnNameAppendix . '"'
|
|
. ' value="' . $specialChars . '" size="' . $fieldsize . '"'
|
|
. (isset($column['is_char']) && $column['is_char']
|
|
? ' data-maxlength="' . $fieldsize . '"'
|
|
: '')
|
|
. ($readOnly ? ' readonly="readonly"' : '')
|
|
. ($inputMinMax ? ' ' . $inputMinMax : '')
|
|
. ' data-type="' . $dataType . '"'
|
|
. ' class="' . $theClass . '" ' . $onChangeClause
|
|
. ' tabindex="' . ($tabindex + $tabindexForValue) . '"'
|
|
. ($isInteger ? ' inputmode="numeric"' : '')
|
|
. ' id="field_' . $idindex . '_3">';
|
|
}
|
|
|
|
/**
|
|
* Get HTML select option for upload
|
|
*
|
|
* @param string $vkey [multi_edit]['row_id']
|
|
* @param string $fieldHashMd5 array index as an MD5 to avoid having special characters
|
|
*
|
|
* @return string an HTML snippet
|
|
*/
|
|
private function getSelectOptionForUpload(string $vkey, string $fieldHashMd5): string
|
|
{
|
|
$files = $this->fileListing->getFileSelectOptions(
|
|
Util::userDir((string) ($GLOBALS['cfg']['UploadDir'] ?? ''))
|
|
);
|
|
|
|
if ($files === false) {
|
|
return '<span style="color:red">' . __('Error') . '</span><br>' . "\n"
|
|
. __('The directory you set for upload work cannot be reached.') . "\n";
|
|
}
|
|
|
|
if ($files === '') {
|
|
return '';
|
|
}
|
|
|
|
return "<br>\n"
|
|
. '<i>' . __('Or') . '</i> '
|
|
. __('web server upload directory:') . '<br>' . "\n"
|
|
. '<select size="1" name="fields_uploadlocal'
|
|
. $vkey . '[' . $fieldHashMd5 . ']">' . "\n"
|
|
. '<option value="" selected="selected"></option>' . "\n"
|
|
. $files
|
|
. '</select>' . "\n";
|
|
}
|
|
|
|
/**
|
|
* Retrieve the maximum upload file size
|
|
*
|
|
* @param string $pma_type column type
|
|
* @param int $biggestMaxFileSize biggest max file size for uploading
|
|
*
|
|
* @return array an html snippet and $biggest_max_file_size
|
|
* @psalm-return array{non-empty-string, int}
|
|
*/
|
|
private function getMaxUploadSize(string $pma_type, $biggestMaxFileSize): array
|
|
{
|
|
// find maximum upload size, based on field type
|
|
/**
|
|
* @todo with functions this is not so easy, as you can basically
|
|
* process any data with function like MD5
|
|
*/
|
|
$maxFieldSizes = [
|
|
'tinyblob' => 256,
|
|
'blob' => 65536,
|
|
'mediumblob' => 16777216,
|
|
'longblob' => 4294967296,// yeah, really
|
|
];
|
|
|
|
$thisFieldMaxSize = (int) $GLOBALS['config']->get('max_upload_size'); // from PHP max
|
|
if ($thisFieldMaxSize > $maxFieldSizes[$pma_type]) {
|
|
$thisFieldMaxSize = $maxFieldSizes[$pma_type];
|
|
}
|
|
|
|
$htmlOutput = Util::getFormattedMaximumUploadSize($thisFieldMaxSize) . "\n";
|
|
// do not generate here the MAX_FILE_SIZE, because we should
|
|
// put only one in the form to accommodate the biggest field
|
|
if ($thisFieldMaxSize > $biggestMaxFileSize) {
|
|
$biggestMaxFileSize = $thisFieldMaxSize;
|
|
}
|
|
|
|
return [
|
|
$htmlOutput,
|
|
$biggestMaxFileSize,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get HTML for the Value column of other datatypes
|
|
* (here, "column" is used in the sense of HTML column in HTML table)
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param string $defaultCharEditing default char editing mode which is stored
|
|
* in the config.inc.php script
|
|
* @param string $backupField hidden input field
|
|
* @param string $columnNameAppendix the name attribute
|
|
* @param string $onChangeClause onchange clause for fields
|
|
* @param int $tabindex tab index
|
|
* @param string $specialChars special characters
|
|
* @param int $tabindexForValue offset for the values tabindex
|
|
* @param int $idindex id index
|
|
* @param string $textDir text direction
|
|
* @param string $specialCharsEncoded replaced char if the string starts
|
|
* with a \r\n pair (0x0d0a) add an extra \n
|
|
* @param string $data data to edit
|
|
* @param array $extractedColumnspec associative array containing type,
|
|
* spec_in_brackets and possibly
|
|
* enum_set_values (another array)
|
|
* @param bool $readOnly is column read only or not
|
|
*
|
|
* @return string an html snippet
|
|
*/
|
|
private function getValueColumnForOtherDatatypes(
|
|
array $column,
|
|
$defaultCharEditing,
|
|
$backupField,
|
|
$columnNameAppendix,
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$specialChars,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
$textDir,
|
|
$specialCharsEncoded,
|
|
$data,
|
|
array $extractedColumnspec,
|
|
$readOnly
|
|
): string {
|
|
// HTML5 data-* attribute data-type
|
|
$dataType = $this->dbi->types->getTypeClass($column['True_Type']);
|
|
$fieldsize = $this->getColumnSize($column, $extractedColumnspec['spec_in_brackets']);
|
|
|
|
$isTextareaRequired = $column['is_char']
|
|
&& ($GLOBALS['cfg']['CharEditing'] === 'textarea' || str_contains($data, "\n"));
|
|
if ($isTextareaRequired) {
|
|
$GLOBALS['cfg']['CharEditing'] = $defaultCharEditing;
|
|
$htmlField = $this->getTextarea(
|
|
$column,
|
|
$backupField,
|
|
$columnNameAppendix,
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
$textDir,
|
|
$specialCharsEncoded,
|
|
$dataType,
|
|
$readOnly
|
|
);
|
|
} else {
|
|
$htmlField = $this->getHtmlInput(
|
|
$column,
|
|
$columnNameAppendix,
|
|
$specialChars,
|
|
$fieldsize,
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
$dataType,
|
|
$readOnly
|
|
);
|
|
}
|
|
|
|
return $this->template->render('table/insert/value_column_for_other_datatype', [
|
|
'html_field' => $htmlField,
|
|
'backup_field' => $backupField,
|
|
'is_textarea' => $isTextareaRequired,
|
|
'columnNameAppendix' => $columnNameAppendix,
|
|
'column' => $column,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get the field size
|
|
*
|
|
* @param array $column description of column in given table
|
|
* @param string $specInBrackets text in brackets inside column definition
|
|
*
|
|
* @return int field size
|
|
*/
|
|
private function getColumnSize(array $column, string $specInBrackets): int
|
|
{
|
|
if ($column['is_char']) {
|
|
$fieldsize = (int) $specInBrackets;
|
|
if ($fieldsize > $GLOBALS['cfg']['MaxSizeForInputField']) {
|
|
/**
|
|
* This case happens for CHAR or VARCHAR columns which have
|
|
* a size larger than the maximum size for input field.
|
|
*/
|
|
$GLOBALS['cfg']['CharEditing'] = 'textarea';
|
|
}
|
|
} else {
|
|
/**
|
|
* This case happens for example for INT or DATE columns;
|
|
* in these situations, the value returned in $column['len']
|
|
* seems appropriate.
|
|
*/
|
|
$fieldsize = $column['len'];
|
|
}
|
|
|
|
return min(
|
|
max($fieldsize, $GLOBALS['cfg']['MinSizeForInputField']),
|
|
$GLOBALS['cfg']['MaxSizeForInputField']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* get html for continue insertion form
|
|
*
|
|
* @param string $table name of the table
|
|
* @param string $db name of the database
|
|
* @param array $whereClauseArray array of where clauses
|
|
* @param string $errorUrl error url
|
|
*
|
|
* @return string an html snippet
|
|
*/
|
|
public function getContinueInsertionForm(
|
|
$table,
|
|
$db,
|
|
array $whereClauseArray,
|
|
$errorUrl
|
|
): string {
|
|
return $this->template->render('table/insert/continue_insertion_form', [
|
|
'db' => $db,
|
|
'table' => $table,
|
|
'where_clause_array' => $whereClauseArray,
|
|
'err_url' => $errorUrl,
|
|
'goto' => $GLOBALS['goto'],
|
|
'sql_query' => $_POST['sql_query'] ?? null,
|
|
'has_where_clause' => isset($_POST['where_clause']),
|
|
'insert_rows_default' => $GLOBALS['cfg']['InsertRows'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param string[]|string|null $whereClause
|
|
*
|
|
* @psalm-pure
|
|
*/
|
|
public static function isWhereClauseNumeric($whereClause): bool
|
|
{
|
|
if ($whereClause === null) {
|
|
return false;
|
|
}
|
|
|
|
if (! is_array($whereClause)) {
|
|
$whereClause = [$whereClause];
|
|
}
|
|
|
|
// If we have just numeric primary key, we can also edit next
|
|
// we are looking for `table_name`.`field_name` = numeric_value
|
|
foreach ($whereClause as $clause) {
|
|
// preg_match() returns 1 if there is a match
|
|
$isNumeric = preg_match('@^[\s]*`[^`]*`[\.]`[^`]*` = [0-9]+@', $clause) === 1;
|
|
if ($isNumeric) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get table head and table foot for insert row table
|
|
*
|
|
* @param array $urlParams url parameters
|
|
*
|
|
* @return string an html snippet
|
|
*/
|
|
private function getHeadAndFootOfInsertRowTable(array $urlParams): string
|
|
{
|
|
$type = '';
|
|
$function = '';
|
|
|
|
if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) {
|
|
$type = $this->showTypeOrFunction('type', $urlParams, true);
|
|
}
|
|
|
|
if ($GLOBALS['cfg']['ShowFunctionFields']) {
|
|
$function = $this->showTypeOrFunction('function', $urlParams, true);
|
|
}
|
|
|
|
$template = new Template();
|
|
|
|
return $template->render('table/insert/get_head_and_foot_of_insert_row_table', [
|
|
'type' => $type,
|
|
'function' => $function,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Prepares the field value and retrieve special chars, backup field and data array
|
|
*
|
|
* @param array $currentRow a row of the table
|
|
* @param array $column description of column in given table
|
|
* @param array $extractedColumnspec associative array containing type,
|
|
* spec_in_brackets and possibly
|
|
* enum_set_values (another array)
|
|
* @param array $gisDataTypes list of GIS data types
|
|
* @param string $columnNameAppendix string to append to column name in input
|
|
* @param bool $asIs use the data as is, used in repopulating
|
|
*
|
|
* @return array $real_null_value, $data, $special_chars, $backup_field,
|
|
* $special_chars_encoded
|
|
*/
|
|
private function getSpecialCharsAndBackupFieldForExistingRow(
|
|
array $currentRow,
|
|
array $column,
|
|
array $extractedColumnspec,
|
|
array $gisDataTypes,
|
|
$columnNameAppendix,
|
|
$asIs
|
|
) {
|
|
$specialCharsEncoded = '';
|
|
$data = null;
|
|
$realNullValue = false;
|
|
// (we are editing)
|
|
if (! isset($currentRow[$column['Field']])) {
|
|
$realNullValue = true;
|
|
$currentRow[$column['Field']] = '';
|
|
$specialChars = '';
|
|
$data = $currentRow[$column['Field']];
|
|
} elseif ($column['True_Type'] === 'bit') {
|
|
$specialChars = $asIs
|
|
? $currentRow[$column['Field']]
|
|
: Util::printableBitValue(
|
|
(int) $currentRow[$column['Field']],
|
|
(int) $extractedColumnspec['spec_in_brackets']
|
|
);
|
|
} elseif (
|
|
(substr($column['True_Type'], 0, 9) === 'timestamp'
|
|
|| $column['True_Type'] === 'datetime'
|
|
|| $column['True_Type'] === 'time')
|
|
&& (str_contains($currentRow[$column['Field']], '.'))
|
|
) {
|
|
$currentRow[$column['Field']] = $asIs
|
|
? $currentRow[$column['Field']]
|
|
: Util::addMicroseconds($currentRow[$column['Field']]);
|
|
$specialChars = htmlspecialchars($currentRow[$column['Field']], ENT_COMPAT);
|
|
} elseif (in_array($column['True_Type'], $gisDataTypes)) {
|
|
// Convert gis data to Well Know Text format
|
|
$currentRow[$column['Field']] = $asIs
|
|
? $currentRow[$column['Field']]
|
|
: Gis::convertToWellKnownText($currentRow[$column['Field']], true);
|
|
$specialChars = htmlspecialchars($currentRow[$column['Field']], ENT_COMPAT);
|
|
} else {
|
|
// special binary "characters"
|
|
if ($column['is_binary'] || ($column['is_blob'] && $GLOBALS['cfg']['ProtectBinary'] !== 'all')) {
|
|
$currentRow[$column['Field']] = $asIs
|
|
? $currentRow[$column['Field']]
|
|
: bin2hex($currentRow[$column['Field']]);
|
|
}
|
|
|
|
$specialChars = htmlspecialchars($currentRow[$column['Field']], ENT_COMPAT);
|
|
|
|
//We need to duplicate the first \n or otherwise we will lose
|
|
//the first newline entered in a VARCHAR or TEXT column
|
|
$specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
|
|
|
|
$data = $currentRow[$column['Field']];
|
|
}
|
|
|
|
//when copying row, it is useful to empty auto-increment column
|
|
// to prevent duplicate key error
|
|
if (isset($_POST['default_action']) && $_POST['default_action'] === 'insert') {
|
|
if ($column['Key'] === 'PRI' && str_contains($column['Extra'], 'auto_increment')) {
|
|
$data = $specialCharsEncoded = $specialChars = null;
|
|
}
|
|
}
|
|
|
|
// If a timestamp field value is not included in an update
|
|
// statement MySQL auto-update it to the current timestamp;
|
|
// however, things have changed since MySQL 4.1, so
|
|
// it's better to set a fields_prev in this situation
|
|
$backupField = '<input type="hidden" name="fields_prev'
|
|
. $columnNameAppendix . '" value="'
|
|
. htmlspecialchars($currentRow[$column['Field']], ENT_COMPAT) . '">';
|
|
|
|
return [
|
|
$realNullValue,
|
|
$specialCharsEncoded,
|
|
$specialChars,
|
|
$data,
|
|
$backupField,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* display default values
|
|
*
|
|
* @param array $column description of column in given table
|
|
*
|
|
* @return array $real_null_value, $data, $special_chars,
|
|
* $backup_field, $special_chars_encoded
|
|
* @psalm-return array{bool, mixed, string, string, string}
|
|
*/
|
|
private function getSpecialCharsAndBackupFieldForInsertingMode(
|
|
array $column
|
|
) {
|
|
if (! isset($column['Default'])) {
|
|
$column['Default'] = '';
|
|
$realNullValue = true;
|
|
$data = '';
|
|
} else {
|
|
$realNullValue = false;
|
|
$data = $column['Default'];
|
|
}
|
|
|
|
$trueType = $column['True_Type'];
|
|
|
|
if ($trueType === 'bit') {
|
|
$specialChars = Util::convertBitDefaultValue($column['Default']);
|
|
} elseif (substr($trueType, 0, 9) === 'timestamp' || $trueType === 'datetime' || $trueType === 'time') {
|
|
$specialChars = Util::addMicroseconds($column['Default']);
|
|
} elseif ($trueType === 'binary' || $trueType === 'varbinary') {
|
|
$specialChars = bin2hex($column['Default']);
|
|
} elseif (substr($trueType, -4) === 'text') {
|
|
$textDefault = substr($column['Default'], 1, -1);
|
|
$specialChars = stripcslashes($textDefault !== false ? $textDefault : $column['Default']);
|
|
} else {
|
|
$specialChars = htmlspecialchars($column['Default']);
|
|
}
|
|
|
|
$specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
|
|
|
|
return [
|
|
$realNullValue,
|
|
$data,
|
|
$specialChars,
|
|
'',
|
|
$specialCharsEncoded,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Prepares the update/insert of a row
|
|
*
|
|
* @return array $loop_array, $using_key, $is_insert, $is_insertignore
|
|
* @psalm-return array{array, bool, bool, bool}
|
|
*/
|
|
public function getParamsForUpdateOrInsert()
|
|
{
|
|
if (isset($_POST['where_clause'])) {
|
|
// we were editing something => use the WHERE clause
|
|
$loopArray = is_array($_POST['where_clause'])
|
|
? $_POST['where_clause']
|
|
: [$_POST['where_clause']];
|
|
$usingKey = true;
|
|
$isInsert = isset($_POST['submit_type'])
|
|
&& ($_POST['submit_type'] === 'insert'
|
|
|| $_POST['submit_type'] === 'showinsert'
|
|
|| $_POST['submit_type'] === 'insertignore');
|
|
} else {
|
|
// new row => use indexes
|
|
$loopArray = [];
|
|
if (! empty($_POST['fields'])) {
|
|
$loopArray = array_keys($_POST['fields']['multi_edit']);
|
|
}
|
|
|
|
$usingKey = false;
|
|
$isInsert = true;
|
|
}
|
|
|
|
$isInsertIgnore = isset($_POST['submit_type'])
|
|
&& $_POST['submit_type'] === 'insertignore';
|
|
|
|
return [
|
|
$loopArray,
|
|
$usingKey,
|
|
$isInsert,
|
|
$isInsertIgnore,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* set $_SESSION for edit_next
|
|
*
|
|
* @param string $oneWhereClause one where clause from where clauses array
|
|
*/
|
|
public function setSessionForEditNext($oneWhereClause): void
|
|
{
|
|
$localQuery = 'SELECT * FROM ' . Util::backquote($GLOBALS['db'])
|
|
. '.' . Util::backquote($GLOBALS['table']) . ' WHERE '
|
|
. str_replace('` =', '` >', $oneWhereClause) . ' LIMIT 1;';
|
|
|
|
$res = $this->dbi->query($localQuery);
|
|
$row = $res->fetchRow();
|
|
$meta = $this->dbi->getFieldsMeta($res);
|
|
// must find a unique condition based on unique key,
|
|
// not a combination of all fields
|
|
[$uniqueCondition] = Util::getUniqueCondition(
|
|
count($meta),
|
|
$meta,
|
|
$row,
|
|
true
|
|
);
|
|
if (! $uniqueCondition) {
|
|
return;
|
|
}
|
|
|
|
$_SESSION['edit_next'] = $uniqueCondition;
|
|
}
|
|
|
|
/**
|
|
* set $goto_include variable for different cases and retrieve like,
|
|
* if $GLOBALS['goto'] empty, if $goto_include previously not defined
|
|
* and new_insert, same_insert, edit_next
|
|
*
|
|
* @param string|false $gotoInclude store some script for include, otherwise it is
|
|
* boolean false
|
|
*/
|
|
public function getGotoInclude($gotoInclude): string
|
|
{
|
|
$validOptions = [
|
|
'new_insert',
|
|
'same_insert',
|
|
'edit_next',
|
|
];
|
|
if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $validOptions)) {
|
|
return '/table/change';
|
|
}
|
|
|
|
if (! empty($GLOBALS['goto'])) {
|
|
if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) {
|
|
// this should NOT happen
|
|
//$GLOBALS['goto'] = false;
|
|
if ($GLOBALS['goto'] === 'index.php?route=/sql') {
|
|
$gotoInclude = '/sql';
|
|
} else {
|
|
$gotoInclude = false;
|
|
}
|
|
} else {
|
|
$gotoInclude = $GLOBALS['goto'];
|
|
}
|
|
|
|
if ($GLOBALS['goto'] === 'index.php?route=/database/sql' && strlen($GLOBALS['table']) > 0) {
|
|
$GLOBALS['table'] = '';
|
|
}
|
|
}
|
|
|
|
if (! $gotoInclude) {
|
|
if (strlen($GLOBALS['table']) === 0) {
|
|
$gotoInclude = '/database/sql';
|
|
} else {
|
|
$gotoInclude = '/table/sql';
|
|
}
|
|
}
|
|
|
|
return $gotoInclude;
|
|
}
|
|
|
|
/**
|
|
* Defines the url to return in case of failure of the query
|
|
*
|
|
* @param array $urlParams url parameters
|
|
*
|
|
* @return string error url for query failure
|
|
*/
|
|
public function getErrorUrl(array $urlParams)
|
|
{
|
|
if (isset($_POST['err_url'])) {
|
|
return $_POST['err_url'];
|
|
}
|
|
|
|
return Url::getFromRoute('/table/change', $urlParams);
|
|
}
|
|
|
|
/**
|
|
* Builds the sql query
|
|
*
|
|
* @param bool $isInsertIgnore $_POST['submit_type'] === 'insertignore'
|
|
* @param array $queryFields column names array
|
|
* @param array $valueSets array of query values
|
|
*
|
|
* @return array of query
|
|
* @psalm-return array{string}
|
|
*/
|
|
public function buildSqlQuery(bool $isInsertIgnore, array $queryFields, array $valueSets)
|
|
{
|
|
if ($isInsertIgnore) {
|
|
$insertCommand = 'INSERT IGNORE ';
|
|
} else {
|
|
$insertCommand = 'INSERT ';
|
|
}
|
|
|
|
return [
|
|
$insertCommand . 'INTO '
|
|
. Util::backquote($GLOBALS['table'])
|
|
. ' (' . implode(', ', $queryFields) . ') VALUES ('
|
|
. implode('), (', $valueSets) . ')',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Executes the sql query and get the result, then move back to the calling page
|
|
*
|
|
* @param array $urlParams url parameters array
|
|
* @param array $query built query from buildSqlQuery()
|
|
*
|
|
* @return array $url_params, $total_affected_rows, $last_messages
|
|
* $warning_messages, $error_messages, $return_to_sql_query
|
|
*/
|
|
public function executeSqlQuery(array $urlParams, array $query)
|
|
{
|
|
$returnToSqlQuery = '';
|
|
if (! empty($GLOBALS['sql_query'])) {
|
|
$urlParams['sql_query'] = $GLOBALS['sql_query'];
|
|
$returnToSqlQuery = $GLOBALS['sql_query'];
|
|
}
|
|
|
|
$GLOBALS['sql_query'] = implode('; ', $query) . ';';
|
|
// to ensure that the query is displayed in case of
|
|
// "insert as new row" and then "insert another new row"
|
|
$GLOBALS['display_query'] = $GLOBALS['sql_query'];
|
|
|
|
$totalAffectedRows = 0;
|
|
$lastMessages = [];
|
|
$warningMessages = [];
|
|
$errorMessages = [];
|
|
|
|
foreach ($query as $singleQuery) {
|
|
if (isset($_POST['submit_type']) && $_POST['submit_type'] === 'showinsert') {
|
|
$lastMessages[] = Message::notice(__('Showing SQL query'));
|
|
continue;
|
|
}
|
|
|
|
if ($GLOBALS['cfg']['IgnoreMultiSubmitErrors']) {
|
|
$result = $this->dbi->tryQuery($singleQuery);
|
|
} else {
|
|
$result = $this->dbi->query($singleQuery);
|
|
}
|
|
|
|
if (! $result) {
|
|
$errorMessages[] = $this->dbi->getError();
|
|
} else {
|
|
$totalAffectedRows += $this->dbi->affectedRows();
|
|
|
|
$insertId = $this->dbi->insertId();
|
|
if ($insertId) {
|
|
// insert_id is id of FIRST record inserted in one insert, so if we
|
|
// inserted multiple rows, we had to increment this
|
|
|
|
if ($totalAffectedRows > 0) {
|
|
$insertId += $totalAffectedRows - 1;
|
|
}
|
|
|
|
$lastMessage = Message::notice(__('Inserted row id: %1$d'));
|
|
$lastMessage->addParam($insertId);
|
|
$lastMessages[] = $lastMessage;
|
|
}
|
|
}
|
|
|
|
$warningMessages = $this->getWarningMessages();
|
|
}
|
|
|
|
return [
|
|
$urlParams,
|
|
$totalAffectedRows,
|
|
$lastMessages,
|
|
$warningMessages,
|
|
$errorMessages,
|
|
$returnToSqlQuery,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* get the warning messages array
|
|
*
|
|
* @return string[]
|
|
*/
|
|
private function getWarningMessages(): array
|
|
{
|
|
$warningMessages = [];
|
|
foreach ($this->dbi->getWarnings() as $warning) {
|
|
$warningMessages[] = htmlspecialchars((string) $warning);
|
|
}
|
|
|
|
return $warningMessages;
|
|
}
|
|
|
|
/**
|
|
* Column to display from the foreign table?
|
|
*
|
|
* @param string $whereComparison string that contain relation field value
|
|
* @param array $map all Relations to foreign tables for a given
|
|
* table or optionally a given column in a table
|
|
* @param string $relationField relation field
|
|
*
|
|
* @return string display value from the foreign table
|
|
*/
|
|
public function getDisplayValueForForeignTableColumn(
|
|
$whereComparison,
|
|
array $map,
|
|
$relationField
|
|
) {
|
|
$foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
|
|
|
|
if (! is_array($foreigner)) {
|
|
return '';
|
|
}
|
|
|
|
$displayField = $this->relation->getDisplayField($foreigner['foreign_db'], $foreigner['foreign_table']);
|
|
// Field to display from the foreign table?
|
|
if (is_string($displayField) && strlen($displayField) > 0) {
|
|
$dispsql = 'SELECT ' . Util::backquote($displayField)
|
|
. ' FROM ' . Util::backquote($foreigner['foreign_db'])
|
|
. '.' . Util::backquote($foreigner['foreign_table'])
|
|
. ' WHERE ' . Util::backquote($foreigner['foreign_field'])
|
|
. $whereComparison;
|
|
$dispresult = $this->dbi->tryQuery($dispsql);
|
|
if ($dispresult && $dispresult->numRows() > 0) {
|
|
return (string) $dispresult->fetchValue();
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Display option in the cell according to user choices
|
|
*
|
|
* @param array $map all Relations to foreign tables for a given
|
|
* table or optionally a given column in a table
|
|
* @param string $relationField relation field
|
|
* @param string $whereComparison string that contain relation field value
|
|
* @param string $dispval display value from the foreign table
|
|
* @param string $relationFieldValue relation field value
|
|
*
|
|
* @return string HTML <a> tag
|
|
*/
|
|
public function getLinkForRelationalDisplayField(
|
|
array $map,
|
|
$relationField,
|
|
$whereComparison,
|
|
$dispval,
|
|
$relationFieldValue
|
|
): string {
|
|
$foreigner = $this->relation->searchColumnInForeigners($map, $relationField);
|
|
|
|
if (! is_array($foreigner)) {
|
|
return '';
|
|
}
|
|
|
|
if ($_SESSION['tmpval']['relational_display'] === 'K') {
|
|
// user chose "relational key" in the display options, so
|
|
// the title contains the display field
|
|
$title = $dispval
|
|
? ' title="' . htmlspecialchars($dispval) . '"'
|
|
: '';
|
|
} else {
|
|
$title = ' title="' . htmlspecialchars($relationFieldValue) . '"';
|
|
}
|
|
|
|
$sqlQuery = 'SELECT * FROM '
|
|
. Util::backquote($foreigner['foreign_db'])
|
|
. '.' . Util::backquote($foreigner['foreign_table'])
|
|
. ' WHERE ' . Util::backquote($foreigner['foreign_field'])
|
|
. $whereComparison;
|
|
$urlParams = [
|
|
'db' => $foreigner['foreign_db'],
|
|
'table' => $foreigner['foreign_table'],
|
|
'pos' => '0',
|
|
'sql_signature' => Core::signSqlQuery($sqlQuery),
|
|
'sql_query' => $sqlQuery,
|
|
];
|
|
$output = '<a href="' . Url::getFromRoute('/sql', $urlParams) . '"' . $title . '>';
|
|
|
|
if ($_SESSION['tmpval']['relational_display'] === 'D') {
|
|
// user chose "relational display field" in the
|
|
// display options, so show display field in the cell
|
|
$output .= htmlspecialchars($dispval);
|
|
} else {
|
|
// otherwise display data in the cell
|
|
$output .= htmlspecialchars($relationFieldValue);
|
|
}
|
|
|
|
$output .= '</a>';
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Transform edited values
|
|
*
|
|
* @param string $db db name
|
|
* @param string $table table name
|
|
* @param array $transformation mimetypes for all columns of a table
|
|
* [field_name][field_key]
|
|
* @param array $editedValues transform columns list and new values
|
|
* @param string $file file containing the transformation plugin
|
|
* @param string $columnName column name
|
|
* @param array $extraData extra data array
|
|
* @param string $type the type of transformation
|
|
*
|
|
* @return array
|
|
*/
|
|
public function transformEditedValues(
|
|
$db,
|
|
$table,
|
|
array $transformation,
|
|
array &$editedValues,
|
|
$file,
|
|
$columnName,
|
|
array $extraData,
|
|
$type
|
|
) {
|
|
$includeFile = 'libraries/classes/Plugins/Transformations/' . $file;
|
|
if (is_file(ROOT_PATH . $includeFile)) {
|
|
// $cfg['SaveCellsAtOnce'] = true; JS code sends an array
|
|
$whereClause = is_array($_POST['where_clause']) ? $_POST['where_clause'][0] : $_POST['where_clause'];
|
|
$urlParams = [
|
|
'db' => $db,
|
|
'table' => $table,
|
|
'where_clause_sign' => Core::signSqlQuery($whereClause),
|
|
'where_clause' => $whereClause,
|
|
'transform_key' => $columnName,
|
|
];
|
|
$transformOptions = $this->transformations->getOptions($transformation[$type . '_options'] ?? '');
|
|
$transformOptions['wrapper_link'] = Url::getCommon($urlParams);
|
|
$transformOptions['wrapper_params'] = $urlParams;
|
|
$className = $this->transformations->getClassName($includeFile);
|
|
if (class_exists($className)) {
|
|
/** @var TransformationsPlugin $transformationPlugin */
|
|
$transformationPlugin = new $className();
|
|
|
|
foreach ($editedValues as $cellIndex => $currCellEditedValues) {
|
|
if (! isset($currCellEditedValues[$columnName])) {
|
|
continue;
|
|
}
|
|
|
|
$extraData['transformations'][$cellIndex] = $transformationPlugin->applyTransformation(
|
|
$currCellEditedValues[$columnName],
|
|
$transformOptions
|
|
);
|
|
$editedValues[$cellIndex][$columnName] = $extraData['transformations'][$cellIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $extraData;
|
|
}
|
|
|
|
/**
|
|
* Get value part if a function was specified
|
|
*/
|
|
private function formatAsSqlFunction(
|
|
EditField $editField
|
|
): string {
|
|
if ($editField->function === 'PHP_PASSWORD_HASH') {
|
|
/**
|
|
* @see https://github.com/vimeo/psalm/issues/3350
|
|
*
|
|
* @psalm-suppress InvalidArgument
|
|
*/
|
|
$hash = password_hash($editField->value, PASSWORD_DEFAULT);
|
|
|
|
return "'" . $this->dbi->escapeString($hash) . "'";
|
|
}
|
|
|
|
if ($editField->function === 'UUID') {
|
|
/* This way user will know what UUID new row has */
|
|
$uuid = (string) $this->dbi->fetchValue('SELECT UUID()');
|
|
|
|
return "'" . $this->dbi->escapeString($uuid) . "'";
|
|
}
|
|
|
|
if (
|
|
in_array($editField->function, $this->getGisFromTextFunctions())
|
|
|| in_array($editField->function, $this->getGisFromWKBFunctions())
|
|
) {
|
|
return $editField->function . "('" . $this->dbi->escapeString($editField->value) . "')";
|
|
}
|
|
|
|
if (
|
|
! in_array($editField->function, self::FUNC_NO_PARAM)
|
|
|| ($editField->value !== '' && in_array($editField->function, self::FUNC_OPTIONAL_PARAM))
|
|
) {
|
|
if (
|
|
($editField->salt !== null
|
|
&& ($editField->function === 'AES_ENCRYPT'
|
|
|| $editField->function === 'AES_DECRYPT'))
|
|
|| ($editField->salt
|
|
&& ($editField->function === 'DES_ENCRYPT'
|
|
|| $editField->function === 'DES_DECRYPT'
|
|
|| $editField->function === 'ENCRYPT'))
|
|
) {
|
|
return $editField->function . "('" . $this->dbi->escapeString($editField->value) . "','"
|
|
. $this->dbi->escapeString($editField->salt) . "')";
|
|
}
|
|
|
|
return $editField->function . "('" . $this->dbi->escapeString($editField->value) . "')";
|
|
}
|
|
|
|
return $editField->function . '()';
|
|
}
|
|
|
|
/**
|
|
* Get the field value formatted for use in a SQL statement.
|
|
* Used in both INSERT and UPDATE statements.
|
|
*/
|
|
private function getValueFormattedAsSql(
|
|
EditField $editField,
|
|
string $protectedValue = ''
|
|
): string {
|
|
if ($editField->isUploaded) {
|
|
return $editField->value;
|
|
}
|
|
|
|
if ($editField->function !== '') {
|
|
return $this->formatAsSqlFunction($editField);
|
|
}
|
|
|
|
return $this->formatAsSqlValueBasedOnType(
|
|
$editField,
|
|
$protectedValue
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get query values array and query fields array for insert and update in multi edit
|
|
*
|
|
* @param string|int $whereClause Either a positional index or string representing selected row
|
|
*/
|
|
public function getQueryValueForInsert(
|
|
EditField $editField,
|
|
bool $usingKey,
|
|
$whereClause
|
|
): string {
|
|
$protectedValue = '';
|
|
if ($editField->type === 'protected' && $usingKey && $whereClause !== '') {
|
|
// Fetch the current values of a row to use in case we have a protected field
|
|
$protectedValue = $this->dbi->fetchValue(
|
|
'SELECT ' . Util::backquote($editField->columnName)
|
|
. ' FROM ' . Util::backquote($GLOBALS['table'])
|
|
. ' WHERE ' . $whereClause
|
|
) ?: '';
|
|
}
|
|
|
|
return $this->getValueFormattedAsSql($editField, $protectedValue);
|
|
}
|
|
|
|
/**
|
|
* Get field-value pairs for update SQL.
|
|
* During update, we build the SQL only with the fields that should be updated.
|
|
*/
|
|
public function getQueryValueForUpdate(EditField $editField): string
|
|
{
|
|
$currentValueFormattedAsSql = $this->getValueFormattedAsSql($editField);
|
|
|
|
// avoid setting a field to NULL when it's already NULL
|
|
// (field had the null checkbox before the update; field still has the null checkbox)
|
|
if ($editField->wasPreviouslyNull && $editField->isNull) {
|
|
return '';
|
|
}
|
|
|
|
// A blob field that hasn't been changed will have no value
|
|
if ($currentValueFormattedAsSql === '') {
|
|
return '';
|
|
}
|
|
|
|
if (
|
|
// Field had the null checkbox before the update; field no longer has the null checkbox
|
|
$editField->wasPreviouslyNull ||
|
|
// Field was marked as NULL (the value will be unchanged if it was an empty string)
|
|
$editField->isNull ||
|
|
// A function was applied to the field
|
|
$editField->function !== '' ||
|
|
// The value was changed
|
|
$editField->value !== $editField->previousValue
|
|
) {
|
|
return Util::backquote($editField->columnName) . ' = ' . $currentValueFormattedAsSql;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get the current column value in the form for different data types
|
|
*/
|
|
private function formatAsSqlValueBasedOnType(
|
|
EditField $editField,
|
|
string $protectedValue
|
|
): string {
|
|
if ($editField->type === 'protected') {
|
|
// here we are in protected mode (asked in the config)
|
|
// so tbl_change has put this special value in the
|
|
// columns array, so we do not change the column value
|
|
// but we can still handle column upload
|
|
|
|
// when in UPDATE mode, do not alter field's contents. When in INSERT
|
|
// mode, insert empty field because no values were submitted.
|
|
// If protected blobs where set, insert original fields content.
|
|
if ($protectedValue !== '') {
|
|
return '0x' . bin2hex($protectedValue);
|
|
}
|
|
|
|
if ($editField->isNull) {
|
|
return 'NULL';
|
|
}
|
|
|
|
// The Null checkbox was unchecked for this field
|
|
if ($editField->wasPreviouslyNull) {
|
|
return "''";
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
if ($editField->value === '') {
|
|
// When the field is autoIncrement, the best way to avoid problems
|
|
// in strict mode is to set the value to null (works also in non-strict mode)
|
|
|
|
// If the value is empty and the null checkbox is checked, set it to null
|
|
return $editField->autoIncrement || $editField->isNull ? 'NULL' : "''";
|
|
}
|
|
|
|
if ($editField->type === 'hex') {
|
|
if (substr($editField->value, 0, 2) != '0x') {
|
|
return '0x' . $editField->value;
|
|
}
|
|
|
|
return $editField->value;
|
|
}
|
|
|
|
if ($editField->type === 'bit') {
|
|
$currentValue = (string) preg_replace('/[^01]/', '0', $editField->value);
|
|
|
|
return "b'" . $this->dbi->escapeString($currentValue) . "'";
|
|
}
|
|
|
|
if (
|
|
($editField->type !== 'datetime' && $editField->type !== 'timestamp' && $editField->type !== 'date')
|
|
|| ($editField->value !== 'CURRENT_TIMESTAMP' && $editField->value !== 'current_timestamp()')
|
|
) {
|
|
return "'" . $this->dbi->escapeString($editField->value) . "'";
|
|
}
|
|
|
|
// If there is a value, we ignore the Null checkbox;
|
|
// this could be possible if Javascript is disabled in the browser
|
|
return $editField->value;
|
|
}
|
|
|
|
/**
|
|
* Check whether inline edited value can be truncated or not,
|
|
* and add additional parameters for extra_data array if needed
|
|
*
|
|
* @param string $db Database name
|
|
* @param string $table Table name
|
|
* @param string $columnName Column name
|
|
* @param array $extraData Extra data for ajax response
|
|
*/
|
|
public function verifyWhetherValueCanBeTruncatedAndAppendExtraData(
|
|
$db,
|
|
$table,
|
|
$columnName,
|
|
array &$extraData
|
|
): void {
|
|
$extraData['isNeedToRecheck'] = false;
|
|
|
|
$sqlForRealValue = 'SELECT ' . Util::backquote($table) . '.'
|
|
. Util::backquote($columnName)
|
|
. ' FROM ' . Util::backquote($db) . '.'
|
|
. Util::backquote($table)
|
|
. ' WHERE ' . $_POST['where_clause'][0];
|
|
|
|
$result = $this->dbi->tryQuery($sqlForRealValue);
|
|
|
|
if (! $result) {
|
|
return;
|
|
}
|
|
|
|
$fieldsMeta = $this->dbi->getFieldsMeta($result);
|
|
$meta = $fieldsMeta[0];
|
|
$newValue = $result->fetchValue();
|
|
|
|
if ($newValue === false) {
|
|
return;
|
|
}
|
|
|
|
if ($meta->isTimeType()) {
|
|
$newValue = Util::addMicroseconds($newValue);
|
|
} elseif ($meta->isBinary()) {
|
|
$newValue = '0x' . bin2hex($newValue);
|
|
}
|
|
|
|
$extraData['isNeedToRecheck'] = true;
|
|
$extraData['truncatableFieldValue'] = $newValue;
|
|
}
|
|
|
|
/**
|
|
* Function to get the columns of a table
|
|
*
|
|
* @param string $db current db
|
|
* @param string $table current table
|
|
*
|
|
* @return array[]
|
|
*/
|
|
public function getTableColumns($db, $table)
|
|
{
|
|
$this->dbi->selectDb($db);
|
|
|
|
return array_values($this->dbi->getColumns($db, $table, true));
|
|
}
|
|
|
|
/**
|
|
* Function to determine Insert/Edit rows
|
|
*
|
|
* @param string[]|string|null $whereClause where clause
|
|
* @param string $db current database
|
|
* @param string $table current table
|
|
*
|
|
* @return array<int, bool|string[]|string|ResultInterface|ResultInterface[]|null>
|
|
* @phpstan-return array{
|
|
* bool,
|
|
* string[]|string|null,
|
|
* string[],
|
|
* string[]|null,
|
|
* ResultInterface[]|ResultInterface,
|
|
* array<string, string|null>[]|false[],
|
|
* bool,
|
|
* string|null
|
|
* }
|
|
*/
|
|
public function determineInsertOrEdit($whereClause, $db, $table): array
|
|
{
|
|
if (isset($_POST['where_clause'])) {
|
|
$whereClause = $_POST['where_clause'];
|
|
}
|
|
|
|
if (isset($_SESSION['edit_next'])) {
|
|
$whereClause = $_SESSION['edit_next'];
|
|
unset($_SESSION['edit_next']);
|
|
$afterInsert = 'edit_next';
|
|
}
|
|
|
|
if (isset($_POST['ShowFunctionFields'])) {
|
|
$GLOBALS['cfg']['ShowFunctionFields'] = $_POST['ShowFunctionFields'];
|
|
}
|
|
|
|
if (isset($_POST['ShowFieldTypesInDataEditView'])) {
|
|
$GLOBALS['cfg']['ShowFieldTypesInDataEditView'] = $_POST['ShowFieldTypesInDataEditView'];
|
|
}
|
|
|
|
if (isset($_POST['after_insert'])) {
|
|
$afterInsert = $_POST['after_insert'];
|
|
}
|
|
|
|
if (isset($whereClause)) {
|
|
// we are editing
|
|
$insertMode = false;
|
|
$whereClauseArray = $this->getWhereClauseArray($whereClause);
|
|
[$whereClauses, $result, $rows, $foundUniqueKey] = $this->analyzeWhereClauses(
|
|
$whereClauseArray,
|
|
$table,
|
|
$db
|
|
);
|
|
} else {
|
|
// we are inserting
|
|
$insertMode = true;
|
|
$whereClause = null;
|
|
[$result, $rows] = $this->loadFirstRow($table, $db);
|
|
$whereClauses = null;
|
|
$whereClauseArray = [];
|
|
$foundUniqueKey = false;
|
|
}
|
|
|
|
// Copying a row - fetched data will be inserted as a new row,
|
|
// therefore the where clause is needless.
|
|
if (isset($_POST['default_action']) && $_POST['default_action'] === 'insert') {
|
|
$whereClause = $whereClauses = null;
|
|
}
|
|
|
|
return [
|
|
$insertMode,
|
|
$whereClause,
|
|
$whereClauseArray,
|
|
$whereClauses,
|
|
$result,
|
|
$rows,
|
|
$foundUniqueKey,
|
|
$afterInsert ?? null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Function to get comments for the table columns
|
|
*
|
|
* @param string $db current database
|
|
* @param string $table current table
|
|
*
|
|
* @return array comments for columns
|
|
*/
|
|
public function getCommentsMap($db, $table): array
|
|
{
|
|
if ($GLOBALS['cfg']['ShowPropertyComments']) {
|
|
return $this->relation->getComments($db, $table);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Function to get html for the gis editor div
|
|
*/
|
|
public function getHtmlForGisEditor(): string
|
|
{
|
|
return '<div id="gis_editor"></div><div id="popup_background"></div><br>';
|
|
}
|
|
|
|
/**
|
|
* Function to get html for the ignore option in insert mode
|
|
*
|
|
* @param int $rowId row id
|
|
* @param bool $checked ignore option is checked or not
|
|
*/
|
|
public function getHtmlForIgnoreOption($rowId, $checked = true): string
|
|
{
|
|
return '<input type="checkbox"'
|
|
. ($checked ? ' checked="checked"' : '')
|
|
. ' name="insert_ignore_' . $rowId . '"'
|
|
. ' id="insert_ignore_' . $rowId . '">'
|
|
. '<label for="insert_ignore_' . $rowId . '">'
|
|
. __('Ignore')
|
|
. '</label><br>' . "\n";
|
|
}
|
|
|
|
/**
|
|
* Function to get html for the insert edit form header
|
|
*
|
|
* @param bool $hasBlobField whether has blob field
|
|
* @param bool $isUpload whether is upload
|
|
*/
|
|
public function getHtmlForInsertEditFormHeader($hasBlobField, $isUpload): string
|
|
{
|
|
$template = new Template();
|
|
|
|
return $template->render('table/insert/get_html_for_insert_edit_form_header', [
|
|
'has_blob_field' => $hasBlobField,
|
|
'is_upload' => $isUpload,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Function to get html for each insert/edit column
|
|
*
|
|
* @param array $column column
|
|
* @param int $columnNumber column index in table_columns
|
|
* @param array $commentsMap comments map
|
|
* @param bool $timestampSeen whether timestamp seen
|
|
* @param ResultInterface $currentResult current result
|
|
* @param string $chgEvtHandler javascript change event handler
|
|
* @param string $jsvkey javascript validation key
|
|
* @param string $vkey validation key
|
|
* @param bool $insertMode whether insert mode
|
|
* @param array $currentRow current row
|
|
* @param int $oRows row offset
|
|
* @param int $tabindex tab index
|
|
* @param int $columnsCnt columns count
|
|
* @param bool $isUpload whether upload
|
|
* @param array $foreigners foreigners
|
|
* @param int $tabindexForValue tab index offset for value
|
|
* @param string $table table
|
|
* @param string $db database
|
|
* @param int $rowId row id
|
|
* @param int $biggestMaxFileSize biggest max file size
|
|
* @param string $defaultCharEditing default char editing mode which is stored in the config.inc.php script
|
|
* @param string $textDir text direction
|
|
* @param array $repopulate the data to be repopulated
|
|
* @param array $columnMime the mime information of column
|
|
* @param string $whereClause the where clause
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getHtmlForInsertEditFormColumn(
|
|
array $column,
|
|
int $columnNumber,
|
|
array $commentsMap,
|
|
$timestampSeen,
|
|
ResultInterface $currentResult,
|
|
$chgEvtHandler,
|
|
$jsvkey,
|
|
$vkey,
|
|
$insertMode,
|
|
array $currentRow,
|
|
$oRows,
|
|
&$tabindex,
|
|
$columnsCnt,
|
|
$isUpload,
|
|
array $foreigners,
|
|
$tabindexForValue,
|
|
$table,
|
|
$db,
|
|
$rowId,
|
|
$biggestMaxFileSize,
|
|
$defaultCharEditing,
|
|
$textDir,
|
|
array $repopulate,
|
|
array $columnMime,
|
|
$whereClause
|
|
) {
|
|
$readOnly = false;
|
|
|
|
if (! isset($column['processed'])) {
|
|
$column = $this->analyzeTableColumnsArray($column, $commentsMap, $timestampSeen);
|
|
}
|
|
|
|
$asIs = false;
|
|
/** @var string $fieldHashMd5 */
|
|
$fieldHashMd5 = $column['Field_md5'];
|
|
if ($repopulate && array_key_exists($fieldHashMd5, $currentRow)) {
|
|
$currentRow[$column['Field']] = $repopulate[$fieldHashMd5];
|
|
$asIs = true;
|
|
}
|
|
|
|
$extractedColumnspec = Util::extractColumnSpec($column['Type']);
|
|
|
|
if ($column['len'] === -1) {
|
|
$column['len'] = $this->dbi->getFieldsMeta($currentResult)[$columnNumber]->length;
|
|
// length is unknown for geometry fields,
|
|
// make enough space to edit very simple WKTs
|
|
if ($column['len'] === -1) {
|
|
$column['len'] = 30;
|
|
}
|
|
}
|
|
|
|
//Call validation when the form submitted...
|
|
$onChangeClause = $chgEvtHandler
|
|
. "=\"return verificationsAfterFieldChange('"
|
|
. Sanitize::escapeJsString($fieldHashMd5) . "', '"
|
|
. Sanitize::escapeJsString($jsvkey) . "','" . $column['pma_type'] . "')\"";
|
|
|
|
// Use an MD5 as an array index to avoid having special characters
|
|
// in the name attribute (see bug #1746964 )
|
|
$columnNameAppendix = $vkey . '[' . $fieldHashMd5 . ']';
|
|
|
|
if ($column['Type'] === 'datetime' && $column['Null'] !== 'YES' && ! isset($column['Default']) && $insertMode) {
|
|
$column['Default'] = date('Y-m-d H:i:s', time());
|
|
}
|
|
|
|
// Get a list of GIS data types.
|
|
$gisDataTypes = Gis::getDataTypes();
|
|
|
|
// Prepares the field value
|
|
if ($currentRow) {
|
|
// (we are editing)
|
|
[
|
|
$realNullValue,
|
|
$specialCharsEncoded,
|
|
$specialChars,
|
|
$data,
|
|
$backupField,
|
|
] = $this->getSpecialCharsAndBackupFieldForExistingRow(
|
|
$currentRow,
|
|
$column,
|
|
$extractedColumnspec,
|
|
$gisDataTypes,
|
|
$columnNameAppendix,
|
|
$asIs
|
|
);
|
|
} else {
|
|
// (we are inserting)
|
|
// display default values
|
|
$tmp = $column;
|
|
if (isset($repopulate[$fieldHashMd5])) {
|
|
$tmp['Default'] = $repopulate[$fieldHashMd5];
|
|
}
|
|
|
|
[
|
|
$realNullValue,
|
|
$data,
|
|
$specialChars,
|
|
$backupField,
|
|
$specialCharsEncoded,
|
|
] = $this->getSpecialCharsAndBackupFieldForInsertingMode($tmp);
|
|
unset($tmp);
|
|
}
|
|
|
|
$idindex = ($oRows * $columnsCnt) + $columnNumber + 1;
|
|
$tabindex = $idindex;
|
|
|
|
// The function column
|
|
// -------------------
|
|
$foreignData = $this->relation->getForeignData($foreigners, $column['Field'], false, '', '');
|
|
$isColumnBinary = $this->isColumnBinary($column, $isUpload);
|
|
$functionOptions = '';
|
|
|
|
if ($GLOBALS['cfg']['ShowFunctionFields']) {
|
|
$functionOptions = Generator::getFunctionsForField($column, $insertMode, $foreignData);
|
|
}
|
|
|
|
// nullify code is needed by the js nullify() function to be able to generate calls to nullify() in jQuery
|
|
$nullifyCode = $this->getNullifyCodeForNullColumn($column, $foreigners, $foreignData);
|
|
|
|
// The value column (depends on type)
|
|
// ----------------
|
|
// See bug #1667887 for the reason why we don't use the maxlength
|
|
// HTML attribute
|
|
|
|
//add data attributes "no of decimals" and "data type"
|
|
$noDecimals = 0;
|
|
$type = current(explode('(', $column['pma_type']));
|
|
if (preg_match('/\(([^()]+)\)/', $column['pma_type'], $match)) {
|
|
$match[0] = trim($match[0], '()');
|
|
$noDecimals = $match[0];
|
|
}
|
|
|
|
// Check input transformation of column
|
|
$transformedHtml = '';
|
|
if (! empty($columnMime['input_transformation'])) {
|
|
$file = $columnMime['input_transformation'];
|
|
$includeFile = 'libraries/classes/Plugins/Transformations/' . $file;
|
|
if (is_file(ROOT_PATH . $includeFile)) {
|
|
$className = $this->transformations->getClassName($includeFile);
|
|
if (class_exists($className)) {
|
|
$transformationPlugin = new $className();
|
|
$transformationOptions = $this->transformations->getOptions(
|
|
$columnMime['input_transformation_options']
|
|
);
|
|
$urlParams = [
|
|
'db' => $db,
|
|
'table' => $table,
|
|
'transform_key' => $column['Field'],
|
|
'where_clause_sign' => Core::signSqlQuery($whereClause),
|
|
'where_clause' => $whereClause,
|
|
];
|
|
$transformationOptions['wrapper_link'] = Url::getCommon($urlParams);
|
|
$transformationOptions['wrapper_params'] = $urlParams;
|
|
$currentValue = '';
|
|
if (isset($currentRow[$column['Field']])) {
|
|
$currentValue = $currentRow[$column['Field']];
|
|
}
|
|
|
|
if (method_exists($transformationPlugin, 'getInputHtml')) {
|
|
$transformedHtml = $transformationPlugin->getInputHtml(
|
|
$column,
|
|
$rowId,
|
|
$columnNameAppendix,
|
|
$transformationOptions,
|
|
$currentValue,
|
|
$textDir,
|
|
$tabindex,
|
|
$tabindexForValue,
|
|
$idindex
|
|
);
|
|
}
|
|
|
|
if (method_exists($transformationPlugin, 'getScripts')) {
|
|
$GLOBALS['plugin_scripts'] = array_merge(
|
|
$GLOBALS['plugin_scripts'],
|
|
$transformationPlugin->getScripts()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$columnValue = '';
|
|
$foreignDropdown = '';
|
|
$dataType = '';
|
|
$textAreaRows = $GLOBALS['cfg']['TextareaRows'];
|
|
$textareaCols = $GLOBALS['cfg']['TextareaCols'];
|
|
$maxlength = '';
|
|
$enumSelectedValue = '';
|
|
$columnSetValues = [];
|
|
$setSelectSize = 0;
|
|
$isColumnProtectedBlob = false;
|
|
$blobValue = '';
|
|
$blobValueUnit = '';
|
|
$maxUploadSize = 0;
|
|
$selectOptionForUpload = '';
|
|
$inputFieldHtml = '';
|
|
if (empty($transformedHtml)) {
|
|
if (is_array($foreignData['disp_row'])) {
|
|
$foreignDropdown = $this->relation->foreignDropdown(
|
|
$foreignData['disp_row'],
|
|
$foreignData['foreign_field'],
|
|
$foreignData['foreign_display'],
|
|
$data,
|
|
$GLOBALS['cfg']['ForeignKeyMaxLimit']
|
|
);
|
|
}
|
|
|
|
$dataType = $this->dbi->types->getTypeClass($column['True_Type']);
|
|
|
|
if ($column['is_char']) {
|
|
$textAreaRows = max($GLOBALS['cfg']['CharTextareaRows'], 7);
|
|
$textareaCols = $GLOBALS['cfg']['CharTextareaCols'];
|
|
$maxlength = $extractedColumnspec['spec_in_brackets'];
|
|
} elseif ($GLOBALS['cfg']['LongtextDoubleTextarea'] && mb_strstr($column['pma_type'], 'longtext')) {
|
|
$textAreaRows = $GLOBALS['cfg']['TextareaRows'] * 2;
|
|
$textareaCols = $GLOBALS['cfg']['TextareaCols'] * 2;
|
|
}
|
|
|
|
if ($column['pma_type'] === 'enum') {
|
|
if (! isset($column['values'])) {
|
|
$column['values'] = $this->getColumnEnumValues($extractedColumnspec['enum_set_values']);
|
|
}
|
|
|
|
foreach ($column['values'] as $enumValue) {
|
|
if (
|
|
$data == $enumValue['plain'] || ($data == ''
|
|
&& (! isset($_POST['where_clause']) || $column['Null'] !== 'YES')
|
|
&& isset($column['Default']) && $enumValue['plain'] == $column['Default'])
|
|
) {
|
|
$enumSelectedValue = $enumValue['plain'];
|
|
break;
|
|
}
|
|
}
|
|
} elseif ($column['pma_type'] === 'set') {
|
|
[$columnSetValues, $setSelectSize] = $this->getColumnSetValueAndSelectSize(
|
|
$column,
|
|
$extractedColumnspec['enum_set_values']
|
|
);
|
|
} elseif ($column['is_binary'] || $column['is_blob']) {
|
|
$isColumnProtectedBlob = ($GLOBALS['cfg']['ProtectBinary'] === 'blob' && $column['is_blob'])
|
|
|| ($GLOBALS['cfg']['ProtectBinary'] === 'all')
|
|
|| ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' && ! $column['is_blob']);
|
|
if ($isColumnProtectedBlob && isset($data)) {
|
|
$blobSize = Util::formatByteDown(mb_strlen(stripslashes($data)), 3, 1);
|
|
if ($blobSize !== null) {
|
|
[$blobValue, $blobValueUnit] = $blobSize;
|
|
}
|
|
}
|
|
|
|
if ($isUpload && $column['is_blob']) {
|
|
[$maxUploadSize] = $this->getMaxUploadSize($column['pma_type'], $biggestMaxFileSize);
|
|
}
|
|
|
|
if (! empty($GLOBALS['cfg']['UploadDir'])) {
|
|
$selectOptionForUpload = $this->getSelectOptionForUpload($vkey, $fieldHashMd5);
|
|
}
|
|
|
|
if (
|
|
! $isColumnProtectedBlob
|
|
&& ! ($column['is_blob'] || ($column['len'] > $GLOBALS['cfg']['LimitChars']))
|
|
) {
|
|
$inputFieldHtml = $this->getHtmlInput(
|
|
$column,
|
|
$columnNameAppendix,
|
|
$specialChars,
|
|
min(max($column['len'], 4), $GLOBALS['cfg']['LimitChars']),
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
'HEX',
|
|
$readOnly
|
|
);
|
|
}
|
|
} else {
|
|
$columnValue = $this->getValueColumnForOtherDatatypes(
|
|
$column,
|
|
$defaultCharEditing,
|
|
$backupField,
|
|
$columnNameAppendix,
|
|
$onChangeClause,
|
|
$tabindex,
|
|
$specialChars,
|
|
$tabindexForValue,
|
|
$idindex,
|
|
$textDir,
|
|
$specialCharsEncoded,
|
|
$data,
|
|
$extractedColumnspec,
|
|
$readOnly
|
|
);
|
|
}
|
|
}
|
|
|
|
return $this->template->render('table/insert/column_row', [
|
|
'db' => $db,
|
|
'table' => $table,
|
|
'column' => $column,
|
|
'row_id' => $rowId,
|
|
'show_field_types_in_data_edit_view' => $GLOBALS['cfg']['ShowFieldTypesInDataEditView'],
|
|
'show_function_fields' => $GLOBALS['cfg']['ShowFunctionFields'],
|
|
'is_column_binary' => $isColumnBinary,
|
|
'function_options' => $functionOptions,
|
|
'read_only' => $readOnly,
|
|
'nullify_code' => $nullifyCode,
|
|
'real_null_value' => $realNullValue,
|
|
'id_index' => $idindex,
|
|
'type' => $type,
|
|
'decimals' => $noDecimals,
|
|
'special_chars' => $specialChars,
|
|
'transformed_value' => $transformedHtml,
|
|
'value' => $columnValue,
|
|
'is_value_foreign_link' => $foreignData['foreign_link'] === true,
|
|
'backup_field' => $backupField,
|
|
'data' => $data,
|
|
'gis_data_types' => $gisDataTypes,
|
|
'foreign_dropdown' => $foreignDropdown,
|
|
'data_type' => $dataType,
|
|
'textarea_cols' => $textareaCols,
|
|
'textarea_rows' => $textAreaRows,
|
|
'text_dir' => $textDir,
|
|
'max_length' => $maxlength,
|
|
'longtext_double_textarea' => $GLOBALS['cfg']['LongtextDoubleTextarea'],
|
|
'enum_selected_value' => $enumSelectedValue,
|
|
'set_values' => $columnSetValues,
|
|
'set_select_size' => $setSelectSize,
|
|
'is_column_protected_blob' => $isColumnProtectedBlob,
|
|
'blob_value' => $blobValue,
|
|
'blob_value_unit' => $blobValueUnit,
|
|
'is_upload' => $isUpload,
|
|
'max_upload_size' => $maxUploadSize,
|
|
'select_option_for_upload' => $selectOptionForUpload,
|
|
'limit_chars' => $GLOBALS['cfg']['LimitChars'],
|
|
'input_field_html' => $inputFieldHtml,
|
|
]);
|
|
}
|
|
|
|
private function isColumnBinary(array $column, bool $isUpload): bool
|
|
{
|
|
if (! $GLOBALS['cfg']['ShowFunctionFields']) {
|
|
return false;
|
|
}
|
|
|
|
return ($GLOBALS['cfg']['ProtectBinary'] === 'blob' && $column['is_blob'] && ! $isUpload)
|
|
|| ($GLOBALS['cfg']['ProtectBinary'] === 'all' && $column['is_binary'])
|
|
|| ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' && $column['is_binary']);
|
|
}
|
|
|
|
/**
|
|
* Function to get html for each insert/edit row
|
|
*
|
|
* @param array $urlParams url parameters
|
|
* @param array[] $tableColumns table columns
|
|
* @param array $commentsMap comments map
|
|
* @param bool $timestampSeen whether timestamp seen
|
|
* @param ResultInterface $currentResult current result
|
|
* @param string $chgEvtHandler javascript change event handler
|
|
* @param string $jsvkey javascript validation key
|
|
* @param string $vkey validation key
|
|
* @param bool $insertMode whether insert mode
|
|
* @param array $currentRow current row
|
|
* @param int $oRows row offset
|
|
* @param int $tabindex tab index
|
|
* @param int $columnsCnt columns count
|
|
* @param bool $isUpload whether upload
|
|
* @param array $foreigners foreigners
|
|
* @param int $tabindexForValue tab index offset for value
|
|
* @param string $table table
|
|
* @param string $db database
|
|
* @param int $rowId row id
|
|
* @param int $biggestMaxFileSize biggest max file size
|
|
* @param string $textDir text direction
|
|
* @param array $repopulate the data to be repopulated
|
|
* @param array $whereClauseArray the array of where clauses
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getHtmlForInsertEditRow(
|
|
array $urlParams,
|
|
array $tableColumns,
|
|
array $commentsMap,
|
|
$timestampSeen,
|
|
ResultInterface $currentResult,
|
|
$chgEvtHandler,
|
|
$jsvkey,
|
|
$vkey,
|
|
$insertMode,
|
|
array $currentRow,
|
|
&$oRows,
|
|
&$tabindex,
|
|
$columnsCnt,
|
|
$isUpload,
|
|
array $foreigners,
|
|
$tabindexForValue,
|
|
$table,
|
|
$db,
|
|
$rowId,
|
|
$biggestMaxFileSize,
|
|
$textDir,
|
|
array $repopulate,
|
|
array $whereClauseArray
|
|
) {
|
|
$htmlOutput = $this->getHeadAndFootOfInsertRowTable($urlParams)
|
|
. '<tbody>';
|
|
|
|
//store the default value for CharEditing
|
|
$defaultCharEditing = $GLOBALS['cfg']['CharEditing'];
|
|
$mimeMap = $this->transformations->getMime($db, $table);
|
|
$whereClause = '';
|
|
if (isset($whereClauseArray[$rowId])) {
|
|
$whereClause = $whereClauseArray[$rowId];
|
|
}
|
|
|
|
for ($columnNumber = 0; $columnNumber < $columnsCnt; $columnNumber++) {
|
|
$tableColumn = $tableColumns[$columnNumber];
|
|
$columnMime = [];
|
|
if (isset($mimeMap[$tableColumn['Field']])) {
|
|
$columnMime = $mimeMap[$tableColumn['Field']];
|
|
}
|
|
|
|
$virtual = [
|
|
'VIRTUAL',
|
|
'PERSISTENT',
|
|
'VIRTUAL GENERATED',
|
|
'STORED GENERATED',
|
|
];
|
|
if (in_array($tableColumn['Extra'], $virtual)) {
|
|
continue;
|
|
}
|
|
|
|
$htmlOutput .= $this->getHtmlForInsertEditFormColumn(
|
|
$tableColumn,
|
|
$columnNumber,
|
|
$commentsMap,
|
|
$timestampSeen,
|
|
$currentResult,
|
|
$chgEvtHandler,
|
|
$jsvkey,
|
|
$vkey,
|
|
$insertMode,
|
|
$currentRow,
|
|
$oRows,
|
|
$tabindex,
|
|
$columnsCnt,
|
|
$isUpload,
|
|
$foreigners,
|
|
$tabindexForValue,
|
|
$table,
|
|
$db,
|
|
$rowId,
|
|
$biggestMaxFileSize,
|
|
$defaultCharEditing,
|
|
$textDir,
|
|
$repopulate,
|
|
$columnMime,
|
|
$whereClause
|
|
);
|
|
}
|
|
|
|
$oRows++;
|
|
|
|
return $htmlOutput . ' </tbody>'
|
|
. '</table></div><br>'
|
|
. '<div class="clearfloat"></div>';
|
|
}
|
|
|
|
/**
|
|
* Returns list of function names that accept WKB as text
|
|
*
|
|
* @return string[]
|
|
*/
|
|
private function getGisFromTextFunctions(): array
|
|
{
|
|
return $this->dbi->getVersion() >= 50600 ?
|
|
[
|
|
'ST_GeomFromText',
|
|
'ST_GeomCollFromText',
|
|
'ST_LineFromText',
|
|
'ST_MLineFromText',
|
|
'ST_PointFromText',
|
|
'ST_MPointFromText',
|
|
'ST_PolyFromText',
|
|
'ST_MPolyFromText',
|
|
] :
|
|
[
|
|
'GeomFromText',
|
|
'GeomCollFromText',
|
|
'LineFromText',
|
|
'MLineFromText',
|
|
'PointFromText',
|
|
'MPointFromText',
|
|
'PolyFromText',
|
|
'MPolyFromText',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns list of function names that accept WKB as binary
|
|
*
|
|
* @return string[]
|
|
*/
|
|
private function getGisFromWKBFunctions(): array
|
|
{
|
|
return $this->dbi->getVersion() >= 50600 ?
|
|
[
|
|
'ST_GeomFromWKB',
|
|
'ST_GeomCollFromWKB',
|
|
'ST_LineFromWKB',
|
|
'ST_MLineFromWKB',
|
|
'ST_PointFromWKB',
|
|
'ST_MPointFromWKB',
|
|
'ST_PolyFromWKB',
|
|
'ST_MPolyFromWKB',
|
|
] :
|
|
[
|
|
'GeomFromWKB',
|
|
'GeomCollFromWKB',
|
|
'LineFromWKB',
|
|
'MLineFromWKB',
|
|
'PointFromWKB',
|
|
'MPointFromWKB',
|
|
'PolyFromWKB',
|
|
'MPolyFromWKB',
|
|
];
|
|
}
|
|
}
|