Util.php 86 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Dbal\ResultInterface;
  5. use PhpMyAdmin\Html\Generator;
  6. use PhpMyAdmin\Html\MySQLDocumentation;
  7. use PhpMyAdmin\Query\Compatibility;
  8. use PhpMyAdmin\Query\Utilities;
  9. use PhpMyAdmin\SqlParser\Components\Expression;
  10. use PhpMyAdmin\SqlParser\Context;
  11. use PhpMyAdmin\SqlParser\Token;
  12. use PhpMyAdmin\Utils\SessionCache;
  13. use Stringable;
  14. use function __;
  15. use function _pgettext;
  16. use function abs;
  17. use function array_key_exists;
  18. use function array_map;
  19. use function array_merge;
  20. use function array_shift;
  21. use function array_unique;
  22. use function basename;
  23. use function bin2hex;
  24. use function chr;
  25. use function count;
  26. use function ctype_digit;
  27. use function date;
  28. use function decbin;
  29. use function explode;
  30. use function extension_loaded;
  31. use function fclose;
  32. use function floatval;
  33. use function floor;
  34. use function fread;
  35. use function function_exists;
  36. use function htmlentities;
  37. use function htmlspecialchars;
  38. use function htmlspecialchars_decode;
  39. use function implode;
  40. use function in_array;
  41. use function ini_get;
  42. use function is_array;
  43. use function is_callable;
  44. use function is_object;
  45. use function is_scalar;
  46. use function is_string;
  47. use function log10;
  48. use function mb_detect_encoding;
  49. use function mb_strlen;
  50. use function mb_strpos;
  51. use function mb_strrpos;
  52. use function mb_strstr;
  53. use function mb_strtolower;
  54. use function mb_substr;
  55. use function number_format;
  56. use function ord;
  57. use function parse_url;
  58. use function preg_match;
  59. use function preg_quote;
  60. use function preg_replace;
  61. use function random_bytes;
  62. use function range;
  63. use function reset;
  64. use function round;
  65. use function rtrim;
  66. use function set_time_limit;
  67. use function sort;
  68. use function sprintf;
  69. use function str_contains;
  70. use function str_getcsv;
  71. use function str_pad;
  72. use function str_replace;
  73. use function strcasecmp;
  74. use function strftime;
  75. use function strlen;
  76. use function strrev;
  77. use function strtolower;
  78. use function strtr;
  79. use function substr;
  80. use function time;
  81. use function trim;
  82. use function uksort;
  83. use const ENT_COMPAT;
  84. use const ENT_QUOTES;
  85. use const PHP_INT_SIZE;
  86. use const PHP_MAJOR_VERSION;
  87. use const STR_PAD_LEFT;
  88. /**
  89. * Misc functions used all over the scripts.
  90. */
  91. class Util
  92. {
  93. /**
  94. * Checks whether configuration value tells to show icons.
  95. *
  96. * @param string $value Configuration option name
  97. */
  98. public static function showIcons($value): bool
  99. {
  100. return in_array($GLOBALS['cfg'][$value], ['icons', 'both']);
  101. }
  102. /**
  103. * Checks whether configuration value tells to show text.
  104. *
  105. * @param string $value Configuration option name
  106. */
  107. public static function showText($value): bool
  108. {
  109. return in_array($GLOBALS['cfg'][$value], ['text', 'both']);
  110. }
  111. /**
  112. * Returns the formatted maximum size for an upload
  113. *
  114. * @param int|float|string $maxUploadSize the size
  115. *
  116. * @return string the message
  117. */
  118. public static function getFormattedMaximumUploadSize($maxUploadSize): string
  119. {
  120. // I have to reduce the second parameter (sensitiveness) from 6 to 4
  121. // to avoid weird results like 512 kKib
  122. [$maxSize, $maxUnit] = self::formatByteDown($maxUploadSize, 4);
  123. return '(' . sprintf(__('Max: %s%s'), $maxSize, $maxUnit) . ')';
  124. }
  125. /**
  126. * Add slashes before "_" and "%" characters for using them in MySQL
  127. * database, table and field names.
  128. * Note: This function does not escape backslashes!
  129. *
  130. * @param string $name the string to escape
  131. *
  132. * @return string the escaped string
  133. */
  134. public static function escapeMysqlWildcards($name): string
  135. {
  136. return strtr($name, ['_' => '\\_', '%' => '\\%']);
  137. }
  138. /**
  139. * removes slashes before "_" and "%" characters
  140. * Note: This function does not unescape backslashes!
  141. *
  142. * @param string $name the string to escape
  143. *
  144. * @return string the escaped string
  145. */
  146. public static function unescapeMysqlWildcards($name): string
  147. {
  148. return strtr($name, ['\\_' => '_', '\\%' => '%']);
  149. }
  150. /**
  151. * removes quotes (',",`) from a quoted string
  152. *
  153. * checks if the string is quoted and removes this quotes
  154. *
  155. * @param string $quotedString string to remove quotes from
  156. * @param string $quote type of quote to remove
  157. *
  158. * @return string unquoted string
  159. */
  160. public static function unQuote(string $quotedString, ?string $quote = null): string
  161. {
  162. $quotes = [];
  163. if ($quote === null) {
  164. $quotes[] = '`';
  165. $quotes[] = '"';
  166. $quotes[] = "'";
  167. } else {
  168. $quotes[] = $quote;
  169. }
  170. foreach ($quotes as $quote) {
  171. if (mb_substr($quotedString, 0, 1) === $quote && mb_substr($quotedString, -1, 1) === $quote) {
  172. $unquotedString = mb_substr($quotedString, 1, -1);
  173. // replace escaped quotes
  174. $unquotedString = str_replace($quote . $quote, $quote, $unquotedString);
  175. return $unquotedString;
  176. }
  177. }
  178. return $quotedString;
  179. }
  180. /**
  181. * Get a URL link to the official MySQL documentation
  182. *
  183. * @param string $link contains name of page/anchor that is being linked
  184. * @param string $anchor anchor to page part
  185. *
  186. * @return string the URL link
  187. */
  188. public static function getMySQLDocuURL(string $link, string $anchor = ''): string
  189. {
  190. global $dbi;
  191. // Fixup for newly used names:
  192. $link = str_replace('_', '-', mb_strtolower($link));
  193. if (empty($link)) {
  194. $link = 'index';
  195. }
  196. $mysql = '5.5';
  197. $lang = 'en';
  198. if (isset($dbi)) {
  199. $serverVersion = $dbi->getVersion();
  200. if ($serverVersion >= 80000) {
  201. $mysql = '8.0';
  202. } elseif ($serverVersion >= 50700) {
  203. $mysql = '5.7';
  204. } elseif ($serverVersion >= 50600) {
  205. $mysql = '5.6';
  206. } elseif ($serverVersion >= 50500) {
  207. $mysql = '5.5';
  208. }
  209. }
  210. $url = 'https://dev.mysql.com/doc/refman/'
  211. . $mysql . '/' . $lang . '/' . $link . '.html';
  212. if (! empty($anchor)) {
  213. $url .= '#' . $anchor;
  214. }
  215. return Core::linkURL($url);
  216. }
  217. /**
  218. * Get a URL link to the official documentation page of either MySQL
  219. * or MariaDB depending on the database server
  220. * of the user.
  221. *
  222. * @param bool $isMariaDB if the database server is MariaDB
  223. *
  224. * @return string The URL link
  225. */
  226. public static function getDocuURL(bool $isMariaDB = false): string
  227. {
  228. if ($isMariaDB) {
  229. $url = 'https://mariadb.com/kb/en/documentation/';
  230. return Core::linkURL($url);
  231. }
  232. return self::getMySQLDocuURL('');
  233. }
  234. /**
  235. * Check the correct row count
  236. *
  237. * @param string $db the db name
  238. * @param array $table the table infos
  239. *
  240. * @return int the possibly modified row count
  241. */
  242. private static function checkRowCount($db, array $table)
  243. {
  244. global $dbi;
  245. $rowCount = 0;
  246. if ($table['Rows'] === null) {
  247. // Do not check exact row count here,
  248. // if row count is invalid possibly the table is defect
  249. // and this would break the navigation panel;
  250. // but we can check row count if this is a view or the
  251. // information_schema database
  252. // since Table::countRecords() returns a limited row count
  253. // in this case.
  254. // set this because Table::countRecords() can use it
  255. $tableIsView = $table['TABLE_TYPE'] === 'VIEW';
  256. if ($tableIsView || Utilities::isSystemSchema($db)) {
  257. $rowCount = $dbi
  258. ->getTable($db, $table['Name'])
  259. ->countRecords();
  260. }
  261. }
  262. return $rowCount;
  263. }
  264. /**
  265. * returns array with tables of given db with extended information and grouped
  266. *
  267. * @param string $db
  268. *
  269. * @return array (recursive) grouped table list
  270. */
  271. public static function getTableList($db): array
  272. {
  273. global $dbi;
  274. $sep = $GLOBALS['cfg']['NavigationTreeTableSeparator'];
  275. $tables = $dbi->getTablesFull($db);
  276. if ($GLOBALS['cfg']['NaturalOrder']) {
  277. uksort($tables, 'strnatcasecmp');
  278. }
  279. if (count($tables) < 1) {
  280. return $tables;
  281. }
  282. $default = [
  283. 'Name' => '',
  284. 'Rows' => 0,
  285. 'Comment' => '',
  286. 'disp_name' => '',
  287. ];
  288. $tableGroups = [];
  289. foreach ($tables as $table) {
  290. /** @var string $tableName */
  291. $tableName = $table['TABLE_NAME'];
  292. $table['Rows'] = self::checkRowCount($db, $table);
  293. // in $group we save the reference to the place in $table_groups
  294. // where to store the table info
  295. if ($GLOBALS['cfg']['NavigationTreeEnableGrouping'] && $sep && mb_strstr($tableName, $sep)) {
  296. $parts = explode($sep, $tableName);
  297. $group =& $tableGroups;
  298. $i = 0;
  299. $groupNameFull = '';
  300. $partsCount = count($parts) - 1;
  301. while (($i < $partsCount) && ($i < $GLOBALS['cfg']['NavigationTreeTableLevel'])) {
  302. $groupName = $parts[$i] . $sep;
  303. $groupNameFull .= $groupName;
  304. if (! isset($group[$groupName])) {
  305. $group[$groupName] = [];
  306. $group[$groupName]['is' . $sep . 'group'] = true;
  307. $group[$groupName]['tab' . $sep . 'count'] = 1;
  308. $group[$groupName]['tab' . $sep . 'group'] = $groupNameFull;
  309. } elseif (! isset($group[$groupName]['is' . $sep . 'group'])) {
  310. $table = $group[$groupName];
  311. $group[$groupName] = [];
  312. $group[$groupName][$groupName] = $table;
  313. $group[$groupName]['is' . $sep . 'group'] = true;
  314. $group[$groupName]['tab' . $sep . 'count'] = 1;
  315. $group[$groupName]['tab' . $sep . 'group'] = $groupNameFull;
  316. } else {
  317. $group[$groupName]['tab' . $sep . 'count']++;
  318. }
  319. $group =& $group[$groupName];
  320. $i++;
  321. }
  322. } else {
  323. if (! isset($tableGroups[$tableName])) {
  324. $tableGroups[$tableName] = [];
  325. }
  326. $group =& $tableGroups;
  327. }
  328. $table['disp_name'] = $table['Name'];
  329. $group[$tableName] = array_merge($default, $table);
  330. }
  331. return $tableGroups;
  332. }
  333. /* ----------------------- Set of misc functions ----------------------- */
  334. /**
  335. * Adds backquotes on both sides of a database, table or field name.
  336. * and escapes backquotes inside the name with another backquote
  337. *
  338. * example:
  339. * <code>
  340. * echo backquote('owner`s db'); // `owner``s db`
  341. *
  342. * </code>
  343. *
  344. * @param Stringable|string|null $identifier the database, table or field name to "backquote"
  345. */
  346. public static function backquote($identifier): string
  347. {
  348. return static::backquoteCompat($identifier, 'NONE', true);
  349. }
  350. /**
  351. * Adds backquotes on both sides of a database, table or field name.
  352. * in compatibility mode
  353. *
  354. * example:
  355. * <code>
  356. * echo backquoteCompat('owner`s db'); // `owner``s db`
  357. *
  358. * </code>
  359. *
  360. * @param Stringable|string|null $identifier the database, table or field name to "backquote"
  361. * @param string $compatibility string compatibility mode (used by dump functions)
  362. * @param bool|null $doIt a flag to bypass this function (used by dump functions)
  363. */
  364. public static function backquoteCompat(
  365. $identifier,
  366. string $compatibility = 'MSSQL',
  367. $doIt = true
  368. ): string {
  369. $identifier = (string) $identifier;
  370. if ($identifier === '' || $identifier === '*') {
  371. return $identifier;
  372. }
  373. if (! $doIt && ! ((int) Context::isKeyword($identifier) & Token::FLAG_KEYWORD_RESERVED)) {
  374. return $identifier;
  375. }
  376. $quote = '`';
  377. $escapeChar = '`';
  378. if ($compatibility === 'MSSQL') {
  379. $quote = '"';
  380. $escapeChar = '\\';
  381. }
  382. return $quote . str_replace($quote, $escapeChar . $quote, $identifier) . $quote;
  383. }
  384. /**
  385. * Formats $value to byte view
  386. *
  387. * @param float|int|string|null $value the value to format
  388. * @param int $limes the sensitiveness
  389. * @param int $comma the number of decimals to retain
  390. *
  391. * @return array|null the formatted value and its unit
  392. */
  393. public static function formatByteDown($value, $limes = 6, $comma = 0): ?array
  394. {
  395. if ($value === null) {
  396. return null;
  397. }
  398. if (is_string($value)) {
  399. $value = (float) $value;
  400. }
  401. $byteUnits = [
  402. /* l10n: shortcuts for Byte */
  403. __('B'),
  404. /* l10n: shortcuts for Kilobyte */
  405. __('KiB'),
  406. /* l10n: shortcuts for Megabyte */
  407. __('MiB'),
  408. /* l10n: shortcuts for Gigabyte */
  409. __('GiB'),
  410. /* l10n: shortcuts for Terabyte */
  411. __('TiB'),
  412. /* l10n: shortcuts for Petabyte */
  413. __('PiB'),
  414. /* l10n: shortcuts for Exabyte */
  415. __('EiB'),
  416. ];
  417. $dh = 10 ** $comma;
  418. $li = 10 ** $limes;
  419. $unit = $byteUnits[0];
  420. for ($d = 6, $ex = 15; $d >= 1; $d--, $ex -= 3) {
  421. $unitSize = $li * 10 ** $ex;
  422. if (isset($byteUnits[$d]) && $value >= $unitSize) {
  423. // use 1024.0 to avoid integer overflow on 64-bit machines
  424. $value = round($value / (1024 ** $d / $dh)) / $dh;
  425. $unit = $byteUnits[$d];
  426. break 1;
  427. }
  428. }
  429. if ($unit != $byteUnits[0]) {
  430. // if the unit is not bytes (as represented in current language)
  431. // reformat with max length of 5
  432. // 4th parameter=true means do not reformat if value < 1
  433. $returnValue = self::formatNumber($value, 5, $comma, true, false);
  434. } else {
  435. // do not reformat, just handle the locale
  436. $returnValue = self::formatNumber($value, 0);
  437. }
  438. return [
  439. trim($returnValue),
  440. $unit,
  441. ];
  442. }
  443. /**
  444. * Formats $value to the given length and appends SI prefixes
  445. * with a $length of 0 no truncation occurs, number is only formatted
  446. * to the current locale
  447. *
  448. * examples:
  449. * <code>
  450. * echo formatNumber(123456789, 6); // 123,457 k
  451. * echo formatNumber(-123456789, 4, 2); // -123.46 M
  452. * echo formatNumber(-0.003, 6); // -3 m
  453. * echo formatNumber(0.003, 3, 3); // 0.003
  454. * echo formatNumber(0.00003, 3, 2); // 0.03 m
  455. * echo formatNumber(0, 6); // 0
  456. * </code>
  457. *
  458. * @param float|int|string $value the value to format
  459. * @param int $digitsLeft number of digits left of the comma
  460. * @param int $digitsRight number of digits right of the comma
  461. * @param bool $onlyDown do not reformat numbers below 1
  462. * @param bool $noTrailingZero removes trailing zeros right of the comma (default: true)
  463. *
  464. * @return string the formatted value and its unit
  465. */
  466. public static function formatNumber(
  467. $value,
  468. $digitsLeft = 3,
  469. $digitsRight = 0,
  470. $onlyDown = false,
  471. $noTrailingZero = true
  472. ) {
  473. if ($value == 0) {
  474. return '0';
  475. }
  476. if (is_string($value)) {
  477. $value = (float) $value;
  478. }
  479. $originalValue = $value;
  480. //number_format is not multibyte safe, str_replace is safe
  481. if ($digitsLeft === 0) {
  482. $value = number_format(
  483. (float) $value,
  484. $digitsRight,
  485. /* l10n: Decimal separator */
  486. __('.'),
  487. /* l10n: Thousands separator */
  488. __(',')
  489. );
  490. if (($originalValue != 0) && (floatval($value) == 0)) {
  491. $value = ' <' . (1 / 10 ** $digitsRight);
  492. }
  493. return $value;
  494. }
  495. // this units needs no translation, ISO
  496. $units = [
  497. -8 => 'y',
  498. -7 => 'z',
  499. -6 => 'a',
  500. -5 => 'f',
  501. -4 => 'p',
  502. -3 => 'n',
  503. -2 => 'µ',
  504. -1 => 'm',
  505. 0 => ' ',
  506. 1 => 'k',
  507. 2 => 'M',
  508. 3 => 'G',
  509. 4 => 'T',
  510. 5 => 'P',
  511. 6 => 'E',
  512. 7 => 'Z',
  513. 8 => 'Y',
  514. ];
  515. /* l10n: Decimal separator */
  516. $decimalSep = __('.');
  517. /* l10n: Thousands separator */
  518. $thousandsSep = __(',');
  519. // check for negative value to retain sign
  520. if ($value < 0) {
  521. $sign = '-';
  522. $value = abs($value);
  523. } else {
  524. $sign = '';
  525. }
  526. $dh = 10 ** $digitsRight;
  527. /*
  528. * This gives us the right SI prefix already,
  529. * but $digits_left parameter not incorporated
  530. */
  531. $d = floor(log10((float) $value) / 3);
  532. /*
  533. * Lowering the SI prefix by 1 gives us an additional 3 zeros
  534. * So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits)
  535. * to use, then lower the SI prefix
  536. */
  537. $curDigits = floor(log10($value / 1000 ** $d) + 1);
  538. if ($digitsLeft > $curDigits) {
  539. $d -= floor(($digitsLeft - $curDigits) / 3);
  540. }
  541. if ($d < 0 && $onlyDown) {
  542. $d = 0;
  543. }
  544. $value = round($value / (1000 ** $d / $dh)) / $dh;
  545. $unit = $units[$d];
  546. // number_format is not multibyte safe, str_replace is safe
  547. $formattedValue = number_format($value, $digitsRight, $decimalSep, $thousandsSep);
  548. // If we don't want any zeros, remove them now
  549. if ($noTrailingZero && str_contains($formattedValue, $decimalSep)) {
  550. $formattedValue = preg_replace('/' . preg_quote($decimalSep, '/') . '?0+$/', '', $formattedValue);
  551. }
  552. if ($originalValue != 0 && floatval($value) == 0) {
  553. return ' <' . number_format(1 / 10 ** $digitsRight, $digitsRight, $decimalSep, $thousandsSep) . ' ' . $unit;
  554. }
  555. return $sign . $formattedValue . ' ' . $unit;
  556. }
  557. /**
  558. * Returns the number of bytes when a formatted size is given
  559. *
  560. * @param string|int $formattedSize the size expression (for example 8MB)
  561. *
  562. * @return int|float The numerical part of the expression (for example 8)
  563. */
  564. public static function extractValueFromFormattedSize($formattedSize)
  565. {
  566. $returnValue = -1;
  567. $formattedSize = (string) $formattedSize;
  568. if (preg_match('/^[0-9]+GB$/', $formattedSize)) {
  569. $returnValue = (int) mb_substr($formattedSize, 0, -2) * 1024 ** 3;
  570. } elseif (preg_match('/^[0-9]+MB$/', $formattedSize)) {
  571. $returnValue = (int) mb_substr($formattedSize, 0, -2) * 1024 ** 2;
  572. } elseif (preg_match('/^[0-9]+K$/', $formattedSize)) {
  573. $returnValue = (int) mb_substr($formattedSize, 0, -1) * 1024 ** 1;
  574. }
  575. return $returnValue;
  576. }
  577. /**
  578. * Writes localised date
  579. *
  580. * @param int $timestamp the current timestamp
  581. * @param string $format format
  582. *
  583. * @return string the formatted date
  584. */
  585. public static function localisedDate($timestamp = -1, $format = '')
  586. {
  587. $month = [
  588. /* l10n: Short month name */
  589. __('Jan'),
  590. /* l10n: Short month name */
  591. __('Feb'),
  592. /* l10n: Short month name */
  593. __('Mar'),
  594. /* l10n: Short month name */
  595. __('Apr'),
  596. /* l10n: Short month name */
  597. _pgettext('Short month name', 'May'),
  598. /* l10n: Short month name */
  599. __('Jun'),
  600. /* l10n: Short month name */
  601. __('Jul'),
  602. /* l10n: Short month name */
  603. __('Aug'),
  604. /* l10n: Short month name */
  605. __('Sep'),
  606. /* l10n: Short month name */
  607. __('Oct'),
  608. /* l10n: Short month name */
  609. __('Nov'),
  610. /* l10n: Short month name */
  611. __('Dec'),
  612. ];
  613. $dayOfWeek = [
  614. /* l10n: Short week day name for Sunday */
  615. _pgettext('Short week day name for Sunday', 'Sun'),
  616. /* l10n: Short week day name for Monday */
  617. __('Mon'),
  618. /* l10n: Short week day name for Tuesday */
  619. __('Tue'),
  620. /* l10n: Short week day name for Wednesday */
  621. __('Wed'),
  622. /* l10n: Short week day name for Thursday */
  623. __('Thu'),
  624. /* l10n: Short week day name for Friday */
  625. __('Fri'),
  626. /* l10n: Short week day name for Saturday */
  627. __('Sat'),
  628. ];
  629. if ($format == '') {
  630. /* l10n: See https://www.php.net/manual/en/function.strftime.php */
  631. $format = __('%B %d, %Y at %I:%M %p');
  632. }
  633. if ($timestamp == -1) {
  634. $timestamp = time();
  635. }
  636. $date = (string) preg_replace(
  637. '@%[aA]@',
  638. $dayOfWeek[(int) @strftime('%w', (int) $timestamp)],
  639. $format
  640. );
  641. $date = (string) preg_replace(
  642. '@%[bB]@',
  643. $month[(int) @strftime('%m', (int) $timestamp) - 1],
  644. $date
  645. );
  646. /* Fill in AM/PM */
  647. $hours = (int) date('H', (int) $timestamp);
  648. if ($hours >= 12) {
  649. $amPm = _pgettext('AM/PM indication in time', 'PM');
  650. } else {
  651. $amPm = _pgettext('AM/PM indication in time', 'AM');
  652. }
  653. $date = (string) preg_replace('@%[pP]@', $amPm, $date);
  654. // Can return false on windows for Japanese language
  655. // See https://github.com/phpmyadmin/phpmyadmin/issues/15830
  656. $ret = @strftime($date, (int) $timestamp);
  657. // Some OSes such as Win8.1 Traditional Chinese version did not produce UTF-8
  658. // output here. See https://github.com/phpmyadmin/phpmyadmin/issues/10598
  659. if ($ret === false || mb_detect_encoding($ret, 'UTF-8', true) !== 'UTF-8') {
  660. $ret = date('Y-m-d H:i:s', (int) $timestamp);
  661. }
  662. return $ret;
  663. }
  664. /**
  665. * Splits a URL string by parameter
  666. *
  667. * @param string $url the URL
  668. *
  669. * @return array<int, string> the parameter/value pairs, for example [0] db=sakila
  670. */
  671. public static function splitURLQuery($url): array
  672. {
  673. // decode encoded url separators
  674. $separator = Url::getArgSeparator();
  675. // on most places separator is still hard coded ...
  676. if ($separator !== '&') {
  677. // ... so always replace & with $separator
  678. $url = str_replace([htmlentities('&'), '&'], [$separator, $separator], $url);
  679. }
  680. $url = str_replace(htmlentities($separator), $separator, $url);
  681. // end decode
  682. $urlParts = parse_url($url);
  683. if (is_array($urlParts) && isset($urlParts['query']) && strlen($separator) > 0) {
  684. return explode($separator, $urlParts['query']);
  685. }
  686. return [];
  687. }
  688. /**
  689. * Returns a given timespan value in a readable format.
  690. *
  691. * @param int $seconds the timespan
  692. *
  693. * @return string the formatted value
  694. */
  695. public static function timespanFormat($seconds): string
  696. {
  697. $days = floor($seconds / 86400);
  698. if ($days > 0) {
  699. $seconds -= $days * 86400;
  700. }
  701. $hours = floor($seconds / 3600);
  702. if ($days > 0 || $hours > 0) {
  703. $seconds -= $hours * 3600;
  704. }
  705. $minutes = floor($seconds / 60);
  706. if ($days > 0 || $hours > 0 || $minutes > 0) {
  707. $seconds -= $minutes * 60;
  708. }
  709. return sprintf(
  710. __('%s days, %s hours, %s minutes and %s seconds'),
  711. (string) $days,
  712. (string) $hours,
  713. (string) $minutes,
  714. (string) $seconds
  715. );
  716. }
  717. /**
  718. * Function added to avoid path disclosures.
  719. * Called by each script that needs parameters, it displays
  720. * an error message and, by default, stops the execution.
  721. *
  722. * @param string[] $params The names of the parameters needed by the calling
  723. * script
  724. * @param bool $request Check parameters in request
  725. */
  726. public static function checkParameters($params, $request = false): void
  727. {
  728. $reportedScriptName = basename($GLOBALS['PMA_PHP_SELF']);
  729. $foundError = false;
  730. $errorMessage = '';
  731. if ($request) {
  732. $array = $_REQUEST;
  733. } else {
  734. $array = $GLOBALS;
  735. }
  736. foreach ($params as $param) {
  737. if (isset($array[$param])) {
  738. continue;
  739. }
  740. $errorMessage .= $reportedScriptName
  741. . ': ' . __('Missing parameter:') . ' '
  742. . $param
  743. . MySQLDocumentation::showDocumentation('faq', 'faqmissingparameters', true)
  744. . '[br]';
  745. $foundError = true;
  746. }
  747. if (! $foundError) {
  748. return;
  749. }
  750. Core::fatalError($errorMessage);
  751. }
  752. /**
  753. * Build a condition and with a value
  754. *
  755. * @param string|int|float|null $row The row value
  756. * @param FieldMetadata $meta The field metadata
  757. * @param int $fieldsCount A number of fields
  758. * @param string $conditionKey A key used for BINARY fields functions
  759. * @param string $condition The condition
  760. *
  761. * @return array<int,string|null>
  762. * @psalm-return array{string|null, string}
  763. */
  764. private static function getConditionValue(
  765. $row,
  766. FieldMetadata $meta,
  767. int $fieldsCount,
  768. string $conditionKey,
  769. string $condition
  770. ): array {
  771. global $dbi;
  772. if ($row === null) {
  773. return ['IS NULL', $condition];
  774. }
  775. $conditionValue = '';
  776. $isBinaryString = $meta->isType(FieldMetadata::TYPE_STRING) && $meta->isBinary();
  777. // 63 is the binary charset, see: https://dev.mysql.com/doc/internals/en/charsets.html
  778. $isBlobAndIsBinaryCharset = $meta->isType(FieldMetadata::TYPE_BLOB) && $meta->charsetnr === 63;
  779. if ($meta->isNumeric) {
  780. $conditionValue = '= ' . $row;
  781. } elseif ($isBlobAndIsBinaryCharset || (! empty($row) && $isBinaryString)) {
  782. // hexify only if this is a true not empty BLOB or a BINARY
  783. // do not waste memory building a too big condition
  784. $rowLength = mb_strlen((string) $row);
  785. if ($rowLength > 0 && $rowLength < 1000) {
  786. // use a CAST if possible, to avoid problems
  787. // if the field contains wildcard characters % or _
  788. $conditionValue = '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)';
  789. } elseif ($fieldsCount === 1) {
  790. // when this blob is the only field present
  791. // try settling with length comparison
  792. $condition = ' CHAR_LENGTH(' . $conditionKey . ') ';
  793. $conditionValue = ' = ' . $rowLength;
  794. } else {
  795. // this blob won't be part of the final condition
  796. $conditionValue = null;
  797. }
  798. } elseif ($meta->isMappedTypeGeometry && ! empty($row)) {
  799. // do not build a too big condition
  800. if (mb_strlen((string) $row) < 5000) {
  801. $condition .= '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)';
  802. } else {
  803. $condition = '';
  804. }
  805. } elseif ($meta->isMappedTypeBit) {
  806. $conditionValue = "= b'"
  807. . self::printableBitValue((int) $row, (int) $meta->length) . "'";
  808. } else {
  809. $conditionValue = '= \''
  810. . $dbi->escapeString((string) $row) . '\'';
  811. }
  812. return [$conditionValue, $condition];
  813. }
  814. /**
  815. * Function to generate unique condition for specified row.
  816. *
  817. * @param int $fieldsCount number of fields
  818. * @param FieldMetadata[] $fieldsMeta meta information about fields
  819. * @param array $row current row
  820. * @param bool $forceUnique generate condition only on pk or unique
  821. * @param string|bool $restrictToTable restrict the unique condition to this table or false if none
  822. * @param Expression[] $expressions An array of Expression instances.
  823. * @psalm-param array<int, mixed> $row
  824. *
  825. * @return array the calculated condition and whether condition is unique
  826. */
  827. public static function getUniqueCondition(
  828. $fieldsCount,
  829. array $fieldsMeta,
  830. array $row,
  831. $forceUnique = false,
  832. $restrictToTable = false,
  833. array $expressions = []
  834. ): array {
  835. global $dbi;
  836. $primaryKey = '';
  837. $uniqueKey = '';
  838. $nonPrimaryCondition = '';
  839. $preferredCondition = '';
  840. $primaryKeyArray = [];
  841. $uniqueKeyArray = [];
  842. $nonPrimaryConditionArray = [];
  843. $conditionArray = [];
  844. for ($i = 0; $i < $fieldsCount; ++$i) {
  845. $meta = $fieldsMeta[$i];
  846. // do not use a column alias in a condition
  847. if ($meta->orgname === '') {
  848. $meta->orgname = $meta->name;
  849. foreach ($expressions as $expression) {
  850. if (empty($expression->alias) || empty($expression->column)) {
  851. continue;
  852. }
  853. if (strcasecmp($meta->name, $expression->alias) == 0) {
  854. $meta->orgname = $expression->column;
  855. break;
  856. }
  857. }
  858. }
  859. // Do not use a table alias in a condition.
  860. // Test case is:
  861. // select * from galerie x WHERE
  862. //(select count(*) from galerie y where y.datum=x.datum)>1
  863. //
  864. // But orgtable is present only with mysqli extension so the
  865. // fix is only for mysqli.
  866. // Also, do not use the original table name if we are dealing with
  867. // a view because this view might be updatable.
  868. // (The isView() verification should not be costly in most cases
  869. // because there is some caching in the function).
  870. if (
  871. $meta->table !== $meta->orgtable
  872. && ! $dbi->getTable($GLOBALS['db'], $meta->table)->isView()
  873. ) {
  874. $meta->table = $meta->orgtable;
  875. }
  876. // If this field is not from the table which the unique clause needs
  877. // to be restricted to.
  878. if ($restrictToTable && $restrictToTable != $meta->table) {
  879. continue;
  880. }
  881. // to fix the bug where float fields (primary or not)
  882. // can't be matched because of the imprecision of
  883. // floating comparison, use CONCAT
  884. // (also, the syntax "CONCAT(field) IS NULL"
  885. // that we need on the next "if" will work)
  886. if ($meta->isType(FieldMetadata::TYPE_REAL)) {
  887. $conKey = 'CONCAT(' . self::backquote($meta->table) . '.'
  888. . self::backquote($meta->orgname) . ')';
  889. } else {
  890. $conKey = self::backquote($meta->table) . '.'
  891. . self::backquote($meta->orgname);
  892. }
  893. $condition = ' ' . $conKey . ' ';
  894. [$conVal, $condition] = self::getConditionValue($row[$i] ?? null, $meta, $fieldsCount, $conKey, $condition);
  895. if ($conVal === null) {
  896. continue;
  897. }
  898. $condition .= $conVal . ' AND';
  899. if ($meta->isPrimaryKey()) {
  900. $primaryKey .= $condition;
  901. $primaryKeyArray[$conKey] = $conVal;
  902. } elseif ($meta->isUniqueKey()) {
  903. $uniqueKey .= $condition;
  904. $uniqueKeyArray[$conKey] = $conVal;
  905. }
  906. $nonPrimaryCondition .= $condition;
  907. $nonPrimaryConditionArray[$conKey] = $conVal;
  908. }
  909. // Correction University of Virginia 19991216:
  910. // prefer primary or unique keys for condition,
  911. // but use conjunction of all values if no primary key
  912. $clauseIsUnique = true;
  913. if ($primaryKey) {
  914. $preferredCondition = $primaryKey;
  915. $conditionArray = $primaryKeyArray;
  916. } elseif ($uniqueKey) {
  917. $preferredCondition = $uniqueKey;
  918. $conditionArray = $uniqueKeyArray;
  919. } elseif (! $forceUnique) {
  920. $preferredCondition = $nonPrimaryCondition;
  921. $conditionArray = $nonPrimaryConditionArray;
  922. $clauseIsUnique = false;
  923. }
  924. $whereClause = trim((string) preg_replace('|\s?AND$|', '', $preferredCondition));
  925. return [
  926. $whereClause,
  927. $clauseIsUnique,
  928. $conditionArray,
  929. ];
  930. }
  931. /**
  932. * Generate the charset query part
  933. *
  934. * @param string $collation Collation
  935. * @param bool $override (optional) force 'CHARACTER SET' keyword
  936. */
  937. public static function getCharsetQueryPart(string $collation, bool $override = false): string
  938. {
  939. [$charset] = explode('_', $collation);
  940. $keyword = ' CHARSET=';
  941. if ($override) {
  942. $keyword = ' CHARACTER SET ';
  943. }
  944. return $keyword . $charset
  945. . ($charset == $collation ? '' : ' COLLATE ' . $collation);
  946. }
  947. /**
  948. * Generate a pagination selector for browsing resultsets
  949. *
  950. * @param string $name The name for the request parameter
  951. * @param int $rows Number of rows in the pagination set
  952. * @param int $pageNow current page number
  953. * @param int $nbTotalPage number of total pages
  954. * @param int $showAll If the number of pages is lower than this
  955. * variable, no pages will be omitted in pagination
  956. * @param int $sliceStart How many rows at the beginning should always
  957. * be shown?
  958. * @param int $sliceEnd How many rows at the end should always be shown?
  959. * @param int $percent Percentage of calculation page offsets to hop to a
  960. * next page
  961. * @param int $range Near the current page, how many pages should
  962. * be considered "nearby" and displayed as well?
  963. * @param string $prompt The prompt to display (sometimes empty)
  964. */
  965. public static function pageselector(
  966. $name,
  967. $rows,
  968. $pageNow = 1,
  969. $nbTotalPage = 1,
  970. $showAll = 200,
  971. $sliceStart = 5,
  972. $sliceEnd = 5,
  973. $percent = 20,
  974. $range = 10,
  975. $prompt = ''
  976. ): string {
  977. $increment = floor($nbTotalPage / $percent);
  978. $pageNowMinusRange = $pageNow - $range;
  979. $pageNowPlusRange = $pageNow + $range;
  980. $gotoPage = $prompt . ' <select class="pageselector ajax"';
  981. $gotoPage .= ' name="' . $name . '" >';
  982. if ($nbTotalPage < $showAll) {
  983. $pages = range(1, $nbTotalPage);
  984. } else {
  985. $pages = [];
  986. // Always show first X pages
  987. for ($i = 1; $i <= $sliceStart; $i++) {
  988. $pages[] = $i;
  989. }
  990. // Always show last X pages
  991. for ($i = $nbTotalPage - $sliceEnd; $i <= $nbTotalPage; $i++) {
  992. $pages[] = $i;
  993. }
  994. // Based on the number of results we add the specified
  995. // $percent percentage to each page number,
  996. // so that we have a representing page number every now and then to
  997. // immediately jump to specific pages.
  998. // As soon as we get near our currently chosen page ($pageNow -
  999. // $range), every page number will be shown.
  1000. $i = $sliceStart;
  1001. $x = $nbTotalPage - $sliceEnd;
  1002. $metBoundary = false;
  1003. while ($i <= $x) {
  1004. if ($i >= $pageNowMinusRange && $i <= $pageNowPlusRange) {
  1005. // If our pageselector comes near the current page, we use 1
  1006. // counter increments
  1007. $i++;
  1008. $metBoundary = true;
  1009. } else {
  1010. // We add the percentage increment to our current page to
  1011. // hop to the next one in range
  1012. $i += $increment;
  1013. // Make sure that we do not cross our boundaries.
  1014. if ($i > $pageNowMinusRange && ! $metBoundary) {
  1015. $i = $pageNowMinusRange;
  1016. }
  1017. }
  1018. if ($i <= 0 || $i > $x) {
  1019. continue;
  1020. }
  1021. $pages[] = $i;
  1022. }
  1023. /*
  1024. Add page numbers with "geometrically increasing" distances.
  1025. This helps me a lot when navigating through giant tables.
  1026. Test case: table with 2.28 million sets, 76190 pages. Page of interest
  1027. is between 72376 and 76190.
  1028. Selecting page 72376.
  1029. Now, old version enumerated only +/- 10 pages around 72376 and the
  1030. percentage increment produced steps of about 3000.
  1031. The following code adds page numbers +/- 2,4,8,16,32,64,128,256 etc.
  1032. around the current page.
  1033. */
  1034. $i = $pageNow;
  1035. $dist = 1;
  1036. while ($i < $x) {
  1037. $dist = 2 * $dist;
  1038. $i = $pageNow + $dist;
  1039. if ($i <= 0 || $i > $x) {
  1040. continue;
  1041. }
  1042. $pages[] = $i;
  1043. }
  1044. $i = $pageNow;
  1045. $dist = 1;
  1046. while ($i > 0) {
  1047. $dist = 2 * $dist;
  1048. $i = $pageNow - $dist;
  1049. if ($i <= 0 || $i > $x) {
  1050. continue;
  1051. }
  1052. $pages[] = $i;
  1053. }
  1054. // Since because of ellipsing of the current page some numbers may be
  1055. // double, we unify our array:
  1056. sort($pages);
  1057. $pages = array_unique($pages);
  1058. }
  1059. if ($pageNow > $nbTotalPage) {
  1060. $pages[] = $pageNow;
  1061. }
  1062. foreach ($pages as $i) {
  1063. if ($i == $pageNow) {
  1064. $selected = 'selected="selected" style="font-weight: bold"';
  1065. } else {
  1066. $selected = '';
  1067. }
  1068. $gotoPage .= ' <option ' . $selected
  1069. . ' value="' . (($i - 1) * $rows) . '">' . $i . '</option>' . "\n";
  1070. }
  1071. $gotoPage .= ' </select>';
  1072. return $gotoPage;
  1073. }
  1074. /**
  1075. * Calculate page number through position
  1076. *
  1077. * @param int $pos position of first item
  1078. * @param int $maxCount number of items per page
  1079. *
  1080. * @return int $page_num
  1081. */
  1082. public static function getPageFromPosition($pos, $maxCount)
  1083. {
  1084. return (int) floor($pos / $maxCount) + 1;
  1085. }
  1086. /**
  1087. * replaces %u in given path with current user name
  1088. *
  1089. * example:
  1090. * <code>
  1091. * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/'
  1092. *
  1093. * </code>
  1094. *
  1095. * @param string $dir with wildcard for user
  1096. *
  1097. * @return string per user directory
  1098. */
  1099. public static function userDir(string $dir): string
  1100. {
  1101. // add trailing slash
  1102. if (mb_substr($dir, -1) !== '/') {
  1103. $dir .= '/';
  1104. }
  1105. return str_replace('%u', Core::securePath($GLOBALS['cfg']['Server']['user']), $dir);
  1106. }
  1107. /**
  1108. * Clears cache content which needs to be refreshed on user change.
  1109. */
  1110. public static function clearUserCache(): void
  1111. {
  1112. SessionCache::remove('is_superuser');
  1113. SessionCache::remove('is_createuser');
  1114. SessionCache::remove('is_grantuser');
  1115. SessionCache::remove('mysql_cur_user');
  1116. SessionCache::remove('mysql_cur_role');
  1117. }
  1118. /**
  1119. * Converts a bit value to printable format;
  1120. * in MySQL a BIT field can be from 1 to 64 bits so we need this
  1121. * function because in PHP, decbin() supports only 32 bits
  1122. * on 32-bit servers
  1123. *
  1124. * @param int $value coming from a BIT field
  1125. * @param int $length length
  1126. *
  1127. * @return string the printable value
  1128. */
  1129. public static function printableBitValue(int $value, int $length): string
  1130. {
  1131. // if running on a 64-bit server or the length is safe for decbin()
  1132. if (PHP_INT_SIZE == 8 || $length < 33) {
  1133. $printable = decbin($value);
  1134. } else {
  1135. // FIXME: does not work for the leftmost bit of a 64-bit value
  1136. $i = 0;
  1137. $printable = '';
  1138. while ($value >= 2 ** $i) {
  1139. ++$i;
  1140. }
  1141. if ($i != 0) {
  1142. --$i;
  1143. }
  1144. while ($i >= 0) {
  1145. if ($value - 2 ** $i < 0) {
  1146. $printable = '0' . $printable;
  1147. } else {
  1148. $printable = '1' . $printable;
  1149. $value -= 2 ** $i;
  1150. }
  1151. --$i;
  1152. }
  1153. $printable = strrev($printable);
  1154. }
  1155. $printable = str_pad($printable, $length, '0', STR_PAD_LEFT);
  1156. return $printable;
  1157. }
  1158. /**
  1159. * Converts a BIT type default value
  1160. * for example, b'010' becomes 010
  1161. *
  1162. * @param string|null $bitDefaultValue value
  1163. *
  1164. * @return string the converted value
  1165. */
  1166. public static function convertBitDefaultValue(?string $bitDefaultValue): string
  1167. {
  1168. return (string) preg_replace(
  1169. "/^b'(\d*)'?$/",
  1170. '$1',
  1171. htmlspecialchars_decode((string) $bitDefaultValue, ENT_QUOTES),
  1172. 1
  1173. );
  1174. }
  1175. /**
  1176. * Extracts the various parts from a column spec
  1177. *
  1178. * @param string $columnSpecification Column specification
  1179. *
  1180. * @return array associative array containing type, spec_in_brackets
  1181. * and possibly enum_set_values (another array)
  1182. */
  1183. public static function extractColumnSpec($columnSpecification)
  1184. {
  1185. $firstBracketPos = mb_strpos($columnSpecification, '(');
  1186. if ($firstBracketPos) {
  1187. $specInBrackets = rtrim(
  1188. mb_substr(
  1189. $columnSpecification,
  1190. $firstBracketPos + 1,
  1191. mb_strrpos($columnSpecification, ')') - $firstBracketPos - 1
  1192. )
  1193. );
  1194. // convert to lowercase just to be sure
  1195. $type = mb_strtolower(
  1196. rtrim(mb_substr($columnSpecification, 0, $firstBracketPos))
  1197. );
  1198. } else {
  1199. // Split trailing attributes such as unsigned,
  1200. // binary, zerofill and get data type name
  1201. $typeParts = explode(' ', $columnSpecification);
  1202. $type = mb_strtolower($typeParts[0]);
  1203. $specInBrackets = '';
  1204. }
  1205. if ($type === 'enum' || $type === 'set') {
  1206. // Define our working vars
  1207. $enumSetValues = self::parseEnumSetValues($columnSpecification, false);
  1208. $printType = $type
  1209. . '(' . str_replace("','", "', '", $specInBrackets) . ')';
  1210. $binary = false;
  1211. $unsigned = false;
  1212. $zerofill = false;
  1213. $compressed = false;
  1214. } else {
  1215. $enumSetValues = [];
  1216. /* Create printable type name */
  1217. $printType = mb_strtolower($columnSpecification);
  1218. // Strip the "BINARY" attribute, except if we find "BINARY(" because
  1219. // this would be a BINARY or VARBINARY column type;
  1220. // by the way, a BLOB should not show the BINARY attribute
  1221. // because this is not accepted in MySQL syntax.
  1222. if (str_contains($printType, 'binary') && ! preg_match('@binary[\(]@', $printType)) {
  1223. $printType = str_replace('binary', '', $printType);
  1224. $binary = true;
  1225. } else {
  1226. $binary = false;
  1227. }
  1228. $printType = (string) preg_replace('@zerofill@', '', $printType, -1, $zerofillCount);
  1229. $zerofill = ($zerofillCount > 0);
  1230. $printType = (string) preg_replace('@unsigned@', '', $printType, -1, $unsignedCount);
  1231. $unsigned = ($unsignedCount > 0);
  1232. $printType = (string) preg_replace('@\/\*!100301 compressed\*\/@', '', $printType, -1, $compressedCount);
  1233. $compressed = ($compressedCount > 0);
  1234. $printType = trim($printType);
  1235. }
  1236. $attribute = ' ';
  1237. if ($binary) {
  1238. $attribute = 'BINARY';
  1239. }
  1240. if ($unsigned) {
  1241. $attribute = 'UNSIGNED';
  1242. }
  1243. if ($zerofill) {
  1244. $attribute = 'UNSIGNED ZEROFILL';
  1245. }
  1246. if ($compressed) {
  1247. // With InnoDB page compression, multiple compression algorithms are supported.
  1248. // In contrast, with InnoDB's COMPRESSED row format, zlib is the only supported compression algorithm.
  1249. // This means that the COMPRESSED row format has less compression options than InnoDB page compression does.
  1250. // @see https://mariadb.com/kb/en/innodb-page-compression/#comparison-with-the-compressed-row-format
  1251. $attribute = 'COMPRESSED=zlib';
  1252. }
  1253. $canContainCollation = false;
  1254. if (! $binary && preg_match('@^(char|varchar|text|tinytext|mediumtext|longtext|set|enum)@', $type)) {
  1255. $canContainCollation = true;
  1256. }
  1257. // for the case ENUM('&#8211;','&ldquo;')
  1258. $displayedType = htmlspecialchars($printType, ENT_COMPAT);
  1259. if (mb_strlen($printType) > $GLOBALS['cfg']['LimitChars']) {
  1260. $displayedType = '<abbr title="' . htmlspecialchars($printType) . '">';
  1261. $displayedType .= htmlspecialchars(
  1262. mb_substr(
  1263. $printType,
  1264. 0,
  1265. (int) $GLOBALS['cfg']['LimitChars']
  1266. ) . '...',
  1267. ENT_COMPAT
  1268. );
  1269. $displayedType .= '</abbr>';
  1270. }
  1271. return [
  1272. 'type' => $type,
  1273. 'spec_in_brackets' => $specInBrackets,
  1274. 'enum_set_values' => $enumSetValues,
  1275. 'print_type' => $printType,
  1276. 'binary' => $binary,
  1277. 'unsigned' => $unsigned,
  1278. 'zerofill' => $zerofill,
  1279. 'attribute' => $attribute,
  1280. 'can_contain_collation' => $canContainCollation,
  1281. 'displayed_type' => $displayedType,
  1282. ];
  1283. }
  1284. /**
  1285. * If the string starts with a \r\n pair (0x0d0a) add an extra \n
  1286. *
  1287. * @param string $string string
  1288. *
  1289. * @return string with the chars replaced
  1290. */
  1291. public static function duplicateFirstNewline(string $string): string
  1292. {
  1293. $firstOccurrence = mb_strpos($string, "\r\n");
  1294. if ($firstOccurrence === 0) {
  1295. $string = "\n" . $string;
  1296. }
  1297. return $string;
  1298. }
  1299. /**
  1300. * Get the action word corresponding to a script name
  1301. * in order to display it as a title in navigation panel
  1302. *
  1303. * @param string $target a valid value for $cfg['NavigationTreeDefaultTabTable'],
  1304. * $cfg['NavigationTreeDefaultTabTable2'],
  1305. * $cfg['DefaultTabTable'] or $cfg['DefaultTabDatabase']
  1306. *
  1307. * @return string|bool Title for the $cfg value
  1308. */
  1309. public static function getTitleForTarget($target)
  1310. {
  1311. $mapping = [
  1312. 'structure' => __('Structure'),
  1313. 'sql' => __('SQL'),
  1314. 'search' => __('Search'),
  1315. 'insert' => __('Insert'),
  1316. 'browse' => __('Browse'),
  1317. 'operations' => __('Operations'),
  1318. ];
  1319. return $mapping[$target] ?? false;
  1320. }
  1321. /**
  1322. * Get the script name corresponding to a plain English config word
  1323. * in order to append in links on navigation and main panel
  1324. *
  1325. * @param string $target a valid value for
  1326. * $cfg['NavigationTreeDefaultTabTable'],
  1327. * $cfg['NavigationTreeDefaultTabTable2'],
  1328. * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
  1329. * $cfg['DefaultTabServer']
  1330. * @param string $location one out of 'server', 'table', 'database'
  1331. *
  1332. * @return string script name corresponding to the config word
  1333. */
  1334. public static function getScriptNameForOption($target, string $location): string
  1335. {
  1336. return Url::getFromRoute(self::getUrlForOption($target, $location));
  1337. }
  1338. /**
  1339. * Get the URL corresponding to a plain English config word
  1340. * in order to append in links on navigation and main panel
  1341. *
  1342. * @param string $target a valid value for
  1343. * $cfg['NavigationTreeDefaultTabTable'],
  1344. * $cfg['NavigationTreeDefaultTabTable2'],
  1345. * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
  1346. * $cfg['DefaultTabServer']
  1347. * @param string $location one out of 'server', 'table', 'database'
  1348. *
  1349. * @return string The URL corresponding to the config word
  1350. */
  1351. public static function getUrlForOption($target, string $location): string
  1352. {
  1353. if ($location === 'server') {
  1354. // Values for $cfg['DefaultTabServer']
  1355. switch ($target) {
  1356. case 'welcome':
  1357. case 'index.php':
  1358. return '/';
  1359. case 'databases':
  1360. case 'server_databases.php':
  1361. return '/server/databases';
  1362. case 'status':
  1363. case 'server_status.php':
  1364. return '/server/status';
  1365. case 'variables':
  1366. case 'server_variables.php':
  1367. return '/server/variables';
  1368. case 'privileges':
  1369. case 'server_privileges.php':
  1370. return '/server/privileges';
  1371. }
  1372. } elseif ($location === 'database') {
  1373. // Values for $cfg['DefaultTabDatabase']
  1374. switch ($target) {
  1375. case 'structure':
  1376. case 'db_structure.php':
  1377. return '/database/structure';
  1378. case 'sql':
  1379. case 'db_sql.php':
  1380. return '/database/sql';
  1381. case 'search':
  1382. case 'db_search.php':
  1383. return '/database/search';
  1384. case 'operations':
  1385. case 'db_operations.php':
  1386. return '/database/operations';
  1387. }
  1388. } elseif ($location === 'table') {
  1389. // Values for $cfg['DefaultTabTable'],
  1390. // $cfg['NavigationTreeDefaultTabTable'] and
  1391. // $cfg['NavigationTreeDefaultTabTable2']
  1392. switch ($target) {
  1393. case 'structure':
  1394. case 'tbl_structure.php':
  1395. return '/table/structure';
  1396. case 'sql':
  1397. case 'tbl_sql.php':
  1398. return '/table/sql';
  1399. case 'search':
  1400. case 'tbl_select.php':
  1401. return '/table/search';
  1402. case 'insert':
  1403. case 'tbl_change.php':
  1404. return '/table/change';
  1405. case 'browse':
  1406. case 'sql.php':
  1407. return '/sql';
  1408. }
  1409. }
  1410. return '/';
  1411. }
  1412. /**
  1413. * Formats user string, expanding @VARIABLES@, accepting strftime format
  1414. * string.
  1415. *
  1416. * @param string $string Text where to do expansion.
  1417. * @param array|string $escape Function to call for escaping variable values.
  1418. * Can also be an array of:
  1419. * - the escape method name
  1420. * - the class that contains the method
  1421. * - location of the class (for inclusion)
  1422. * @param array $updates Array with overrides for default parameters
  1423. * (obtained from GLOBALS).
  1424. *
  1425. * @return string
  1426. */
  1427. public static function expandUserString(
  1428. $string,
  1429. $escape = null,
  1430. array $updates = []
  1431. ) {
  1432. global $dbi;
  1433. /* Content */
  1434. $vars = [];
  1435. $vars['http_host'] = Core::getenv('HTTP_HOST');
  1436. $vars['server_name'] = $GLOBALS['cfg']['Server']['host'];
  1437. $vars['server_verbose'] = $GLOBALS['cfg']['Server']['verbose'];
  1438. if (empty($GLOBALS['cfg']['Server']['verbose'])) {
  1439. $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['host'];
  1440. } else {
  1441. $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['verbose'];
  1442. }
  1443. $vars['database'] = $GLOBALS['db'];
  1444. $vars['table'] = $GLOBALS['table'];
  1445. $vars['phpmyadmin_version'] = 'phpMyAdmin ' . Version::VERSION;
  1446. /* Update forced variables */
  1447. foreach ($updates as $key => $val) {
  1448. $vars[$key] = $val;
  1449. }
  1450. /* Replacement mapping */
  1451. /*
  1452. * The __VAR__ ones are for backward compatibility, because user
  1453. * might still have it in cookies.
  1454. */
  1455. $replace = [
  1456. '@HTTP_HOST@' => $vars['http_host'],
  1457. '@SERVER@' => $vars['server_name'],
  1458. '__SERVER__' => $vars['server_name'],
  1459. '@VERBOSE@' => $vars['server_verbose'],
  1460. '@VSERVER@' => $vars['server_verbose_or_name'],
  1461. '@DATABASE@' => $vars['database'],
  1462. '__DB__' => $vars['database'],
  1463. '@TABLE@' => $vars['table'],
  1464. '__TABLE__' => $vars['table'],
  1465. '@PHPMYADMIN@' => $vars['phpmyadmin_version'],
  1466. ];
  1467. /* Optional escaping */
  1468. if ($escape !== null) {
  1469. if (is_array($escape)) {
  1470. $escapeClass = new $escape[1]();
  1471. $escapeMethod = $escape[0];
  1472. }
  1473. foreach ($replace as $key => $val) {
  1474. if (isset($escapeClass, $escapeMethod)) {
  1475. $replace[$key] = $escapeClass->$escapeMethod($val);
  1476. } elseif ($escape === 'backquote') {
  1477. $replace[$key] = self::backquote($val);
  1478. } elseif (is_callable($escape)) {
  1479. $replace[$key] = $escape($val);
  1480. }
  1481. }
  1482. }
  1483. /* Backward compatibility in 3.5.x */
  1484. if (str_contains($string, '@FIELDS@')) {
  1485. $string = strtr($string, ['@FIELDS@' => '@COLUMNS@']);
  1486. }
  1487. /* Fetch columns list if required */
  1488. if (str_contains($string, '@COLUMNS@')) {
  1489. $columnsList = $dbi->getColumns($GLOBALS['db'], $GLOBALS['table']);
  1490. // sometimes the table no longer exists at this point
  1491. if ($columnsList !== null) {
  1492. $columnNames = [];
  1493. foreach ($columnsList as $column) {
  1494. if ($escape !== null) {
  1495. $columnNames[] = self::$escape($column['Field']);
  1496. } else {
  1497. $columnNames[] = $column['Field'];
  1498. }
  1499. }
  1500. $replace['@COLUMNS@'] = implode(',', $columnNames);
  1501. } else {
  1502. $replace['@COLUMNS@'] = '*';
  1503. }
  1504. }
  1505. /* Do the replacement */
  1506. return strtr((string) @strftime($string), $replace);
  1507. }
  1508. /**
  1509. * This function processes the datatypes supported by the DB,
  1510. * as specified in Types->getColumns() and either returns an array
  1511. * (useful for quickly checking if a datatype is supported)
  1512. * or an HTML snippet that creates a drop-down list.
  1513. *
  1514. * @param bool $html Whether to generate an html snippet or an array
  1515. * @param string $selected The value to mark as selected in HTML mode
  1516. *
  1517. * @return mixed An HTML snippet or an array of datatypes.
  1518. */
  1519. public static function getSupportedDatatypes($html = false, $selected = '')
  1520. {
  1521. global $dbi;
  1522. if ($html) {
  1523. $retval = Generator::getSupportedDatatypes($selected);
  1524. } else {
  1525. $retval = [];
  1526. foreach ($dbi->types->getColumns() as $value) {
  1527. if (is_array($value)) {
  1528. foreach ($value as $subvalue) {
  1529. if ($subvalue === '-') {
  1530. continue;
  1531. }
  1532. $retval[] = $subvalue;
  1533. }
  1534. } else {
  1535. if ($value !== '-') {
  1536. $retval[] = $value;
  1537. }
  1538. }
  1539. }
  1540. }
  1541. return $retval;
  1542. }
  1543. /**
  1544. * Returns a list of datatypes that are not (yet) handled by PMA.
  1545. * Used by: /table/change and libraries/Routines.php
  1546. *
  1547. * @return array list of datatypes
  1548. */
  1549. public static function unsupportedDatatypes(): array
  1550. {
  1551. return [];
  1552. }
  1553. /**
  1554. * This function is to check whether database support UUID
  1555. */
  1556. public static function isUUIDSupported(): bool
  1557. {
  1558. return Compatibility::isUUIDSupported($GLOBALS['dbi']);
  1559. }
  1560. /**
  1561. * Checks if the current user has a specific privilege and returns true if the
  1562. * user indeed has that privilege or false if they don't. This function must
  1563. * only be used for features that are available since MySQL 5, because it
  1564. * relies on the INFORMATION_SCHEMA database to be present.
  1565. *
  1566. * Example: currentUserHasPrivilege('CREATE ROUTINE', 'mydb');
  1567. * // Checks if the currently logged in user has the global
  1568. * // 'CREATE ROUTINE' privilege or, if not, checks if the
  1569. * // user has this privilege on database 'mydb'.
  1570. *
  1571. * @param string $priv The privilege to check
  1572. * @param string|null $db null, to only check global privileges
  1573. * string, db name where to also check
  1574. * for privileges
  1575. * @param string|null $tbl null, to only check global/db privileges
  1576. * string, table name where to also check
  1577. * for privileges
  1578. */
  1579. public static function currentUserHasPrivilege(string $priv, ?string $db = null, ?string $tbl = null): bool
  1580. {
  1581. global $dbi;
  1582. // Get the username for the current user in the format
  1583. // required to use in the information schema database.
  1584. [$user, $host] = $dbi->getCurrentUserAndHost();
  1585. // MySQL is started with --skip-grant-tables
  1586. if ($user === '') {
  1587. return true;
  1588. }
  1589. $username = "''";
  1590. $username .= str_replace("'", "''", $user);
  1591. $username .= "''@''";
  1592. $username .= str_replace("'", "''", $host);
  1593. $username .= "''";
  1594. // Prepare the query
  1595. $query = 'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`%s` '
  1596. . "WHERE GRANTEE='%s' AND PRIVILEGE_TYPE='%s'";
  1597. // Check global privileges first.
  1598. $userPrivileges = $dbi->fetchValue(
  1599. sprintf(
  1600. $query,
  1601. 'USER_PRIVILEGES',
  1602. $username,
  1603. $priv
  1604. )
  1605. );
  1606. if ($userPrivileges) {
  1607. return true;
  1608. }
  1609. // If a database name was provided and user does not have the
  1610. // required global privilege, try database-wise permissions.
  1611. if ($db === null) {
  1612. // There was no database name provided and the user
  1613. // does not have the correct global privilege.
  1614. return false;
  1615. }
  1616. $query .= " AND '%s' LIKE `TABLE_SCHEMA`";
  1617. $schemaPrivileges = $dbi->fetchValue(
  1618. sprintf(
  1619. $query,
  1620. 'SCHEMA_PRIVILEGES',
  1621. $username,
  1622. $priv,
  1623. $dbi->escapeString($db)
  1624. )
  1625. );
  1626. if ($schemaPrivileges) {
  1627. return true;
  1628. }
  1629. // If a table name was also provided and we still didn't
  1630. // find any valid privileges, try table-wise privileges.
  1631. if ($tbl !== null) {
  1632. $query .= " AND TABLE_NAME='%s'";
  1633. $tablePrivileges = $dbi->fetchValue(
  1634. sprintf(
  1635. $query,
  1636. 'TABLE_PRIVILEGES',
  1637. $username,
  1638. $priv,
  1639. $dbi->escapeString($db),
  1640. $dbi->escapeString($tbl)
  1641. )
  1642. );
  1643. if ($tablePrivileges) {
  1644. return true;
  1645. }
  1646. }
  1647. /**
  1648. * If we reached this point, the user does not
  1649. * have even valid table-wise privileges.
  1650. */
  1651. return false;
  1652. }
  1653. /**
  1654. * Returns server type for current connection
  1655. *
  1656. * Known types are: MariaDB, Percona Server and MySQL (default)
  1657. *
  1658. * @phpstan-return 'MariaDB'|'Percona Server'|'MySQL'
  1659. */
  1660. public static function getServerType(): string
  1661. {
  1662. global $dbi;
  1663. if ($dbi->isMariaDB()) {
  1664. return 'MariaDB';
  1665. }
  1666. if ($dbi->isPercona()) {
  1667. return 'Percona Server';
  1668. }
  1669. return 'MySQL';
  1670. }
  1671. /**
  1672. * Parses ENUM/SET values
  1673. *
  1674. * @param string $definition The definition of the column
  1675. * for which to parse the values
  1676. * @param bool $escapeHtml Whether to escape html entities
  1677. *
  1678. * @return array
  1679. */
  1680. public static function parseEnumSetValues($definition, $escapeHtml = true)
  1681. {
  1682. // There is a JS port of the below parser in functions.js
  1683. // If you are fixing something here,
  1684. // you need to also update the JS port.
  1685. // This should really be delegated to MySQL but since we also want to HTML encode it,
  1686. // it is easier this way.
  1687. // It future replace str_getcsv with $dbi->fetchSingleRow('SELECT '.$expressionInBrackets[1]);
  1688. preg_match('/\((.*)\)/', $definition, $expressionInBrackets);
  1689. $matches = str_getcsv($expressionInBrackets[1], ',', "'", '\\');
  1690. $values = [];
  1691. foreach ($matches as $value) {
  1692. $value = strtr($value, ['\\\\' => '\\']); // str_getcsv doesn't unescape backslashes so we do it ourselves
  1693. $values[] = $escapeHtml ? htmlspecialchars($value, ENT_QUOTES, 'UTF-8') : $value;
  1694. }
  1695. return $values;
  1696. }
  1697. /**
  1698. * Return the list of tabs for the menu with corresponding names
  1699. *
  1700. * @param string $level 'server', 'db' or 'table' level
  1701. *
  1702. * @return array|null list of tabs for the menu
  1703. */
  1704. public static function getMenuTabList($level = null)
  1705. {
  1706. $tabList = [
  1707. 'server' => [
  1708. 'databases' => __('Databases'),
  1709. 'sql' => __('SQL'),
  1710. 'status' => __('Status'),
  1711. 'rights' => __('Users'),
  1712. 'export' => __('Export'),
  1713. 'import' => __('Import'),
  1714. 'settings' => __('Settings'),
  1715. 'binlog' => __('Binary log'),
  1716. 'replication' => __('Replication'),
  1717. 'vars' => __('Variables'),
  1718. 'charset' => __('Charsets'),
  1719. 'plugins' => __('Plugins'),
  1720. 'engine' => __('Engines'),
  1721. ],
  1722. 'db' => [
  1723. 'structure' => __('Structure'),
  1724. 'sql' => __('SQL'),
  1725. 'search' => __('Search'),
  1726. 'query' => __('Query'),
  1727. 'export' => __('Export'),
  1728. 'import' => __('Import'),
  1729. 'operation' => __('Operations'),
  1730. 'privileges' => __('Privileges'),
  1731. 'routines' => __('Routines'),
  1732. 'events' => __('Events'),
  1733. 'triggers' => __('Triggers'),
  1734. 'tracking' => __('Tracking'),
  1735. 'designer' => __('Designer'),
  1736. 'central_columns' => __('Central columns'),
  1737. ],
  1738. 'table' => [
  1739. 'browse' => __('Browse'),
  1740. 'structure' => __('Structure'),
  1741. 'sql' => __('SQL'),
  1742. 'search' => __('Search'),
  1743. 'insert' => __('Insert'),
  1744. 'export' => __('Export'),
  1745. 'import' => __('Import'),
  1746. 'privileges' => __('Privileges'),
  1747. 'operation' => __('Operations'),
  1748. 'tracking' => __('Tracking'),
  1749. 'triggers' => __('Triggers'),
  1750. ],
  1751. ];
  1752. if ($level == null) {
  1753. return $tabList;
  1754. }
  1755. if (array_key_exists($level, $tabList)) {
  1756. return $tabList[$level];
  1757. }
  1758. return null;
  1759. }
  1760. /**
  1761. * Add fractional seconds to time, datetime and timestamp strings.
  1762. * If the string contains fractional seconds,
  1763. * pads it with 0s up to 6 decimal places.
  1764. *
  1765. * @param string $value time, datetime or timestamp strings
  1766. *
  1767. * @return string time, datetime or timestamp strings with fractional seconds
  1768. */
  1769. public static function addMicroseconds($value)
  1770. {
  1771. if ($value === '' || preg_match('/^current_timestamp(\([0-6]?\))?$/i', $value)) {
  1772. return $value;
  1773. }
  1774. if (! str_contains($value, '.')) {
  1775. return $value . '.000000';
  1776. }
  1777. $value .= '000000';
  1778. return mb_substr(
  1779. $value,
  1780. 0,
  1781. mb_strpos($value, '.') + 7
  1782. );
  1783. }
  1784. /**
  1785. * Reads the file, detects the compression MIME type, closes the file
  1786. * and returns the MIME type
  1787. *
  1788. * @param resource $file the file handle
  1789. *
  1790. * @return string the MIME type for compression, or 'none'
  1791. */
  1792. public static function getCompressionMimeType($file)
  1793. {
  1794. $test = fread($file, 4);
  1795. if ($test === false) {
  1796. fclose($file);
  1797. return 'none';
  1798. }
  1799. $len = strlen($test);
  1800. fclose($file);
  1801. if ($len >= 2 && $test[0] == chr(31) && $test[1] == chr(139)) {
  1802. return 'application/gzip';
  1803. }
  1804. if ($len >= 3 && substr($test, 0, 3) === 'BZh') {
  1805. return 'application/bzip2';
  1806. }
  1807. if ($len >= 4 && $test == "PK\003\004") {
  1808. return 'application/zip';
  1809. }
  1810. return 'none';
  1811. }
  1812. /**
  1813. * Provide COLLATE clause, if required, to perform case sensitive comparisons
  1814. * for queries on information_schema.
  1815. *
  1816. * @return string COLLATE clause if needed or empty string.
  1817. */
  1818. public static function getCollateForIS()
  1819. {
  1820. global $dbi;
  1821. $names = $dbi->getLowerCaseNames();
  1822. if ($names === '0') {
  1823. return 'COLLATE utf8_bin';
  1824. }
  1825. if ($names === '2') {
  1826. return 'COLLATE utf8_general_ci';
  1827. }
  1828. return '';
  1829. }
  1830. /**
  1831. * Process the index data.
  1832. *
  1833. * @param array $indexes index data
  1834. *
  1835. * @return array processes index data
  1836. */
  1837. public static function processIndexData(array $indexes)
  1838. {
  1839. $lastIndex = '';
  1840. $primary = '';
  1841. $pkArray = []; // will be use to emphasis prim. keys in the table
  1842. $indexesInfo = [];
  1843. $indexesData = [];
  1844. // view
  1845. foreach ($indexes as $row) {
  1846. // Backups the list of primary keys
  1847. if ($row['Key_name'] === 'PRIMARY') {
  1848. $primary .= $row['Column_name'] . ', ';
  1849. $pkArray[$row['Column_name']] = 1;
  1850. }
  1851. // Retains keys informations
  1852. if ($row['Key_name'] != $lastIndex) {
  1853. $indexes[] = $row['Key_name'];
  1854. $lastIndex = $row['Key_name'];
  1855. }
  1856. $indexesInfo[$row['Key_name']]['Sequences'][] = $row['Seq_in_index'];
  1857. $indexesInfo[$row['Key_name']]['Non_unique'] = $row['Non_unique'];
  1858. if (isset($row['Cardinality'])) {
  1859. $indexesInfo[$row['Key_name']]['Cardinality'] = $row['Cardinality'];
  1860. }
  1861. // I don't know what does following column mean....
  1862. // $indexes_info[$row['Key_name']]['Packed'] = $row['Packed'];
  1863. $indexesInfo[$row['Key_name']]['Comment'] = $row['Comment'];
  1864. $indexesData[$row['Key_name']][$row['Seq_in_index']]['Column_name'] = $row['Column_name'];
  1865. if (! isset($row['Sub_part'])) {
  1866. continue;
  1867. }
  1868. $indexesData[$row['Key_name']][$row['Seq_in_index']]['Sub_part'] = $row['Sub_part'];
  1869. }
  1870. return [
  1871. $primary,
  1872. $pkArray,
  1873. $indexesInfo,
  1874. $indexesData,
  1875. ];
  1876. }
  1877. /**
  1878. * Gets the list of tables in the current db and information about these
  1879. * tables if possible
  1880. *
  1881. * @param string $db database name
  1882. * @param string $subPart part of script name
  1883. *
  1884. * @return array
  1885. */
  1886. public static function getDbInfo($db, string $subPart)
  1887. {
  1888. global $cfg, $dbi;
  1889. /**
  1890. * limits for table list
  1891. */
  1892. if (! isset($_SESSION['tmpval']['table_limit_offset']) || $_SESSION['tmpval']['table_limit_offset_db'] != $db) {
  1893. $_SESSION['tmpval']['table_limit_offset'] = 0;
  1894. $_SESSION['tmpval']['table_limit_offset_db'] = $db;
  1895. }
  1896. if (isset($_REQUEST['pos'])) {
  1897. $_SESSION['tmpval']['table_limit_offset'] = (int) $_REQUEST['pos'];
  1898. }
  1899. $pos = $_SESSION['tmpval']['table_limit_offset'];
  1900. /**
  1901. * whether to display extended stats
  1902. */
  1903. $isShowStats = $cfg['ShowStats'];
  1904. /**
  1905. * whether selected db is information_schema
  1906. */
  1907. $isSystemSchema = false;
  1908. if (Utilities::isSystemSchema($db)) {
  1909. $isShowStats = false;
  1910. $isSystemSchema = true;
  1911. }
  1912. /**
  1913. * information about tables in db
  1914. */
  1915. $tables = [];
  1916. $tooltipTrueName = [];
  1917. $tooltipAliasName = [];
  1918. // Special speedup for newer MySQL Versions (in 4.0 format changed)
  1919. if ($cfg['SkipLockedTables'] === true) {
  1920. $dbInfoResult = $dbi->query(
  1921. 'SHOW OPEN TABLES FROM ' . self::backquote($db) . ' WHERE In_use > 0;'
  1922. );
  1923. // Blending out tables in use
  1924. if ($dbInfoResult->numRows() > 0) {
  1925. $tables = self::getTablesWhenOpen($db, $dbInfoResult);
  1926. }
  1927. }
  1928. if (empty($tables)) {
  1929. // Set some sorting defaults
  1930. $sort = 'Name';
  1931. $sortOrder = 'ASC';
  1932. if (isset($_REQUEST['sort'])) {
  1933. $sortableNameMappings = [
  1934. 'table' => 'Name',
  1935. 'records' => 'Rows',
  1936. 'type' => 'Engine',
  1937. 'collation' => 'Collation',
  1938. 'size' => 'Data_length',
  1939. 'overhead' => 'Data_free',
  1940. 'creation' => 'Create_time',
  1941. 'last_update' => 'Update_time',
  1942. 'last_check' => 'Check_time',
  1943. 'comment' => 'Comment',
  1944. ];
  1945. // Make sure the sort type is implemented
  1946. if (isset($sortableNameMappings[$_REQUEST['sort']])) {
  1947. $sort = $sortableNameMappings[$_REQUEST['sort']];
  1948. if ($_REQUEST['sort_order'] === 'DESC') {
  1949. $sortOrder = 'DESC';
  1950. }
  1951. }
  1952. }
  1953. $groupWithSeparator = false;
  1954. $tableType = null;
  1955. $limitOffset = 0;
  1956. $limitCount = false;
  1957. $groupTable = [];
  1958. if (! empty($_REQUEST['tbl_group']) || ! empty($_REQUEST['tbl_type'])) {
  1959. if (! empty($_REQUEST['tbl_type'])) {
  1960. // only tables for selected type
  1961. $tableType = $_REQUEST['tbl_type'];
  1962. }
  1963. if (! empty($_REQUEST['tbl_group'])) {
  1964. // only tables for selected group
  1965. $tableGroup = $_REQUEST['tbl_group'];
  1966. // include the table with the exact name of the group if such
  1967. // exists
  1968. $groupTable = $dbi->getTablesFull(
  1969. $db,
  1970. $tableGroup,
  1971. false,
  1972. $limitOffset,
  1973. $limitCount,
  1974. $sort,
  1975. $sortOrder,
  1976. $tableType
  1977. );
  1978. $groupWithSeparator = $tableGroup
  1979. . $GLOBALS['cfg']['NavigationTreeTableSeparator'];
  1980. }
  1981. } else {
  1982. // all tables in db
  1983. // - get the total number of tables
  1984. // (needed for proper working of the MaxTableList feature)
  1985. $tables = $dbi->getTables($db);
  1986. $totalNumTables = count($tables);
  1987. if ($subPart !== '_export') {
  1988. // fetch the details for a possible limited subset
  1989. $limitOffset = $pos;
  1990. $limitCount = true;
  1991. }
  1992. }
  1993. $tables = $groupTable + $dbi->getTablesFull(
  1994. $db,
  1995. $groupWithSeparator !== false ? $groupWithSeparator : $tables,
  1996. $groupWithSeparator !== false,
  1997. $limitOffset,
  1998. $limitCount,
  1999. $sort,
  2000. $sortOrder,
  2001. $tableType
  2002. );
  2003. }
  2004. $numTables = count($tables);
  2005. // (needed for proper working of the MaxTableList feature)
  2006. if (! isset($totalNumTables)) {
  2007. $totalNumTables = $numTables;
  2008. }
  2009. /**
  2010. * If coming from a Show MySQL link on the home page,
  2011. * put something in $sub_part
  2012. */
  2013. if ($subPart === '') {
  2014. $subPart = '_structure';
  2015. }
  2016. return [
  2017. $tables,
  2018. $numTables,
  2019. $totalNumTables,
  2020. $subPart,
  2021. $isShowStats,
  2022. $isSystemSchema,
  2023. $tooltipTrueName,
  2024. $tooltipAliasName,
  2025. $pos,
  2026. ];
  2027. }
  2028. /**
  2029. * Gets the list of tables in the current db, taking into account
  2030. * that they might be "in use"
  2031. *
  2032. * @param string $db database name
  2033. * @param ResultInterface $dbInfoResult result set
  2034. *
  2035. * @return array list of tables
  2036. */
  2037. public static function getTablesWhenOpen($db, ResultInterface $dbInfoResult): array
  2038. {
  2039. global $dbi;
  2040. $sotCache = [];
  2041. $tables = [];
  2042. foreach ($dbInfoResult as $tmp) {
  2043. $sotCache[$tmp['Table']] = true;
  2044. }
  2045. // is there at least one "in use" table?
  2046. if (count($sotCache) > 0) {
  2047. $tblGroupSql = '';
  2048. $whereAdded = false;
  2049. if (
  2050. isset($_REQUEST['tbl_group'])
  2051. && is_scalar($_REQUEST['tbl_group'])
  2052. && strlen((string) $_REQUEST['tbl_group']) > 0
  2053. ) {
  2054. $group = $dbi->escapeMysqlLikeString((string) $_REQUEST['tbl_group']);
  2055. $groupWithSeparator = $dbi->escapeMysqlLikeString(
  2056. $_REQUEST['tbl_group']
  2057. . $GLOBALS['cfg']['NavigationTreeTableSeparator']
  2058. );
  2059. $tblGroupSql .= ' WHERE ('
  2060. . self::backquote('Tables_in_' . $db)
  2061. . " LIKE '" . $groupWithSeparator . "%'"
  2062. . ' OR '
  2063. . self::backquote('Tables_in_' . $db)
  2064. . " LIKE '" . $group . "')";
  2065. $whereAdded = true;
  2066. }
  2067. if (isset($_REQUEST['tbl_type']) && in_array($_REQUEST['tbl_type'], ['table', 'view'])) {
  2068. $tblGroupSql .= $whereAdded ? ' AND' : ' WHERE';
  2069. if ($_REQUEST['tbl_type'] === 'view') {
  2070. $tblGroupSql .= " `Table_type` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')";
  2071. } else {
  2072. $tblGroupSql .= " `Table_type` IN ('BASE TABLE', 'SYSTEM VERSIONED')";
  2073. }
  2074. }
  2075. $dbInfoResult = $dbi->query('SHOW FULL TABLES FROM ' . self::backquote($db) . $tblGroupSql);
  2076. unset($tblGroupSql, $whereAdded);
  2077. if ($dbInfoResult->numRows() > 0) {
  2078. $names = [];
  2079. while ($tmp = $dbInfoResult->fetchRow()) {
  2080. if (! isset($sotCache[$tmp[0]])) {
  2081. $names[] = $tmp[0];
  2082. } else { // table in use
  2083. $tables[$tmp[0]] = [
  2084. 'TABLE_NAME' => $tmp[0],
  2085. 'ENGINE' => '',
  2086. 'TABLE_TYPE' => '',
  2087. 'TABLE_ROWS' => 0,
  2088. 'TABLE_COMMENT' => '',
  2089. ];
  2090. }
  2091. }
  2092. if (count($names) > 0) {
  2093. $tables += $dbi->getTablesFull($db, $names);
  2094. }
  2095. if ($GLOBALS['cfg']['NaturalOrder']) {
  2096. uksort($tables, 'strnatcasecmp');
  2097. }
  2098. }
  2099. unset($sotCache);
  2100. }
  2101. return $tables;
  2102. }
  2103. /**
  2104. * Checks whether database extension is loaded
  2105. *
  2106. * @param string $extension mysql extension to check
  2107. */
  2108. public static function checkDbExtension(string $extension = 'mysqli'): bool
  2109. {
  2110. return function_exists($extension . '_connect');
  2111. }
  2112. /**
  2113. * Returns list of used PHP extensions.
  2114. *
  2115. * @return string[]
  2116. */
  2117. public static function listPHPExtensions(): array
  2118. {
  2119. $result = [];
  2120. if (self::checkDbExtension('mysqli')) {
  2121. $result[] = 'mysqli';
  2122. }
  2123. if (extension_loaded('curl')) {
  2124. $result[] = 'curl';
  2125. }
  2126. if (extension_loaded('mbstring')) {
  2127. $result[] = 'mbstring';
  2128. }
  2129. if (extension_loaded('sodium')) {
  2130. $result[] = 'sodium';
  2131. }
  2132. return $result;
  2133. }
  2134. /**
  2135. * Converts given (request) parameter to string
  2136. *
  2137. * @param mixed $value Value to convert
  2138. */
  2139. public static function requestString($value): string
  2140. {
  2141. while (is_array($value) || is_object($value)) {
  2142. if (is_object($value)) {
  2143. $value = (array) $value;
  2144. }
  2145. $value = reset($value);
  2146. }
  2147. return trim((string) $value);
  2148. }
  2149. /**
  2150. * Generates random string consisting of ASCII chars
  2151. *
  2152. * @param int $length Length of string
  2153. * @param bool $asHex (optional) Send the result as hex
  2154. */
  2155. public static function generateRandom(int $length, bool $asHex = false): string
  2156. {
  2157. $result = '';
  2158. while (strlen($result) < $length) {
  2159. // Get random byte and strip highest bit
  2160. // to get ASCII only range
  2161. $byte = ord(random_bytes(1)) & 0x7f;
  2162. // We want only ASCII chars and no DEL character (127)
  2163. if ($byte <= 32 || $byte === 127) {
  2164. continue;
  2165. }
  2166. $result .= chr($byte);
  2167. }
  2168. return $asHex ? bin2hex($result) : $result;
  2169. }
  2170. /**
  2171. * Wrapper around PHP date function
  2172. *
  2173. * @param string $format Date format string
  2174. *
  2175. * @return string
  2176. */
  2177. public static function date($format)
  2178. {
  2179. return date($format);
  2180. }
  2181. /**
  2182. * Wrapper around php's set_time_limit
  2183. */
  2184. public static function setTimeLimit(): void
  2185. {
  2186. // The function can be disabled in php.ini
  2187. if (! function_exists('set_time_limit')) {
  2188. return;
  2189. }
  2190. @set_time_limit((int) $GLOBALS['cfg']['ExecTimeLimit']);
  2191. }
  2192. /**
  2193. * Access to a multidimensional array by dot notation
  2194. *
  2195. * @param array $array List of values
  2196. * @param string|array $path Path to searched value
  2197. * @param mixed $default Default value
  2198. *
  2199. * @return mixed Searched value
  2200. */
  2201. public static function getValueByKey(array $array, $path, $default = null)
  2202. {
  2203. if (is_string($path)) {
  2204. $path = explode('.', $path);
  2205. }
  2206. $p = array_shift($path);
  2207. while (isset($p)) {
  2208. if (! isset($array[$p])) {
  2209. return $default;
  2210. }
  2211. $array = $array[$p];
  2212. $p = array_shift($path);
  2213. }
  2214. return $array;
  2215. }
  2216. /**
  2217. * Creates a clickable column header for table information
  2218. *
  2219. * @param string $title Title to use for the link
  2220. * @param string $sort Corresponds to sortable data name mapped
  2221. * in Util::getDbInfo
  2222. * @param string $initialSortOrder Initial sort order
  2223. *
  2224. * @return string Link to be displayed in the table header
  2225. */
  2226. public static function sortableTableHeader($title, $sort, $initialSortOrder = 'ASC')
  2227. {
  2228. $requestedSort = 'table';
  2229. $requestedSortOrder = $futureSortOrder = $initialSortOrder;
  2230. // If the user requested a sort
  2231. if (isset($_REQUEST['sort'])) {
  2232. $requestedSort = $_REQUEST['sort'];
  2233. if (isset($_REQUEST['sort_order'])) {
  2234. $requestedSortOrder = $_REQUEST['sort_order'];
  2235. }
  2236. }
  2237. $orderImg = '';
  2238. $orderLinkParams = [];
  2239. $orderLinkParams['title'] = __('Sort');
  2240. // If this column was requested to be sorted.
  2241. if ($requestedSort == $sort) {
  2242. if ($requestedSortOrder === 'ASC') {
  2243. $futureSortOrder = 'DESC';
  2244. // current sort order is ASC
  2245. $orderImg = ' ' . Generator::getImage(
  2246. 's_asc',
  2247. __('Ascending'),
  2248. [
  2249. 'class' => 'sort_arrow',
  2250. 'title' => '',
  2251. ]
  2252. );
  2253. $orderImg .= ' ' . Generator::getImage(
  2254. 's_desc',
  2255. __('Descending'),
  2256. [
  2257. 'class' => 'sort_arrow hide',
  2258. 'title' => '',
  2259. ]
  2260. );
  2261. // but on mouse over, show the reverse order (DESC)
  2262. $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
  2263. // on mouse out, show current sort order (ASC)
  2264. $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
  2265. } else {
  2266. $futureSortOrder = 'ASC';
  2267. // current sort order is DESC
  2268. $orderImg = ' ' . Generator::getImage(
  2269. 's_asc',
  2270. __('Ascending'),
  2271. [
  2272. 'class' => 'sort_arrow hide',
  2273. 'title' => '',
  2274. ]
  2275. );
  2276. $orderImg .= ' ' . Generator::getImage(
  2277. 's_desc',
  2278. __('Descending'),
  2279. [
  2280. 'class' => 'sort_arrow',
  2281. 'title' => '',
  2282. ]
  2283. );
  2284. // but on mouse over, show the reverse order (ASC)
  2285. $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
  2286. // on mouse out, show current sort order (DESC)
  2287. $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
  2288. }
  2289. }
  2290. $urlParams = [
  2291. 'db' => $_REQUEST['db'],
  2292. 'pos' => 0, // We set the position back to 0 every time they sort.
  2293. 'sort' => $sort,
  2294. 'sort_order' => $futureSortOrder,
  2295. ];
  2296. if (isset($_REQUEST['tbl_type']) && in_array($_REQUEST['tbl_type'], ['view', 'table'])) {
  2297. $urlParams['tbl_type'] = $_REQUEST['tbl_type'];
  2298. }
  2299. if (! empty($_REQUEST['tbl_group'])) {
  2300. $urlParams['tbl_group'] = $_REQUEST['tbl_group'];
  2301. }
  2302. $url = Url::getFromRoute('/database/structure', $urlParams, false);
  2303. return Generator::linkOrButton($url, null, $title . $orderImg, $orderLinkParams);
  2304. }
  2305. /**
  2306. * Check that input is an int or an int in a string
  2307. *
  2308. * @param mixed $input input to check
  2309. */
  2310. public static function isInteger($input): bool
  2311. {
  2312. return ctype_digit((string) $input);
  2313. }
  2314. /**
  2315. * Get the protocol from the RFC 7239 Forwarded header
  2316. *
  2317. * @param string $headerContents The Forwarded header contents
  2318. *
  2319. * @return string the protocol http/https
  2320. */
  2321. public static function getProtoFromForwardedHeader(string $headerContents): string
  2322. {
  2323. if (str_contains($headerContents, '=')) {// does not contain any equal sign
  2324. $hops = explode(',', $headerContents);
  2325. $parts = explode(';', $hops[0]);
  2326. foreach ($parts as $part) {
  2327. $keyValueArray = explode('=', $part, 2);
  2328. if (count($keyValueArray) !== 2) {
  2329. continue;
  2330. }
  2331. [
  2332. $keyName,
  2333. $value,
  2334. ] = $keyValueArray;
  2335. $value = trim(strtolower($value));
  2336. if (strtolower(trim($keyName)) === 'proto' && in_array($value, ['http', 'https'])) {
  2337. return $value;
  2338. }
  2339. }
  2340. }
  2341. return '';
  2342. }
  2343. /**
  2344. * Check if error reporting is available
  2345. */
  2346. public static function isErrorReportingAvailable(): bool
  2347. {
  2348. // issue #16256 - PHP 7.x does not return false for a core function
  2349. if (PHP_MAJOR_VERSION < 8) {
  2350. $disabled = ini_get('disable_functions');
  2351. if (is_string($disabled)) {
  2352. $disabled = explode(',', $disabled);
  2353. $disabled = array_map(static function (string $part) {
  2354. return trim($part);
  2355. }, $disabled);
  2356. return ! in_array('error_reporting', $disabled);
  2357. }
  2358. }
  2359. return function_exists('error_reporting');
  2360. }
  2361. }