DatabaseInterface.php 75 KB

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