ImportCsv.php 31 KB

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