extension = $ext; $this->links = []; if (defined('TESTSUITE')) { $this->links[self::CONNECT_USER] = 1; $this->links[self::CONNECT_CONTROL] = 2; } $this->currentUser = []; $this->cache = new Cache(); $this->types = new Types($this); } /** * runs a query * * @param string $query SQL query to execute * @param mixed $link optional database link to use * @param int $options optional query options * @param bool $cache_affected_rows whether to cache affected rows */ public function query( string $query, $link = self::CONNECT_USER, int $options = self::QUERY_BUFFERED, bool $cache_affected_rows = true ): ResultInterface { $result = $this->tryQuery($query, $link, $options, $cache_affected_rows); if (! $result) { // The following statement will exit Generator::mysqlDie($this->getError($link), $query); exit; } return $result; } public function getCache(): Cache { return $this->cache; } /** * runs a query and returns the result * * @param string $query query to run * @param mixed $link link type * @param int $options if DatabaseInterface::QUERY_UNBUFFERED * is provided, it will instruct the extension * to use unbuffered mode * @param bool $cache_affected_rows whether to cache affected row * * @return ResultInterface|false */ public function tryQuery( string $query, $link = self::CONNECT_USER, int $options = self::QUERY_BUFFERED, bool $cache_affected_rows = true ) { $debug = isset($GLOBALS['cfg']['DBG']) && $GLOBALS['cfg']['DBG']['sql']; if (! isset($this->links[$link])) { return false; } $time = microtime(true); $result = $this->extension->realQuery($query, $this->links[$link], $options); if ($cache_affected_rows) { $GLOBALS['cached_affected_rows'] = $this->affectedRows($link, false); } $this->lastQueryExecutionTime = microtime(true) - $time; if ($debug) { $errorMessage = $this->getError($link); Utilities::debugLogQueryIntoSession( $query, $errorMessage !== '' ? $errorMessage : null, $result, $this->lastQueryExecutionTime ); if ($GLOBALS['cfg']['DBG']['sqllog']) { $warningsCount = 0; if (isset($this->links[$link]->warning_count)) { $warningsCount = $this->links[$link]->warning_count; } openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER); syslog( LOG_INFO, sprintf( 'SQL[%s?route=%s]: %0.3f(W:%d,C:%s,L:0x%02X) > %s', basename($_SERVER['SCRIPT_NAME']), Routing::getCurrentRoute(), $this->lastQueryExecutionTime, $warningsCount, $cache_affected_rows ? 'y' : 'n', $link, $query ) ); closelog(); } } if ($result !== false && Tracker::isActive()) { Tracker::handleQuery($query); } return $result; } /** * Send multiple SQL queries to the database server and execute the first one * * @param string $multiQuery multi query statement to execute * @param int $linkIndex index of the opened database link */ public function tryMultiQuery( string $multiQuery = '', $linkIndex = self::CONNECT_USER ): bool { if (! isset($this->links[$linkIndex])) { return false; } return $this->extension->realMultiQuery($this->links[$linkIndex], $multiQuery); } /** * Executes a query as controluser. * The result is always buffered and never cached * * @param string $sql the query to execute * * @return ResultInterface the result set */ public function queryAsControlUser(string $sql): ResultInterface { // Avoid caching of the number of rows affected; for example, this function // is called for tracking purposes but we want to display the correct number // of rows affected by the original query, not by the query generated for // tracking. return $this->query($sql, self::CONNECT_CONTROL, self::QUERY_BUFFERED, false); } /** * Executes a query as controluser. * The result is always buffered and never cached * * @param string $sql the query to execute * * @return ResultInterface|false the result set, or false if the query failed */ public function tryQueryAsControlUser(string $sql) { // Avoid caching of the number of rows affected; for example, this function // is called for tracking purposes but we want to display the correct number // of rows affected by the original query, not by the query generated for // tracking. return $this->tryQuery($sql, self::CONNECT_CONTROL, self::QUERY_BUFFERED, false); } /** * returns array with table names for given db * * @param string $database name of database * @param mixed $link mysql link resource|object * * @return array tables names */ public function getTables(string $database, $link = self::CONNECT_USER): array { if ($database === '') { return []; } $tables = $this->fetchResult( 'SHOW TABLES FROM ' . Util::backquote($database) . ';', null, 0, $link ); if ($GLOBALS['cfg']['NaturalOrder']) { usort($tables, 'strnatcasecmp'); } return $tables; } /** * returns array of all tables in given db or dbs * this function expects unquoted names: * RIGHT: my_database * WRONG: `my_database` * WRONG: my\_database * if $tbl_is_group is true, $table is used as filter for table names * * * $dbi->getTablesFull('my_database'); * $dbi->getTablesFull('my_database', 'my_table')); * $dbi->getTablesFull('my_database', 'my_tables_', true)); * * * @param string $database database * @param string|array $table table name(s) * @param bool $tbl_is_group $table is a table group * @param int $limit_offset zero-based offset for the count * @param bool|int $limit_count number of tables to return * @param string $sort_by table attribute to sort by * @param string $sort_order direction to sort (ASC or DESC) * @param string|null $table_type whether table or view * @param mixed $link link type * * @return array list of tables in given db(s) * * @todo move into Table */ public function getTablesFull( string $database, $table = '', bool $tbl_is_group = false, int $limit_offset = 0, $limit_count = false, string $sort_by = 'Name', string $sort_order = 'ASC', ?string $table_type = null, $link = self::CONNECT_USER ): array { if ($limit_count === true) { $limit_count = $GLOBALS['cfg']['MaxTableList']; } $tables = []; if (! $GLOBALS['cfg']['Server']['DisableIS']) { $sql_where_table = QueryGenerator::getTableCondition( is_array($table) ? array_map( [ $this, 'escapeString', ], $table ) : $this->escapeString($table), $tbl_is_group, $table_type ); // for PMA bc: // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME` // // on non-Windows servers, // added BINARY in the WHERE clause to force a case sensitive // comparison (if we are looking for the db Aa we don't want // to find the db aa) $sql = QueryGenerator::getSqlForTablesFull([$this->escapeString($database)], $sql_where_table); // Sort the tables $sql .= ' ORDER BY ' . $sort_by . ' ' . $sort_order; if ($limit_count) { $sql .= ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset; } $tables = $this->fetchResult( $sql, [ 'TABLE_SCHEMA', 'TABLE_NAME', ], null, $link ); // here, we check for Mroonga engine and compute the good data_length and index_length // in the StructureController only we need to sum the two values as the other engines foreach ($tables as $one_database_name => $one_database_tables) { foreach ($one_database_tables as $one_table_name => $one_table_data) { if ($one_table_data['Engine'] !== 'Mroonga') { continue; } if (! StorageEngine::hasMroongaEngine()) { continue; } [ $tables[$one_database_name][$one_table_name]['Data_length'], $tables[$one_database_name][$one_table_name]['Index_length'], ] = StorageEngine::getMroongaLengths($one_database_name, $one_table_name); } } if ($sort_by === 'Name' && $GLOBALS['cfg']['NaturalOrder']) { // here, the array's first key is by schema name foreach ($tables as $one_database_name => $one_database_tables) { uksort($one_database_tables, 'strnatcasecmp'); if ($sort_order === 'DESC') { $one_database_tables = array_reverse($one_database_tables); } $tables[$one_database_name] = $one_database_tables; } } elseif ($sort_by === 'Data_length') { // Size = Data_length + Index_length foreach ($tables as $one_database_name => $one_database_tables) { uasort( $one_database_tables, /** * @param array $a * @param array $b */ static function ($a, $b) { $aLength = $a['Data_length'] + $a['Index_length']; $bLength = $b['Data_length'] + $b['Index_length']; return $aLength <=> $bLength; } ); if ($sort_order === 'DESC') { $one_database_tables = array_reverse($one_database_tables); } $tables[$one_database_name] = $one_database_tables; } } } // If permissions are wrong on even one database directory, // information_schema does not return any table info for any database // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002 if ($tables === []) { $sql = 'SHOW TABLE STATUS FROM ' . Util::backquote($database); if ($table || ($tbl_is_group === true) || $table_type) { $sql .= ' WHERE'; $needAnd = false; if ($table || ($tbl_is_group === true)) { if (is_array($table)) { $sql .= ' `Name` IN (\'' . implode( '\', \'', array_map( [ $this, 'escapeString', ], $table, $link ) ) . '\')'; } else { $sql .= " `Name` LIKE '" . $this->escapeMysqlLikeString($table, $link) . "%'"; } $needAnd = true; } if ($table_type) { if ($needAnd) { $sql .= ' AND'; } if ($table_type === 'view') { $sql .= " `Comment` = 'VIEW'"; } elseif ($table_type === 'table') { $sql .= " `Comment` != 'VIEW'"; } } } $each_tables = $this->fetchResult($sql, 'Name', null, $link); // here, we check for Mroonga engine and compute the good data_length and index_length // in the StructureController only we need to sum the two values as the other engines foreach ($each_tables as $table_name => $table_data) { if ($table_data['Engine'] !== 'Mroonga') { continue; } if (! StorageEngine::hasMroongaEngine()) { continue; } [ $each_tables[$table_name]['Data_length'], $each_tables[$table_name]['Index_length'], ] = StorageEngine::getMroongaLengths($database, $table_name); } // Sort naturally if the config allows it and we're sorting // the Name column. if ($sort_by === 'Name' && $GLOBALS['cfg']['NaturalOrder']) { uksort($each_tables, 'strnatcasecmp'); if ($sort_order === 'DESC') { $each_tables = array_reverse($each_tables); } } else { // Prepare to sort by creating array of the selected sort // value to pass to array_multisort // Size = Data_length + Index_length $sortValues = []; if ($sort_by === 'Data_length') { foreach ($each_tables as $table_name => $table_data) { $sortValues[$table_name] = strtolower( (string) ($table_data['Data_length'] + $table_data['Index_length']) ); } } else { foreach ($each_tables as $table_name => $table_data) { $sortValues[$table_name] = strtolower($table_data[$sort_by] ?? ''); } } if ($sortValues) { if ($sort_order === 'DESC') { array_multisort($sortValues, SORT_DESC, $each_tables); } else { array_multisort($sortValues, SORT_ASC, $each_tables); } } // cleanup the temporary sort array unset($sortValues); } if ($limit_count) { $each_tables = array_slice($each_tables, $limit_offset, $limit_count); } $tables[$database] = Compatibility::getISCompatForGetTablesFull($each_tables, $database); } // cache table data // so Table does not require to issue SHOW TABLE STATUS again $this->cache->cacheTableData($tables, $table); if (isset($tables[$database])) { return $tables[$database]; } if (isset($tables[mb_strtolower($database)])) { // on windows with lower_case_table_names = 1 // MySQL returns // with SHOW DATABASES or information_schema.SCHEMATA: `Test` // but information_schema.TABLES gives `test` // see https://github.com/phpmyadmin/phpmyadmin/issues/8402 return $tables[mb_strtolower($database)]; } return $tables; } /** * Get VIEWs in a particular database * * @param string $db Database name to look in * * @return Table[] Set of VIEWs inside the database */ public function getVirtualTables(string $db): array { $tables_full = array_keys($this->getTablesFull($db)); $views = []; foreach ($tables_full as $table) { $table = $this->getTable($db, (string) $table); if (! $table->isView()) { continue; } $views[] = $table; } return $views; } /** * returns array with databases containing extended infos about them * * @param string|null $database database * @param bool $force_stats retrieve stats also for MySQL < 5 * @param int $link link type * @param string $sort_by column to order by * @param string $sort_order ASC or DESC * @param int $limit_offset starting offset for LIMIT * @param bool|int $limit_count row count for LIMIT or true * for $GLOBALS['cfg']['MaxDbList'] * * @return array * * @todo move into ListDatabase? */ public function getDatabasesFull( ?string $database = null, bool $force_stats = false, $link = self::CONNECT_USER, string $sort_by = 'SCHEMA_NAME', string $sort_order = 'ASC', int $limit_offset = 0, $limit_count = false ): array { $sort_order = strtoupper($sort_order); if ($limit_count === true) { $limit_count = $GLOBALS['cfg']['MaxDbList']; } $apply_limit_and_order_manual = true; if (! $GLOBALS['cfg']['Server']['DisableIS']) { /** * if $GLOBALS['cfg']['NaturalOrder'] is enabled, we cannot use LIMIT * cause MySQL does not support natural ordering, * we have to do it afterward */ $limit = ''; if (! $GLOBALS['cfg']['NaturalOrder']) { if ($limit_count) { $limit = ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset; } $apply_limit_and_order_manual = false; } // get table information from information_schema $sqlWhereSchema = ''; if ($database !== null) { $sqlWhereSchema = 'WHERE `SCHEMA_NAME` LIKE \'' . $this->escapeString($database, $link) . '\''; } $sql = QueryGenerator::getInformationSchemaDatabasesFullRequest( $force_stats, $sqlWhereSchema, $sort_by, $sort_order, $limit ); $databases = $this->fetchResult($sql, 'SCHEMA_NAME', null, $link); $mysql_error = $this->getError($link); if (! count($databases) && isset($GLOBALS['errno'])) { Generator::mysqlDie($mysql_error, $sql); } // display only databases also in official database list // f.e. to apply hide_db and only_db $drops = array_diff( array_keys($databases), (array) $GLOBALS['dblist']->databases ); foreach ($drops as $drop) { unset($databases[$drop]); } } else { $databases = []; foreach ($GLOBALS['dblist']->databases as $database_name) { // Compatibility with INFORMATION_SCHEMA output $databases[$database_name]['SCHEMA_NAME'] = $database_name; $databases[$database_name]['DEFAULT_COLLATION_NAME'] = $this->getDbCollation($database_name); if (! $force_stats) { continue; } // get additional info about tables $databases[$database_name]['SCHEMA_TABLES'] = 0; $databases[$database_name]['SCHEMA_TABLE_ROWS'] = 0; $databases[$database_name]['SCHEMA_DATA_LENGTH'] = 0; $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] = 0; $databases[$database_name]['SCHEMA_INDEX_LENGTH'] = 0; $databases[$database_name]['SCHEMA_LENGTH'] = 0; $databases[$database_name]['SCHEMA_DATA_FREE'] = 0; $res = $this->query( 'SHOW TABLE STATUS FROM ' . Util::backquote($database_name) . ';' ); while ($row = $res->fetchAssoc()) { $databases[$database_name]['SCHEMA_TABLES']++; $databases[$database_name]['SCHEMA_TABLE_ROWS'] += $row['Rows']; $databases[$database_name]['SCHEMA_DATA_LENGTH'] += $row['Data_length']; $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] += $row['Max_data_length']; $databases[$database_name]['SCHEMA_INDEX_LENGTH'] += $row['Index_length']; // for InnoDB, this does not contain the number of // overhead bytes but the total free space if ($row['Engine'] !== 'InnoDB') { $databases[$database_name]['SCHEMA_DATA_FREE'] += $row['Data_free']; } $databases[$database_name]['SCHEMA_LENGTH'] += $row['Data_length'] + $row['Index_length']; } unset($res); } } /** * apply limit and order manually now * (caused by older MySQL < 5 or $GLOBALS['cfg']['NaturalOrder']) */ if ($apply_limit_and_order_manual) { usort( $databases, static function ($a, $b) use ($sort_by, $sort_order) { return Utilities::usortComparisonCallback($a, $b, $sort_by, $sort_order); } ); /** * now apply limit */ if ($limit_count) { $databases = array_slice($databases, $limit_offset, $limit_count); } } return $databases; } /** * returns detailed array with all columns for sql * * @param string $sql_query target SQL query to get columns * @param array $view_columns alias for columns * * @return array * @psalm-return list> */ public function getColumnMapFromSql(string $sql_query, array $view_columns = []): array { $result = $this->tryQuery($sql_query); if ($result === false) { return []; } $meta = $this->getFieldsMeta($result); $column_map = []; $nbColumns = count($view_columns); foreach ($meta as $i => $field) { $map = [ 'table_name' => $field->table, 'refering_column' => $field->name, ]; if ($nbColumns >= $i) { $map['real_column'] = $view_columns[$i]; } $column_map[] = $map; } return $column_map; } /** * returns detailed array with all columns for given table in database, * or all tables/databases * * @param string|null $database name of database * @param string|null $table name of table to retrieve columns from * @param string|null $column name of specific column * @param mixed $link mysql link resource * * @return array */ public function getColumnsFull( ?string $database = null, ?string $table = null, ?string $column = null, $link = self::CONNECT_USER ): array { if (! $GLOBALS['cfg']['Server']['DisableIS']) { [$sql, $arrayKeys] = QueryGenerator::getInformationSchemaColumnsFullRequest( $database !== null ? $this->escapeString($database, $link) : null, $table !== null ? $this->escapeString($table, $link) : null, $column !== null ? $this->escapeString($column, $link) : null ); return $this->fetchResult($sql, $arrayKeys, null, $link); } $columns = []; if ($database === null) { foreach ($GLOBALS['dblist']->databases as $database) { $columns[$database] = $this->getColumnsFull($database, null, null, $link); } return $columns; } if ($table === null) { $tables = $this->getTables($database); foreach ($tables as $table) { $columns[$table] = $this->getColumnsFull($database, $table, null, $link); } return $columns; } $sql = 'SHOW FULL COLUMNS FROM ' . Util::backquote($database) . '.' . Util::backquote($table); if ($column !== null) { $sql .= " LIKE '" . $this->escapeString($column, $link) . "'"; } $columns = $this->fetchResult($sql, 'Field', null, $link); $columns = Compatibility::getISCompatForGetColumnsFull($columns, $database, $table); if ($column !== null) { return reset($columns); } return $columns; } /** * Returns description of a $column in given table * * @param string $database name of database * @param string $table name of table to retrieve columns from * @param string $column name of column * @param bool $full whether to return full info or only column names * @param int $link link type * * @return array flat array description */ public function getColumn( string $database, string $table, string $column, bool $full = false, $link = self::CONNECT_USER ): array { $sql = QueryGenerator::getColumnsSql( $database, $table, $this->escapeMysqlLikeString($column), $full ); /** @var array $fields */ $fields = $this->fetchResult($sql, 'Field', null, $link); $columns = $this->attachIndexInfoToColumns($database, $table, $fields); return array_shift($columns) ?? []; } /** * Returns descriptions of columns in given table * * @param string $database name of database * @param string $table name of table to retrieve columns from * @param bool $full whether to return full info or only column names * @param int $link link type * * @return array array indexed by column names */ public function getColumns( string $database, string $table, bool $full = false, $link = self::CONNECT_USER ): array { $sql = QueryGenerator::getColumnsSql( $database, $table, null, $full ); /** @var array $fields */ $fields = $this->fetchResult($sql, 'Field', null, $link); return $this->attachIndexInfoToColumns($database, $table, $fields); } /** * Attach index information to the column definition * * @param string $database name of database * @param string $table name of table to retrieve columns from * @param array $fields column array indexed by their names * * @return array Column defintions with index information */ private function attachIndexInfoToColumns( string $database, string $table, array $fields ): array { if (! $fields) { return []; } // Check if column is a part of multiple-column index and set its 'Key'. $indexes = Index::getFromTable($table, $database); foreach ($fields as $field => $field_data) { if (! empty($field_data['Key'])) { continue; } foreach ($indexes as $index) { if (! $index->hasColumn($field)) { continue; } $index_columns = $index->getColumns(); if ($index_columns[$field]->getSeqInIndex() <= 1) { continue; } if ($index->isUnique()) { $fields[$field]['Key'] = 'UNI'; } else { $fields[$field]['Key'] = 'MUL'; } } } return $fields; } /** * Returns all column names in given table * * @param string $database name of database * @param string $table name of table to retrieve columns from * @param mixed $link mysql link resource * * @return string[] */ public function getColumnNames( string $database, string $table, $link = self::CONNECT_USER ): array { $sql = QueryGenerator::getColumnsSql($database, $table); // We only need the 'Field' column which contains the table's column names return $this->fetchResult($sql, null, 'Field', $link); } /** * Returns indexes of a table * * @param string $database name of database * @param string $table name of the table whose indexes are to be retrieved * @param mixed $link mysql link resource * * @return array */ public function getTableIndexes( string $database, string $table, $link = self::CONNECT_USER ): array { $sql = QueryGenerator::getTableIndexesSql($database, $table); return $this->fetchResult($sql, null, null, $link); } /** * returns value of given mysql server variable * * @param string $var mysql server variable name * @param int $type DatabaseInterface::GETVAR_SESSION | * DatabaseInterface::GETVAR_GLOBAL * @param int $link mysql link resource|object * * @return false|string|null value for mysql server variable */ public function getVariable( string $var, int $type = self::GETVAR_SESSION, $link = self::CONNECT_USER ) { switch ($type) { case self::GETVAR_SESSION: $modifier = ' SESSION'; break; case self::GETVAR_GLOBAL: $modifier = ' GLOBAL'; break; default: $modifier = ''; } return $this->fetchValue('SHOW' . $modifier . ' VARIABLES LIKE \'' . $var . '\';', 1, $link); } /** * Sets new value for a variable if it is different from the current value * * @param string $var variable name * @param string $value value to set * @param int $link mysql link resource|object */ public function setVariable( string $var, string $value, $link = self::CONNECT_USER ): bool { $current_value = $this->getVariable($var, self::GETVAR_SESSION, $link); if ($current_value == $value) { return true; } return (bool) $this->query('SET ' . $var . ' = ' . $value . ';', $link); } /** * Function called just after a connection to the MySQL database server has * been established. It sets the connection collation, and determines the * version of MySQL which is running. */ public function postConnect(): void { $version = $this->fetchSingleRow('SELECT @@version, @@version_comment'); if (is_array($version)) { $this->versionString = $version['@@version'] ?? ''; $this->versionInt = Utilities::versionToInt($this->versionString); $this->versionComment = $version['@@version_comment'] ?? ''; if (stripos($this->versionString, 'mariadb') !== false) { $this->isMariaDb = true; } if (stripos($this->versionComment, 'percona') !== false) { $this->isPercona = true; } } if ($this->versionInt > 50503) { $default_charset = 'utf8mb4'; $default_collation = 'utf8mb4_general_ci'; } else { $default_charset = 'utf8'; $default_collation = 'utf8_general_ci'; } $GLOBALS['collation_connection'] = $default_collation; $GLOBALS['charset_connection'] = $default_charset; $this->query(sprintf('SET NAMES \'%s\' COLLATE \'%s\';', $default_charset, $default_collation)); /* Locale for messages */ $locale = LanguageManager::getInstance()->getCurrentLanguage()->getMySQLLocale(); if ($locale) { $this->query("SET lc_messages = '" . $locale . "';"); } // Set timezone for the session, if required. if ($GLOBALS['cfg']['Server']['SessionTimeZone'] != '') { $sql_query_tz = 'SET ' . Util::backquote('time_zone') . ' = ' . '\'' . $this->escapeString($GLOBALS['cfg']['Server']['SessionTimeZone']) . '\''; if (! $this->tryQuery($sql_query_tz)) { $error_message_tz = sprintf( __( 'Unable to use timezone "%1$s" for server %2$d. ' . 'Please check your configuration setting for ' . '[em]$cfg[\'Servers\'][%3$d][\'SessionTimeZone\'][/em]. ' . 'phpMyAdmin is currently using the default time zone ' . 'of the database server.' ), $GLOBALS['cfg']['Server']['SessionTimeZone'], $GLOBALS['server'], $GLOBALS['server'] ); trigger_error($error_message_tz, E_USER_WARNING); } } /* Loads closest context to this version. */ Context::loadClosest(($this->isMariaDb ? 'MariaDb' : 'MySql') . $this->versionInt); /** * the DatabaseList class as a stub for the ListDatabase class */ $GLOBALS['dblist'] = new DatabaseList(); } /** * Sets collation connection for user link * * @param string $collation collation to set */ public function setCollation(string $collation): void { $charset = $GLOBALS['charset_connection']; /* Automatically adjust collation if not supported by server */ if ($charset === 'utf8' && str_starts_with($collation, 'utf8mb4_')) { $collation = 'utf8_' . substr($collation, 8); } $result = $this->tryQuery( "SET collation_connection = '" . $this->escapeString($collation) . "';" ); if ($result === false) { trigger_error( __('Failed to set configured collation connection!'), E_USER_WARNING ); return; } $GLOBALS['collation_connection'] = $collation; } /** * Function called just after a connection to the MySQL database server has * been established. It sets the connection collation, and determines the * version of MySQL which is running. */ public function postConnectControl(Relation $relation): void { // If Zero configuration mode enabled, check PMA tables in current db. if ($GLOBALS['cfg']['ZeroConf'] != true) { return; } /** * the DatabaseList class as a stub for the ListDatabase class */ $GLOBALS['dblist'] = new DatabaseList(); $relation->initRelationParamsCache(); } /** * returns a single value from the given result or query, * if the query or the result has more than one row or field * the first field of the first row is returned * * * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123'; * $user_name = $dbi->fetchValue($sql); * // produces * // $user_name = 'John Doe' * * * @param string $query The query to execute * @param int|string $field field to fetch the value from, * starting at 0, with 0 being default * @param int $link link type * * @return string|false|null value of first field in first row from result or false if not found */ public function fetchValue( string $query, $field = 0, $link = self::CONNECT_USER ) { $result = $this->tryQuery($query, $link, self::QUERY_BUFFERED, false); if ($result === false) { return false; } return $result->fetchValue($field); } /** * Returns only the first row from the result or null if result is empty. * * * $sql = 'SELECT * FROM `user` WHERE `id` = 123'; * $user = $dbi->fetchSingleRow($sql); * // produces * // $user = array('id' => 123, 'name' => 'John Doe') * * * @param string $query The query to execute * @param string $type NUM|ASSOC|BOTH returned array should either numeric * associative or both * @param int $link link type * @psalm-param DatabaseInterface::FETCH_NUM|DatabaseInterface::FETCH_ASSOC $type */ public function fetchSingleRow( string $query, string $type = DbalInterface::FETCH_ASSOC, $link = self::CONNECT_USER ): ?array { $result = $this->tryQuery($query, $link, self::QUERY_BUFFERED, false); if ($result === false) { return null; } return $this->fetchByMode($result, $type) ?: null; } /** * Returns row or element of a row * * @param array|string $row Row to process * @param string|int|null $value Which column to return * * @return mixed */ private function fetchValueOrValueByIndex($row, $value) { return $value === null ? $row : $row[$value]; } /** * returns array of rows with numeric or associative keys * * @param ResultInterface $result result set identifier * @param string $mode either self::FETCH_NUM, self::FETCH_ASSOC or self::FETCH_BOTH * @psalm-param self::FETCH_NUM|self::FETCH_ASSOC $mode */ private function fetchByMode(ResultInterface $result, string $mode): array { if ($mode === self::FETCH_NUM) { return $result->fetchRow(); } return $result->fetchAssoc(); } /** * returns all rows in the resultset in one array * * * $sql = 'SELECT * FROM `user`'; * $users = $dbi->fetchResult($sql); * // produces * // $users[] = array('id' => 123, 'name' => 'John Doe') * * $sql = 'SELECT `id`, `name` FROM `user`'; * $users = $dbi->fetchResult($sql, 'id'); * // produces * // $users['123'] = array('id' => 123, 'name' => 'John Doe') * * $sql = 'SELECT `id`, `name` FROM `user`'; * $users = $dbi->fetchResult($sql, 0); * // produces * // $users['123'] = array(0 => 123, 1 => 'John Doe') * * $sql = 'SELECT `id`, `name` FROM `user`'; * $users = $dbi->fetchResult($sql, 'id', 'name'); * // or * $users = $dbi->fetchResult($sql, 0, 1); * // produces * // $users['123'] = 'John Doe' * * $sql = 'SELECT `name` FROM `user`'; * $users = $dbi->fetchResult($sql); * // produces * // $users[] = 'John Doe' * * $sql = 'SELECT `group`, `name` FROM `user`' * $users = $dbi->fetchResult($sql, array('group', null), 'name'); * // produces * // $users['admin'][] = 'John Doe' * * $sql = 'SELECT `group`, `name` FROM `user`' * $users = $dbi->fetchResult($sql, array('group', 'name'), 'id'); * // produces * // $users['admin']['John Doe'] = '123' * * * @param string $query query to execute * @param string|int|array|null $key field-name or offset * used as key for array * or array of those * @param string|int|null $value value-name or offset * used as value for array * @param int $link link type * * @return array resultrows or values indexed by $key */ public function fetchResult( string $query, $key = null, $value = null, $link = self::CONNECT_USER ): array { $resultrows = []; $result = $this->tryQuery($query, $link, self::QUERY_BUFFERED, false); // return empty array if result is empty or false if ($result === false) { return $resultrows; } $fetch_function = self::FETCH_ASSOC; if ($key === null) { // no nested array if only one field is in result if ($result->numFields() === 1) { $value = 0; $fetch_function = self::FETCH_NUM; } while ($row = $this->fetchByMode($result, $fetch_function)) { $resultrows[] = $this->fetchValueOrValueByIndex($row, $value); } } elseif (is_array($key)) { while ($row = $this->fetchByMode($result, $fetch_function)) { $result_target =& $resultrows; foreach ($key as $key_index) { if ($key_index === null) { $result_target =& $result_target[]; continue; } if (! isset($result_target[$row[$key_index]])) { $result_target[$row[$key_index]] = []; } $result_target =& $result_target[$row[$key_index]]; } $result_target = $this->fetchValueOrValueByIndex($row, $value); } } else { // if $key is an integer use non associative mysql fetch function if (is_int($key)) { $fetch_function = self::FETCH_NUM; } while ($row = $this->fetchByMode($result, $fetch_function)) { $resultrows[$row[$key]] = $this->fetchValueOrValueByIndex($row, $value); } } return $resultrows; } /** * Get supported SQL compatibility modes * * @return array supported SQL compatibility modes */ public function getCompatibilities(): array { return [ 'NONE', 'ANSI', 'DB2', 'MAXDB', 'MYSQL323', 'MYSQL40', 'MSSQL', 'ORACLE', // removed; in MySQL 5.0.33, this produces exports that // can't be read by POSTGRESQL (see our bug #1596328) // 'POSTGRESQL', 'TRADITIONAL', ]; } /** * returns warnings for last query * * @param int $link link type * * @return Warning[] warnings */ public function getWarnings($link = self::CONNECT_USER): array { $result = $this->tryQuery('SHOW WARNINGS', $link, 0, false); if ($result === false) { return []; } $warnings = []; while ($row = $result->fetchAssoc()) { $warnings[] = Warning::fromArray($row); } return $warnings; } /** * returns an array of PROCEDURE or FUNCTION names for a db * * @param string $db db name * @param string $which PROCEDURE | FUNCTION * @param int $link link type * * @return array the procedure names or function names */ public function getProceduresOrFunctions( string $db, string $which, $link = self::CONNECT_USER ): array { $shows = $this->fetchResult('SHOW ' . $which . ' STATUS;', null, null, $link); $result = []; foreach ($shows as $one_show) { if ($one_show['Db'] != $db || $one_show['Type'] != $which) { continue; } $result[] = $one_show['Name']; } return $result; } /** * returns the definition of a specific PROCEDURE, FUNCTION, EVENT or VIEW * * @param string $db db name * @param string $which PROCEDURE | FUNCTION | EVENT | VIEW * @param string $name the procedure|function|event|view name * @param int $link link type * * @return string|null the definition */ public function getDefinition( string $db, string $which, string $name, $link = self::CONNECT_USER ): ?string { $returned_field = [ 'PROCEDURE' => 'Create Procedure', 'FUNCTION' => 'Create Function', 'EVENT' => 'Create Event', 'VIEW' => 'Create View', ]; $query = 'SHOW CREATE ' . $which . ' ' . Util::backquote($db) . '.' . Util::backquote($name); $result = $this->fetchValue($query, $returned_field[$which], $link); return is_string($result) ? $result : null; } /** * returns details about the PROCEDUREs or FUNCTIONs for a specific database * or details about a specific routine * * @param string $db db name * @param string|null $which PROCEDURE | FUNCTION or null for both * @param string $name name of the routine (to fetch a specific routine) * * @return array information about PROCEDUREs or FUNCTIONs */ public function getRoutines( string $db, ?string $which = null, string $name = '' ): array { if (! $GLOBALS['cfg']['Server']['DisableIS']) { $query = QueryGenerator::getInformationSchemaRoutinesRequest( $this->escapeString($db), isset($which) && in_array($which, ['FUNCTION', 'PROCEDURE']) ? $which : null, empty($name) ? null : $this->escapeString($name) ); $routines = $this->fetchResult($query); } else { $routines = []; if ($which === 'FUNCTION' || $which == null) { $query = 'SHOW FUNCTION STATUS' . " WHERE `Db` = '" . $this->escapeString($db) . "'"; if ($name) { $query .= " AND `Name` = '" . $this->escapeString($name) . "'"; } $routines = $this->fetchResult($query); } if ($which === 'PROCEDURE' || $which == null) { $query = 'SHOW PROCEDURE STATUS' . " WHERE `Db` = '" . $this->escapeString($db) . "'"; if ($name) { $query .= " AND `Name` = '" . $this->escapeString($name) . "'"; } $routines = array_merge($routines, $this->fetchResult($query)); } } $ret = []; foreach ($routines as $routine) { $ret[] = [ 'db' => $routine['Db'], 'name' => $routine['Name'], 'type' => $routine['Type'], 'definer' => $routine['Definer'], 'returns' => $routine['DTD_IDENTIFIER'] ?? '', ]; } // Sort results by name $name = array_column($ret, 'name'); array_multisort($name, SORT_ASC, $ret); return $ret; } /** * returns details about the EVENTs for a specific database * * @param string $db db name * @param string $name event name * * @return array information about EVENTs */ public function getEvents(string $db, string $name = ''): array { if (! $GLOBALS['cfg']['Server']['DisableIS']) { $query = QueryGenerator::getInformationSchemaEventsRequest( $this->escapeString($db), empty($name) ? null : $this->escapeString($name) ); } else { $query = 'SHOW EVENTS FROM ' . Util::backquote($db); if ($name) { $query .= " WHERE `Name` = '" . $this->escapeString($name) . "'"; } } $result = []; $events = $this->fetchResult($query); foreach ($events as $event) { $result[] = [ 'name' => $event['Name'], 'type' => $event['Type'], 'status' => $event['Status'], ]; } // Sort results by name $name = array_column($result, 'name'); array_multisort($name, SORT_ASC, $result); return $result; } /** * returns details about the TRIGGERs for a specific table or database * * @param string $db db name * @param string $table table name * @param string $delimiter the delimiter to use (may be empty) * * @return array information about triggers (may be empty) */ public function getTriggers(string $db, string $table = '', string $delimiter = '//'): array { $result = []; if (! $GLOBALS['cfg']['Server']['DisableIS']) { $query = QueryGenerator::getInformationSchemaTriggersRequest( $this->escapeString($db), empty($table) ? null : $this->escapeString($table) ); } else { $query = 'SHOW TRIGGERS FROM ' . Util::backquote($db); if ($table) { $query .= " LIKE '" . $this->escapeString($table) . "';"; } } $triggers = $this->fetchResult($query); foreach ($triggers as $trigger) { if ($GLOBALS['cfg']['Server']['DisableIS']) { $trigger['TRIGGER_NAME'] = $trigger['Trigger']; $trigger['ACTION_TIMING'] = $trigger['Timing']; $trigger['EVENT_MANIPULATION'] = $trigger['Event']; $trigger['EVENT_OBJECT_TABLE'] = $trigger['Table']; $trigger['ACTION_STATEMENT'] = $trigger['Statement']; $trigger['DEFINER'] = $trigger['Definer']; } $one_result = []; $one_result['name'] = $trigger['TRIGGER_NAME']; $one_result['table'] = $trigger['EVENT_OBJECT_TABLE']; $one_result['action_timing'] = $trigger['ACTION_TIMING']; $one_result['event_manipulation'] = $trigger['EVENT_MANIPULATION']; $one_result['definition'] = $trigger['ACTION_STATEMENT']; $one_result['definer'] = $trigger['DEFINER']; // do not prepend the schema name; this way, importing the // definition into another schema will work $one_result['full_trigger_name'] = Util::backquote($trigger['TRIGGER_NAME']); $one_result['drop'] = 'DROP TRIGGER IF EXISTS ' . $one_result['full_trigger_name']; $one_result['create'] = 'CREATE TRIGGER ' . $one_result['full_trigger_name'] . ' ' . $trigger['ACTION_TIMING'] . ' ' . $trigger['EVENT_MANIPULATION'] . ' ON ' . Util::backquote($trigger['EVENT_OBJECT_TABLE']) . "\n" . ' FOR EACH ROW ' . $trigger['ACTION_STATEMENT'] . "\n" . $delimiter . "\n"; $result[] = $one_result; } // Sort results by name $name = array_column($result, 'name'); array_multisort($name, SORT_ASC, $result); return $result; } /** * gets the current user with host * * @return string the current user i.e. user@host */ public function getCurrentUser(): string { if (SessionCache::has('mysql_cur_user')) { return SessionCache::get('mysql_cur_user'); } $user = $this->fetchValue('SELECT CURRENT_USER();'); if ($user !== false) { SessionCache::set('mysql_cur_user', $user); return $user; } return '@'; } public function isSuperUser(): bool { if (SessionCache::has('is_superuser')) { return (bool) SessionCache::get('is_superuser'); } if (! $this->isConnected()) { return false; } $result = $this->tryQuery('SELECT 1 FROM mysql.user LIMIT 1'); $isSuperUser = false; if ($result) { $isSuperUser = (bool) $result->numRows(); } SessionCache::set('is_superuser', $isSuperUser); return $isSuperUser; } public function isGrantUser(): bool { global $cfg; if (SessionCache::has('is_grantuser')) { return (bool) SessionCache::get('is_grantuser'); } if (! $this->isConnected()) { return false; } $hasGrantPrivilege = false; if ($cfg['Server']['DisableIS']) { $grants = $this->getCurrentUserGrants(); foreach ($grants as $grant) { if (str_contains($grant, 'WITH GRANT OPTION')) { $hasGrantPrivilege = true; break; } } SessionCache::set('is_grantuser', $hasGrantPrivilege); return $hasGrantPrivilege; } [$user, $host] = $this->getCurrentUserAndHost(); $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($user, $host); $result = $this->tryQuery($query); if ($result) { $hasGrantPrivilege = (bool) $result->numRows(); } SessionCache::set('is_grantuser', $hasGrantPrivilege); return $hasGrantPrivilege; } public function isCreateUser(): bool { global $cfg; if (SessionCache::has('is_createuser')) { return (bool) SessionCache::get('is_createuser'); } if (! $this->isConnected()) { return false; } $hasCreatePrivilege = false; if ($cfg['Server']['DisableIS']) { $grants = $this->getCurrentUserGrants(); foreach ($grants as $grant) { if (str_contains($grant, 'ALL PRIVILEGES ON *.*') || str_contains($grant, 'CREATE USER')) { $hasCreatePrivilege = true; break; } } SessionCache::set('is_createuser', $hasCreatePrivilege); return $hasCreatePrivilege; } [$user, $host] = $this->getCurrentUserAndHost(); $query = QueryGenerator::getInformationSchemaDataForCreateRequest($user, $host); $result = $this->tryQuery($query); if ($result) { $hasCreatePrivilege = (bool) $result->numRows(); } SessionCache::set('is_createuser', $hasCreatePrivilege); return $hasCreatePrivilege; } public function isConnected(): bool { return isset($this->links[self::CONNECT_USER]); } private function getCurrentUserGrants(): array { return $this->fetchResult('SHOW GRANTS FOR CURRENT_USER();'); } /** * Get the current user and host * * @return array array of username and hostname */ public function getCurrentUserAndHost(): array { if (count($this->currentUser) === 0) { $user = $this->getCurrentUser(); $this->currentUser = explode('@', $user); } return $this->currentUser; } /** * Returns value for lower_case_table_names variable * * @return string */ public function getLowerCaseNames() { if ($this->lowerCaseTableNames === null) { $this->lowerCaseTableNames = $this->fetchValue('SELECT @@lower_case_table_names') ?: ''; } return $this->lowerCaseTableNames; } /** * connects to the database server * * @param int $mode Connection mode on of CONNECT_USER, CONNECT_CONTROL * or CONNECT_AUXILIARY. * @param array|null $server Server information like host/port/socket/persistent * @param int|null $target How to store connection link, defaults to $mode * * @return mixed false on error or a connection object on success */ public function connect(int $mode, ?array $server = null, ?int $target = null) { [$user, $password, $server] = Config::getConnectionParams($mode, $server); if ($target === null) { $target = $mode; } if ($user === null || $password === null) { trigger_error( __('Missing connection parameters!'), E_USER_WARNING ); return false; } // Do not show location and backtrace for connection errors $GLOBALS['errorHandler']->setHideLocation(true); $result = $this->extension->connect($user, $password, $server); $GLOBALS['errorHandler']->setHideLocation(false); if ($result) { $this->links[$target] = $result; /* Run post connect for user connections */ if ($target == self::CONNECT_USER) { $this->postConnect(); } return $result; } if ($mode == self::CONNECT_CONTROL) { trigger_error( __( 'Connection for controluser as defined in your configuration failed.' ), E_USER_WARNING ); return false; } if ($mode == self::CONNECT_AUXILIARY) { // Do not go back to main login if connection failed // (currently used only in unit testing) return false; } return $result; } /** * selects given database * * @param string|DatabaseName $dbname database name to select * @param int $link link type */ public function selectDb($dbname, $link = self::CONNECT_USER): bool { if (! isset($this->links[$link])) { return false; } return $this->extension->selectDb($dbname, $this->links[$link]); } /** * Check if there are any more query results from a multi query * * @param int $link link type */ public function moreResults($link = self::CONNECT_USER): bool { if (! isset($this->links[$link])) { return false; } return $this->extension->moreResults($this->links[$link]); } /** * Prepare next result from multi_query * * @param int $link link type */ public function nextResult($link = self::CONNECT_USER): bool { if (! isset($this->links[$link])) { return false; } return $this->extension->nextResult($this->links[$link]); } /** * Store the result returned from multi query * * @param int $link link type * * @return ResultInterface|false false when empty results / result set when not empty */ public function storeResult($link = self::CONNECT_USER) { if (! isset($this->links[$link])) { return false; } return $this->extension->storeResult($this->links[$link]); } /** * Returns a string representing the type of connection used * * @param int $link link type * * @return string|bool type of connection used */ public function getHostInfo($link = self::CONNECT_USER) { if (! isset($this->links[$link])) { return false; } return $this->extension->getHostInfo($this->links[$link]); } /** * Returns the version of the MySQL protocol used * * @param int $link link type * * @return int|bool version of the MySQL protocol used */ public function getProtoInfo($link = self::CONNECT_USER) { if (! isset($this->links[$link])) { return false; } return $this->extension->getProtoInfo($this->links[$link]); } /** * returns a string that represents the client library version * * @return string MySQL client library version */ public function getClientInfo(): string { return $this->extension->getClientInfo(); } /** * Returns last error message or an empty string if no errors occurred. * * @param int $link link type */ public function getError($link = self::CONNECT_USER): string { if (! isset($this->links[$link])) { return ''; } return $this->extension->getError($this->links[$link]); } /** * returns the number of rows returned by last query * used with tryQuery as it accepts false * * @param string $query query to run * * @return string|int * @psalm-return int|numeric-string */ public function queryAndGetNumRows(string $query) { $result = $this->tryQuery($query); if (! $result) { return 0; } return $result->numRows(); } /** * returns last inserted auto_increment id for given $link * or $GLOBALS['userlink'] * * @param int $link link type */ public function insertId($link = self::CONNECT_USER): int { // If the primary key is BIGINT we get an incorrect result // (sometimes negative, sometimes positive) // and in the present function we don't know if the PK is BIGINT // so better play safe and use LAST_INSERT_ID() // // When no controluser is defined, using mysqli_insert_id($link) // does not always return the last insert id due to a mixup with // the tracking mechanism, but this works: return (int) $this->fetchValue('SELECT LAST_INSERT_ID();', 0, $link); } /** * returns the number of rows affected by last query * * @param int $link link type * @param bool $get_from_cache whether to retrieve from cache * * @return int|string * @psalm-return int|numeric-string */ public function affectedRows( $link = self::CONNECT_USER, bool $get_from_cache = true ) { if (! isset($this->links[$link])) { return -1; } if ($get_from_cache) { return $GLOBALS['cached_affected_rows']; } return $this->extension->affectedRows($this->links[$link]); } /** * returns metainfo for fields in $result * * @param ResultInterface $result result set identifier * * @return FieldMetadata[] meta info for fields in $result */ public function getFieldsMeta(ResultInterface $result): array { $fields = $result->getFieldsMeta(); if ($this->getLowerCaseNames() === '2') { /** * Fixup orgtable for lower_case_table_names = 2 * * In this setup MySQL server reports table name lower case * but we still need to operate on original case to properly * match existing strings */ foreach ($fields as $value) { if ( strlen($value->orgtable) === 0 || mb_strtolower($value->orgtable) !== mb_strtolower($value->table) ) { continue; } $value->orgtable = $value->table; } } return $fields; } /** * returns properly escaped string for use in MySQL queries * * @param string $str string to be escaped * @param mixed $link optional database link to use * * @return string a MySQL escaped string */ public function escapeString(string $str, $link = self::CONNECT_USER) { if ($this->extension === null || ! isset($this->links[$link])) { return $str; } return $this->extension->escapeString($this->links[$link], $str); } /** * returns properly escaped string for use in MySQL LIKE clauses * * @param string $str string to be escaped * @param int $link optional database link to use * * @return string a MySQL escaped LIKE string */ public function escapeMysqlLikeString(string $str, int $link = self::CONNECT_USER) { return $this->escapeString(strtr($str, ['\\' => '\\\\', '_' => '\\_', '%' => '\\%']), $link); } /** * Checks if this database server is running on Amazon RDS. */ public function isAmazonRds(): bool { if (SessionCache::has('is_amazon_rds')) { return (bool) SessionCache::get('is_amazon_rds'); } $sql = 'SELECT @@basedir'; $result = (string) $this->fetchValue($sql); $rds = str_starts_with($result, '/rdsdbbin/'); SessionCache::set('is_amazon_rds', $rds); return $rds; } /** * Gets SQL for killing a process. * * @param int $process Process ID */ public function getKillQuery(int $process): string { if ($this->isAmazonRds()) { return 'CALL mysql.rds_kill(' . $process . ');'; } return 'KILL ' . $process . ';'; } /** * Get the phpmyadmin database manager */ public function getSystemDatabase(): SystemDatabase { return new SystemDatabase($this); } /** * Get a table with database name and table name * * @param string $db_name DB name * @param string $table_name Table name */ public function getTable(string $db_name, string $table_name): Table { return new Table($table_name, $db_name, $this); } /** * returns collation of given db * * @param string $db name of db * * @return string collation of $db */ public function getDbCollation(string $db): string { if (Utilities::isSystemSchema($db)) { // We don't have to check the collation of the virtual // information_schema database: We know it! return 'utf8_general_ci'; } if (! $GLOBALS['cfg']['Server']['DisableIS']) { // this is slow with thousands of databases $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA' . ' WHERE SCHEMA_NAME = \'' . $this->escapeString($db) . '\' LIMIT 1'; return (string) $this->fetchValue($sql); } $this->selectDb($db); $return = (string) $this->fetchValue('SELECT @@collation_database'); if ($db !== $GLOBALS['db']) { $this->selectDb($GLOBALS['db']); } return $return; } /** * returns default server collation from show variables */ public function getServerCollation(): string { return (string) $this->fetchValue('SELECT @@collation_server'); } /** * Server version as number * * @example 80011 */ public function getVersion(): int { return $this->versionInt; } /** * Server version */ public function getVersionString(): string { return $this->versionString; } /** * Server version comment */ public function getVersionComment(): string { return $this->versionComment; } /** * Whether connection is MariaDB */ public function isMariaDB(): bool { return $this->isMariaDb; } /** * Whether connection is PerconaDB */ public function isPercona(): bool { return $this->isPercona; } /** * Load correct database driver * * @param DbiExtension|null $extension Force the use of an alternative extension */ public static function load(?DbiExtension $extension = null): self { if ($extension !== null) { return new self($extension); } if (! Util::checkDbExtension('mysqli')) { $docLink = sprintf( __('See %sour documentation%s for more information.'), '[doc@faqmysql]', '[/doc]' ); Core::warnMissingExtension('mysqli', true, $docLink); } return new self(new DbiMysqli()); } /** * Prepare an SQL statement for execution. * * @param string $query The query, as a string. * @param int $link Link type. * * @return object|false A statement object or false. */ public function prepare(string $query, $link = self::CONNECT_USER) { return $this->extension->prepare($this->links[$link], $query); } }