DatabaseInterface.php 75 KB

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