PageRenderTime 47ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/DatabaseInterface.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 2436 lines | 1411 code | 322 blank | 703 comment | 202 complexity | f4ff167b5d8484758988cb40e8b24eee MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. /**
  3. * Main interface for database interactions
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use mysqli_result;
  8. use PhpMyAdmin\Database\DatabaseList;
  9. use PhpMyAdmin\Dbal\DatabaseName;
  10. use PhpMyAdmin\Dbal\DbalInterface;
  11. use PhpMyAdmin\Dbal\DbiExtension;
  12. use PhpMyAdmin\Dbal\DbiMysqli;
  13. use PhpMyAdmin\Dbal\Warning;
  14. use PhpMyAdmin\Html\Generator;
  15. use PhpMyAdmin\Query\Cache;
  16. use PhpMyAdmin\Query\Compatibility;
  17. use PhpMyAdmin\Query\Generator as QueryGenerator;
  18. use PhpMyAdmin\Query\Utilities;
  19. use PhpMyAdmin\SqlParser\Context;
  20. use PhpMyAdmin\Utils\SessionCache;
  21. use function __;
  22. use function array_diff;
  23. use function array_keys;
  24. use function array_map;
  25. use function array_merge;
  26. use function array_multisort;
  27. use function array_reverse;
  28. use function array_shift;
  29. use function array_slice;
  30. use function basename;
  31. use function closelog;
  32. use function count;
  33. use function defined;
  34. use function explode;
  35. use function implode;
  36. use function in_array;
  37. use function is_array;
  38. use function is_int;
  39. use function is_string;
  40. use function mb_strtolower;
  41. use function microtime;
  42. use function openlog;
  43. use function reset;
  44. use function sprintf;
  45. use function str_contains;
  46. use function str_starts_with;
  47. use function stripos;
  48. use function strlen;
  49. use function strtolower;
  50. use function strtoupper;
  51. use function substr;
  52. use function syslog;
  53. use function trigger_error;
  54. use function uasort;
  55. use function uksort;
  56. use function usort;
  57. use const E_USER_WARNING;
  58. use const LOG_INFO;
  59. use const LOG_NDELAY;
  60. use const LOG_PID;
  61. use const LOG_USER;
  62. use const SORT_ASC;
  63. use const SORT_DESC;
  64. /**
  65. * Main interface for database interactions
  66. */
  67. class DatabaseInterface implements DbalInterface
  68. {
  69. /**
  70. * Force STORE_RESULT method, ignored by classic MySQL.
  71. */
  72. public const QUERY_STORE = 1;
  73. /**
  74. * Do not read whole query.
  75. */
  76. public const QUERY_UNBUFFERED = 2;
  77. /**
  78. * Get session variable.
  79. */
  80. public const GETVAR_SESSION = 1;
  81. /**
  82. * Get global variable.
  83. */
  84. public const GETVAR_GLOBAL = 2;
  85. /**
  86. * User connection.
  87. */
  88. public const CONNECT_USER = 0x100;
  89. /**
  90. * Control user connection.
  91. */
  92. public const CONNECT_CONTROL = 0x101;
  93. /**
  94. * Auxiliary connection.
  95. *
  96. * Used for example for replication setup.
  97. */
  98. public const CONNECT_AUXILIARY = 0x102;
  99. /** @var DbiExtension */
  100. private $extension;
  101. /**
  102. * Opened database links
  103. *
  104. * @var array
  105. */
  106. private $links;
  107. /** @var array Current user and host cache */
  108. private $currentUser;
  109. /** @var string|null lower_case_table_names value cache */
  110. private $lowerCaseTableNames = null;
  111. /** @var bool Whether connection is MariaDB */
  112. private $isMariaDb = false;
  113. /** @var bool Whether connection is Percona */
  114. private $isPercona = false;
  115. /** @var int Server version as number */
  116. private $versionInt = 55000;
  117. /** @var string Server version */
  118. private $versionString = '5.50.0';
  119. /** @var string Server version comment */
  120. private $versionComment = '';
  121. /** @var Types MySQL types data */
  122. public $types;
  123. /** @var Relation */
  124. private $relation;
  125. /** @var Cache */
  126. private $cache;
  127. /**
  128. * @param DbiExtension $ext Object to be used for database queries
  129. */
  130. public function __construct(DbiExtension $ext)
  131. {
  132. $this->extension = $ext;
  133. $this->links = [];
  134. if (defined('TESTSUITE')) {
  135. $this->links[self::CONNECT_USER] = 1;
  136. $this->links[self::CONNECT_CONTROL] = 2;
  137. }
  138. $this->currentUser = [];
  139. $this->cache = new Cache();
  140. $this->types = new Types($this);
  141. $this->relation = new Relation($this);
  142. }
  143. /**
  144. * runs a query
  145. *
  146. * @param string $query SQL query to execute
  147. * @param mixed $link optional database link to use
  148. * @param int $options optional query options
  149. * @param bool $cache_affected_rows whether to cache affected rows
  150. *
  151. * @return mixed
  152. */
  153. public function query(
  154. string $query,
  155. $link = self::CONNECT_USER,
  156. int $options = 0,
  157. bool $cache_affected_rows = true
  158. ) {
  159. $result = $this->tryQuery($query, $link, $options, $cache_affected_rows);
  160. if (! $result) {
  161. Generator::mysqlDie($this->getError($link), $query);
  162. return false;
  163. }
  164. return $result;
  165. }
  166. public function getCache(): Cache
  167. {
  168. return $this->cache;
  169. }
  170. /**
  171. * runs a query and returns the result
  172. *
  173. * @param string $query query to run
  174. * @param mixed $link link type
  175. * @param int $options query options
  176. * @param bool $cache_affected_rows whether to cache affected row
  177. *
  178. * @return mixed
  179. */
  180. public function tryQuery(
  181. string $query,
  182. $link = self::CONNECT_USER,
  183. int $options = 0,
  184. bool $cache_affected_rows = true
  185. ) {
  186. $debug = isset($GLOBALS['cfg']['DBG']) ? $GLOBALS['cfg']['DBG']['sql'] : false;
  187. if (! isset($this->links[$link])) {
  188. return false;
  189. }
  190. $time = 0;
  191. if ($debug) {
  192. $time = microtime(true);
  193. }
  194. $result = $this->extension->realQuery($query, $this->links[$link], $options);
  195. if ($cache_affected_rows) {
  196. $GLOBALS['cached_affected_rows'] = $this->affectedRows($link, false);
  197. }
  198. if ($debug) {
  199. $time = microtime(true) - $time;
  200. $errorMessage = $this->getError($link);
  201. Utilities::debugLogQueryIntoSession(
  202. $query,
  203. is_string($errorMessage) ? $errorMessage : null,
  204. $result,
  205. $time
  206. );
  207. if ($GLOBALS['cfg']['DBG']['sqllog']) {
  208. $warningsCount = '';
  209. if (($options & self::QUERY_STORE) == self::QUERY_STORE) {
  210. if (isset($this->links[$link]->warning_count)) {
  211. $warningsCount = $this->links[$link]->warning_count;
  212. }
  213. }
  214. openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER);
  215. syslog(
  216. LOG_INFO,
  217. sprintf(
  218. 'SQL[%s?route=%s]: %0.3f(W:%s,C:%s,L:0x%02X) > %s',
  219. basename($_SERVER['SCRIPT_NAME']),
  220. Routing::getCurrentRoute(),
  221. $time,
  222. $warningsCount,
  223. $cache_affected_rows ? 'y' : 'n',
  224. $link,
  225. $query
  226. )
  227. );
  228. closelog();
  229. }
  230. }
  231. if ($result !== false && Tracker::isActive()) {
  232. Tracker::handleQuery($query);
  233. }
  234. return $result;
  235. }
  236. /**
  237. * Run multi query statement and return results
  238. *
  239. * @param string $multiQuery multi query statement to execute
  240. * @param int $linkIndex index of the opened database link
  241. *
  242. * @return mysqli_result[]|bool (false)
  243. */
  244. public function tryMultiQuery(
  245. string $multiQuery = '',
  246. $linkIndex = self::CONNECT_USER
  247. ) {
  248. if (! isset($this->links[$linkIndex])) {
  249. return false;
  250. }
  251. return $this->extension->realMultiQuery($this->links[$linkIndex], $multiQuery);
  252. }
  253. /**
  254. * returns array with table names for given db
  255. *
  256. * @param string $database name of database
  257. * @param mixed $link mysql link resource|object
  258. *
  259. * @return array tables names
  260. */
  261. public function getTables(string $database, $link = self::CONNECT_USER): array
  262. {
  263. $tables = $this->fetchResult(
  264. 'SHOW TABLES FROM ' . Util::backquote($database) . ';',
  265. null,
  266. 0,
  267. $link,
  268. self::QUERY_STORE
  269. );
  270. if ($GLOBALS['cfg']['NaturalOrder']) {
  271. usort($tables, 'strnatcasecmp');
  272. }
  273. return $tables;
  274. }
  275. /**
  276. * returns array of all tables in given db or dbs
  277. * this function expects unquoted names:
  278. * RIGHT: my_database
  279. * WRONG: `my_database`
  280. * WRONG: my\_database
  281. * if $tbl_is_group is true, $table is used as filter for table names
  282. *
  283. * <code>
  284. * $dbi->getTablesFull('my_database');
  285. * $dbi->getTablesFull('my_database', 'my_table'));
  286. * $dbi->getTablesFull('my_database', 'my_tables_', true));
  287. * </code>
  288. *
  289. * @param string $database database
  290. * @param string|array $table table name(s)
  291. * @param bool $tbl_is_group $table is a table group
  292. * @param int $limit_offset zero-based offset for the count
  293. * @param bool|int $limit_count number of tables to return
  294. * @param string $sort_by table attribute to sort by
  295. * @param string $sort_order direction to sort (ASC or DESC)
  296. * @param string $table_type whether table or view
  297. * @param mixed $link link type
  298. *
  299. * @return array list of tables in given db(s)
  300. *
  301. * @todo move into Table
  302. */
  303. public function getTablesFull(
  304. string $database,
  305. $table = '',
  306. bool $tbl_is_group = false,
  307. int $limit_offset = 0,
  308. $limit_count = false,
  309. string $sort_by = 'Name',
  310. string $sort_order = 'ASC',
  311. ?string $table_type = null,
  312. $link = self::CONNECT_USER
  313. ): array {
  314. if ($limit_count === true) {
  315. $limit_count = $GLOBALS['cfg']['MaxTableList'];
  316. }
  317. $databases = [$database];
  318. $tables = [];
  319. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  320. $sql_where_table = QueryGenerator::getTableCondition(
  321. is_array($table) ? array_map(
  322. [
  323. $this,
  324. 'escapeString',
  325. ],
  326. $table
  327. ) : $this->escapeString($table),
  328. $tbl_is_group,
  329. $table_type
  330. );
  331. // for PMA bc:
  332. // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME`
  333. //
  334. // on non-Windows servers,
  335. // added BINARY in the WHERE clause to force a case sensitive
  336. // comparison (if we are looking for the db Aa we don't want
  337. // to find the db aa)
  338. $this_databases = array_map(
  339. [
  340. $this,
  341. 'escapeString',
  342. ],
  343. $databases
  344. );
  345. $sql = QueryGenerator::getSqlForTablesFull($this_databases, $sql_where_table);
  346. // Sort the tables
  347. $sql .= ' ORDER BY ' . $sort_by . ' ' . $sort_order;
  348. if ($limit_count) {
  349. $sql .= ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset;
  350. }
  351. $tables = $this->fetchResult(
  352. $sql,
  353. [
  354. 'TABLE_SCHEMA',
  355. 'TABLE_NAME',
  356. ],
  357. null,
  358. $link
  359. );
  360. // here, we check for Mroonga engine and compute the good data_length and index_length
  361. // in the StructureController only we need to sum the two values as the other engines
  362. foreach ($tables as $one_database_name => $one_database_tables) {
  363. foreach ($one_database_tables as $one_table_name => $one_table_data) {
  364. if ($one_table_data['Engine'] !== 'Mroonga') {
  365. continue;
  366. }
  367. if (! StorageEngine::hasMroongaEngine()) {
  368. continue;
  369. }
  370. [
  371. $tables[$one_database_name][$one_table_name]['Data_length'],
  372. $tables[$one_database_name][$one_table_name]['Index_length'],
  373. ] = StorageEngine::getMroongaLengths($one_database_name, $one_table_name);
  374. }
  375. }
  376. if ($sort_by === 'Name' && $GLOBALS['cfg']['NaturalOrder']) {
  377. // here, the array's first key is by schema name
  378. foreach ($tables as $one_database_name => $one_database_tables) {
  379. uksort($one_database_tables, 'strnatcasecmp');
  380. if ($sort_order === 'DESC') {
  381. $one_database_tables = array_reverse($one_database_tables);
  382. }
  383. $tables[$one_database_name] = $one_database_tables;
  384. }
  385. } elseif ($sort_by === 'Data_length') {
  386. // Size = Data_length + Index_length
  387. foreach ($tables as $one_database_name => $one_database_tables) {
  388. uasort(
  389. $one_database_tables,
  390. /**
  391. * @param array $a
  392. * @param array $b
  393. */
  394. static function ($a, $b) {
  395. $aLength = $a['Data_length'] + $a['Index_length'];
  396. $bLength = $b['Data_length'] + $b['Index_length'];
  397. return $aLength <=> $bLength;
  398. }
  399. );
  400. if ($sort_order === 'DESC') {
  401. $one_database_tables = array_reverse($one_database_tables);
  402. }
  403. $tables[$one_database_name] = $one_database_tables;
  404. }
  405. }
  406. }
  407. // If permissions are wrong on even one database directory,
  408. // information_schema does not return any table info for any database
  409. // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002
  410. if (empty($tables)) {
  411. foreach ($databases as $each_database) {
  412. if ($table || ($tbl_is_group === true) || ! empty($table_type)) {
  413. $sql = 'SHOW TABLE STATUS FROM '
  414. . Util::backquote($each_database)
  415. . ' WHERE';
  416. $needAnd = false;
  417. if ($table || ($tbl_is_group === true)) {
  418. if (is_array($table)) {
  419. $sql .= ' `Name` IN (\''
  420. . implode(
  421. '\', \'',
  422. array_map(
  423. [
  424. $this,
  425. 'escapeString',
  426. ],
  427. $table,
  428. $link
  429. )
  430. ) . '\')';
  431. } else {
  432. $sql .= " `Name` LIKE '"
  433. . Util::escapeMysqlWildcards(
  434. $this->escapeString($table, $link)
  435. )
  436. . "%'";
  437. }
  438. $needAnd = true;
  439. }
  440. if (! empty($table_type)) {
  441. if ($needAnd) {
  442. $sql .= ' AND';
  443. }
  444. if ($table_type === 'view') {
  445. $sql .= " `Comment` = 'VIEW'";
  446. } elseif ($table_type === 'table') {
  447. $sql .= " `Comment` != 'VIEW'";
  448. }
  449. }
  450. } else {
  451. $sql = 'SHOW TABLE STATUS FROM '
  452. . Util::backquote($each_database);
  453. }
  454. $each_tables = $this->fetchResult($sql, 'Name', null, $link);
  455. // here, we check for Mroonga engine and compute the good data_length and index_length
  456. // in the StructureController only we need to sum the two values as the other engines
  457. foreach ($each_tables as $table_name => $table_data) {
  458. if ($table_data['Engine'] !== 'Mroonga') {
  459. continue;
  460. }
  461. if (! StorageEngine::hasMroongaEngine()) {
  462. continue;
  463. }
  464. [
  465. $each_tables[$table_name]['Data_length'],
  466. $each_tables[$table_name]['Index_length'],
  467. ] = StorageEngine::getMroongaLengths($each_database, $table_name);
  468. }
  469. // Sort naturally if the config allows it and we're sorting
  470. // the Name column.
  471. if ($sort_by === 'Name' && $GLOBALS['cfg']['NaturalOrder']) {
  472. uksort($each_tables, 'strnatcasecmp');
  473. if ($sort_order === 'DESC') {
  474. $each_tables = array_reverse($each_tables);
  475. }
  476. } else {
  477. // Prepare to sort by creating array of the selected sort
  478. // value to pass to array_multisort
  479. // Size = Data_length + Index_length
  480. if ($sort_by === 'Data_length') {
  481. foreach ($each_tables as $table_name => $table_data) {
  482. ${$sort_by}[$table_name] = strtolower(
  483. (string) ($table_data['Data_length']
  484. + $table_data['Index_length'])
  485. );
  486. }
  487. } else {
  488. foreach ($each_tables as $table_name => $table_data) {
  489. ${$sort_by}[$table_name] = strtolower($table_data[$sort_by] ?? '');
  490. }
  491. }
  492. if (! empty(${$sort_by})) {
  493. if ($sort_order === 'DESC') {
  494. array_multisort(${$sort_by}, SORT_DESC, $each_tables);
  495. } else {
  496. array_multisort(${$sort_by}, SORT_ASC, $each_tables);
  497. }
  498. }
  499. // cleanup the temporary sort array
  500. unset(${$sort_by});
  501. }
  502. if ($limit_count) {
  503. $each_tables = array_slice($each_tables, $limit_offset, $limit_count);
  504. }
  505. $tables[$each_database] = Compatibility::getISCompatForGetTablesFull($each_tables, $each_database);
  506. }
  507. }
  508. // cache table data
  509. // so Table does not require to issue SHOW TABLE STATUS again
  510. $this->cache->cacheTableData($tables, $table);
  511. if (isset($tables[$database])) {
  512. return $tables[$database];
  513. }
  514. if (isset($tables[mb_strtolower($database)])) {
  515. // on windows with lower_case_table_names = 1
  516. // MySQL returns
  517. // with SHOW DATABASES or information_schema.SCHEMATA: `Test`
  518. // but information_schema.TABLES gives `test`
  519. // see https://github.com/phpmyadmin/phpmyadmin/issues/8402
  520. return $tables[mb_strtolower($database)];
  521. }
  522. return $tables;
  523. }
  524. /**
  525. * Get VIEWs in a particular database
  526. *
  527. * @param string $db Database name to look in
  528. *
  529. * @return array Set of VIEWs inside the database
  530. */
  531. public function getVirtualTables(string $db): array
  532. {
  533. $tables_full = $this->getTablesFull($db);
  534. $views = [];
  535. foreach ($tables_full as $table => $tmp) {
  536. $table = $this->getTable($db, (string) $table);
  537. if (! $table->isView()) {
  538. continue;
  539. }
  540. $views[] = $table;
  541. }
  542. return $views;
  543. }
  544. /**
  545. * returns array with databases containing extended infos about them
  546. *
  547. * @param string $database database
  548. * @param bool $force_stats retrieve stats also for MySQL < 5
  549. * @param int $link link type
  550. * @param string $sort_by column to order by
  551. * @param string $sort_order ASC or DESC
  552. * @param int $limit_offset starting offset for LIMIT
  553. * @param bool|int $limit_count row count for LIMIT or true
  554. * for $GLOBALS['cfg']['MaxDbList']
  555. *
  556. * @return array
  557. *
  558. * @todo move into ListDatabase?
  559. */
  560. public function getDatabasesFull(
  561. ?string $database = null,
  562. bool $force_stats = false,
  563. $link = self::CONNECT_USER,
  564. string $sort_by = 'SCHEMA_NAME',
  565. string $sort_order = 'ASC',
  566. int $limit_offset = 0,
  567. $limit_count = false
  568. ): array {
  569. $sort_order = strtoupper($sort_order);
  570. if ($limit_count === true) {
  571. $limit_count = $GLOBALS['cfg']['MaxDbList'];
  572. }
  573. $apply_limit_and_order_manual = true;
  574. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  575. /**
  576. * if $GLOBALS['cfg']['NaturalOrder'] is enabled, we cannot use LIMIT
  577. * cause MySQL does not support natural ordering,
  578. * we have to do it afterward
  579. */
  580. $limit = '';
  581. if (! $GLOBALS['cfg']['NaturalOrder']) {
  582. if ($limit_count) {
  583. $limit = ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset;
  584. }
  585. $apply_limit_and_order_manual = false;
  586. }
  587. // get table information from information_schema
  588. $sqlWhereSchema = '';
  589. if (! empty($database)) {
  590. $sqlWhereSchema = 'WHERE `SCHEMA_NAME` LIKE \''
  591. . $this->escapeString($database, $link) . '\'';
  592. }
  593. $sql = QueryGenerator::getInformationSchemaDatabasesFullRequest(
  594. $force_stats,
  595. $sqlWhereSchema,
  596. $sort_by,
  597. $sort_order,
  598. $limit
  599. );
  600. $databases = $this->fetchResult($sql, 'SCHEMA_NAME', null, $link);
  601. $mysql_error = $this->getError($link);
  602. if (! count($databases) && isset($GLOBALS['errno'])) {
  603. Generator::mysqlDie($mysql_error, $sql);
  604. }
  605. // display only databases also in official database list
  606. // f.e. to apply hide_db and only_db
  607. $drops = array_diff(
  608. array_keys($databases),
  609. (array) $GLOBALS['dblist']->databases
  610. );
  611. foreach ($drops as $drop) {
  612. unset($databases[$drop]);
  613. }
  614. } else {
  615. $databases = [];
  616. foreach ($GLOBALS['dblist']->databases as $database_name) {
  617. // Compatibility with INFORMATION_SCHEMA output
  618. $databases[$database_name]['SCHEMA_NAME'] = $database_name;
  619. $databases[$database_name]['DEFAULT_COLLATION_NAME'] = $this->getDbCollation($database_name);
  620. if (! $force_stats) {
  621. continue;
  622. }
  623. // get additional info about tables
  624. $databases[$database_name]['SCHEMA_TABLES'] = 0;
  625. $databases[$database_name]['SCHEMA_TABLE_ROWS'] = 0;
  626. $databases[$database_name]['SCHEMA_DATA_LENGTH'] = 0;
  627. $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] = 0;
  628. $databases[$database_name]['SCHEMA_INDEX_LENGTH'] = 0;
  629. $databases[$database_name]['SCHEMA_LENGTH'] = 0;
  630. $databases[$database_name]['SCHEMA_DATA_FREE'] = 0;
  631. $res = $this->query(
  632. 'SHOW TABLE STATUS FROM '
  633. . Util::backquote($database_name) . ';'
  634. );
  635. if ($res === false) {
  636. unset($res);
  637. continue;
  638. }
  639. while ($row = $this->fetchAssoc($res)) {
  640. $databases[$database_name]['SCHEMA_TABLES']++;
  641. $databases[$database_name]['SCHEMA_TABLE_ROWS'] += $row['Rows'];
  642. $databases[$database_name]['SCHEMA_DATA_LENGTH'] += $row['Data_length'];
  643. $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] += $row['Max_data_length'];
  644. $databases[$database_name]['SCHEMA_INDEX_LENGTH'] += $row['Index_length'];
  645. // for InnoDB, this does not contain the number of
  646. // overhead bytes but the total free space
  647. if ($row['Engine'] !== 'InnoDB') {
  648. $databases[$database_name]['SCHEMA_DATA_FREE'] += $row['Data_free'];
  649. }
  650. $databases[$database_name]['SCHEMA_LENGTH'] += $row['Data_length'] + $row['Index_length'];
  651. }
  652. $this->freeResult($res);
  653. unset($res);
  654. }
  655. }
  656. /**
  657. * apply limit and order manually now
  658. * (caused by older MySQL < 5 or $GLOBALS['cfg']['NaturalOrder'])
  659. */
  660. if ($apply_limit_and_order_manual) {
  661. usort(
  662. $databases,
  663. static function ($a, $b) use ($sort_by, $sort_order) {
  664. return Utilities::usortComparisonCallback($a, $b, $sort_by, $sort_order);
  665. }
  666. );
  667. /**
  668. * now apply limit
  669. */
  670. if ($limit_count) {
  671. $databases = array_slice($databases, $limit_offset, $limit_count);
  672. }
  673. }
  674. return $databases;
  675. }
  676. /**
  677. * returns detailed array with all columns for sql
  678. *
  679. * @param string $sql_query target SQL query to get columns
  680. * @param array $view_columns alias for columns
  681. *
  682. * @return array
  683. */
  684. public function getColumnMapFromSql(string $sql_query, array $view_columns = []): array
  685. {
  686. $result = $this->tryQuery($sql_query);
  687. if ($result === false) {
  688. return [];
  689. }
  690. /** @var FieldMetadata[] $meta */
  691. $meta = $this->getFieldsMeta($result);
  692. $nbFields = count($meta);
  693. if ($nbFields <= 0) {
  694. return [];
  695. }
  696. $column_map = [];
  697. $nbColumns = count($view_columns);
  698. for ($i = 0; $i < $nbFields; $i++) {
  699. $map = [];
  700. $map['table_name'] = $meta[$i]->table;
  701. $map['refering_column'] = $meta[$i]->name;
  702. if ($nbColumns > 1) {
  703. $map['real_column'] = $view_columns[$i];
  704. }
  705. $column_map[] = $map;
  706. }
  707. return $column_map;
  708. }
  709. /**
  710. * returns detailed array with all columns for given table in database,
  711. * or all tables/databases
  712. *
  713. * @param string $database name of database
  714. * @param string $table name of table to retrieve columns from
  715. * @param string $column name of specific column
  716. * @param mixed $link mysql link resource
  717. *
  718. * @return array
  719. */
  720. public function getColumnsFull(
  721. ?string $database = null,
  722. ?string $table = null,
  723. ?string $column = null,
  724. $link = self::CONNECT_USER
  725. ): array {
  726. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  727. [$sql, $arrayKeys] = QueryGenerator::getInformationSchemaColumnsFullRequest(
  728. $database !== null ? $this->escapeString($database, $link) : null,
  729. $table !== null ? $this->escapeString($table, $link) : null,
  730. $column !== null ? $this->escapeString($column, $link) : null
  731. );
  732. return $this->fetchResult($sql, $arrayKeys, null, $link);
  733. }
  734. $columns = [];
  735. if ($database === null) {
  736. foreach ($GLOBALS['dblist']->databases as $database) {
  737. $columns[$database] = $this->getColumnsFull($database, null, null, $link);
  738. }
  739. return $columns;
  740. }
  741. if ($table === null) {
  742. $tables = $this->getTables($database);
  743. foreach ($tables as $table) {
  744. $columns[$table] = $this->getColumnsFull($database, $table, null, $link);
  745. }
  746. return $columns;
  747. }
  748. $sql = 'SHOW FULL COLUMNS FROM '
  749. . Util::backquote($database) . '.' . Util::backquote($table);
  750. if ($column !== null) {
  751. $sql .= " LIKE '" . $this->escapeString($column, $link) . "'";
  752. }
  753. $columns = $this->fetchResult($sql, 'Field', null, $link);
  754. $columns = Compatibility::getISCompatForGetColumnsFull($columns, $database, $table);
  755. if ($column !== null) {
  756. return reset($columns);
  757. }
  758. return $columns;
  759. }
  760. /**
  761. * Returns descriptions of columns in given table (all or given by $column)
  762. *
  763. * @param string $database name of database
  764. * @param string $table name of table to retrieve columns from
  765. * @param string $column name of column, null to show all columns
  766. * @param bool $full whether to return full info or only column names
  767. * @param int $link link type
  768. *
  769. * @return array array indexed by column names or,
  770. * if $column is given, flat array description
  771. */
  772. public function getColumns(
  773. string $database,
  774. string $table,
  775. ?string $column = null,
  776. bool $full = false,
  777. $link = self::CONNECT_USER
  778. ): array {
  779. $sql = QueryGenerator::getColumnsSql(
  780. $database,
  781. $table,
  782. $column === null ? null : Util::escapeMysqlWildcards($this->escapeString($column)),
  783. $full
  784. );
  785. $fields = $this->fetchResult($sql, 'Field', null, $link);
  786. if (! is_array($fields) || count($fields) === 0) {
  787. return [];
  788. }
  789. // Check if column is a part of multiple-column index and set its 'Key'.
  790. $indexes = Index::getFromTable($table, $database);
  791. foreach ($fields as $field => $field_data) {
  792. if (! empty($field_data['Key'])) {
  793. continue;
  794. }
  795. foreach ($indexes as $index) {
  796. /** @var Index $index */
  797. if (! $index->hasColumn($field)) {
  798. continue;
  799. }
  800. $index_columns = $index->getColumns();
  801. if ($index_columns[$field]->getSeqInIndex() <= 1) {
  802. continue;
  803. }
  804. if ($index->isUnique()) {
  805. $fields[$field]['Key'] = 'UNI';
  806. } else {
  807. $fields[$field]['Key'] = 'MUL';
  808. }
  809. }
  810. }
  811. return $column != null ? array_shift($fields) : $fields;
  812. }
  813. /**
  814. * Returns all column names in given table
  815. *
  816. * @param string $database name of database
  817. * @param string $table name of table to retrieve columns from
  818. * @param mixed $link mysql link resource
  819. *
  820. * @return array|null
  821. */
  822. public function getColumnNames(
  823. string $database,
  824. string $table,
  825. $link = self::CONNECT_USER
  826. ): ?array {
  827. $sql = QueryGenerator::getColumnsSql($database, $table);
  828. // We only need the 'Field' column which contains the table's column names
  829. $fields = array_keys($this->fetchResult($sql, 'Field', null, $link));
  830. if (! is_array($fields) || count($fields) === 0) {
  831. return null;
  832. }
  833. return $fields;
  834. }
  835. /**
  836. * Returns indexes of a table
  837. *
  838. * @param string $database name of database
  839. * @param string $table name of the table whose indexes are to be retrieved
  840. * @param mixed $link mysql link resource
  841. *
  842. * @return array
  843. */
  844. public function getTableIndexes(
  845. string $database,
  846. string $table,
  847. $link = self::CONNECT_USER
  848. ): array {
  849. $sql = QueryGenerator::getTableIndexesSql($database, $table);
  850. $indexes = $this->fetchResult($sql, null, null, $link);
  851. if (! is_array($indexes) || count($indexes) < 1) {
  852. return [];
  853. }
  854. return $indexes;
  855. }
  856. /**
  857. * returns value of given mysql server variable
  858. *
  859. * @param string $var mysql server variable name
  860. * @param int $type DatabaseInterface::GETVAR_SESSION |
  861. * DatabaseInterface::GETVAR_GLOBAL
  862. * @param mixed $link mysql link resource|object
  863. *
  864. * @return mixed value for mysql server variable
  865. */
  866. public function getVariable(
  867. string $var,
  868. int $type = self::GETVAR_SESSION,
  869. $link = self::CONNECT_USER
  870. ) {
  871. switch ($type) {
  872. case self::GETVAR_SESSION:
  873. $modifier = ' SESSION';
  874. break;
  875. case self::GETVAR_GLOBAL:
  876. $modifier = ' GLOBAL';
  877. break;
  878. default:
  879. $modifier = '';
  880. }
  881. return $this->fetchValue('SHOW' . $modifier . ' VARIABLES LIKE \'' . $var . '\';', 0, 1, $link);
  882. }
  883. /**
  884. * Sets new value for a variable if it is different from the current value
  885. *
  886. * @param string $var variable name
  887. * @param string $value value to set
  888. * @param mixed $link mysql link resource|object
  889. */
  890. public function setVariable(
  891. string $var,
  892. string $value,
  893. $link = self::CONNECT_USER
  894. ): bool {
  895. $current_value = $this->getVariable($var, self::GETVAR_SESSION, $link);
  896. if ($current_value == $value) {
  897. return true;
  898. }
  899. return (bool) $this->query('SET ' . $var . ' = ' . $value . ';', $link);
  900. }
  901. /**
  902. * Function called just after a connection to the MySQL database server has
  903. * been established. It sets the connection collation, and determines the
  904. * version of MySQL which is running.
  905. */
  906. public function postConnect(): void
  907. {
  908. $version = $this->fetchSingleRow('SELECT @@version, @@version_comment', 'ASSOC', self::CONNECT_USER);
  909. if (is_array($version)) {
  910. $this->versionString = $version['@@version'] ?? '';
  911. $this->versionInt = Utilities::versionToInt($this->versionString);
  912. $this->versionComment = $version['@@version_comment'] ?? '';
  913. if (stripos($this->versionString, 'mariadb') !== false) {
  914. $this->isMariaDb = true;
  915. }
  916. if (stripos($this->versionComment, 'percona') !== false) {
  917. $this->isPercona = true;
  918. }
  919. }
  920. if ($this->versionInt > 50503) {
  921. $default_charset = 'utf8mb4';
  922. $default_collation = 'utf8mb4_general_ci';
  923. } else {
  924. $default_charset = 'utf8';
  925. $default_collation = 'utf8_general_ci';
  926. }
  927. $GLOBALS['collation_connection'] = $default_collation;
  928. $GLOBALS['charset_connection'] = $default_charset;
  929. $this->query(
  930. sprintf('SET NAMES \'%s\' COLLATE \'%s\';', $default_charset, $default_collation),
  931. self::CONNECT_USER,
  932. self::QUERY_STORE
  933. );
  934. /* Locale for messages */
  935. $locale = LanguageManager::getInstance()->getCurrentLanguage()->getMySQLLocale();
  936. if (! empty($locale)) {
  937. $this->query("SET lc_messages = '" . $locale . "';", self::CONNECT_USER, self::QUERY_STORE);
  938. }
  939. // Set timezone for the session, if required.
  940. if ($GLOBALS['cfg']['Server']['SessionTimeZone'] != '') {
  941. $sql_query_tz = 'SET ' . Util::backquote('time_zone') . ' = '
  942. . '\''
  943. . $this->escapeString($GLOBALS['cfg']['Server']['SessionTimeZone'])
  944. . '\'';
  945. if (! $this->tryQuery($sql_query_tz)) {
  946. $error_message_tz = sprintf(
  947. __(
  948. 'Unable to use timezone "%1$s" for server %2$d. '
  949. . 'Please check your configuration setting for '
  950. . '[em]$cfg[\'Servers\'][%3$d][\'SessionTimeZone\'][/em]. '
  951. . 'phpMyAdmin is currently using the default time zone '
  952. . 'of the database server.'
  953. ),
  954. $GLOBALS['cfg']['Server']['SessionTimeZone'],
  955. $GLOBALS['server'],
  956. $GLOBALS['server']
  957. );
  958. trigger_error($error_message_tz, E_USER_WARNING);
  959. }
  960. }
  961. /* Loads closest context to this version. */
  962. Context::loadClosest(($this->isMariaDb ? 'MariaDb' : 'MySql') . $this->versionInt);
  963. /**
  964. * the DatabaseList class as a stub for the ListDatabase class
  965. */
  966. $GLOBALS['dblist'] = new DatabaseList();
  967. }
  968. /**
  969. * Sets collation connection for user link
  970. *
  971. * @param string $collation collation to set
  972. */
  973. public function setCollation(string $collation): void
  974. {
  975. $charset = $GLOBALS['charset_connection'];
  976. /* Automatically adjust collation if not supported by server */
  977. if ($charset === 'utf8' && str_starts_with($collation, 'utf8mb4_')) {
  978. $collation = 'utf8_' . substr($collation, 8);
  979. }
  980. $result = $this->tryQuery(
  981. "SET collation_connection = '"
  982. . $this->escapeString($collation, self::CONNECT_USER)
  983. . "';",
  984. self::CONNECT_USER,
  985. self::QUERY_STORE
  986. );
  987. if ($result === false) {
  988. trigger_error(
  989. __('Failed to set configured collation connection!'),
  990. E_USER_WARNING
  991. );
  992. return;
  993. }
  994. $GLOBALS['collation_connection'] = $collation;
  995. }
  996. /**
  997. * This function checks and initializes the phpMyAdmin configuration
  998. * storage state before it is used into session cache.
  999. */
  1000. public function initRelationParamsCache(): void
  1001. {
  1002. if (strlen($GLOBALS['db'])) {
  1003. $cfgRelation = $this->relation->getRelationsParam();
  1004. if (empty($cfgRelation['db'])) {
  1005. $this->relation->fixPmaTables($GLOBALS['db'], false);
  1006. }
  1007. }
  1008. $storageDbName = $GLOBALS['cfg']['Server']['pmadb'] ?? '';
  1009. // Use "phpmyadmin" as a default database name to check to keep the behavior consistent
  1010. $storageDbName = empty($storageDbName) ? 'phpmyadmin' : $storageDbName;
  1011. $this->relation->fixPmaTables($storageDbName, false);
  1012. }
  1013. /**
  1014. * Function called just after a connection to the MySQL database server has
  1015. * been established. It sets the connection collation, and determines the
  1016. * version of MySQL which is running.
  1017. */
  1018. public function postConnectControl(): void
  1019. {
  1020. // If Zero configuration mode enabled, check PMA tables in current db.
  1021. if ($GLOBALS['cfg']['ZeroConf'] != true) {
  1022. return;
  1023. }
  1024. /**
  1025. * the DatabaseList class as a stub for the ListDatabase class
  1026. */
  1027. $GLOBALS['dblist'] = new DatabaseList();
  1028. $this->initRelationParamsCache();
  1029. }
  1030. /**
  1031. * returns a single value from the given result or query,
  1032. * if the query or the result has more than one row or field
  1033. * the first field of the first row is returned
  1034. *
  1035. * <code>
  1036. * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123';
  1037. * $user_name = $dbi->fetchValue($sql);
  1038. * // produces
  1039. * // $user_name = 'John Doe'
  1040. * </code>
  1041. *
  1042. * @param string $query The query to execute
  1043. * @param int $row_number row to fetch the value from,
  1044. * starting at 0, with 0 being default
  1045. * @param int|string $field field to fetch the value from,
  1046. * starting at 0, with 0 being default
  1047. * @param int $link link type
  1048. *
  1049. * @return mixed|false value of first field in first row from result or false if not found
  1050. */
  1051. public function fetchValue(
  1052. string $query,
  1053. int $row_number = 0,
  1054. $field = 0,
  1055. $link = self::CONNECT_USER
  1056. ) {
  1057. $value = false;
  1058. $result = $this->tryQuery($query, $link, self::QUERY_STORE, false);
  1059. if ($result === false) {
  1060. return false;
  1061. }
  1062. // return false if result is empty or false
  1063. // or requested row is larger than rows in result
  1064. if ($this->numRows($result) < $row_number + 1) {
  1065. return $value;
  1066. }
  1067. // get requested row
  1068. for ($i = 0; $i <= $row_number; $i++) {
  1069. // if $field is an integer use non associative mysql fetch function
  1070. if (is_int($field)) {
  1071. $row = $this->fetchRow($result);
  1072. continue;
  1073. }
  1074. $row = $this->fetchAssoc($result);
  1075. }
  1076. $this->freeResult($result);
  1077. // return requested field
  1078. if (isset($row[$field])) {
  1079. $value = $row[$field];
  1080. }
  1081. return $value;
  1082. }
  1083. /**
  1084. * Returns only the first row from the result or null if result is empty.
  1085. *
  1086. * <code>
  1087. * $sql = 'SELECT * FROM `user` WHERE `id` = 123';
  1088. * $user = $dbi->fetchSingleRow($sql);
  1089. * // produces
  1090. * // $user = array('id' => 123, 'name' => 'John Doe')
  1091. * </code>
  1092. *
  1093. * @param string $query The query to execute
  1094. * @param string $type NUM|ASSOC|BOTH returned array should either numeric
  1095. * associative or both
  1096. * @param int $link link type
  1097. */
  1098. public function fetchSingleRow(
  1099. string $query,
  1100. string $type = 'ASSOC',
  1101. $link = self::CONNECT_USER
  1102. ): ?array {
  1103. $result = $this->tryQuery($query, $link, self::QUERY_STORE, false);
  1104. if ($result === false) {
  1105. return null;
  1106. }
  1107. if (! $this->numRows($result)) {
  1108. return null;
  1109. }
  1110. switch ($type) {
  1111. case 'NUM':
  1112. $row = $this->fetchRow($result);
  1113. break;
  1114. case 'ASSOC':
  1115. $row = $this->fetchAssoc($result);
  1116. break;
  1117. case 'BOTH':
  1118. default:
  1119. $row = $this->fetchArray($result);
  1120. break;
  1121. }
  1122. $this->freeResult($result);
  1123. return $row;
  1124. }
  1125. /**
  1126. * Returns row or element of a row
  1127. *
  1128. * @param array|string $row Row to process
  1129. * @param string|int|null $value Which column to return
  1130. *
  1131. * @return mixed
  1132. */
  1133. private function fetchValueOrValueByIndex($row, $value)
  1134. {
  1135. return $value === null ? $row : $row[$value];
  1136. }
  1137. /**
  1138. * returns all rows in the resultset in one array
  1139. *
  1140. * <code>
  1141. * $sql = 'SELECT * FROM `user`';
  1142. * $users = $dbi->fetchResult($sql);
  1143. * // produces
  1144. * // $users[] = array('id' => 123, 'name' => 'John Doe')
  1145. *
  1146. * $sql = 'SELECT `id`, `name` FROM `user`';
  1147. * $users = $dbi->fetchResult($sql, 'id');
  1148. * // produces
  1149. * // $users['123'] = array('id' => 123, 'name' => 'John Doe')
  1150. *
  1151. * $sql = 'SELECT `id`, `name` FROM `user`';
  1152. * $users = $dbi->fetchResult($sql, 0);
  1153. * // produces
  1154. * // $users['123'] = array(0 => 123, 1 => 'John Doe')
  1155. *
  1156. * $sql = 'SELECT `id`, `name` FROM `user`';
  1157. * $users = $dbi->fetchResult($sql, 'id', 'name');
  1158. * // or
  1159. * $users = $dbi->fetchResult($sql, 0, 1);
  1160. * // produces
  1161. * // $users['123'] = 'John Doe'
  1162. *
  1163. * $sql = 'SELECT `name` FROM `user`';
  1164. * $users = $dbi->fetchResult($sql);
  1165. * // produces
  1166. * // $users[] = 'John Doe'
  1167. *
  1168. * $sql = 'SELECT `group`, `name` FROM `user`'
  1169. * $users = $dbi->fetchResult($sql, array('group', null), 'name');
  1170. * // produces
  1171. * // $users['admin'][] = 'John Doe'
  1172. *
  1173. * $sql = 'SELECT `group`, `name` FROM `user`'
  1174. * $users = $dbi->fetchResult($sql, array('group', 'name'), 'id');
  1175. * // produces
  1176. * // $users['admin']['John Doe'] = '123'
  1177. * </code>
  1178. *
  1179. * @param string $query query to execute
  1180. * @param string|int|array $key field-name or offset
  1181. * used as key for array
  1182. * or array of those
  1183. * @param string|int $value value-name or offset
  1184. * used as value for array
  1185. * @param int $link link type
  1186. * @param int $options query options
  1187. *
  1188. * @return array resultrows or values indexed by $key
  1189. */
  1190. public function fetchResult(
  1191. string $query,
  1192. $key = null,
  1193. $value = null,
  1194. $link = self::CONNECT_USER,
  1195. int $options = 0
  1196. ) {
  1197. $resultrows = [];
  1198. $result = $this->tryQuery($query, $link, $options, false);
  1199. // return empty array if result is empty or false
  1200. if ($result === false) {
  1201. return $resultrows;
  1202. }
  1203. $fetch_function = 'fetchAssoc';
  1204. // no nested array if only one field is in result
  1205. if ($key === null && $this->numFields($result) === 1) {
  1206. $value = 0;
  1207. $fetch_function = 'fetchRow';
  1208. }
  1209. // if $key is an integer use non associative mysql fetch function
  1210. if (is_int($key)) {
  1211. $fetch_function = 'fetchRow';
  1212. }
  1213. if ($key === null) {
  1214. while ($row = $this->$fetch_function($result)) {
  1215. $resultrows[] = $this->fetchValueOrValueByIndex($row, $value);
  1216. }
  1217. } else {
  1218. if (is_array($key)) {
  1219. while ($row = $this->$fetch_function($result)) {
  1220. $result_target =& $resultrows;
  1221. foreach ($key as $key_index) {
  1222. if ($key_index === null) {
  1223. $result_target =& $result_target[];
  1224. continue;
  1225. }
  1226. if (! isset($result_target[$row[$key_index]])) {
  1227. $result_target[$row[$key_index]] = [];
  1228. }
  1229. $result_target =& $result_target[$row[$key_index]];
  1230. }
  1231. $result_target = $this->fetchValueOrValueByIndex($row, $value);
  1232. }
  1233. } else {
  1234. while ($row = $this->$fetch_function($result)) {
  1235. $resultrows[$row[$key]] = $this->fetchValueOrValueByIndex($row, $value);
  1236. }
  1237. }
  1238. }
  1239. $this->freeResult($result);
  1240. return $resultrows;
  1241. }
  1242. /**
  1243. * Get supported SQL compatibility modes
  1244. *
  1245. * @return array supported SQL compatibility modes
  1246. */
  1247. public function getCompatibilities(): array
  1248. {
  1249. $compats = ['NONE'];
  1250. $compats[] = 'ANSI';
  1251. $compats[] = 'DB2';
  1252. $compats[] = 'MAXDB';
  1253. $compats[] = 'MYSQL323';
  1254. $compats[] = 'MYSQL40';
  1255. $compats[] = 'MSSQL';
  1256. $compats[] = 'ORACLE';
  1257. // removed; in MySQL 5.0.33, this produces exports that
  1258. // can't be read by POSTGRESQL (see our bug #1596328)
  1259. //$compats[] = 'POSTGRESQL';
  1260. $compats[] = 'TRADITIONAL';
  1261. return $compats;
  1262. }
  1263. /**
  1264. * returns warnings for last query
  1265. *
  1266. * @param int $link link type
  1267. *
  1268. * @return Warning[] warnings
  1269. */
  1270. public function getWarnings($link = self::CONNECT_USER): array
  1271. {
  1272. /** @var object|false $result */
  1273. $result = $this->tryQuery('SHOW WARNINGS', $link, 0, false);
  1274. if ($result === false) {
  1275. return [];
  1276. }
  1277. $warnings = [];
  1278. while ($row = $this->fetchAssoc($result)) {
  1279. $warnings[] = Warning::fromArray($row);
  1280. }
  1281. return $warnings;
  1282. }
  1283. /**
  1284. * returns an array of PROCEDURE or FUNCTION names for a db
  1285. *
  1286. * @param string $db db name
  1287. * @param string $which PROCEDURE | FUNCTION
  1288. * @param int $link link type
  1289. *
  1290. * @return array the procedure names or function names
  1291. */
  1292. public function getProceduresOrFunctions(
  1293. string $db,
  1294. string $which,
  1295. $link = self::CONNECT_USER
  1296. ): array {
  1297. $shows = $this->fetchResult('SHOW ' . $which . ' STATUS;', null, null, $link);
  1298. $result = [];
  1299. foreach ($shows as $one_show) {
  1300. if ($one_show['Db'] != $db || $one_show['Type'] != $which) {
  1301. continue;
  1302. }
  1303. $result[] = $one_show['Name'];
  1304. }
  1305. return $result;
  1306. }
  1307. /**
  1308. * returns the definition of a specific PROCEDURE, FUNCTION, EVENT or VIEW
  1309. *
  1310. * @param string $db db name
  1311. * @param string $which PROCEDURE | FUNCTION | EVENT | VIEW
  1312. * @param string $name the procedure|function|event|view name
  1313. * @param int $link link type
  1314. *
  1315. * @return string|null the definition
  1316. */
  1317. public function getDefinition(
  1318. string $db,
  1319. string $which,
  1320. string $name,
  1321. $link = self::CONNECT_USER
  1322. ): ?string {
  1323. $returned_field = [
  1324. 'PROCEDURE' => 'Create Procedure',
  1325. 'FUNCTION' => 'Create Function',
  1326. 'EVENT' => 'Create Event',
  1327. 'VIEW' => 'Create View',
  1328. ];
  1329. $query = 'SHOW CREATE ' . $which . ' '
  1330. . Util::backquote($db) . '.'
  1331. . Util::backquote($name);
  1332. $result = $this->fetchValue($query, 0, $returned_field[$which], $link);
  1333. return is_string($result) ? $result : null;
  1334. }
  1335. /**
  1336. * returns details about the PROCEDUREs or FUNCTIONs for a specific database
  1337. * or details about a specific routine
  1338. *
  1339. * @param string $db db name
  1340. * @param string $which PROCEDURE | FUNCTION or null for both
  1341. * @param string $name name of the routine (to fetch a specific routine)
  1342. *
  1343. * @return array information about PROCEDUREs or FUNCTIONs
  1344. */
  1345. public function getRoutines(
  1346. string $db,
  1347. ?string $which = null,
  1348. string $name = ''
  1349. ): array {
  1350. $routines = [];
  1351. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  1352. $query = QueryGenerator::getInformationSchemaRoutinesRequest(
  1353. $this->escapeString($db),
  1354. isset($which) && in_array($which, ['FUNCTION', 'PROCEDURE']) ? $which : null,
  1355. empty($name) ? null : $this->escapeString($name)
  1356. );
  1357. $result = $this->fetchResult($query);
  1358. if (! empty($result)) {
  1359. $routines = $result;
  1360. }
  1361. } else {
  1362. if ($which === 'FUNCTION' || $which == null) {
  1363. $query = 'SHOW FUNCTION STATUS'
  1364. . " WHERE `Db` = '" . $this->escapeString($db) . "'";
  1365. if (! empty($name)) {
  1366. $query .= " AND `Name` = '"
  1367. . $this->escapeString($name) . "'";
  1368. }
  1369. $result = $this->fetchResult($query);
  1370. if (! empty($result)) {
  1371. $routines = array_merge($routines, $result);
  1372. }
  1373. }
  1374. if ($which === 'PROCEDURE' || $which == null) {
  1375. $query = 'SHOW PROCEDURE STATUS'
  1376. . " WHERE `Db` = '" . $this->escapeString($db) . "'";
  1377. if (! empty($name)) {
  1378. $query .= " AND `Name` = '"
  1379. . $this->escapeString($name) . "'";
  1380. }
  1381. $result = $this->fetchResult($query);
  1382. if (! empty($result)) {
  1383. $routines = array_merge($routines, $result);
  1384. }
  1385. }
  1386. }
  1387. $ret = [];
  1388. foreach ($routines as $routine) {
  1389. $one_result = [];
  1390. $one_result['db'] = $routine['Db'];
  1391. $one_result['name'] = $routine['Name'];
  1392. $one_result['type'] = $routine['Type'];
  1393. $one_result['definer'] = $routine['Definer'];
  1394. $one_result['returns'] = $routine['DTD_IDENTIFIER'] ?? '';
  1395. $ret[] = $one_result;
  1396. }
  1397. // Sort results by name
  1398. $name = [];
  1399. foreach ($ret as $value) {
  1400. $name[] = $value['name'];
  1401. }
  1402. array_multisort($name, SORT_ASC, $ret);
  1403. return $ret;
  1404. }
  1405. /**
  1406. * returns details about the EVENTs for a specific database
  1407. *
  1408. * @param string $db db name
  1409. * @param string $name event name
  1410. *
  1411. * @return array information about EVENTs
  1412. */
  1413. public function getEvents(string $db, string $name = ''): array
  1414. {
  1415. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  1416. $query = QueryGenerator::getInformationSchemaEventsRequest(
  1417. $this->escapeString($db),
  1418. empty($name) ? null : $this->escapeString($name)
  1419. );
  1420. } else {
  1421. $query = 'SHOW EVENTS FROM ' . Util::backquote($db);
  1422. if (! empty($name)) {
  1423. $query .= " WHERE `Name` = '"
  1424. . $this->escapeString($name) . "'";
  1425. }
  1426. }
  1427. $result = [];
  1428. $events = $this->fetchResult($query);
  1429. foreach ($events as $event) {
  1430. $result[] = [
  1431. 'name' => $event['Name'],
  1432. 'type' => $event['Type'],
  1433. 'status' => $event['Status'],
  1434. ];
  1435. }
  1436. // Sort results by name
  1437. $name = [];
  1438. foreach ($result as $value) {
  1439. $name[] = $value['name'];
  1440. }
  1441. array_multisort($name, SORT_ASC, $result);
  1442. return $result;
  1443. }
  1444. /**
  1445. * returns details about the TRIGGERs for a specific table or database
  1446. *
  1447. * @param string $db db name
  1448. * @param string $table table name
  1449. * @param string $delimiter the delimiter to use (may be empty)
  1450. *
  1451. * @return array information about triggers (may be empty)
  1452. */
  1453. public function getTriggers(string $db, string $table = '', $delimiter = '//')
  1454. {
  1455. $result = [];
  1456. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  1457. $query = QueryGenerator::getInformationSchemaTriggersRequest(
  1458. $this->escapeString($db),
  1459. empty($table) ? null : $this->escapeString($table)
  1460. );
  1461. } else {
  1462. $query = 'SHOW TRIGGERS FROM ' . Util::backquote($db);
  1463. if (! empty($table)) {
  1464. $query .= " LIKE '" . $this->escapeString($table) . "';";
  1465. }
  1466. }
  1467. $triggers = $this->fetchResult($query);
  1468. foreach ($triggers as $trigger) {
  1469. if ($GLOBALS['cfg']['Server']['DisableIS']) {
  1470. $trigger['TRIGGER_NAME'] = $trigger['Trigger'];
  1471. $trigger['ACTION_TIMING'] = $trigger['Timing'];
  1472. $trigger['EVENT_MANIPULATION'] = $trigger['Event'];
  1473. $trigger['EVENT_OBJECT_TABLE'] = $trigger['Table'];
  1474. $trigger['ACTION_STATEMENT'] = $trigger['Statement'];
  1475. $trigger['DEFINER'] = $trigger['Definer'];
  1476. }
  1477. $one_result = [];
  1478. $one_result['name'] = $trigger['TRIGGER_NAME'];
  1479. $one_result['table'] = $trigger['EVENT_OBJECT_TABLE'];
  1480. $one_result['action_timing'] = $trigger['ACTION_TIMING'];
  1481. $one_result['event_manipulation'] = $trigger['EVENT_MANIPULATION'];
  1482. $one_result['definition'] = $trigger['ACTION_STATEMENT'];
  1483. $one_result['definer'] = $trigger['DEFINER'];
  1484. // do not prepend the schema name; this way, importing the
  1485. // definition into another schema will work
  1486. $one_result['full_trigger_name'] = Util::backquote($trigger['TRIGGER_NAME']);
  1487. $one_result['drop'] = 'DROP TRIGGER IF EXISTS '
  1488. . $one_result['full_trigger_name'];
  1489. $one_result['create'] = 'CREATE TRIGGER '
  1490. . $one_result['full_trigger_name'] . ' '
  1491. . $trigger['ACTION_TIMING'] . ' '
  1492. . $trigger['EVENT_MANIPULATION']
  1493. . ' ON ' . Util::backquote($trigger['EVENT_OBJECT_TABLE'])
  1494. . "\n" . ' FOR EACH ROW '
  1495. . $trigger['ACTION_STATEMENT'] . "\n" . $delimiter . "\n";
  1496. $result[] = $one_result;
  1497. }
  1498. // Sort results by name
  1499. $name = [];
  1500. foreach ($result as $value) {
  1501. $name[] = $value['name'];
  1502. }
  1503. array_multisort($name, SORT_ASC, $result);
  1504. return $result;
  1505. }
  1506. /**
  1507. * gets the current user with host
  1508. *
  1509. * @return string the current user i.e. user@host
  1510. */
  1511. public function getCurrentUser(): string
  1512. {
  1513. if (SessionCache::has('mysql_cur_user')) {
  1514. return SessionCache::get('mysql_cur_user');
  1515. }
  1516. $user = $this->fetchValue('SELECT CURRENT_USER();');
  1517. if ($user !== false) {
  1518. SessionCache::set('mysql_cur_user', $user);
  1519. return $user;
  1520. }
  1521. return '@';
  1522. }
  1523. public function isSuperUser(): bool
  1524. {
  1525. if (SessionCache::has('is_superuser')) {
  1526. return (bool) SessionCache::get('is_superuser');
  1527. }
  1528. if (! $this->isConnected()) {
  1529. return false;
  1530. }
  1531. $result = $this->tryQuery('SELECT 1 FROM mysql.user LIMIT 1', self::CONNECT_USER, self::QUERY_STORE);
  1532. $isSuperUser = false;
  1533. if ($result) {
  1534. $isSuperUser = (bool) $this->numRows($result);
  1535. }
  1536. $this->freeResult($result);
  1537. SessionCache::set('is_superuser', $isSuperUser);
  1538. return $isSuperUser;
  1539. }
  1540. public function isGrantUser(): bool
  1541. {
  1542. global $cfg;
  1543. if (SessionCache::has('is_grantuser')) {
  1544. return (bool) SessionCache::get('is_grantuser');
  1545. }
  1546. if (! $this->isConnected()) {
  1547. return false;
  1548. }
  1549. $hasGrantPrivilege = false;
  1550. if ($cfg['Server']['DisableIS']) {
  1551. $grants = $this->getCurrentUserGrants();
  1552. foreach ($grants as $grant) {
  1553. if (str_contains($grant, 'WITH GRANT OPTION')) {
  1554. $hasGrantPrivilege = true;
  1555. break;
  1556. }
  1557. }
  1558. SessionCache::set('is_grantuser', $hasGrantPrivilege);
  1559. return $hasGrantPrivilege;
  1560. }
  1561. [$user, $host] = $this->getCurrentUserAndHost();
  1562. $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($user, $host);
  1563. $result = $this->tryQuery($query, self::CONNECT_USER, self::QUERY_STORE);
  1564. if ($result) {
  1565. $hasGrantPrivilege = (bool) $this->numRows($result);
  1566. }
  1567. $this->freeResult($result);
  1568. SessionCache::set('is_grantuser', $hasGrantPrivilege);
  1569. return $hasGrantPrivilege;
  1570. }
  1571. public function isCreateUser(): bool
  1572. {
  1573. global $cfg;
  1574. if (SessionCache::has('is_createuser')) {
  1575. return (bool) SessionCache::get('is_createuser');
  1576. }
  1577. if (! $this->isConnected()) {
  1578. return false;
  1579. }
  1580. $hasCreatePrivilege = false;
  1581. if ($cfg['Server']['DisableIS']) {
  1582. $grants = $this->getCurrentUserGrants();
  1583. foreach ($grants as $grant) {
  1584. if (str_contains($grant, 'ALL PRIVILEGES ON *.*') || str_contains($grant, 'CREATE USER')) {
  1585. $hasCreatePrivilege = true;
  1586. break;
  1587. }
  1588. }
  1589. SessionCache::set('is_createuser', $hasCreatePrivilege);
  1590. return $hasCreatePrivilege;
  1591. }
  1592. [$user, $host] = $this->getCurrentUserAndHost();
  1593. $query = QueryGenerator::getInformationSchemaDataForCreateRequest($user, $host);
  1594. $result = $this->tryQuery($query, self::CONNECT_USER, self::QUERY_STORE);
  1595. if ($result) {
  1596. $hasCreatePrivilege = (bool) $this->numRows($result);
  1597. }
  1598. $this->freeResult($result);
  1599. SessionCache::set('is_createuser', $hasCreatePrivilege);
  1600. return $hasCreatePrivilege;
  1601. }
  1602. public function isConnected(): bool
  1603. {
  1604. return isset($this->links[self::CONNECT_USER]);
  1605. }
  1606. private function getCurrentUserGrants(): array
  1607. {
  1608. return $this->fetchResult('SHOW GRANTS FOR CURRENT_USER();', null, null, self::CONNECT_USER, self::QUERY_STORE);
  1609. }
  1610. /**
  1611. * Get the current user and host
  1612. *
  1613. * @return array array of username and hostname
  1614. */
  1615. public function getCurrentUserAndHost(): array
  1616. {
  1617. if (count($this->currentUser) === 0) {
  1618. $user = $this->getCurrentUser();
  1619. $this->currentUser = explode('@', $user);
  1620. }
  1621. return $this->currentUser;
  1622. }
  1623. /**
  1624. * Returns value for lower_case_table_names variable
  1625. *
  1626. * @return string|bool
  1627. */
  1628. public function getLowerCaseNames()
  1629. {
  1630. if ($this->lowerCaseTableNames === null) {
  1631. $this->lowerCaseTableNames = $this->fetchValue('SELECT @@lower_case_table_names');
  1632. }
  1633. return $this->lowerCaseTableNames;
  1634. }
  1635. /**
  1636. * connects to the database server
  1637. *
  1638. * @param int $mode Connection mode on of CONNECT_USER, CONNECT_CONTROL
  1639. * or CONNECT_AUXILIARY.
  1640. * @param array|null $server Server information like host/port/socket/persistent
  1641. * @param int $target How to store connection link, defaults to $mode
  1642. *
  1643. * @return mixed false on error or a connection object on success
  1644. */
  1645. public function connect(int $mode, ?array $server = null, ?int $target = null)
  1646. {
  1647. [$user, $password, $server] = Config::getConnectionParams($mode, $server);
  1648. if ($target === null) {
  1649. $target = $mode;
  1650. }
  1651. if ($user === null || $password === null) {
  1652. trigger_error(
  1653. __('Missing connection parameters!'),
  1654. E_USER_WARNING
  1655. );
  1656. return false;
  1657. }
  1658. // Do not show location and backtrace for connection errors
  1659. $GLOBALS['errorHandler']->setHideLocation(true);
  1660. $result = $this->extension->connect($user, $password, $server);
  1661. $GLOBALS['errorHandler']->setHideLocation(false);
  1662. if ($result) {
  1663. $this->links[$target] = $result;
  1664. /* Run post connect for user connections */
  1665. if ($target == self::CONNECT_USER) {
  1666. $this->postConnect();
  1667. }
  1668. return $result;
  1669. }
  1670. if ($mode == self::CONNECT_CONTROL) {
  1671. trigger_error(
  1672. __(
  1673. 'Connection for controluser as defined in your configuration failed.'
  1674. ),
  1675. E_USER_WARNING
  1676. );
  1677. return false;
  1678. }
  1679. if ($mode == self::CONNECT_AUXILIARY) {
  1680. // Do not go back to main login if connection failed
  1681. // (currently used only in unit testing)
  1682. return false;
  1683. }
  1684. return $result;
  1685. }
  1686. /**
  1687. * selects given database
  1688. *
  1689. * @param string|DatabaseName $dbname database name to select
  1690. * @param int $link link type
  1691. */
  1692. public function selectDb($dbname, $link = self::CONNECT_USER): bool
  1693. {
  1694. if (! isset($this->links[$link])) {
  1695. return false;
  1696. }
  1697. return $this->extension->selectDb($dbname, $this->links[$link]);
  1698. }
  1699. /**
  1700. * returns array of rows with associative and numeric keys from $result
  1701. *
  1702. * @param object $result result set identifier
  1703. */
  1704. public function fetchArray($result): ?array
  1705. {
  1706. return $this->extension->fetchArray($result);
  1707. }
  1708. /**
  1709. * returns array of rows with associative keys from $result
  1710. *
  1711. * @param object $result result set identifier
  1712. */
  1713. public function fetchAssoc($result): ?array
  1714. {
  1715. return $this->extension->fetchAssoc($result);
  1716. }
  1717. /**
  1718. * returns array of rows with numeric keys from $result
  1719. *
  1720. * @param object $result result set identifier
  1721. */
  1722. public function fetchRow($result): ?array
  1723. {
  1724. return $this->extension->fetchRow($result);
  1725. }
  1726. /**
  1727. * Adjusts the result pointer to an arbitrary row in the result
  1728. *
  1729. * @param object $result database result
  1730. * @param int $offset offset to seek
  1731. */
  1732. public function dataSeek($result, int $offset): bool
  1733. {
  1734. return $this->extension->dataSeek($result, $offset);
  1735. }
  1736. /**
  1737. * Frees memory associated with the result
  1738. *
  1739. * @param object $result database result
  1740. */
  1741. public function freeResult($result): void
  1742. {
  1743. $this->extension->freeResult($result);
  1744. }
  1745. /**
  1746. * Check if there are any more query results from a multi query
  1747. *
  1748. * @param int $link link type
  1749. */
  1750. public function moreResults($link = self::CONNECT_USER): bool
  1751. {
  1752. if (! isset($this->links[$link])) {
  1753. return false;
  1754. }
  1755. return $this->extension->moreResults($this->links[$link]);
  1756. }
  1757. /**
  1758. * Prepare next result from multi_query
  1759. *
  1760. * @param int $link link type
  1761. */
  1762. public function nextResult($link = self::CONNECT_USER): bool
  1763. {
  1764. if (! isset($this->links[$link])) {
  1765. return false;
  1766. }
  1767. return $this->extension->nextResult($this->links[$link]);
  1768. }
  1769. /**
  1770. * Store the result returned from multi query
  1771. *
  1772. * @param int $link link type
  1773. *
  1774. * @return mixed false when empty results / result set when not empty
  1775. */
  1776. public function storeResult($link = self::CONNECT_USER)
  1777. {
  1778. if (! isset($this->links[$link])) {
  1779. return false;
  1780. }
  1781. return $this->extension->storeResult($this->links[$link]);
  1782. }
  1783. /**
  1784. * Returns a string representing the type of connection used
  1785. *
  1786. * @param int $link link type
  1787. *
  1788. * @return string|bool type of connection used
  1789. */
  1790. public function getHostInfo($link = self::CONNECT_USER)
  1791. {
  1792. if (! isset($this->links[$link])) {
  1793. return false;
  1794. }
  1795. return $this->extension->getHostInfo($this->links[$link]);
  1796. }
  1797. /**
  1798. * Returns the version of the MySQL protocol used
  1799. *
  1800. * @param int $link link type
  1801. *
  1802. * @return int|bool version of the MySQL protocol used
  1803. */
  1804. public function getProtoInfo($link = self::CONNECT_USER)
  1805. {
  1806. if (! isset($this->links[$link])) {
  1807. return false;
  1808. }
  1809. return $this->extension->getProtoInfo($this->links[$link]);
  1810. }
  1811. /**
  1812. * returns a string that represents the client library version
  1813. *
  1814. * @return string MySQL client library version
  1815. */
  1816. public function getClientInfo(): string
  1817. {
  1818. return $this->extension->getClientInfo();
  1819. }
  1820. /**
  1821. * returns last error message or false if no errors occurred
  1822. *
  1823. * @param int $link link type
  1824. *
  1825. * @return string|bool error or false
  1826. */
  1827. public function getError($link = self::CONNECT_USER)
  1828. {
  1829. if (! isset($this->links[$link])) {
  1830. return false;
  1831. }
  1832. return $this->extension->getError($this->links[$link]);
  1833. }
  1834. /**
  1835. * returns the number of rows returned by last query
  1836. *
  1837. * @param object|bool $result result set identifier
  1838. *
  1839. * @return string|int
  1840. * @psalm-return int|numeric-string
  1841. */
  1842. public function numRows($result)
  1843. {
  1844. return $this->extension->numRows($result);
  1845. }
  1846. /**
  1847. * returns last inserted auto_increment id for given $link
  1848. * or $GLOBALS['userlink']
  1849. *
  1850. * @param int $link link type
  1851. *
  1852. * @return int|false
  1853. */
  1854. public function insertId($link = self::CONNECT_USER)
  1855. {
  1856. // If the primary key is BIGINT we get an incorrect result
  1857. // (sometimes negative, sometimes positive)
  1858. // and in the present function we don't know if the PK is BIGINT
  1859. // so better play safe and use LAST_INSERT_ID()
  1860. //
  1861. // When no controluser is defined, using mysqli_insert_id($link)
  1862. // does not always return the last insert id due to a mixup with
  1863. // the tracking mechanism, but this works:
  1864. return $this->fetchValue('SELECT LAST_INSERT_ID();', 0, 0, $link);
  1865. }
  1866. /**
  1867. * returns the number of rows affected by last query
  1868. *
  1869. * @param int $link link type
  1870. * @param bool $get_from_cache whether to retrieve from cache
  1871. *
  1872. * @return int|string
  1873. * @psalm-return int|numeric-string
  1874. */
  1875. public function affectedRows(
  1876. $link = self::CONNECT_USER,
  1877. bool $get_from_cache = true
  1878. ) {
  1879. if (! isset($this->links[$link])) {
  1880. return -1;
  1881. }
  1882. if ($get_from_cache) {
  1883. return $GLOBALS['cached_affected_rows'];
  1884. }
  1885. return $this->extension->affectedRows($this->links[$link]);
  1886. }
  1887. /**
  1888. * returns metainfo for fields in $result
  1889. *
  1890. * @param object $result result set identifier
  1891. *
  1892. * @return FieldMetadata[]|null meta info for fields in $result
  1893. */
  1894. public function getFieldsMeta($result): ?array
  1895. {
  1896. $result = $this->extension->getFieldsMeta($result);
  1897. if ($result !== null && $this->getLowerCaseNames() === '2') {
  1898. /**
  1899. * Fixup orgtable for lower_case_table_names = 2
  1900. *
  1901. * In this setup MySQL server reports table name lower case
  1902. * but we still need to operate on original case to properly
  1903. * match existing strings
  1904. */
  1905. foreach ($result as $value) {
  1906. if (
  1907. strlen($value->orgtable) === 0 ||
  1908. mb_strtolower($value->orgtable) !== mb_strtolower($value->table)
  1909. ) {
  1910. continue;
  1911. }
  1912. $value->orgtable = $value->table;
  1913. }
  1914. }
  1915. return $result;
  1916. }
  1917. /**
  1918. * return number of fields in given $result
  1919. *
  1920. * @param object $result result set identifier
  1921. *
  1922. * @return int field count
  1923. */
  1924. public function numFields($result): int
  1925. {
  1926. return $this->extension->numFields($result);
  1927. }
  1928. /**
  1929. * returns the length of the given field $i in $result
  1930. *
  1931. * @param object $result result set identifier
  1932. * @param int $i field
  1933. *
  1934. * @return int|bool length of field
  1935. */
  1936. public function fieldLen($result, int $i)
  1937. {
  1938. return $this->extension->fieldLen($result, $i);
  1939. }
  1940. /**
  1941. * returns name of $i. field in $result
  1942. *
  1943. * @param object $result result set identifier
  1944. * @param int $i field
  1945. *
  1946. * @return string name of $i. field in $result
  1947. */
  1948. public function fieldName($result, int $i): string
  1949. {
  1950. return $this->extension->fieldName($result, $i);
  1951. }
  1952. /**
  1953. * returns properly escaped string for use in MySQL queries
  1954. *
  1955. * @param string $str string to be escaped
  1956. * @param mixed $link optional database link to use
  1957. *
  1958. * @return string a MySQL escaped string
  1959. */
  1960. public function escapeString(string $str, $link = self::CONNECT_USER)
  1961. {
  1962. if ($this->extension === null || ! isset($this->links[$link])) {
  1963. return $str;
  1964. }
  1965. return $this->extension->escapeString($this->links[$link], $str);
  1966. }
  1967. /**
  1968. * Checks if this database server is running on Amazon RDS.
  1969. */
  1970. public function isAmazonRds(): bool
  1971. {
  1972. if (SessionCache::has('is_amazon_rds')) {
  1973. return (bool) SessionCache::get('is_amazon_rds');
  1974. }
  1975. $sql = 'SELECT @@basedir';
  1976. $result = (string) $this->fetchValue($sql);
  1977. $rds = (substr($result, 0, 10) === '/rdsdbbin/');
  1978. SessionCache::set('is_amazon_rds', $rds);
  1979. return $rds;
  1980. }
  1981. /**
  1982. * Gets SQL for killing a process.
  1983. *
  1984. * @param int $process Process ID
  1985. */
  1986. public function getKillQuery(int $process): string
  1987. {
  1988. if ($this->isAmazonRds()) {
  1989. return 'CALL mysql.rds_kill(' . $process . ');';
  1990. }
  1991. return 'KILL ' . $process . ';';
  1992. }
  1993. /**
  1994. * Get the phpmyadmin database manager
  1995. */
  1996. public function getSystemDatabase(): SystemDatabase
  1997. {
  1998. return new SystemDatabase($this);
  1999. }
  2000. /**
  2001. * Get a table with database name and table name
  2002. *
  2003. * @param string $db_name DB name
  2004. * @param string $table_name Table name
  2005. */
  2006. public function getTable(string $db_name, string $table_name): Table
  2007. {
  2008. return new Table($table_name, $db_name, $this);
  2009. }
  2010. /**
  2011. * returns collation of given db
  2012. *
  2013. * @param string $db name of db
  2014. *
  2015. * @return string collation of $db
  2016. */
  2017. public function getDbCollation(string $db): string
  2018. {
  2019. if (Utilities::isSystemSchema($db)) {
  2020. // We don't have to check the collation of the virtual
  2021. // information_schema database: We know it!
  2022. return 'utf8_general_ci';
  2023. }
  2024. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  2025. // this is slow with thousands of databases
  2026. $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA'
  2027. . ' WHERE SCHEMA_NAME = \'' . $this->escapeString($db)
  2028. . '\' LIMIT 1';
  2029. return (string) $this->fetchValue($sql);
  2030. }
  2031. $this->selectDb($db);
  2032. $return = (string) $this->fetchValue('SELECT @@collation_database');
  2033. if ($db !== $GLOBALS['db']) {
  2034. $this->selectDb($GLOBALS['db']);
  2035. }
  2036. return $return;
  2037. }
  2038. /**
  2039. * returns default server collation from show variables
  2040. */
  2041. public function getServerCollation(): string
  2042. {
  2043. return (string) $this->fetchValue('SELECT @@collation_server');
  2044. }
  2045. /**
  2046. * Server version as number
  2047. *
  2048. * @example 80011
  2049. */
  2050. public function getVersion(): int
  2051. {
  2052. return $this->versionInt;
  2053. }
  2054. /**
  2055. * Server version
  2056. */
  2057. public function getVersionString(): string
  2058. {
  2059. return $this->versionString;
  2060. }
  2061. /**
  2062. * Server version comment
  2063. */
  2064. public function getVersionComment(): string
  2065. {
  2066. return $this->versionComment;
  2067. }
  2068. /**
  2069. * Whether connection is MariaDB
  2070. */
  2071. public function isMariaDB(): bool
  2072. {
  2073. return $this->isMariaDb;
  2074. }
  2075. /**
  2076. * Whether connection is PerconaDB
  2077. */
  2078. public function isPercona(): bool
  2079. {
  2080. return $this->isPercona;
  2081. }
  2082. /**
  2083. * Load correct database driver
  2084. *
  2085. * @param DbiExtension|null $extension Force the use of an alternative extension
  2086. */
  2087. public static function load(?DbiExtension $extension = null): self
  2088. {
  2089. if ($extension !== null) {
  2090. return new self($extension);
  2091. }
  2092. if (! Util::checkDbExtension('mysqli')) {
  2093. $docLink = sprintf(
  2094. __('See %sour documentation%s for more information.'),
  2095. '[doc@faqmysql]',
  2096. '[/doc]'
  2097. );
  2098. Core::warnMissingExtension('mysqli', true, $docLink);
  2099. }
  2100. return new self(new DbiMysqli());
  2101. }
  2102. /**
  2103. * Prepare an SQL statement for execution.
  2104. *
  2105. * @param string $query The query, as a string.
  2106. * @param int $link Link type.
  2107. *
  2108. * @return object|false A statement object or false.
  2109. */
  2110. public function prepare(string $query, $link = self::CONNECT_USER)
  2111. {
  2112. return $this->extension->prepare($this->links[$link], $query);
  2113. }
  2114. }