PageRenderTime 53ms CodeModel.GetById 12ms RepoModel.GetById 1ms 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

Large files files are truncated, but you can click here to view the full file

  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. /

Large files files are truncated, but you can click here to view the full file