Import.php 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\SqlParser\Parser;
  5. use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
  6. use PhpMyAdmin\SqlParser\Statements\InsertStatement;
  7. use PhpMyAdmin\SqlParser\Statements\ReplaceStatement;
  8. use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
  9. use PhpMyAdmin\SqlParser\Utils\Query;
  10. use function abs;
  11. use function count;
  12. use function explode;
  13. use function function_exists;
  14. use function htmlspecialchars;
  15. use function implode;
  16. use function is_array;
  17. use function is_numeric;
  18. use function max;
  19. use function mb_chr;
  20. use function mb_ord;
  21. use function mb_stripos;
  22. use function mb_strlen;
  23. use function mb_strpos;
  24. use function mb_strtoupper;
  25. use function mb_substr;
  26. use function mb_substr_count;
  27. use function pow;
  28. use function preg_match;
  29. use function preg_replace;
  30. use function sprintf;
  31. use function strcmp;
  32. use function strlen;
  33. use function strncmp;
  34. use function strpos;
  35. use function strtoupper;
  36. use function substr;
  37. use function time;
  38. use function trim;
  39. /**
  40. * Library that provides common import functions that are used by import plugins
  41. */
  42. class Import
  43. {
  44. /* MySQL type defs */
  45. public const NONE = 0;
  46. public const VARCHAR = 1;
  47. public const INT = 2;
  48. public const DECIMAL = 3;
  49. public const BIGINT = 4;
  50. public const GEOMETRY = 5;
  51. /* Decimal size defs */
  52. public const M = 0;
  53. public const D = 1;
  54. public const FULL = 2;
  55. /* Table array defs */
  56. public const TBL_NAME = 0;
  57. public const COL_NAMES = 1;
  58. public const ROWS = 2;
  59. /* Analysis array defs */
  60. public const TYPES = 0;
  61. public const SIZES = 1;
  62. public const FORMATTEDSQL = 2;
  63. public function __construct()
  64. {
  65. global $dbi;
  66. $GLOBALS['cfg']['Server']['DisableIS'] = false;
  67. $checkUserPrivileges = new CheckUserPrivileges($dbi);
  68. $checkUserPrivileges->getPrivileges();
  69. }
  70. /**
  71. * Checks whether timeout is getting close
  72. *
  73. * @return bool true if timeout is close
  74. *
  75. * @access public
  76. */
  77. public function checkTimeout(): bool
  78. {
  79. global $timestamp, $maximum_time, $timeout_passed;
  80. if ($maximum_time == 0) {
  81. return false;
  82. }
  83. if ($timeout_passed) {
  84. return true;
  85. /* 5 in next row might be too much */
  86. }
  87. if (time() - $timestamp > $maximum_time - 5) {
  88. $timeout_passed = true;
  89. return true;
  90. }
  91. return false;
  92. }
  93. /**
  94. * Runs query inside import buffer. This is needed to allow displaying
  95. * of last SELECT, SHOW or HANDLER results and similar nice stuff.
  96. *
  97. * @param string $sql query to run
  98. * @param string $full query to display, this might be commented
  99. * @param array $sql_data SQL parse data storage
  100. *
  101. * @access public
  102. */
  103. public function executeQuery(string $sql, string $full, array &$sql_data): void
  104. {
  105. global $sql_query, $my_die, $error, $reload, $result, $msg, $cfg, $sql_query_disabled, $db, $dbi;
  106. $result = $dbi->tryQuery($sql);
  107. // USE query changes the database, son need to track
  108. // while running multiple queries
  109. $is_use_query = mb_stripos($sql, 'use ') !== false;
  110. $msg = '# ';
  111. if ($result === false) { // execution failed
  112. if (! isset($my_die)) {
  113. $my_die = [];
  114. }
  115. $my_die[] = [
  116. 'sql' => $full,
  117. 'error' => $dbi->getError(),
  118. ];
  119. $msg .= __('Error');
  120. if (! $cfg['IgnoreMultiSubmitErrors']) {
  121. $error = true;
  122. return;
  123. }
  124. } else {
  125. $a_num_rows = (int) @$dbi->numRows($result);
  126. $a_aff_rows = (int) @$dbi->affectedRows();
  127. if ($a_num_rows > 0) {
  128. $msg .= __('Rows') . ': ' . $a_num_rows;
  129. } elseif ($a_aff_rows > 0) {
  130. $message = Message::getMessageForAffectedRows(
  131. $a_aff_rows
  132. );
  133. $msg .= $message->getMessage();
  134. } else {
  135. $msg .= __(
  136. 'MySQL returned an empty result set (i.e. zero '
  137. . 'rows).'
  138. );
  139. }
  140. if (($a_num_rows > 0) || $is_use_query) {
  141. $sql_data['valid_sql'][] = $sql;
  142. if (! isset($sql_data['valid_queries'])) {
  143. $sql_data['valid_queries'] = 0;
  144. }
  145. $sql_data['valid_queries']++;
  146. }
  147. }
  148. if (! $sql_query_disabled) {
  149. $sql_query .= $msg . "\n";
  150. }
  151. // If a 'USE <db>' SQL-clause was found and the query
  152. // succeeded, set our current $db to the new one
  153. if ($result != false) {
  154. [$db, $reload] = $this->lookForUse(
  155. $sql,
  156. $db,
  157. $reload
  158. );
  159. }
  160. $pattern = '@^[\s]*(DROP|CREATE)[\s]+(IF EXISTS[[:space:]]+)'
  161. . '?(TABLE|DATABASE)[[:space:]]+(.+)@im';
  162. if ($result == false
  163. || ! preg_match($pattern, $sql)
  164. ) {
  165. return;
  166. }
  167. $reload = true;
  168. }
  169. /**
  170. * Runs query inside import buffer. This is needed to allow displaying
  171. * of last SELECT, SHOW or HANDLER results and similar nice stuff.
  172. *
  173. * @param string $sql query to run
  174. * @param string $full query to display, this might be commented
  175. * @param array $sql_data SQL parse data storage
  176. *
  177. * @access public
  178. */
  179. public function runQuery(
  180. string $sql = '',
  181. string $full = '',
  182. array &$sql_data = []
  183. ): void {
  184. global $import_run_buffer, $go_sql, $complete_query, $display_query, $sql_query, $msg,
  185. $skip_queries, $executed_queries, $max_sql_len, $read_multiply, $sql_query_disabled, $run_query;
  186. $read_multiply = 1;
  187. if (! isset($import_run_buffer)) {
  188. // Do we have something to push into buffer?
  189. $import_run_buffer = $this->runQueryPost(
  190. $import_run_buffer,
  191. $sql,
  192. $full
  193. );
  194. return;
  195. }
  196. // Should we skip something?
  197. if ($skip_queries > 0) {
  198. $skip_queries--;
  199. // Do we have something to push into buffer?
  200. $import_run_buffer = $this->runQueryPost(
  201. $import_run_buffer,
  202. $sql,
  203. $full
  204. );
  205. return;
  206. }
  207. if (! empty($import_run_buffer['sql'])
  208. && trim($import_run_buffer['sql']) != ''
  209. ) {
  210. $max_sql_len = max(
  211. $max_sql_len,
  212. mb_strlen($import_run_buffer['sql'])
  213. );
  214. if (! $sql_query_disabled) {
  215. $sql_query .= $import_run_buffer['full'];
  216. }
  217. $executed_queries++;
  218. if ($run_query && $executed_queries < 50) {
  219. $go_sql = true;
  220. if (! $sql_query_disabled) {
  221. $complete_query = $sql_query;
  222. $display_query = $sql_query;
  223. } else {
  224. $complete_query = '';
  225. $display_query = '';
  226. }
  227. $sql_query = $import_run_buffer['sql'];
  228. $sql_data['valid_sql'][] = $import_run_buffer['sql'];
  229. $sql_data['valid_full'][] = $import_run_buffer['full'];
  230. if (! isset($sql_data['valid_queries'])) {
  231. $sql_data['valid_queries'] = 0;
  232. }
  233. $sql_data['valid_queries']++;
  234. } elseif ($run_query) {
  235. /* Handle rollback from go_sql */
  236. if ($go_sql && isset($sql_data['valid_full'])) {
  237. $queries = $sql_data['valid_sql'];
  238. $fulls = $sql_data['valid_full'];
  239. $count = $sql_data['valid_queries'];
  240. $go_sql = false;
  241. $sql_data['valid_sql'] = [];
  242. $sql_data['valid_queries'] = 0;
  243. unset($sql_data['valid_full']);
  244. for ($i = 0; $i < $count; $i++) {
  245. $this->executeQuery(
  246. $queries[$i],
  247. $fulls[$i],
  248. $sql_data
  249. );
  250. }
  251. }
  252. $this->executeQuery(
  253. $import_run_buffer['sql'],
  254. $import_run_buffer['full'],
  255. $sql_data
  256. );
  257. }
  258. } elseif (! empty($import_run_buffer['full'])) {
  259. if ($go_sql) {
  260. $complete_query .= $import_run_buffer['full'];
  261. $display_query .= $import_run_buffer['full'];
  262. } elseif (! $sql_query_disabled) {
  263. $sql_query .= $import_run_buffer['full'];
  264. }
  265. }
  266. // check length of query unless we decided to pass it to /sql
  267. // (if $run_query is false, we are just displaying so show
  268. // the complete query in the textarea)
  269. if (! $go_sql && $run_query && ! empty($sql_query)) {
  270. if (mb_strlen($sql_query) > 50000
  271. || $executed_queries > 50
  272. || $max_sql_len > 1000
  273. ) {
  274. $sql_query = '';
  275. $sql_query_disabled = true;
  276. }
  277. }
  278. // Do we have something to push into buffer?
  279. $import_run_buffer = $this->runQueryPost($import_run_buffer, $sql, $full);
  280. // In case of ROLLBACK, notify the user.
  281. if (! isset($_POST['rollback_query'])) {
  282. return;
  283. }
  284. $msg .= __('[ROLLBACK occurred.]');
  285. }
  286. /**
  287. * Return import run buffer
  288. *
  289. * @param array $import_run_buffer Buffer of queries for import
  290. * @param string $sql SQL query
  291. * @param string $full Query to display
  292. *
  293. * @return array Buffer of queries for import
  294. */
  295. public function runQueryPost(
  296. ?array $import_run_buffer,
  297. string $sql,
  298. string $full
  299. ): ?array {
  300. if (! empty($sql) || ! empty($full)) {
  301. return [
  302. 'sql' => $sql . ';',
  303. 'full' => $full . ';',
  304. ];
  305. }
  306. unset($GLOBALS['import_run_buffer']);
  307. return $import_run_buffer;
  308. }
  309. /**
  310. * Looks for the presence of USE to possibly change current db
  311. *
  312. * @param string $buffer buffer to examine
  313. * @param string $db current db
  314. * @param bool $reload reload
  315. *
  316. * @return array (current or new db, whether to reload)
  317. *
  318. * @access public
  319. */
  320. public function lookForUse(?string $buffer, ?string $db, ?bool $reload): array
  321. {
  322. if (preg_match('@^[\s]*USE[[:space:]]+([\S]+)@i', (string) $buffer, $match)) {
  323. $db = trim($match[1]);
  324. $db = trim($db, ';'); // for example, USE abc;
  325. // $db must not contain the escape characters generated by backquote()
  326. // ( used in buildSql() as: backquote($db_name), and then called
  327. // in runQuery() which in turn calls lookForUse() )
  328. $db = Util::unQuote($db);
  329. $reload = true;
  330. }
  331. return [
  332. $db,
  333. $reload,
  334. ];
  335. }
  336. /**
  337. * Returns next part of imported file/buffer
  338. *
  339. * @param int $size size of buffer to read (this is maximal size function will return)
  340. *
  341. * @return string|bool part of file/buffer
  342. */
  343. public function getNextChunk(?File $importHandle = null, int $size = 32768)
  344. {
  345. global $charset_conversion, $charset_of_file, $read_multiply;
  346. // Add some progression while reading large amount of data
  347. if ($read_multiply <= 8) {
  348. $size *= $read_multiply;
  349. } else {
  350. $size *= 8;
  351. }
  352. $read_multiply++;
  353. // We can not read too much
  354. if ($size > $GLOBALS['read_limit']) {
  355. $size = $GLOBALS['read_limit'];
  356. }
  357. if ($this->checkTimeout()) {
  358. return false;
  359. }
  360. if ($GLOBALS['finished']) {
  361. return true;
  362. }
  363. if ($GLOBALS['import_file'] === 'none') {
  364. // Well this is not yet supported and tested,
  365. // but should return content of textarea
  366. if (mb_strlen($GLOBALS['import_text']) < $size) {
  367. $GLOBALS['finished'] = true;
  368. return $GLOBALS['import_text'];
  369. }
  370. $r = mb_substr($GLOBALS['import_text'], 0, $size);
  371. $GLOBALS['offset'] += $size;
  372. $GLOBALS['import_text'] = mb_substr($GLOBALS['import_text'], $size);
  373. return $r;
  374. }
  375. if ($importHandle === null) {
  376. return false;
  377. }
  378. $result = $importHandle->read($size);
  379. $GLOBALS['finished'] = $importHandle->eof();
  380. $GLOBALS['offset'] += $size;
  381. if ($charset_conversion) {
  382. return Encoding::convertString($charset_of_file, 'utf-8', $result);
  383. }
  384. /**
  385. * Skip possible byte order marks (I do not think we need more
  386. * charsets, but feel free to add more, you can use wikipedia for
  387. * reference: <https://en.wikipedia.org/wiki/Byte_Order_Mark>)
  388. *
  389. * @todo BOM could be used for charset autodetection
  390. */
  391. if ($GLOBALS['offset'] == $size) {
  392. $result = $this->skipByteOrderMarksFromContents($result);
  393. }
  394. return $result;
  395. }
  396. /**
  397. * Skip possible byte order marks (I do not think we need more
  398. * charsets, but feel free to add more, you can use wikipedia for
  399. * reference: <https://en.wikipedia.org/wiki/Byte_Order_Mark>)
  400. *
  401. * @param string $contents The contents to strip BOM
  402. *
  403. * @todo BOM could be used for charset autodetection
  404. */
  405. public function skipByteOrderMarksFromContents(string $contents): string
  406. {
  407. // Do not use mb_ functions they are sensible to mb_internal_encoding()
  408. // UTF-8
  409. if (strncmp($contents, "\xEF\xBB\xBF", 3) === 0) {
  410. return substr($contents, 3);
  411. // UTF-16 BE, LE
  412. }
  413. if (strncmp($contents, "\xFE\xFF", 2) === 0
  414. || strncmp($contents, "\xFF\xFE", 2) === 0
  415. ) {
  416. return substr($contents, 2);
  417. }
  418. return $contents;
  419. }
  420. /**
  421. * Returns the "Excel" column name (i.e. 1 = "A", 26 = "Z", 27 = "AA", etc.)
  422. *
  423. * This functions uses recursion to build the Excel column name.
  424. *
  425. * The column number (1-26) is converted to the responding
  426. * ASCII character (A-Z) and returned.
  427. *
  428. * If the column number is bigger than 26 (= num of letters in alphabet),
  429. * an extra character needs to be added. To find this extra character,
  430. * the number is divided by 26 and this value is passed to another instance
  431. * of the same function (hence recursion). In that new instance the number is
  432. * evaluated again, and if it is still bigger than 26, it is divided again
  433. * and passed to another instance of the same function. This continues until
  434. * the number is smaller than 26. Then the last called function returns
  435. * the corresponding ASCII character to the function that called it.
  436. * Each time a called function ends an extra character is added to the column name.
  437. * When the first function is reached, the last character is added and the complete
  438. * column name is returned.
  439. *
  440. * @param int $num the column number
  441. *
  442. * @return string The column's "Excel" name
  443. *
  444. * @access public
  445. */
  446. public function getColumnAlphaName(int $num): string
  447. {
  448. $A = 65; // ASCII value for capital "A"
  449. $col_name = '';
  450. if ($num > 26) {
  451. $div = (int) ($num / 26);
  452. $remain = $num % 26;
  453. // subtract 1 of divided value in case the modulus is 0,
  454. // this is necessary because A-Z has no 'zero'
  455. if ($remain == 0) {
  456. $div--;
  457. }
  458. // recursive function call
  459. $col_name = $this->getColumnAlphaName($div);
  460. // use modulus as new column number
  461. $num = $remain;
  462. }
  463. if ($num == 0) {
  464. // use 'Z' if column number is 0,
  465. // this is necessary because A-Z has no 'zero'
  466. $col_name .= mb_chr($A + 26 - 1);
  467. } else {
  468. // convert column number to ASCII character
  469. $col_name .= mb_chr($A + $num - 1);
  470. }
  471. return $col_name;
  472. }
  473. /**
  474. * Returns the column number based on the Excel name.
  475. * So "A" = 1, "Z" = 26, "AA" = 27, etc.
  476. *
  477. * Basically this is a base26 (A-Z) to base10 (0-9) conversion.
  478. * It iterates through all characters in the column name and
  479. * calculates the corresponding value, based on character value
  480. * (A = 1, ..., Z = 26) and position in the string.
  481. *
  482. * @param string $name column name(i.e. "A", or "BC", etc.)
  483. *
  484. * @return int The column number
  485. *
  486. * @access public
  487. */
  488. public function getColumnNumberFromName(string $name): int
  489. {
  490. if (empty($name)) {
  491. return 0;
  492. }
  493. $name = mb_strtoupper($name);
  494. $num_chars = mb_strlen($name);
  495. $column_number = 0;
  496. for ($i = 0; $i < $num_chars; ++$i) {
  497. // read string from back to front
  498. $char_pos = $num_chars - 1 - $i;
  499. // convert capital character to ASCII value
  500. // and subtract 64 to get corresponding decimal value
  501. // ASCII value of "A" is 65, "B" is 66, etc.
  502. // Decimal equivalent of "A" is 1, "B" is 2, etc.
  503. $number = (int) (mb_ord($name[$char_pos]) - 64);
  504. // base26 to base10 conversion : multiply each number
  505. // with corresponding value of the position, in this case
  506. // $i=0 : 1; $i=1 : 26; $i=2 : 676; ...
  507. $column_number += $number * pow(26, $i);
  508. }
  509. return $column_number;
  510. }
  511. /**
  512. * Obtains the precision (total # of digits) from a size of type decimal
  513. *
  514. * @param string $last_cumulative_size Size of type decimal
  515. *
  516. * @return int Precision of the given decimal size notation
  517. *
  518. * @access public
  519. */
  520. public function getDecimalPrecision(string $last_cumulative_size): int
  521. {
  522. return (int) substr(
  523. $last_cumulative_size,
  524. 0,
  525. (int) strpos($last_cumulative_size, ',')
  526. );
  527. }
  528. /**
  529. * Obtains the scale (# of digits to the right of the decimal point)
  530. * from a size of type decimal
  531. *
  532. * @param string $last_cumulative_size Size of type decimal
  533. *
  534. * @return int Scale of the given decimal size notation
  535. *
  536. * @access public
  537. */
  538. public function getDecimalScale(string $last_cumulative_size): int
  539. {
  540. return (int) substr(
  541. $last_cumulative_size,
  542. strpos($last_cumulative_size, ',') + 1,
  543. strlen($last_cumulative_size) - strpos($last_cumulative_size, ',')
  544. );
  545. }
  546. /**
  547. * Obtains the decimal size of a given cell
  548. *
  549. * @param string $cell cell content
  550. *
  551. * @return array Contains the precision, scale, and full size
  552. * representation of the given decimal cell
  553. *
  554. * @access public
  555. */
  556. public function getDecimalSize(string $cell): array
  557. {
  558. $curr_size = mb_strlen($cell);
  559. $decPos = mb_strpos($cell, '.');
  560. $decPrecision = $curr_size - 1 - $decPos;
  561. $m = $curr_size - 1;
  562. $d = $decPrecision;
  563. return [
  564. $m,
  565. $d,
  566. $m . ',' . $d,
  567. ];
  568. }
  569. /**
  570. * Obtains the size of the given cell
  571. *
  572. * @param string|int $last_cumulative_size Last cumulative column size
  573. * @param int|null $last_cumulative_type Last cumulative column type
  574. * (NONE or VARCHAR or DECIMAL or INT or BIGINT)
  575. * @param int $curr_type Type of the current cell
  576. * (NONE or VARCHAR or DECIMAL or INT or BIGINT)
  577. * @param string $cell The current cell
  578. *
  579. * @return string|int Size of the given cell in the type-appropriate format
  580. *
  581. * @access public
  582. * @todo Handle the error cases more elegantly
  583. */
  584. public function detectSize(
  585. $last_cumulative_size,
  586. ?int $last_cumulative_type,
  587. int $curr_type,
  588. string $cell
  589. ) {
  590. $curr_size = mb_strlen($cell);
  591. /**
  592. * If the cell is NULL, don't treat it as a varchar
  593. */
  594. if (! strcmp('NULL', $cell)) {
  595. return $last_cumulative_size;
  596. }
  597. if ($curr_type == self::VARCHAR) {
  598. /**
  599. * What to do if the current cell is of type VARCHAR
  600. */
  601. /**
  602. * The last cumulative type was VARCHAR
  603. */
  604. if ($last_cumulative_type == self::VARCHAR) {
  605. if ($curr_size >= $last_cumulative_size) {
  606. return $curr_size;
  607. }
  608. return $last_cumulative_size;
  609. }
  610. if ($last_cumulative_type == self::DECIMAL) {
  611. /**
  612. * The last cumulative type was DECIMAL
  613. */
  614. $oldM = $this->getDecimalPrecision($last_cumulative_size);
  615. if ($curr_size >= $oldM) {
  616. return $curr_size;
  617. }
  618. return $oldM;
  619. }
  620. if ($last_cumulative_type == self::BIGINT || $last_cumulative_type == self::INT) {
  621. /**
  622. * The last cumulative type was BIGINT or INT
  623. */
  624. if ($curr_size >= $last_cumulative_size) {
  625. return $curr_size;
  626. }
  627. return $last_cumulative_size;
  628. }
  629. if (! isset($last_cumulative_type) || $last_cumulative_type == self::NONE) {
  630. /**
  631. * This is the first row to be analyzed
  632. */
  633. return $curr_size;
  634. }
  635. /**
  636. * An error has DEFINITELY occurred
  637. */
  638. /**
  639. * TODO: Handle this MUCH more elegantly
  640. */
  641. return -1;
  642. }
  643. if ($curr_type == self::DECIMAL) {
  644. /**
  645. * What to do if the current cell is of type DECIMAL
  646. */
  647. /**
  648. * The last cumulative type was VARCHAR
  649. */
  650. if ($last_cumulative_type == self::VARCHAR) {
  651. /* Convert $last_cumulative_size from varchar to decimal format */
  652. $size = $this->getDecimalSize($cell);
  653. if ($size[self::M] >= $last_cumulative_size) {
  654. return $size[self::M];
  655. }
  656. return $last_cumulative_size;
  657. }
  658. if ($last_cumulative_type == self::DECIMAL) {
  659. /**
  660. * The last cumulative type was DECIMAL
  661. */
  662. $size = $this->getDecimalSize($cell);
  663. $oldM = $this->getDecimalPrecision($last_cumulative_size);
  664. $oldD = $this->getDecimalScale($last_cumulative_size);
  665. /* New val if M or D is greater than current largest */
  666. if ($size[self::M] > $oldM || $size[self::D] > $oldD) {
  667. /* Take the largest of both types */
  668. return (string) (($size[self::M] > $oldM ? $size[self::M] : $oldM)
  669. . ',' . ($size[self::D] > $oldD ? $size[self::D] : $oldD));
  670. }
  671. return $last_cumulative_size;
  672. }
  673. if ($last_cumulative_type == self::BIGINT || $last_cumulative_type == self::INT) {
  674. /**
  675. * The last cumulative type was BIGINT or INT
  676. */
  677. /* Convert $last_cumulative_size from int to decimal format */
  678. $size = $this->getDecimalSize($cell);
  679. if ($size[self::M] >= $last_cumulative_size) {
  680. return $size[self::FULL];
  681. }
  682. return $last_cumulative_size . ',' . $size[self::D];
  683. }
  684. if (! isset($last_cumulative_type) || $last_cumulative_type == self::NONE) {
  685. /**
  686. * This is the first row to be analyzed
  687. */
  688. /* First row of the column */
  689. $size = $this->getDecimalSize($cell);
  690. return $size[self::FULL];
  691. }
  692. /**
  693. * An error has DEFINITELY occurred
  694. */
  695. /**
  696. * TODO: Handle this MUCH more elegantly
  697. */
  698. return -1;
  699. }
  700. if ($curr_type == self::BIGINT || $curr_type == self::INT) {
  701. /**
  702. * What to do if the current cell is of type BIGINT or INT
  703. */
  704. /**
  705. * The last cumulative type was VARCHAR
  706. */
  707. if ($last_cumulative_type == self::VARCHAR) {
  708. if ($curr_size >= $last_cumulative_size) {
  709. return $curr_size;
  710. }
  711. return $last_cumulative_size;
  712. }
  713. if ($last_cumulative_type == self::DECIMAL) {
  714. /**
  715. * The last cumulative type was DECIMAL
  716. */
  717. $oldM = $this->getDecimalPrecision($last_cumulative_size);
  718. $oldD = $this->getDecimalScale($last_cumulative_size);
  719. $oldInt = $oldM - $oldD;
  720. $newInt = mb_strlen((string) $cell);
  721. /* See which has the larger integer length */
  722. if ($oldInt >= $newInt) {
  723. /* Use old decimal size */
  724. return $last_cumulative_size;
  725. }
  726. /* Use $newInt + $oldD as new M */
  727. return ($newInt + $oldD) . ',' . $oldD;
  728. }
  729. if ($last_cumulative_type == self::BIGINT || $last_cumulative_type == self::INT) {
  730. /**
  731. * The last cumulative type was BIGINT or INT
  732. */
  733. if ($curr_size >= $last_cumulative_size) {
  734. return $curr_size;
  735. }
  736. return $last_cumulative_size;
  737. }
  738. if (! isset($last_cumulative_type) || $last_cumulative_type == self::NONE) {
  739. /**
  740. * This is the first row to be analyzed
  741. */
  742. return $curr_size;
  743. }
  744. /**
  745. * An error has DEFINITELY occurred
  746. */
  747. /**
  748. * TODO: Handle this MUCH more elegantly
  749. */
  750. return -1;
  751. }
  752. /**
  753. * An error has DEFINITELY occurred
  754. */
  755. /**
  756. * TODO: Handle this MUCH more elegantly
  757. */
  758. return -1;
  759. }
  760. /**
  761. * Determines what MySQL type a cell is
  762. *
  763. * @param int $last_cumulative_type Last cumulative column type
  764. * (VARCHAR or INT or BIGINT or DECIMAL or NONE)
  765. * @param string|null $cell String representation of the cell for which
  766. * a best-fit type is to be determined
  767. *
  768. * @return int The MySQL type representation
  769. * (VARCHAR or INT or BIGINT or DECIMAL or NONE)
  770. *
  771. * @access public
  772. */
  773. public function detectType(?int $last_cumulative_type, ?string $cell): int
  774. {
  775. /**
  776. * If numeric, determine if decimal, int or bigint
  777. * Else, we call it varchar for simplicity
  778. */
  779. if (! strcmp('NULL', (string) $cell)) {
  780. if ($last_cumulative_type === null || $last_cumulative_type == self::NONE) {
  781. return self::NONE;
  782. }
  783. return $last_cumulative_type;
  784. }
  785. if (! is_numeric($cell)) {
  786. return self::VARCHAR;
  787. }
  788. if ($cell == (string) (float) $cell
  789. && mb_strpos((string) $cell, '.') !== false
  790. && mb_substr_count((string) $cell, '.') === 1
  791. ) {
  792. return self::DECIMAL;
  793. }
  794. if (abs((int) $cell) > 2147483647) {
  795. return self::BIGINT;
  796. }
  797. if ($cell !== (string) (int) $cell) {
  798. return self::VARCHAR;
  799. }
  800. return self::INT;
  801. }
  802. /**
  803. * Determines if the column types are int, decimal, or string
  804. *
  805. * @link https://wiki.phpmyadmin.net/pma/Import
  806. *
  807. * @param array $table array(string $table_name, array $col_names, array $rows)
  808. *
  809. * @return array|bool array(array $types, array $sizes)
  810. *
  811. * @access public
  812. * @todo Handle the error case more elegantly
  813. */
  814. public function analyzeTable(array &$table)
  815. {
  816. /* Get number of rows in table */
  817. $numRows = count($table[self::ROWS]);
  818. /* Get number of columns */
  819. $numCols = count($table[self::COL_NAMES]);
  820. /* Current type for each column */
  821. $types = [];
  822. $sizes = [];
  823. /* Initialize $sizes to all 0's */
  824. for ($i = 0; $i < $numCols; ++$i) {
  825. $sizes[$i] = 0;
  826. }
  827. /* Initialize $types to NONE */
  828. for ($i = 0; $i < $numCols; ++$i) {
  829. $types[$i] = self::NONE;
  830. }
  831. /* If the passed array is not of the correct form, do not process it */
  832. if (! is_array($table)
  833. || is_array($table[self::TBL_NAME])
  834. || ! is_array($table[self::COL_NAMES])
  835. || ! is_array($table[self::ROWS])
  836. ) {
  837. /**
  838. * TODO: Handle this better
  839. */
  840. return false;
  841. }
  842. /* Analyze each column */
  843. for ($i = 0; $i < $numCols; ++$i) {
  844. /* Analyze the column in each row */
  845. for ($j = 0; $j < $numRows; ++$j) {
  846. $cellValue = $table[self::ROWS][$j][$i];
  847. /* Determine type of the current cell */
  848. $curr_type = $this->detectType($types[$i], $cellValue === null ? null : (string) $cellValue);
  849. /* Determine size of the current cell */
  850. $sizes[$i] = $this->detectSize(
  851. $sizes[$i],
  852. $types[$i],
  853. $curr_type,
  854. (string) $cellValue
  855. );
  856. /**
  857. * If a type for this column has already been declared,
  858. * only alter it if it was a number and a varchar was found
  859. */
  860. if ($curr_type == self::NONE) {
  861. continue;
  862. }
  863. if ($curr_type == self::VARCHAR) {
  864. $types[$i] = self::VARCHAR;
  865. } elseif ($curr_type == self::DECIMAL) {
  866. if ($types[$i] != self::VARCHAR) {
  867. $types[$i] = self::DECIMAL;
  868. }
  869. } elseif ($curr_type == self::BIGINT) {
  870. if ($types[$i] != self::VARCHAR && $types[$i] != self::DECIMAL) {
  871. $types[$i] = self::BIGINT;
  872. }
  873. } elseif ($curr_type == self::INT) {
  874. if ($types[$i] != self::VARCHAR
  875. && $types[$i] != self::DECIMAL
  876. && $types[$i] != self::BIGINT
  877. ) {
  878. $types[$i] = self::INT;
  879. }
  880. }
  881. }
  882. }
  883. /* Check to ensure that all types are valid */
  884. $len = count($types);
  885. for ($n = 0; $n < $len; ++$n) {
  886. if (strcmp((string) self::NONE, (string) $types[$n])) {
  887. continue;
  888. }
  889. $types[$n] = self::VARCHAR;
  890. $sizes[$n] = '10';
  891. }
  892. return [
  893. $types,
  894. $sizes,
  895. ];
  896. }
  897. /**
  898. * Builds and executes SQL statements to create the database and tables
  899. * as necessary, as well as insert all the data.
  900. *
  901. * @link https://wiki.phpmyadmin.net/pma/Import
  902. *
  903. * @param string $db_name Name of the database
  904. * @param array $tables Array of tables for the specified database
  905. * @param array|null $analyses Analyses of the tables
  906. * @param array|null $additional_sql Additional SQL statements to be executed
  907. * @param array|null $options Associative array of options
  908. * @param array $sql_data 2-element array with sql data
  909. *
  910. * @access public
  911. */
  912. public function buildSql(
  913. string $db_name,
  914. array &$tables,
  915. ?array &$analyses = null,
  916. ?array &$additional_sql = null,
  917. ?array $options = null,
  918. array &$sql_data = []
  919. ): void {
  920. global $import_notice, $dbi;
  921. /* Needed to quell the beast that is Message */
  922. $import_notice = null;
  923. /* Take care of the options */
  924. if (isset($options['db_collation']) && $options['db_collation'] !== null) {
  925. $collation = $options['db_collation'];
  926. } else {
  927. $collation = 'utf8_general_ci';
  928. }
  929. if (isset($options['db_charset']) && $options['db_charset'] !== null) {
  930. $charset = $options['db_charset'];
  931. } else {
  932. $charset = 'utf8';
  933. }
  934. if (isset($options['create_db'])) {
  935. $create_db = $options['create_db'];
  936. } else {
  937. $create_db = true;
  938. }
  939. /**
  940. * Create SQL code to handle the database
  941. *
  942. * @var array<int,string> $sql
  943. */
  944. $sql = [];
  945. if ($create_db) {
  946. $sql[] = 'CREATE DATABASE IF NOT EXISTS ' . Util::backquote($db_name)
  947. . ' DEFAULT CHARACTER SET ' . $charset . ' COLLATE ' . $collation
  948. . ';';
  949. }
  950. /**
  951. * The calling plug-in should include this statement,
  952. * if necessary, in the $additional_sql parameter
  953. *
  954. * $sql[] = "USE " . backquote($db_name);
  955. */
  956. /* Execute the SQL statements create above */
  957. $sql_len = count($sql);
  958. for ($i = 0; $i < $sql_len; ++$i) {
  959. $this->runQuery($sql[$i], $sql[$i], $sql_data);
  960. }
  961. /* No longer needed */
  962. unset($sql);
  963. /* Run the $additional_sql statements supplied by the caller plug-in */
  964. if ($additional_sql != null) {
  965. /* Clean the SQL first */
  966. $additional_sql_len = count($additional_sql);
  967. /**
  968. * Only match tables for now, because CREATE IF NOT EXISTS
  969. * syntax is lacking or nonexisting for views, triggers,
  970. * functions, and procedures.
  971. *
  972. * See: https://bugs.mysql.com/bug.php?id=15287
  973. *
  974. * To the best of my knowledge this is still an issue.
  975. *
  976. * $pattern = 'CREATE (TABLE|VIEW|TRIGGER|FUNCTION|PROCEDURE)';
  977. */
  978. $pattern = '/CREATE [^`]*(TABLE)/';
  979. $replacement = 'CREATE \\1 IF NOT EXISTS';
  980. /* Change CREATE statements to CREATE IF NOT EXISTS to support
  981. * inserting into existing structures
  982. */
  983. for ($i = 0; $i < $additional_sql_len; ++$i) {
  984. $additional_sql[$i] = preg_replace(
  985. $pattern,
  986. $replacement,
  987. $additional_sql[$i]
  988. );
  989. /* Execute the resulting statements */
  990. $this->runQuery($additional_sql[$i], $additional_sql[$i], $sql_data);
  991. }
  992. }
  993. if ($analyses != null) {
  994. $type_array = [
  995. self::NONE => 'NULL',
  996. self::VARCHAR => 'varchar',
  997. self::INT => 'int',
  998. self::DECIMAL => 'decimal',
  999. self::BIGINT => 'bigint',
  1000. self::GEOMETRY => 'geometry',
  1001. ];
  1002. /* TODO: Do more checking here to make sure they really are matched */
  1003. if (count($tables) != count($analyses)) {
  1004. exit;
  1005. }
  1006. /* Create SQL code to create the tables */
  1007. $num_tables = count($tables);
  1008. for ($i = 0; $i < $num_tables; ++$i) {
  1009. $num_cols = count($tables[$i][self::COL_NAMES]);
  1010. $tempSQLStr = 'CREATE TABLE IF NOT EXISTS '
  1011. . Util::backquote($db_name)
  1012. . '.' . Util::backquote($tables[$i][self::TBL_NAME]) . ' (';
  1013. for ($j = 0; $j < $num_cols; ++$j) {
  1014. $size = $analyses[$i][self::SIZES][$j];
  1015. if ((int) $size == 0) {
  1016. $size = 10;
  1017. }
  1018. $tempSQLStr .= Util::backquote(
  1019. $tables[$i][self::COL_NAMES][$j]
  1020. ) . ' '
  1021. . $type_array[$analyses[$i][self::TYPES][$j]];
  1022. if ($analyses[$i][self::TYPES][$j] != self::GEOMETRY) {
  1023. $tempSQLStr .= '(' . $size . ')';
  1024. }
  1025. if ($j == count($tables[$i][self::COL_NAMES]) - 1) {
  1026. continue;
  1027. }
  1028. $tempSQLStr .= ', ';
  1029. }
  1030. $tempSQLStr .= ') DEFAULT CHARACTER SET ' . $charset
  1031. . ' COLLATE ' . $collation . ';';
  1032. /**
  1033. * Each SQL statement is executed immediately
  1034. * after it is formed so that we don't have
  1035. * to store them in a (possibly large) buffer
  1036. */
  1037. $this->runQuery($tempSQLStr, $tempSQLStr, $sql_data);
  1038. }
  1039. }
  1040. /**
  1041. * Create the SQL statements to insert all the data
  1042. *
  1043. * Only one insert query is formed for each table
  1044. */
  1045. $tempSQLStr = '';
  1046. $col_count = 0;
  1047. $num_tables = count($tables);
  1048. for ($i = 0; $i < $num_tables; ++$i) {
  1049. $num_cols = count($tables[$i][self::COL_NAMES]);
  1050. $num_rows = count($tables[$i][self::ROWS]);
  1051. $tempSQLStr = 'INSERT INTO ' . Util::backquote($db_name) . '.'
  1052. . Util::backquote($tables[$i][self::TBL_NAME]) . ' (';
  1053. for ($m = 0; $m < $num_cols; ++$m) {
  1054. $tempSQLStr .= Util::backquote($tables[$i][self::COL_NAMES][$m]);
  1055. if ($m == $num_cols - 1) {
  1056. continue;
  1057. }
  1058. $tempSQLStr .= ', ';
  1059. }
  1060. $tempSQLStr .= ') VALUES ';
  1061. for ($j = 0; $j < $num_rows; ++$j) {
  1062. $tempSQLStr .= '(';
  1063. for ($k = 0; $k < $num_cols; ++$k) {
  1064. // If fully formatted SQL, no need to enclose
  1065. // with apostrophes, add slashes etc.
  1066. if ($analyses != null
  1067. && isset($analyses[$i][self::FORMATTEDSQL][$col_count])
  1068. && $analyses[$i][self::FORMATTEDSQL][$col_count] == true
  1069. ) {
  1070. $tempSQLStr .= (string) $tables[$i][self::ROWS][$j][$k];
  1071. } else {
  1072. if ($analyses != null) {
  1073. $is_varchar = ($analyses[$i][self::TYPES][$col_count] === self::VARCHAR);
  1074. } else {
  1075. $is_varchar = ! is_numeric($tables[$i][self::ROWS][$j][$k]);
  1076. }
  1077. /* Don't put quotes around NULL fields */
  1078. if (! strcmp((string) $tables[$i][self::ROWS][$j][$k], 'NULL')) {
  1079. $is_varchar = false;
  1080. }
  1081. $tempSQLStr .= $is_varchar ? "'" : '';
  1082. $tempSQLStr .= $dbi->escapeString(
  1083. (string) $tables[$i][self::ROWS][$j][$k]
  1084. );
  1085. $tempSQLStr .= $is_varchar ? "'" : '';
  1086. }
  1087. if ($k != $num_cols - 1) {
  1088. $tempSQLStr .= ', ';
  1089. }
  1090. if ($col_count == $num_cols - 1) {
  1091. $col_count = 0;
  1092. } else {
  1093. $col_count++;
  1094. }
  1095. /* Delete the cell after we are done with it */
  1096. unset($tables[$i][self::ROWS][$j][$k]);
  1097. }
  1098. $tempSQLStr .= ')';
  1099. if ($j != $num_rows - 1) {
  1100. $tempSQLStr .= ",\n ";
  1101. }
  1102. $col_count = 0;
  1103. /* Delete the row after we are done with it */
  1104. unset($tables[$i][self::ROWS][$j]);
  1105. }
  1106. $tempSQLStr .= ';';
  1107. /**
  1108. * Each SQL statement is executed immediately
  1109. * after it is formed so that we don't have
  1110. * to store them in a (possibly large) buffer
  1111. */
  1112. $this->runQuery($tempSQLStr, $tempSQLStr, $sql_data);
  1113. }
  1114. /* No longer needed */
  1115. unset($tempSQLStr);
  1116. /**
  1117. * A work in progress
  1118. */
  1119. /* Add the viewable structures from $additional_sql
  1120. * to $tables so they are also displayed
  1121. */
  1122. $view_pattern = '@VIEW `[^`]+`\.`([^`]+)@';
  1123. $table_pattern = '@CREATE TABLE IF NOT EXISTS `([^`]+)`@';
  1124. /* Check a third pattern to make sure its not a "USE `db_name`;" statement */
  1125. $regs = [];
  1126. $inTables = false;
  1127. $additional_sql_len = $additional_sql === null ? 0 : count($additional_sql);
  1128. for ($i = 0; $i < $additional_sql_len; ++$i) {
  1129. preg_match($view_pattern, $additional_sql[$i], $regs);
  1130. if (count($regs) === 0) {
  1131. preg_match($table_pattern, $additional_sql[$i], $regs);
  1132. }
  1133. if (count($regs)) {
  1134. for ($n = 0; $n < $num_tables; ++$n) {
  1135. if (! strcmp($regs[1], $tables[$n][self::TBL_NAME])) {
  1136. $inTables = true;
  1137. break;
  1138. }
  1139. }
  1140. if (! $inTables) {
  1141. $tables[] = [self::TBL_NAME => $regs[1]];
  1142. }
  1143. }
  1144. /* Reset the array */
  1145. $regs = [];
  1146. $inTables = false;
  1147. }
  1148. $params = ['db' => $db_name];
  1149. $db_url = Url::getFromRoute('/database/structure', $params);
  1150. $db_ops_url = Url::getFromRoute('/database/operations', $params);
  1151. $message = '<br><br>';
  1152. $message .= '<strong>' . __(
  1153. 'The following structures have either been created or altered. Here you can:'
  1154. ) . '</strong><br>';
  1155. $message .= '<ul><li>' . __(
  1156. "View a structure's contents by clicking on its name."
  1157. ) . '</li>';
  1158. $message .= '<li>' . __(
  1159. 'Change any of its settings by clicking the corresponding "Options" link.'
  1160. ) . '</li>';
  1161. $message .= '<li>' . __('Edit structure by following the "Structure" link.')
  1162. . '</li>';
  1163. $message .= sprintf(
  1164. '<br><li><a href="%s" title="%s">%s</a> (<a href="%s" title="%s">'
  1165. . __('Options') . '</a>)</li>',
  1166. $db_url,
  1167. sprintf(
  1168. __('Go to database: %s'),
  1169. htmlspecialchars(Util::backquote($db_name))
  1170. ),
  1171. htmlspecialchars($db_name),
  1172. $db_ops_url,
  1173. sprintf(
  1174. __('Edit settings for %s'),
  1175. htmlspecialchars(Util::backquote($db_name))
  1176. )
  1177. );
  1178. $message .= '<ul>';
  1179. unset($params);
  1180. foreach ($tables as $table) {
  1181. $params = [
  1182. 'db' => $db_name,
  1183. 'table' => (string) $table[self::TBL_NAME],
  1184. ];
  1185. $tbl_url = Url::getFromRoute('/sql', $params);
  1186. $tbl_struct_url = Url::getFromRoute('/table/structure', $params);
  1187. $tbl_ops_url = Url::getFromRoute('/table/operations', $params);
  1188. unset($params);
  1189. $_table = new Table($table[self::TBL_NAME], $db_name);
  1190. if (! $_table->isView()) {
  1191. $message .= sprintf(
  1192. '<li><a href="%s" title="%s">%s</a> (<a href="%s" title="%s">' . __(
  1193. 'Structure'
  1194. ) . '</a>) (<a href="%s" title="%s">' . __('Options') . '</a>)</li>',
  1195. $tbl_url,
  1196. sprintf(
  1197. __('Go to table: %s'),
  1198. htmlspecialchars(
  1199. Util::backquote($table[self::TBL_NAME])
  1200. )
  1201. ),
  1202. htmlspecialchars($table[self::TBL_NAME]),
  1203. $tbl_struct_url,
  1204. sprintf(
  1205. __('Structure of %s'),
  1206. htmlspecialchars(
  1207. Util::backquote($table[self::TBL_NAME])
  1208. )
  1209. ),
  1210. $tbl_ops_url,
  1211. sprintf(
  1212. __('Edit settings for %s'),
  1213. htmlspecialchars(
  1214. Util::backquote($table[self::TBL_NAME])
  1215. )
  1216. )
  1217. );
  1218. } else {
  1219. $message .= sprintf(
  1220. '<li><a href="%s" title="%s">%s</a></li>',
  1221. $tbl_url,
  1222. sprintf(
  1223. __('Go to view: %s'),
  1224. htmlspecialchars(
  1225. Util::backquote($table[self::TBL_NAME])
  1226. )
  1227. ),
  1228. htmlspecialchars($table[self::TBL_NAME])
  1229. );
  1230. }
  1231. }
  1232. $message .= '</ul></ul>';
  1233. $import_notice = $message;
  1234. }
  1235. /**
  1236. * Handles request for Simulation of UPDATE/DELETE queries.
  1237. */
  1238. public function handleSimulateDmlRequest(): void
  1239. {
  1240. global $dbi;
  1241. $response = Response::getInstance();
  1242. $error = false;
  1243. $error_msg = __('Only single-table UPDATE and DELETE queries can be simulated.');
  1244. $sql_delimiter = $_POST['sql_delimiter'];
  1245. $sql_data = [];
  1246. $queries = explode($sql_delimiter, $GLOBALS['sql_query']);
  1247. foreach ($queries as $sql_query) {
  1248. if (empty($sql_query)) {
  1249. continue;
  1250. }
  1251. // Parsing the query.
  1252. $parser = new Parser($sql_query);
  1253. if (empty($parser->statements[0])) {
  1254. continue;
  1255. }
  1256. $statement = $parser->statements[0];
  1257. $analyzed_sql_results = [
  1258. 'query' => $sql_query,
  1259. 'parser' => $parser,
  1260. 'statement' => $statement,
  1261. ];
  1262. if (! ($statement instanceof UpdateStatement
  1263. || $statement instanceof DeleteStatement)
  1264. || ! empty($statement->join)
  1265. ) {
  1266. $error = $error_msg;
  1267. break;
  1268. }
  1269. $tables = Query::getTables($statement);
  1270. if (count($tables) > 1) {
  1271. $error = $error_msg;
  1272. break;
  1273. }
  1274. // Get the matched rows for the query.
  1275. $result = $this->getMatchedRows($analyzed_sql_results);
  1276. $error = $dbi->getError();
  1277. if ($error) {
  1278. break;
  1279. }
  1280. $sql_data[] = $result;
  1281. }
  1282. if ($error) {
  1283. $message = Message::rawError($error);
  1284. $response->addJSON('message', $message);
  1285. $response->addJSON('sql_data', false);
  1286. } else {
  1287. $response->addJSON('sql_data', $sql_data);
  1288. }
  1289. }
  1290. /**
  1291. * Find the matching rows for UPDATE/DELETE query.
  1292. *
  1293. * @param array $analyzed_sql_results Analyzed SQL results from parser.
  1294. *
  1295. * @return array
  1296. */
  1297. public function getMatchedRows(array $analyzed_sql_results = []): array
  1298. {
  1299. $statement = $analyzed_sql_results['statement'];
  1300. $matched_row_query = '';
  1301. if ($statement instanceof DeleteStatement) {
  1302. $matched_row_query = $this->getSimulatedDeleteQuery($analyzed_sql_results);
  1303. } elseif ($statement instanceof UpdateStatement) {
  1304. $matched_row_query = $this->getSimulatedUpdateQuery($analyzed_sql_results);
  1305. }
  1306. // Execute the query and get the number of matched rows.
  1307. $matched_rows = $this->executeMatchedRowQuery($matched_row_query);
  1308. // URL to matched rows.
  1309. $_url_params = [
  1310. 'db' => $GLOBALS['db'],
  1311. 'sql_query' => $matched_row_query,
  1312. ];
  1313. $matched_rows_url = Url::getFromRoute('/sql', $_url_params);
  1314. return [
  1315. 'sql_query' => Html\Generator::formatSql($analyzed_sql_results['query']),
  1316. 'matched_rows' => $matched_rows,
  1317. 'matched_rows_url' => $matched_rows_url,
  1318. ];
  1319. }
  1320. /**
  1321. * Transforms a UPDATE query into SELECT statement.
  1322. *
  1323. * @param array $analyzed_sql_results Analyzed SQL results from parser.
  1324. *
  1325. * @return string SQL query
  1326. */
  1327. public function getSimulatedUpdateQuery(array $analyzed_sql_results): string
  1328. {
  1329. $table_references = Query::getTables(
  1330. $analyzed_sql_results['statement']
  1331. );
  1332. $where = Query::getClause(
  1333. $analyzed_sql_results['statement'],
  1334. $analyzed_sql_results['parser']->list,
  1335. 'WHERE'
  1336. );
  1337. if (empty($where)) {
  1338. $where = '1';
  1339. }
  1340. $columns = [];
  1341. $diff = [];
  1342. foreach ($analyzed_sql_results['statement']->set as $set) {
  1343. $columns[] = $set->column;
  1344. $not_equal_operator = ' <> ';
  1345. if (strtoupper($set->value) === 'NULL') {
  1346. $not_equal_operator = ' IS NOT ';
  1347. }
  1348. $diff[] = $set->column . $not_equal_operator . $set->value;
  1349. }
  1350. if (! empty($diff)) {
  1351. $where .= ' AND (' . implode(' OR ', $diff) . ')';
  1352. }
  1353. $order_and_limit = '';
  1354. if (! empty($analyzed_sql_results['statement']->order)) {
  1355. $order_and_limit .= ' ORDER BY ' . Query::getClause(
  1356. $analyzed_sql_results['statement'],
  1357. $analyzed_sql_results['parser']->list,
  1358. 'ORDER BY'
  1359. );
  1360. }
  1361. if (! empty($analyzed_sql_results['statement']->limit)) {
  1362. $order_and_limit .= ' LIMIT ' . Query::getClause(
  1363. $analyzed_sql_results['statement'],
  1364. $analyzed_sql_results['parser']->list,
  1365. 'LIMIT'
  1366. );
  1367. }
  1368. return 'SELECT ' . implode(', ', $columns) .
  1369. ' FROM ' . implode(', ', $table_references) .
  1370. ' WHERE ' . $where . $order_and_limit;
  1371. }
  1372. /**
  1373. * Transforms a DELETE query into SELECT statement.
  1374. *
  1375. * @param array $analyzed_sql_results Analyzed SQL results from parser.
  1376. *
  1377. * @return string SQL query
  1378. */
  1379. public function getSimulatedDeleteQuery(array $analyzed_sql_results): string
  1380. {
  1381. $table_references = Query::getTables(
  1382. $analyzed_sql_results['statement']
  1383. );
  1384. $where = Query::getClause(
  1385. $analyzed_sql_results['statement'],
  1386. $analyzed_sql_results['parser']->list,
  1387. 'WHERE'
  1388. );
  1389. if (empty($where)) {
  1390. $where = '1';
  1391. }
  1392. $order_and_limit = '';
  1393. if (! empty($analyzed_sql_results['statement']->order)) {
  1394. $order_and_limit .= ' ORDER BY ' . Query::getClause(
  1395. $analyzed_sql_results['statement'],
  1396. $analyzed_sql_results['parser']->list,
  1397. 'ORDER BY'
  1398. );
  1399. }
  1400. if (! empty($analyzed_sql_results['statement']->limit)) {
  1401. $order_and_limit .= ' LIMIT ' . Query::getClause(
  1402. $analyzed_sql_results['statement'],
  1403. $analyzed_sql_results['parser']->list,
  1404. 'LIMIT'
  1405. );
  1406. }
  1407. return 'SELECT * FROM ' . implode(', ', $table_references) .
  1408. ' WHERE ' . $where . $order_and_limit;
  1409. }
  1410. /**
  1411. * Executes the matched_row_query and returns the resultant row count.
  1412. *
  1413. * @param string $matched_row_query SQL query
  1414. *
  1415. * @return int Number of rows returned
  1416. */
  1417. public function executeMatchedRowQuery(string $matched_row_query): int
  1418. {
  1419. global $dbi;
  1420. $dbi->selectDb($GLOBALS['db']);
  1421. // Execute the query.
  1422. $result = $dbi->tryQuery($matched_row_query);
  1423. // Count the number of rows in the result set.
  1424. $result = $dbi->numRows($result);
  1425. return $result;
  1426. }
  1427. /**
  1428. * Handles request for ROLLBACK.
  1429. *
  1430. * @param string $sql_query SQL query(s)
  1431. */
  1432. public function handleRollbackRequest(string $sql_query): void
  1433. {
  1434. global $dbi;
  1435. $sql_delimiter = $_POST['sql_delimiter'];
  1436. $queries = explode($sql_delimiter, $sql_query);
  1437. $error = false;
  1438. $error_msg = __(
  1439. 'Only INSERT, UPDATE, DELETE and REPLACE '
  1440. . 'SQL queries containing transactional engine tables can be rolled back.'
  1441. );
  1442. foreach ($queries as $sql_query) {
  1443. if (empty($sql_query)) {
  1444. continue;
  1445. }
  1446. // Check each query for ROLLBACK support.
  1447. if ($this->checkIfRollbackPossible($sql_query)) {
  1448. continue;
  1449. }
  1450. $global_error = $dbi->getError();
  1451. if ($global_error) {
  1452. $error = $global_error;
  1453. } else {
  1454. $error = $error_msg;
  1455. }
  1456. break;
  1457. }
  1458. if ($error) {
  1459. unset($_POST['rollback_query']);
  1460. $response = Response::getInstance();
  1461. $message = Message::rawError($error);
  1462. $response->addJSON('message', $message);
  1463. exit;
  1464. }
  1465. // If everything fine, START a transaction.
  1466. $dbi->query('START TRANSACTION');
  1467. }
  1468. /**
  1469. * Checks if ROLLBACK is possible for a SQL query or not.
  1470. *
  1471. * @param string $sql_query SQL query
  1472. */
  1473. public function checkIfRollbackPossible(string $sql_query): bool
  1474. {
  1475. $parser = new Parser($sql_query);
  1476. if (empty($parser->statements[0])) {
  1477. return true;
  1478. }
  1479. $statement = $parser->statements[0];
  1480. // Check if query is supported.
  1481. if (! (($statement instanceof InsertStatement)
  1482. || ($statement instanceof UpdateStatement)
  1483. || ($statement instanceof DeleteStatement)
  1484. || ($statement instanceof ReplaceStatement))
  1485. ) {
  1486. return false;
  1487. }
  1488. // Get table_references from the query.
  1489. $tables = Query::getTables($statement);
  1490. // Check if each table is 'InnoDB'.
  1491. foreach ($tables as $table) {
  1492. if (! $this->isTableTransactional($table)) {
  1493. return false;
  1494. }
  1495. }
  1496. return true;
  1497. }
  1498. /**
  1499. * Checks if a table is 'InnoDB' or not.
  1500. *
  1501. * @param string $table Table details
  1502. */
  1503. public function isTableTransactional(string $table): bool
  1504. {
  1505. global $dbi;
  1506. $table = explode('.', $table);
  1507. if (count($table) === 2) {
  1508. $db = Util::unQuote($table[0]);
  1509. $table = Util::unQuote($table[1]);
  1510. } else {
  1511. $db = $GLOBALS['db'];
  1512. $table = Util::unQuote($table[0]);
  1513. }
  1514. // Query to check if table exists.
  1515. $check_table_query = 'SELECT * FROM ' . Util::backquote($db)
  1516. . '.' . Util::backquote($table) . ' '
  1517. . 'LIMIT 1';
  1518. $result = $dbi->tryQuery($check_table_query);
  1519. if (! $result) {
  1520. return false;
  1521. }
  1522. // List of Transactional Engines.
  1523. $transactional_engines = [
  1524. 'INNODB',
  1525. 'FALCON',
  1526. 'NDB',
  1527. 'INFINIDB',
  1528. 'TOKUDB',
  1529. 'XTRADB',
  1530. 'SEQUENCE',
  1531. 'BDB',
  1532. ];
  1533. // Query to check if table is 'Transactional'.
  1534. $check_query = 'SELECT `ENGINE` FROM `information_schema`.`tables` '
  1535. . 'WHERE `table_name` = "' . $table . '" '
  1536. . 'AND `table_schema` = "' . $db . '" '
  1537. . 'AND UPPER(`engine`) IN ("'
  1538. . implode('", "', $transactional_engines)
  1539. . '")';
  1540. $result = $dbi->tryQuery($check_query);
  1541. return $dbi->numRows($result) == 1;
  1542. }
  1543. /** @return string[] */
  1544. public static function getCompressions(): array
  1545. {
  1546. global $cfg;
  1547. $compressions = [];
  1548. if ($cfg['GZipDump'] && function_exists('gzopen')) {
  1549. $compressions[] = 'gzip';
  1550. }
  1551. if ($cfg['BZipDump'] && function_exists('bzopen')) {
  1552. $compressions[] = 'bzip2';
  1553. }
  1554. if ($cfg['ZipDump'] && function_exists('zip_open')) {
  1555. $compressions[] = 'zip';
  1556. }
  1557. return $compressions;
  1558. }
  1559. /**
  1560. * @param array $importList List of plugin instances.
  1561. *
  1562. * @return false|string
  1563. */
  1564. public static function getLocalFiles(array $importList)
  1565. {
  1566. $fileListing = new FileListing();
  1567. $extensions = '';
  1568. foreach ($importList as $importPlugin) {
  1569. if (! empty($extensions)) {
  1570. $extensions .= '|';
  1571. }
  1572. $extensions .= $importPlugin->getProperties()->getExtension();
  1573. }
  1574. $matcher = '@\.(' . $extensions . ')(\.(' . $fileListing->supportedDecompressions() . '))?$@';
  1575. $active = isset($GLOBALS['timeout_passed'], $GLOBALS['local_import_file']) && $GLOBALS['timeout_passed']
  1576. ? $GLOBALS['local_import_file']
  1577. : '';
  1578. return $fileListing->getFileSelectOptions(
  1579. Util::userDir($GLOBALS['cfg']['UploadDir'] ?? ''),
  1580. $matcher,
  1581. $active
  1582. );
  1583. }
  1584. }