/
var
/
www
/
html
/
stage
/
phpmyadmin
/
src
/
Table
/
Upload File
HOME
<?php declare(strict_types=1); namespace PhpMyAdmin\Table; use PhpMyAdmin\Charsets; use PhpMyAdmin\Config; use PhpMyAdmin\ConfigStorage\Relation; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Partitioning\Partition; use PhpMyAdmin\Partitioning\TablePartitionDefinition; use PhpMyAdmin\Query\Compatibility; use PhpMyAdmin\StorageEngine; use PhpMyAdmin\Transformations; use PhpMyAdmin\UserPrivileges; use PhpMyAdmin\Util; use function array_keys; use function array_merge; use function bin2hex; use function count; use function explode; use function in_array; use function is_array; use function mb_strtoupper; use function preg_quote; use function preg_replace; use function rtrim; use function str_ends_with; use function stripcslashes; use function substr; use function trim; /** * Displays the form used to define the structure of the table */ final class ColumnsDefinition { public function __construct( private DatabaseInterface $dbi, private Relation $relation, private Transformations $transformations, ) { } /** * @param mixed[]|null $selected Selected * @param int $numFields The number of fields * @psalm-param list<array{ * Field: string, * Type: string, * Collation: string|null, * Null:'YES'|'NO', * Key: string, * Default: string|null, * Extra: string, * Privileges: string, * Comment: string * }>|null $fieldsMeta Fields meta * @psalm-param '/table/create'|'/table/add-field'|'/table/structure/save' $action * * @return array<string, mixed> */ public function displayForm( UserPrivileges $userPrivileges, string $action, int $numFields = 0, array|null $selected = null, array|null $fieldsMeta = null, ): array { $GLOBALS['mime_map'] ??= null; $regenerate = false; $lengthValuesInputSize = 8; $contentCells = []; $formParams = ['db' => Current::$database]; if ($action === '/table/create') { $formParams['reload'] = 1; } else { if ($action === '/table/add-field') { $formParams = array_merge($formParams, ['field_where' => $_POST['field_where'] ?? null]); if (isset($_POST['field_where'])) { $formParams['after_field'] = (string) $_POST['after_field']; } } $formParams['table'] = Current::$table; } $formParams['orig_num_fields'] = $numFields; $formParams = array_merge( $formParams, ['orig_field_where' => $_POST['field_where'] ?? null, 'orig_after_field' => $_POST['after_field'] ?? null], ); if (is_array($selected)) { foreach ($selected as $oFldNr => $oFldVal) { $formParams['selected[' . $oFldNr . ']'] = $oFldVal; } } $isBackup = $action !== '/table/create' && $action !== '/table/add-field'; $relationParameters = $this->relation->getRelationParameters(); $commentsMap = $this->relation->getComments(Current::$database, Current::$table); $moveColumns = []; if ($fieldsMeta !== null) { $moveColumns = $this->dbi->getTable(Current::$database, Current::$table)->getColumnsMeta(); } $availableMime = []; $config = Config::getInstance(); if ($relationParameters->browserTransformationFeature !== null && $config->settings['BrowseMIME']) { $GLOBALS['mime_map'] = $this->transformations->getMime(Current::$database, Current::$table); $availableMime = $this->transformations->getAvailableMimeTypes(); } $mimeTypes = ['input_transformation', 'transformation']; foreach ($mimeTypes as $mimeType) { if (! isset($availableMime[$mimeType])) { continue; } foreach (array_keys($availableMime[$mimeType]) as $mimekey) { $availableMime[$mimeType . '_file_quoted'][$mimekey] = preg_quote( $availableMime[$mimeType . '_file'][$mimekey], '@', ); } } if (isset($_POST['submit_num_fields']) || isset($_POST['submit_partition_change'])) { //if adding new fields, set regenerate to keep the original values $regenerate = true; } $foreigners = $this->relation->getForeigners(Current::$database, Current::$table, '', 'foreign'); $childReferences = null; // From MySQL 5.6.6 onwards columns with foreign keys can be renamed. // Hence, no need to get child references if ($this->dbi->getVersion() < 50606) { $childReferences = $this->relation->getChildReferences(Current::$database, Current::$table); } /** @infection-ignore-all */ for ($columnNumber = 0; $columnNumber < $numFields; $columnNumber++) { $type = ''; $length = ''; $columnMeta = []; $submitAttribute = null; $extractedColumnSpec = []; if ($regenerate) { $columnMeta = $this->getColumnMetaForRegeneratedFields($columnNumber); $length = Util::getValueByKey($_POST, ['field_length', $columnNumber], $length); $submitAttribute = Util::getValueByKey($_POST, ['field_attribute', $columnNumber], false); $commentsMap[$columnMeta['Field']] = Util::getValueByKey($_POST, ['field_comments', $columnNumber]); $GLOBALS['mime_map'][$columnMeta['Field']] = array_merge( $GLOBALS['mime_map'][$columnMeta['Field']] ?? [], [ 'mimetype' => Util::getValueByKey($_POST, ['field_mimetype', $columnNumber]), 'transformation' => Util::getValueByKey( $_POST, ['field_transformation' , $columnNumber], ), 'transformation_options' => Util::getValueByKey( $_POST, ['field_transformation_options' , $columnNumber], ), ], ); } elseif (isset($fieldsMeta[$columnNumber])) { $columnMeta = $fieldsMeta[$columnNumber]; $virtual = ['VIRTUAL', 'PERSISTENT', 'VIRTUAL GENERATED', 'STORED GENERATED']; if (in_array($columnMeta['Extra'], $virtual, true)) { $tableObj = new Table(Current::$table, Current::$database, $this->dbi); $expressions = $tableObj->getColumnGenerationExpression($columnMeta['Field']); $columnMeta['Expression'] = is_array($expressions) ? $expressions[$columnMeta['Field']] : null; } $columnMetaDefault = self::decorateColumnMetaDefault( $columnMeta['Type'], $columnMeta['Default'], $columnMeta['Null'] === 'YES', ); $columnMeta = array_merge($columnMeta, $columnMetaDefault); } if (isset($columnMeta['Type'])) { $extractedColumnSpec = Util::extractColumnSpec($columnMeta['Type']); if ($extractedColumnSpec['type'] === 'bit') { $columnMeta['Default'] = Util::convertBitDefaultValue($columnMeta['Default']); } $type = $extractedColumnSpec['type']; if ($length == '') { $length = $extractedColumnSpec['spec_in_brackets']; } } else { // creating a column $columnMeta['Type'] = ''; } // Variable tell if current column is bound in a foreign key constraint or not. // MySQL version from 5.6.6 allow renaming columns with foreign keys if (isset($columnMeta['Field'], $formParams['table']) && $this->dbi->getVersion() < 50606) { $columnMeta['column_status'] = $this->relation->checkChildForeignReferences( $formParams['db'], $formParams['table'], $columnMeta['Field'], $foreigners, $childReferences, ); } // some types, for example longtext, are reported as // "longtext character set latin7" when their charset and / or collation // differs from the ones of the corresponding database. // rtrim the type, for cases like "float unsigned" $type = rtrim( preg_replace('/[\s]character set[\s][\S]+/', '', $type), ); /** * old column attributes */ if ($isBackup) { // old column name if (isset($columnMeta['Field'])) { $formParams['field_orig[' . $columnNumber . ']'] = $columnMeta['Field']; if (isset($columnMeta['column_status']) && ! $columnMeta['column_status']['isEditable']) { $formParams['field_name[' . $columnNumber . ']'] = $columnMeta['Field']; } } else { $formParams['field_orig[' . $columnNumber . ']'] = ''; } // old column type // keep in uppercase because the new type will be in uppercase $formParams['field_type_orig[' . $columnNumber . ']'] = mb_strtoupper($type); if (isset($columnMeta['column_status']) && ! $columnMeta['column_status']['isEditable']) { $formParams['field_type[' . $columnNumber . ']'] = mb_strtoupper($type); } // old column length $formParams['field_length_orig[' . $columnNumber . ']'] = $length; // old column default $formParams = array_merge( $formParams, [ 'field_default_value_orig[' . $columnNumber . ']' => $columnMeta['Default'] ?? '', 'field_default_type_orig[' . $columnNumber . ']' => $columnMeta['DefaultType'] ?? '', 'field_collation_orig[' . $columnNumber . ']' => $columnMeta['Collation'] ?? '', 'field_attribute_orig[' . $columnNumber . ']' => trim($extractedColumnSpec['attribute'] ?? ''), 'field_null_orig[' . $columnNumber . ']' => $columnMeta['Null'] ?? '', 'field_extra_orig[' . $columnNumber . ']' => $columnMeta['Extra'] ?? '', 'field_comments_orig[' . $columnNumber . ']' => $columnMeta['Comment'] ?? '', 'field_virtuality_orig[' . $columnNumber . ']' => $columnMeta['Virtuality'] ?? '', 'field_expression_orig[' . $columnNumber . ']' => $columnMeta['Expression'] ?? '', ], ); } $defaultValue = ''; $typeUpper = mb_strtoupper($type); // For a TIMESTAMP, do not show the string "CURRENT_TIMESTAMP" as a default value if (isset($columnMeta['DefaultValue'])) { $defaultValue = $columnMeta['DefaultValue']; } if ($typeUpper === 'BIT') { $defaultValue = ! empty($columnMeta['DefaultValue']) ? Util::convertBitDefaultValue($columnMeta['DefaultValue']) : ''; } elseif ($typeUpper === 'BINARY' || $typeUpper === 'VARBINARY') { $defaultValue = bin2hex((string) $columnMeta['DefaultValue']); } $contentCells[$columnNumber] = [ 'column_number' => $columnNumber, 'column_meta' => $columnMeta, 'type_upper' => $typeUpper, 'default_value' => $defaultValue, 'length_values_input_size' => $lengthValuesInputSize, 'length' => $length, 'extracted_columnspec' => $extractedColumnSpec, 'submit_attribute' => $submitAttribute, 'comments_map' => $commentsMap, 'fields_meta' => $fieldsMeta ?? null, 'is_backup' => $isBackup, 'move_columns' => $moveColumns, 'available_mime' => $availableMime, 'mime_map' => $GLOBALS['mime_map'] ?? [], ]; } $partitionDetails = TablePartitionDefinition::getDetails(); $charsets = Charsets::getCharsets($this->dbi, $config->selectedServer['DisableIS']); $collations = Charsets::getCollations($this->dbi, $config->selectedServer['DisableIS']); $charsetsList = []; foreach ($charsets as $charset) { $collationsList = []; foreach ($collations[$charset->getName()] as $collation) { $collationsList[] = ['name' => $collation->getName(), 'description' => $collation->getDescription()]; } $charsetsList[] = [ 'name' => $charset->getName(), 'description' => $charset->getDescription(), 'collations' => $collationsList, ]; } $storageEngines = StorageEngine::getArray(); $isIntegersLengthRestricted = Compatibility::isIntegersLengthRestricted($this->dbi); return [ 'is_backup' => $isBackup, 'fields_meta' => $fieldsMeta ?? null, 'relation_parameters' => $relationParameters, 'action' => $action, 'form_params' => $formParams, 'content_cells' => $contentCells, 'partition_details' => $partitionDetails, 'primary_indexes' => $_POST['primary_indexes'] ?? null, 'unique_indexes' => $_POST['unique_indexes'] ?? null, 'indexes' => $_POST['indexes'] ?? null, 'fulltext_indexes' => $_POST['fulltext_indexes'] ?? null, 'spatial_indexes' => $_POST['spatial_indexes'] ?? null, 'table' => $_POST['table'] ?? null, 'comment' => $_POST['comment'] ?? null, 'tbl_collation' => $_POST['tbl_collation'] ?? null, 'charsets' => $charsetsList, 'tbl_storage_engine' => $_POST['tbl_storage_engine'] ?? null, 'storage_engines' => $storageEngines, 'connection' => $_POST['connection'] ?? null, 'change_column' => $_POST['change_column'] ?? $_GET['change_column'] ?? null, 'is_virtual_columns_supported' => Compatibility::isVirtualColumnsSupported($this->dbi->getVersion()), 'is_integers_length_restricted' => $isIntegersLengthRestricted, 'browse_mime' => $config->settings['BrowseMIME'] ?? null, 'supports_stored_keyword' => Compatibility::supportsStoredKeywordForVirtualColumns( $this->dbi->getVersion(), ), 'server_version' => $this->dbi->getVersion(), 'max_rows' => (int) $config->settings['MaxRows'], 'char_editing' => $config->settings['CharEditing'] ?? null, 'attribute_types' => $this->dbi->types->getAttributes(), 'privs_available' => $userPrivileges->column && $userPrivileges->isReload, 'max_length' => $this->dbi->getVersion() >= 50503 ? 1024 : 255, 'have_partitioning' => Partition::havePartitioning(), 'disable_is' => $config->selectedServer['DisableIS'], ]; } /** * Set default type and default value according to the column metadata * * @return array{DefaultType:string, DefaultValue: string, Default?: string} */ public static function decorateColumnMetaDefault(string $type, string|null $default, bool $isNull): array { $metaDefault = ['DefaultType' => 'USER_DEFINED', 'DefaultValue' => '']; switch ($default) { case null: $metaDefault['DefaultType'] = $isNull ? 'NULL' : 'NONE'; break; case 'CURRENT_TIMESTAMP': case 'current_timestamp()': $metaDefault['DefaultType'] = 'CURRENT_TIMESTAMP'; break; case 'UUID': case 'uuid()': $metaDefault['DefaultType'] = 'UUID'; break; default: $metaDefault['DefaultValue'] = $default; if (str_ends_with($type, 'text')) { $textDefault = substr($default, 1, -1); $metaDefault['Default'] = stripcslashes($textDefault); } break; } return $metaDefault; } /** @return mixed[] */ private function getColumnMetaForRegeneratedFields(int $columnNumber): array { $columnMeta = [ 'Field' => Util::getValueByKey($_POST, ['field_name', $columnNumber]), 'Type' => Util::getValueByKey($_POST, ['field_type', $columnNumber]), 'Collation' => Util::getValueByKey($_POST, ['field_collation', $columnNumber], ''), 'Null' => Util::getValueByKey($_POST, ['field_null', $columnNumber], ''), 'DefaultType' => Util::getValueByKey($_POST, ['field_default_type', $columnNumber], 'NONE'), 'DefaultValue' => Util::getValueByKey($_POST, ['field_default_value', $columnNumber], ''), 'Extra' => Util::getValueByKey($_POST, ['field_extra', $columnNumber]), 'Virtuality' => Util::getValueByKey($_POST, ['field_virtuality', $columnNumber], ''), 'Expression' => Util::getValueByKey($_POST, ['field_expression', $columnNumber], ''), 'Key' => '', 'Comment' => false, ]; $parts = explode( '_', Util::getValueByKey($_POST, ['field_key', $columnNumber], ''), 2, ); if (count($parts) === 2 && $parts[1] == $columnNumber) { $columnMeta['Key'] = match ($parts[0]) { 'primary' => 'PRI', 'index' => 'MUL', 'unique' => 'UNI', 'fulltext' => 'FULLTEXT', 'spatial' => 'SPATIAL', default => '', }; } $columnMeta['Default'] = match ($columnMeta['DefaultType']) { 'NONE' => null, 'USER_DEFINED' => $columnMeta['DefaultValue'], default => $columnMeta['DefaultType'], }; return $columnMeta; } }