ImportCsv.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. <?php
  2. /**
  3. * CSV import plugin for phpMyAdmin
  4. *
  5. * @todo add an option for handling NULL values
  6. */
  7. declare(strict_types=1);
  8. namespace PhpMyAdmin\Plugins\Import;
  9. use PhpMyAdmin\File;
  10. use PhpMyAdmin\Html\Generator;
  11. use PhpMyAdmin\Message;
  12. use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup;
  13. use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem;
  14. use PhpMyAdmin\Properties\Options\Items\NumberPropertyItem;
  15. use PhpMyAdmin\Properties\Options\Items\TextPropertyItem;
  16. use PhpMyAdmin\Properties\Plugins\ImportPluginProperties;
  17. use PhpMyAdmin\Util;
  18. use function __;
  19. use function array_shift;
  20. use function array_splice;
  21. use function basename;
  22. use function count;
  23. use function mb_strlen;
  24. use function mb_strtolower;
  25. use function mb_substr;
  26. use function preg_grep;
  27. use function preg_replace;
  28. use function preg_split;
  29. use function rtrim;
  30. use function str_contains;
  31. use function strlen;
  32. use function strtr;
  33. use function trim;
  34. /**
  35. * Handles the import for the CSV format
  36. */
  37. class ImportCsv extends AbstractImportCsv
  38. {
  39. /**
  40. * Whether to analyze tables
  41. *
  42. * @var bool
  43. */
  44. private $analyze;
  45. /**
  46. * @psalm-return non-empty-lowercase-string
  47. */
  48. public function getName(): string
  49. {
  50. return 'csv';
  51. }
  52. protected function setProperties(): ImportPluginProperties
  53. {
  54. $this->setAnalyze(false);
  55. if ($GLOBALS['plugin_param'] !== 'table') {
  56. $this->setAnalyze(true);
  57. }
  58. $importPluginProperties = new ImportPluginProperties();
  59. $importPluginProperties->setText('CSV');
  60. $importPluginProperties->setExtension('csv');
  61. $importPluginProperties->setOptionsText(__('Options'));
  62. // create the root group that will be the options field for
  63. // $importPluginProperties
  64. // this will be shown as "Format specific options"
  65. $importSpecificOptions = new OptionsPropertyRootGroup('Format Specific Options');
  66. $generalOptions = $this->getGeneralOptions();
  67. if ($GLOBALS['plugin_param'] !== 'table') {
  68. $leaf = new TextPropertyItem(
  69. 'new_tbl_name',
  70. __(
  71. 'Name of the new table (optional):'
  72. )
  73. );
  74. $generalOptions->addProperty($leaf);
  75. if ($GLOBALS['plugin_param'] === 'server') {
  76. $leaf = new TextPropertyItem(
  77. 'new_db_name',
  78. __(
  79. 'Name of the new database (optional):'
  80. )
  81. );
  82. $generalOptions->addProperty($leaf);
  83. }
  84. $leaf = new NumberPropertyItem(
  85. 'partial_import',
  86. __(
  87. 'Import these many number of rows (optional):'
  88. )
  89. );
  90. $generalOptions->addProperty($leaf);
  91. $leaf = new BoolPropertyItem(
  92. 'col_names',
  93. __(
  94. 'The first line of the file contains the table column names'
  95. . ' <i>(if this is unchecked, the first line will become part'
  96. . ' of the data)</i>'
  97. )
  98. );
  99. $generalOptions->addProperty($leaf);
  100. } else {
  101. $leaf = new NumberPropertyItem(
  102. 'partial_import',
  103. __(
  104. 'Import these many number of rows (optional):'
  105. )
  106. );
  107. $generalOptions->addProperty($leaf);
  108. $hint = new Message(
  109. __(
  110. 'If the data in each row of the file is not'
  111. . ' in the same order as in the database, list the corresponding'
  112. . ' column names here. Column names must be separated by commas'
  113. . ' and not enclosed in quotations.'
  114. )
  115. );
  116. $leaf = new TextPropertyItem(
  117. 'columns',
  118. __('Column names:') . ' ' . Generator::showHint($hint->getMessage())
  119. );
  120. $generalOptions->addProperty($leaf);
  121. }
  122. $leaf = new BoolPropertyItem(
  123. 'ignore',
  124. __('Do not abort on INSERT error')
  125. );
  126. $generalOptions->addProperty($leaf);
  127. // add the main group to the root group
  128. $importSpecificOptions->addProperty($generalOptions);
  129. // set the options for the import plugin property item
  130. $importPluginProperties->setOptions($importSpecificOptions);
  131. return $importPluginProperties;
  132. }
  133. /**
  134. * Handles the whole import logic
  135. *
  136. * @param array $sql_data 2-element array with sql data
  137. */
  138. public function doImport(?File $importHandle = null, array &$sql_data = []): void
  139. {
  140. global $error, $message, $dbi;
  141. global $db, $table, $csv_terminated, $csv_enclosed, $csv_escaped,
  142. $csv_new_line, $csv_columns, $errorUrl;
  143. // $csv_replace and $csv_ignore should have been here,
  144. // but we use directly from $_POST
  145. global $timeout_passed, $finished;
  146. $replacements = [
  147. '\\n' => "\n",
  148. '\\t' => "\t",
  149. '\\r' => "\r",
  150. ];
  151. $csv_terminated = strtr($csv_terminated, $replacements);
  152. $csv_enclosed = strtr($csv_enclosed, $replacements);
  153. $csv_escaped = strtr($csv_escaped, $replacements);
  154. $csv_new_line = strtr($csv_new_line, $replacements);
  155. [$error, $message] = $this->buildErrorsForParams(
  156. $csv_terminated,
  157. $csv_enclosed,
  158. $csv_escaped,
  159. $csv_new_line,
  160. (string) $errorUrl
  161. );
  162. [$sql_template, $required_fields, $fields] = $this->getSqlTemplateAndRequiredFields($db, $table, $csv_columns);
  163. // Defaults for parser
  164. $i = 0;
  165. $len = 0;
  166. $lastlen = null;
  167. $line = 1;
  168. $lasti = -1;
  169. $values = [];
  170. $csv_finish = false;
  171. $max_lines = 0; // defaults to 0 (get all the lines)
  172. /**
  173. * If we get a negative value, probably someone changed min value
  174. * attribute in DOM or there is an integer overflow, whatever be
  175. * the case, get all the lines.
  176. */
  177. if (isset($_REQUEST['csv_partial_import']) && $_REQUEST['csv_partial_import'] > 0) {
  178. $max_lines = $_REQUEST['csv_partial_import'];
  179. }
  180. $max_lines_constraint = $max_lines + 1;
  181. // if the first row has to be counted as column names, include one more row in the max lines
  182. if (isset($_REQUEST['csv_col_names'])) {
  183. $max_lines_constraint++;
  184. }
  185. $tempRow = [];
  186. $rows = [];
  187. $col_names = [];
  188. $tables = [];
  189. $buffer = '';
  190. $col_count = 0;
  191. $max_cols = 0;
  192. $csv_terminated_len = mb_strlen($csv_terminated);
  193. while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) {
  194. $data = $this->import->getNextChunk($importHandle);
  195. if ($data === false) {
  196. // subtract data we didn't handle yet and stop processing
  197. $GLOBALS['offset'] -= strlen($buffer);
  198. break;
  199. }
  200. if ($data !== true) {
  201. // Append new data to buffer
  202. $buffer .= $data;
  203. unset($data);
  204. // Force a trailing new line at EOF to prevent parsing problems
  205. if ($finished && $buffer) {
  206. $finalch = mb_substr($buffer, -1);
  207. if ($csv_new_line === 'auto' && $finalch != "\r" && $finalch != "\n") {
  208. $buffer .= "\n";
  209. } elseif ($csv_new_line !== 'auto' && $finalch != $csv_new_line) {
  210. $buffer .= $csv_new_line;
  211. }
  212. }
  213. // Do not parse string when we're not at the end
  214. // and don't have new line inside
  215. if (
  216. ($csv_new_line === 'auto'
  217. && ! str_contains($buffer, "\r")
  218. && ! str_contains($buffer, "\n"))
  219. || ($csv_new_line !== 'auto'
  220. && ! str_contains($buffer, $csv_new_line))
  221. ) {
  222. continue;
  223. }
  224. }
  225. // Current length of our buffer
  226. $len = mb_strlen($buffer);
  227. // Currently parsed char
  228. $ch = mb_substr($buffer, $i, 1);
  229. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  230. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  231. $i += $csv_terminated_len - 1;
  232. }
  233. while ($i < $len) {
  234. // Deadlock protection
  235. if ($lasti == $i && $lastlen == $len) {
  236. $message = Message::error(
  237. __('Invalid format of CSV input on line %d.')
  238. );
  239. $message->addParam($line);
  240. $error = true;
  241. break;
  242. }
  243. $lasti = $i;
  244. $lastlen = $len;
  245. // This can happen with auto EOL and \r at the end of buffer
  246. if (! $csv_finish) {
  247. // Grab empty field
  248. if ($ch == $csv_terminated) {
  249. if ($i == $len - 1) {
  250. break;
  251. }
  252. $values[] = '';
  253. $i++;
  254. $ch = mb_substr($buffer, $i, 1);
  255. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  256. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  257. $i += $csv_terminated_len - 1;
  258. }
  259. continue;
  260. }
  261. // Grab one field
  262. $fallbacki = $i;
  263. if ($ch == $csv_enclosed) {
  264. if ($i == $len - 1) {
  265. break;
  266. }
  267. $need_end = true;
  268. $i++;
  269. $ch = mb_substr($buffer, $i, 1);
  270. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  271. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  272. $i += $csv_terminated_len - 1;
  273. }
  274. } else {
  275. $need_end = false;
  276. }
  277. $fail = false;
  278. $value = '';
  279. while (
  280. ($need_end
  281. && ($ch != $csv_enclosed
  282. || $csv_enclosed == $csv_escaped))
  283. || (! $need_end
  284. && ! ($ch == $csv_terminated
  285. || $ch == $csv_new_line
  286. || ($csv_new_line === 'auto'
  287. && ($ch == "\r" || $ch == "\n"))))
  288. ) {
  289. if ($ch == $csv_escaped) {
  290. if ($i == $len - 1) {
  291. $fail = true;
  292. break;
  293. }
  294. $i++;
  295. $ch = mb_substr($buffer, $i, 1);
  296. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  297. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  298. $i += $csv_terminated_len - 1;
  299. }
  300. if (
  301. $csv_enclosed == $csv_escaped
  302. && ($ch == $csv_terminated
  303. || $ch == $csv_new_line
  304. || ($csv_new_line === 'auto'
  305. && ($ch == "\r" || $ch == "\n")))
  306. ) {
  307. break;
  308. }
  309. }
  310. $value .= $ch;
  311. if ($i == $len - 1) {
  312. if (! $finished) {
  313. $fail = true;
  314. }
  315. break;
  316. }
  317. $i++;
  318. $ch = mb_substr($buffer, $i, 1);
  319. if ($csv_terminated_len <= 1 || $ch != $csv_terminated[0]) {
  320. continue;
  321. }
  322. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  323. $i += $csv_terminated_len - 1;
  324. }
  325. // unquoted NULL string
  326. if ($need_end === false && $value === 'NULL') {
  327. $value = null;
  328. }
  329. if ($fail) {
  330. $i = $fallbacki;
  331. $ch = mb_substr($buffer, $i, 1);
  332. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  333. $i += $csv_terminated_len - 1;
  334. }
  335. break;
  336. }
  337. // Need to strip trailing enclosing char?
  338. if ($need_end && $ch == $csv_enclosed) {
  339. if ($finished && $i == $len - 1) {
  340. $ch = null;
  341. } elseif ($i == $len - 1) {
  342. $i = $fallbacki;
  343. $ch = mb_substr($buffer, $i, 1);
  344. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  345. $i += $csv_terminated_len - 1;
  346. }
  347. break;
  348. } else {
  349. $i++;
  350. $ch = mb_substr($buffer, $i, 1);
  351. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  352. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  353. $i += $csv_terminated_len - 1;
  354. }
  355. }
  356. }
  357. // Are we at the end?
  358. if (
  359. $ch == $csv_new_line
  360. || ($csv_new_line === 'auto' && ($ch == "\r" || $ch == "\n"))
  361. || ($finished && $i == $len - 1)
  362. ) {
  363. $csv_finish = true;
  364. }
  365. // Go to next char
  366. if ($ch == $csv_terminated) {
  367. if ($i == $len - 1) {
  368. $i = $fallbacki;
  369. $ch = mb_substr($buffer, $i, 1);
  370. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  371. $i += $csv_terminated_len - 1;
  372. }
  373. break;
  374. }
  375. $i++;
  376. $ch = mb_substr($buffer, $i, 1);
  377. if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) {
  378. $ch = $this->readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len);
  379. $i += $csv_terminated_len - 1;
  380. }
  381. }
  382. // If everything went okay, store value
  383. $values[] = $value;
  384. }
  385. // End of line
  386. if (
  387. ! $csv_finish
  388. && $ch != $csv_new_line
  389. && ($csv_new_line !== 'auto' || ($ch != "\r" && $ch != "\n"))
  390. ) {
  391. continue;
  392. }
  393. if ($csv_new_line === 'auto' && $ch == "\r") { // Handle "\r\n"
  394. if ($i >= ($len - 2) && ! $finished) {
  395. break; // We need more data to decide new line
  396. }
  397. if (mb_substr($buffer, $i + 1, 1) == "\n") {
  398. $i++;
  399. }
  400. }
  401. // We didn't parse value till the end of line, so there was
  402. // empty one
  403. if (! $csv_finish) {
  404. $values[] = '';
  405. }
  406. if ($this->getAnalyze()) {
  407. foreach ($values as $val) {
  408. $tempRow[] = $val;
  409. ++$col_count;
  410. }
  411. if ($col_count > $max_cols) {
  412. $max_cols = $col_count;
  413. }
  414. $col_count = 0;
  415. $rows[] = $tempRow;
  416. $tempRow = [];
  417. } else {
  418. // Do we have correct count of values?
  419. if (count($values) != $required_fields) {
  420. // Hack for excel
  421. if ($values[count($values) - 1] !== ';') {
  422. $message = Message::error(
  423. __(
  424. 'Invalid column count in CSV input on line %d.'
  425. )
  426. );
  427. $message->addParam($line);
  428. $error = true;
  429. break;
  430. }
  431. unset($values[count($values) - 1]);
  432. }
  433. $first = true;
  434. $sql = $sql_template;
  435. foreach ($values as $val) {
  436. if (! $first) {
  437. $sql .= ', ';
  438. }
  439. if ($val === null) {
  440. $sql .= 'NULL';
  441. } else {
  442. $sql .= '\''
  443. . $dbi->escapeString($val)
  444. . '\'';
  445. }
  446. $first = false;
  447. }
  448. $sql .= ')';
  449. if (isset($_POST['csv_replace'])) {
  450. $sql .= ' ON DUPLICATE KEY UPDATE ';
  451. foreach ($fields as $field) {
  452. $fieldName = Util::backquote($field['Field']);
  453. $sql .= $fieldName . ' = VALUES(' . $fieldName
  454. . '), ';
  455. }
  456. $sql = rtrim($sql, ', ');
  457. }
  458. /**
  459. * @todo maybe we could add original line to verbose
  460. * SQL in comment
  461. */
  462. $this->import->runQuery($sql, $sql, $sql_data);
  463. }
  464. $line++;
  465. $csv_finish = false;
  466. $values = [];
  467. $buffer = mb_substr($buffer, $i + 1);
  468. $len = mb_strlen($buffer);
  469. $i = 0;
  470. $lasti = -1;
  471. $ch = mb_substr($buffer, 0, 1);
  472. if ($max_lines > 0 && $line == $max_lines_constraint) {
  473. $finished = 1;
  474. break;
  475. }
  476. }
  477. if ($max_lines > 0 && $line == $max_lines_constraint) {
  478. $finished = 1;
  479. break;
  480. }
  481. }
  482. if ($this->getAnalyze()) {
  483. /* Fill out all rows */
  484. $num_rows = count($rows);
  485. for ($i = 0; $i < $num_rows; ++$i) {
  486. for ($j = count($rows[$i]); $j < $max_cols; ++$j) {
  487. $rows[$i][] = 'NULL';
  488. }
  489. }
  490. $col_names = $this->getColumnNames($col_names, $max_cols, $rows);
  491. /* Remove the first row if it contains the column names */
  492. if (isset($_REQUEST['csv_col_names'])) {
  493. array_shift($rows);
  494. }
  495. $tbl_name = $this->getTableNameFromImport((string) $db);
  496. $tables[] = [
  497. $tbl_name,
  498. $col_names,
  499. $rows,
  500. ];
  501. /* Obtain the best-fit MySQL types for each column */
  502. $analyses = [];
  503. $analyses[] = $this->import->analyzeTable($tables[0]);
  504. /**
  505. * string $db_name (no backquotes)
  506. *
  507. * array $table = array(table_name, array() column_names, array()() rows)
  508. * array $tables = array of "$table"s
  509. *
  510. * array $analysis = array(array() column_types, array() column_sizes)
  511. * array $analyses = array of "$analysis"s
  512. *
  513. * array $create = array of SQL strings
  514. *
  515. * array $options = an associative array of options
  516. */
  517. /* Set database name to the currently selected one, if applicable,
  518. * Otherwise, check if user provided the database name in the request,
  519. * if not, set the default name
  520. */
  521. if (isset($_REQUEST['csv_new_db_name']) && strlen($_REQUEST['csv_new_db_name']) > 0) {
  522. $newDb = $_REQUEST['csv_new_db_name'];
  523. } else {
  524. $result = $dbi->fetchResult('SHOW DATABASES');
  525. $newDb = 'CSV_DB ' . (count($result) + 1);
  526. }
  527. [$db_name, $options] = $this->getDbnameAndOptions($db, $newDb);
  528. /* Non-applicable parameters */
  529. $create = null;
  530. /* Created and execute necessary SQL statements from data */
  531. $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data);
  532. unset($tables, $analyses);
  533. }
  534. // Commit any possible data in buffers
  535. $this->import->runQuery('', '', $sql_data);
  536. if (count($values) == 0 || $error !== false) {
  537. return;
  538. }
  539. $message = Message::error(
  540. __('Invalid format of CSV input on line %d.')
  541. );
  542. $message->addParam($line);
  543. $error = true;
  544. }
  545. private function buildErrorsForParams(
  546. string $csvTerminated,
  547. string $csvEnclosed,
  548. string $csvEscaped,
  549. string $csvNewLine,
  550. string $errUrl
  551. ): array {
  552. global $error, $message;
  553. $param_error = false;
  554. if (strlen($csvTerminated) === 0) {
  555. $message = Message::error(
  556. __('Invalid parameter for CSV import: %s')
  557. );
  558. $message->addParam(__('Columns terminated with'));
  559. $error = true;
  560. $param_error = true;
  561. // The default dialog of MS Excel when generating a CSV produces a
  562. // semi-colon-separated file with no chance of specifying the
  563. // enclosing character. Thus, users who want to import this file
  564. // tend to remove the enclosing character on the Import dialog.
  565. // I could not find a test case where having no enclosing characters
  566. // confuses this script.
  567. // But the parser won't work correctly with strings so we allow just
  568. // one character.
  569. } elseif (mb_strlen($csvEnclosed) > 1) {
  570. $message = Message::error(
  571. __('Invalid parameter for CSV import: %s')
  572. );
  573. $message->addParam(__('Columns enclosed with'));
  574. $error = true;
  575. $param_error = true;
  576. // I could not find a test case where having no escaping characters
  577. // confuses this script.
  578. // But the parser won't work correctly with strings so we allow just
  579. // one character.
  580. } elseif (mb_strlen($csvEscaped) > 1) {
  581. $message = Message::error(
  582. __('Invalid parameter for CSV import: %s')
  583. );
  584. $message->addParam(__('Columns escaped with'));
  585. $error = true;
  586. $param_error = true;
  587. } elseif (mb_strlen($csvNewLine) != 1 && $csvNewLine !== 'auto') {
  588. $message = Message::error(
  589. __('Invalid parameter for CSV import: %s')
  590. );
  591. $message->addParam(__('Lines terminated with'));
  592. $error = true;
  593. $param_error = true;
  594. }
  595. // If there is an error in the parameters entered,
  596. // indicate that immediately.
  597. if ($param_error) {
  598. Generator::mysqlDie(
  599. $message->getMessage(),
  600. '',
  601. false,
  602. $errUrl
  603. );
  604. }
  605. return [$error, $message];
  606. }
  607. private function getTableNameFromImport(string $databaseName): string
  608. {
  609. global $import_file_name, $dbi;
  610. $importFileName = basename($import_file_name, '.csv');
  611. $importFileName = mb_strtolower($importFileName);
  612. $importFileName = (string) preg_replace('/[^a-zA-Z0-9_]/', '_', $importFileName);
  613. // get new table name, if user didn't provide one, set the default name
  614. if (isset($_REQUEST['csv_new_tbl_name']) && strlen($_REQUEST['csv_new_tbl_name']) > 0) {
  615. return $_REQUEST['csv_new_tbl_name'];
  616. }
  617. if (mb_strlen($databaseName)) {
  618. $result = $dbi->fetchResult('SHOW TABLES');
  619. // logic to get table name from filename
  620. // if no table then use filename as table name
  621. if (count($result) === 0) {
  622. return $importFileName;
  623. }
  624. // check to see if {filename} as table exist
  625. $nameArray = preg_grep('/' . $importFileName . '/isU', $result);
  626. // if no use filename as table name
  627. if ($nameArray === false || count($nameArray) === 0) {
  628. return $importFileName;
  629. }
  630. // check if {filename}_ as table exist
  631. $nameArray = preg_grep('/' . $importFileName . '_/isU', $result);
  632. if ($nameArray === false) {
  633. return $importFileName;
  634. }
  635. return $importFileName . '_' . (count($nameArray) + 1);
  636. }
  637. return $importFileName;
  638. }
  639. private function getColumnNames(array $columnNames, int $maxCols, array $rows): array
  640. {
  641. if (isset($_REQUEST['csv_col_names'])) {
  642. $columnNames = array_splice($rows, 0, 1);
  643. $columnNames = $columnNames[0];
  644. // MySQL column names can't end with a space character.
  645. foreach ($columnNames as $key => $col_name) {
  646. $columnNames[$key] = rtrim($col_name);
  647. }
  648. }
  649. if ((isset($columnNames) && count($columnNames) != $maxCols) || ! isset($columnNames)) {
  650. // Fill out column names
  651. for ($i = 0; $i < $maxCols; ++$i) {
  652. $columnNames[] = 'COL ' . ($i + 1);
  653. }
  654. }
  655. return $columnNames;
  656. }
  657. private function getSqlTemplateAndRequiredFields(
  658. ?string $db,
  659. ?string $table,
  660. ?string $csvColumns
  661. ): array {
  662. global $dbi, $error, $message;
  663. $requiredFields = 0;
  664. $sqlTemplate = '';
  665. $fields = [];
  666. if (! $this->getAnalyze() && $db !== null && $table !== null) {
  667. $sqlTemplate = 'INSERT';
  668. if (isset($_POST['csv_ignore'])) {
  669. $sqlTemplate .= ' IGNORE';
  670. }
  671. $sqlTemplate .= ' INTO ' . Util::backquote($table);
  672. $tmp_fields = $dbi->getColumns($db, $table);
  673. if (empty($csvColumns)) {
  674. $fields = $tmp_fields;
  675. } else {
  676. $sqlTemplate .= ' (';
  677. $fields = [];
  678. $tmp = preg_split('/,( ?)/', $csvColumns);
  679. if ($tmp === false) {
  680. $tmp = [];
  681. }
  682. foreach ($tmp as $val) {
  683. if (count($fields) > 0) {
  684. $sqlTemplate .= ', ';
  685. }
  686. /* Trim also `, if user already included backquoted fields */
  687. $val = trim($val, " \t\r\n\0\x0B`");
  688. $found = false;
  689. foreach ($tmp_fields as $field) {
  690. if ($field['Field'] == $val) {
  691. $found = true;
  692. break;
  693. }
  694. }
  695. if (! $found) {
  696. $message = Message::error(
  697. __(
  698. 'Invalid column (%s) specified! Ensure that columns'
  699. . ' names are spelled correctly, separated by commas'
  700. . ', and not enclosed in quotes.'
  701. )
  702. );
  703. $message->addParam($val);
  704. $error = true;
  705. break;
  706. }
  707. if (isset($field)) {
  708. $fields[] = $field;
  709. }
  710. $sqlTemplate .= Util::backquote($val);
  711. }
  712. $sqlTemplate .= ') ';
  713. }
  714. $requiredFields = count($fields);
  715. $sqlTemplate .= ' VALUES (';
  716. }
  717. return [$sqlTemplate, $requiredFields, $fields];
  718. }
  719. /**
  720. * Read the expected column_separated_with String of length
  721. * $csv_terminated_len from the $buffer
  722. * into variable $ch and return the read string $ch
  723. *
  724. * @param string $buffer The original string buffer read from
  725. * csv file
  726. * @param string $ch Partially read "column Separated with"
  727. * string, also used to return after
  728. * reading length equal $csv_terminated_len
  729. * @param int $i Current read counter of buffer string
  730. * @param int $csv_terminated_len The length of "column separated with"
  731. * String
  732. *
  733. * @return string
  734. */
  735. public function readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len)
  736. {
  737. for ($j = 0; $j < $csv_terminated_len - 1; $j++) {
  738. $i++;
  739. $ch .= mb_substr($buffer, $i, 1);
  740. }
  741. return $ch;
  742. }
  743. /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */
  744. /**
  745. * Returns true if the table should be analyzed, false otherwise
  746. */
  747. private function getAnalyze(): bool
  748. {
  749. return $this->analyze;
  750. }
  751. /**
  752. * Sets to true if the table should be analyzed, false otherwise
  753. *
  754. * @param bool $analyze status
  755. */
  756. private function setAnalyze($analyze): void
  757. {
  758. $this->analyze = $analyze;
  759. }
  760. }