/libraries/classes/InsertEdit.php
PHP | 2582 lines | 1689 code | 236 blank | 657 comment | 255 complexity | b062540745e470d7dc86ff763c61d99d MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
Large files files are truncated, but you can click here to view the full file
- <?php
- /**
- * set of functions with the insert/edit features in pma
- */
- declare(strict_types=1);
- namespace PhpMyAdmin;
- use PhpMyAdmin\Html\Generator;
- use PhpMyAdmin\Plugins\TransformationsPlugin;
- use PhpMyAdmin\Utils\Gis;
- use function __;
- use function array_fill;
- 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 mb_substr;
- 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;
- /**
- * PhpMyAdmin\InsertEdit class
- */
- class InsertEdit
- {
- /**
- * DatabaseInterface instance
- *
- * @var DatabaseInterface
- */
- private $dbi;
- /** @var Relation */
- private $relation;
- /** @var Transformations */
- private $transformations;
- /** @var FileListing */
- private $fileListing;
- /** @var Template */
- public $template;
- /**
- * @param DatabaseInterface $dbi DatabaseInterface instance
- */
- public function __construct(DatabaseInterface $dbi)
- {
- $this->dbi = $dbi;
- $this->relation = new Relation($this->dbi);
- $this->transformations = new Transformations();
- $this->fileListing = new FileListing();
- $this->template = new 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 array of insert/edit form parameters
- */
- public function getFormParametersForInsertForm(
- $db,
- $table,
- ?array $whereClauses,
- array $whereClauseArray,
- $errorUrl
- ) {
- $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 array|string|null $whereClause where clause
- *
- * @return array whereClauseArray array of where clauses
- */
- private function getWhereClauseArray($whereClause)
- {
- if (! isset($whereClause)) {
- return [];
- }
- if (is_array($whereClause)) {
- return $whereClause;
- }
- return [0 => $whereClause];
- }
- /**
- * Analysing where clauses array
- *
- * @param array $whereClauseArray array of where clauses
- * @param string $table name of the table
- * @param string $db name of the database
- *
- * @return array $where_clauses, $result, $rows, $found_unique_key
- */
- private function analyzeWhereClauses(
- array $whereClauseArray,
- $table,
- $db
- ) {
- $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,
- DatabaseInterface::CONNECT_USER,
- DatabaseInterface::QUERY_STORE
- );
- $rows[$keyId] = $this->dbi->fetchAssoc($result[$keyId]);
- $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 array $result MySQL result handle
- */
- private function showEmptyResultMessageOrSetUniqueCondition(
- array $rows,
- $keyId,
- array $whereClauseArray,
- $localQuery,
- array $result
- ): bool {
- $hasUniqueCondition = false;
- // 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
- */
- } else {// end if (no row returned)
- $meta = $this->dbi->getFieldsMeta($result[$keyId]) ?? [];
- [$uniqueCondition, $tmpClauseIsUnique] = Util::getUniqueCondition(
- $result[$keyId],
- count($meta),
- $meta,
- $rows[$keyId],
- true
- );
- if (! empty($uniqueCondition)) {
- $hasUniqueCondition = true;
- }
- unset($uniqueCondition, $tmpClauseIsUnique);
- }
- return $hasUniqueCondition;
- }
- /**
- * No primary key given, just load first row
- *
- * @param string $table name of the table
- * @param string $db name of the database
- *
- * @return array containing $result and $rows arrays
- */
- private function loadFirstRow($table, $db)
- {
- $result = $this->dbi->query(
- 'SELECT * FROM ' . Util::backquote($db)
- . '.' . Util::backquote($table) . ' LIMIT 1;',
- DatabaseInterface::CONNECT_USER,
- DatabaseInterface::QUERY_STORE
- );
- // 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)
- {
- $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, '') . '">'
- . $this->showTypeOrFunctionLabel($which)
- . '</a>';
- }
- return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
- . Url::getCommon($thisUrlParams, '')
- . '" title="' . __('Hide') . '">'
- . $this->showTypeOrFunctionLabel($which)
- . '</a></th>';
- }
- /**
- * Show type information or function selectors labels in Insert/Edit
- *
- * @param string $which function|type
- *
- * @return string|null an HTML snippet
- */
- private function showTypeOrFunctionLabel($which)
- {
- switch ($which) {
- case 'function':
- return __('Function');
- case 'type':
- return __('Type');
- }
- return null;
- }
- /**
- * 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_html'] = htmlspecialchars($column['Field']);
- $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)
- {
- if (isset($commentsMap[$column['Field']])) {
- return '<span style="border-bottom: 1px dashed black;" title="'
- . htmlspecialchars($commentsMap[$column['Field']]) . '">'
- . $column['Field_html'] . '</span>';
- }
- return $column['Field_html'];
- }
- /**
- * 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 array $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']
- */
- private function getEnumSetAndTimestampColumns(array $column, $timestampSeen)
- {
- $column['first_timestamp'] = false;
- switch ($column['True_Type']) {
- case 'set':
- $column['pma_type'] = 'set';
- $column['wrap'] = '';
- break;
- case 'enum':
- $column['pma_type'] = 'enum';
- $column['wrap'] = '';
- break;
- case 'timestamp':
- if (! $timestampSeen) { // can only occur once per table
- $column['first_timestamp'] = true;
- }
- $column['pma_type'] = $column['Type'];
- $column['wrap'] = ' text-nowrap';
- break;
- default:
- $column['pma_type'] = $column['Type'];
- $column['wrap'] = ' text-nowrap';
- break;
- }
- return [
- $column['pma_type'],
- $column['wrap'],
- $column['first_timestamp'],
- ];
- }
- /**
- * 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 (! empty($foreigners) && ! empty($foreigner) && $foreignData['foreign_link'] == false) {
- // foreign key in a drop-down
- $nullifyCode = '4';
- } elseif (! empty($foreigners) && ! empty($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
- ) {
- $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"'
- . (! empty($onChangeClause) ? ' ' . $onChangeClause : '')
- . ' tabindex="' . ($tabindex + $tabindexForValue) . '"'
- . ' data-type="' . $dataType . '">'
- . $specialCharsEncoded
- . '</textarea>';
- }
- /**
- * Get column values
- *
- * @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)
- *
- * @return array column values as an associative array
- */
- private function getColumnEnumValues(array $column, array $extractedColumnspec)
- {
- $column['values'] = [];
- foreach ($extractedColumnspec['enum_set_values'] as $val) {
- $column['values'][] = [
- 'plain' => $val,
- 'html' => htmlspecialchars($val),
- ];
- }
- return $column['values'];
- }
- /**
- * Retrieve column 'set' value and select size
- *
- * @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)
- *
- * @return array $column['values'], $column['select_size']
- */
- private function getColumnSetValueAndSelectSize(
- array $column,
- array $extractedColumnspec
- ) {
- if (! isset($column['values'])) {
- $column['values'] = [];
- foreach ($extractedColumnspec['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
- ) {
- $inputType = 'text';
- // do not use the 'date' or 'time' types here; they have no effect on some
- // browsers and create side effects (see bug #4218)
- $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 = false;
- if (in_array($column['True_Type'], $this->dbi->types->getIntegerTypes())) {
- $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';
- }
- return '<input type="' . $inputType . '"'
- . ' name="fields' . $columnNameAppendix . '"'
- . ' value="' . $specialChars . '" size="' . $fieldsize . '"'
- . (isset($column['is_char']) && $column['is_char']
- ? ' data-maxlength="' . $fieldsize . '"'
- : '')
- . ($readOnly ? ' readonly="readonly"' : '')
- . ($inputMinMax !== false ? ' ' . $inputMinMax : '')
- . ' data-type="' . $dataType . '"'
- . ($inputType === 'time' ? ' step="1"' : '')
- . ' class="' . $theClass . '" ' . $onChangeClause
- . ' tabindex="' . ($tabindex + $tabindexForValue) . '"'
- . ' id="field_' . $idindex . '_3">';
- }
- /**
- * Get HTML select option for upload
- *
- * @param string $vkey [multi_edit]['row_id']
- * @param array $column description of column in given table
- *
- * @return string|null an html snippet
- */
- private function getSelectOptionForUpload($vkey, array $column)
- {
- $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 (! empty($files)) {
- return "<br>\n"
- . '<i>' . __('Or') . '</i> '
- . __('web server upload directory:') . '<br>' . "\n"
- . '<select size="1" name="fields_uploadlocal'
- . $vkey . '[' . $column['Field_md5'] . ']">' . "\n"
- . '<option value="" selected="selected"></option>' . "\n"
- . $files
- . '</select>' . "\n";
- }
- return null;
- }
- /**
- * Retrieve the maximum upload file size
- *
- * @param array $column description of column in given table
- * @param int $biggestMaxFileSize biggest max file size for uploading
- *
- * @return array an html snippet and $biggest_max_file_size
- */
- private function getMaxUploadSize(array $column, $biggestMaxFileSize)
- {
- // 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 = $GLOBALS['config']->get('max_upload_size'); // from PHP max
- if ($thisFieldMaxSize > $maxFieldSizes[$column['pma_type']]) {
- $thisFieldMaxSize = $maxFieldSizes[$column['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
- ) {
- // HTML5 data-* attribute data-type
- $dataType = $this->dbi->types->getTypeClass($column['True_Type']);
- $fieldsize = $this->getColumnSize($column, $extractedColumnspec);
- $htmlOutput = $backupField . "\n";
- if ($column['is_char'] && ($GLOBALS['cfg']['CharEditing'] === 'textarea' || str_contains($data, "\n"))) {
- $htmlOutput .= "\n";
- $GLOBALS['cfg']['CharEditing'] = $defaultCharEditing;
- $htmlOutput .= $this->getTextarea(
- $column,
- $backupField,
- $columnNameAppendix,
- $onChangeClause,
- $tabindex,
- $tabindexForValue,
- $idindex,
- $textDir,
- $specialCharsEncoded,
- $dataType,
- $readOnly
- );
- } else {
- $htmlOutput .= $this->getHtmlInput(
- $column,
- $columnNameAppendix,
- $specialChars,
- $fieldsize,
- $onChangeClause,
- $tabindex,
- $tabindexForValue,
- $idindex,
- $dataType,
- $readOnly
- );
- if (
- preg_match('/(VIRTUAL|PERSISTENT|GENERATED)/', $column['Extra'])
- && ! str_contains($column['Extra'], 'DEFAULT_GENERATED')
- ) {
- $htmlOutput .= '<input type="hidden" name="virtual'
- . $columnNameAppendix . '" value="1">';
- }
- if ($column['Extra'] === 'auto_increment') {
- $htmlOutput .= '<input type="hidden" name="auto_increment'
- . $columnNameAppendix . '" value="1">';
- }
- if (substr($column['pma_type'], 0, 9) === 'timestamp') {
- $htmlOutput .= '<input type="hidden" name="fields_type'
- . $columnNameAppendix . '" value="timestamp">';
- }
- if (substr($column['pma_type'], 0, 4) === 'date') {
- $type = substr($column['pma_type'], 0, 8) === 'datetime' ? 'datetime' : 'date';
- $htmlOutput .= '<input type="hidden" name="fields_type'
- . $columnNameAppendix . '" value="' . $type . '">';
- }
- if ($column['True_Type'] === 'bit') {
- $htmlOutput .= '<input type="hidden" name="fields_type'
- . $columnNameAppendix . '" value="bit">';
- }
- }
- return $htmlOutput;
- }
- /**
- * Get the field size
- *
- * @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)
- *
- * @return int field size
- */
- private function getColumnSize(array $column, array $extractedColumnspec)
- {
- if ($column['is_char']) {
- $fieldsize = $extractedColumnspec['spec_in_brackets'];
- 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
- ) {
- 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 (! isset($whereClause)) {
- return false;
- }
- $isNumeric = 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 === true) {
- break;
- }
- }
- return $isNumeric;
- }
- /**
- * 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)
- {
- $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 bool $realNullValue whether column value null or not null
- * @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,
- $realNullValue,
- array $gisDataTypes,
- $columnNameAppendix,
- $asIs
- ) {
- $specialCharsEncoded = '';
- $data = null;
- // (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
- * @param bool $realNullValue whether column value null or not null
- *
- * @return array $real_null_value, $data, $special_chars,
- * $backup_field, $special_chars_encoded
- */
- private function getSpecialCharsAndBackupFieldForInsertingMode(
- array $column,
- $realNullValue
- ) {
- if (! isset($column['Default'])) {
- $column['Default'] = '';
- $realNullValue = true;
- $data = '';
- } else {
- $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']);
- }
- $backupField = '';
- $specialCharsEncoded = Util::duplicateFirstNewline($specialChars);
- return [
- $realNullValue,
- $data,
- $specialChars,
- $backupField,
- $specialCharsEncoded,
- ];
- }
- /**
- * Prepares the update/insert of a row
- *
- * @return array $loop_array, $using_key, $is_insert, $is_insertignore
- */
- 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'])) {
- foreach ($_POST['fields']['multi_edit'] as $key => $dummy) {
- $loopArray[] = $key;
- }
- }
- $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 = $this->dbi->fetchRow($res);
- $meta = $this->dbi->getFieldsMeta($res) ?? [];
- // must find a unique condition based on unique key,
- // not a combination of all fields
- [$uniqueCondition, $clauseIsUnique] = Util::getUniqueCondition(
- $res,
- count($meta),
- $meta,
- $row ?? [],
- true
- );
- if (! empty($uniqueCondition)) {
- $_SESSION['edit_next'] = $uniqueCondition;
- }
- unset($uniqueCondition, $clauseIsUnique);
- }
- /**
- * 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
- *
- * @return string|false
- */
- public function getGotoInclude($gotoInclude)
- {
- $validOptions = [
- 'new_insert',
- 'same_insert',
- 'edit_next',
- ];
- if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $validOptions)) {
- $gotoInclude = '/table/change';
- } elseif (! 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
- */
- public function buildSqlQuery($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 {
- $tmp = @$this->dbi->affectedRows();
- if ($tmp) {
- $totalAffectedRows += $tmp;
- }
- unset($tmp);
- $insertId = $this->dbi->insertId();
- if ($insertId !== false && $insertId != 0) {
- // 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;
- }
- $this->dbi->freeResult($result);
- }
- $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-…
Large files files are truncated, but you can click here to view the full file