diff options
| author | Charles <sircharlesaze@gmail.com> | 2020-01-09 10:55:03 +0100 |
|---|---|---|
| committer | Charles <sircharlesaze@gmail.com> | 2020-01-09 13:09:38 +0100 |
| commit | 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa (patch) | |
| tree | 5c691241355c943a3c68ddb06b8cf8c60aa11319 /srcs/phpmyadmin/libraries/classes/Plugins/Import | |
| parent | 7e0d85db834d6351ed85d01e5126ac31dc510b86 (diff) | |
| download | ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.tar.gz ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.tar.bz2 ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.zip | |
phpmyadmin working
Diffstat (limited to 'srcs/phpmyadmin/libraries/classes/Plugins/Import')
14 files changed, 3566 insertions, 0 deletions
diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php new file mode 100644 index 0000000..5c5a250 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php @@ -0,0 +1,94 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Super class of CSV import plugins for phpMyAdmin + * + * @package PhpMyAdmin-Import + * @subpackage CSV + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup; +use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup; +use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; +use PhpMyAdmin\Properties\Options\Items\TextPropertyItem; +use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; + +/** + * Super class of the import plugins for the CSV format + * + * @package PhpMyAdmin-Import + * @subpackage CSV + */ +abstract class AbstractImportCsv extends ImportPlugin +{ + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return OptionsPropertyMainGroup|void object of the plugin + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + + // create common items and add them to the group + $leaf = new BoolPropertyItem( + "replace", + __( + 'Update data when duplicate keys found on import (add ON DUPLICATE ' + . 'KEY UPDATE)' + ) + ); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "terminated", + __('Columns separated with:') + ); + $leaf->setSize(2); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "enclosed", + __('Columns enclosed with:') + ); + $leaf->setSize(2); + $leaf->setLen(2); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "escaped", + __('Columns escaped with:') + ); + $leaf->setSize(2); + $leaf->setLen(2); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "new_line", + __('Lines terminated with:') + ); + $leaf->setSize(2); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + $this->properties = $importPluginProperties; + + return $generalOptions; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php new file mode 100644 index 0000000..e5494a9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php @@ -0,0 +1,818 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * CSV import plugin for phpMyAdmin + * + * @todo add an option for handling NULL values + * @package PhpMyAdmin-Import + * @subpackage CSV + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Import; +use PhpMyAdmin\Message; +use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; +use PhpMyAdmin\Properties\Options\Items\NumberPropertyItem; +use PhpMyAdmin\Properties\Options\Items\TextPropertyItem; +use PhpMyAdmin\Util; + +/** + * Handles the import for the CSV format + * + * @package PhpMyAdmin-Import + * @subpackage CSV + */ +class ImportCsv extends AbstractImportCsv +{ + /** + * Whether to analyze tables + * + * @var bool + */ + private $_analyze; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $this->_setAnalyze(false); + + if ($GLOBALS['plugin_param'] !== 'table') { + $this->_setAnalyze(true); + } + + $generalOptions = parent::setProperties(); + $this->properties->setText('CSV'); + $this->properties->setExtension('csv'); + + if ($GLOBALS['plugin_param'] !== 'table') { + $leaf = new TextPropertyItem( + "new_tbl_name", + __( + 'Name of the new table (optional):' + ) + ); + $generalOptions->addProperty($leaf); + + if ($GLOBALS['plugin_param'] === 'server') { + $leaf = new TextPropertyItem( + "new_db_name", + __( + 'Name of the new database (optional):' + ) + ); + $generalOptions->addProperty($leaf); + } + + $leaf = new NumberPropertyItem( + "partial_import", + __( + 'Import these many number of rows (optional):' + ) + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "col_names", + __( + 'The first line of the file contains the table column names' + . ' <i>(if this is unchecked, the first line will become part' + . ' of the data)</i>' + ) + ); + $generalOptions->addProperty($leaf); + } else { + $leaf = new NumberPropertyItem( + "partial_import", + __( + 'Import these many number of rows (optional):' + ) + ); + $generalOptions->addProperty($leaf); + + $hint = new Message( + __( + 'If the data in each row of the file is not' + . ' in the same order as in the database, list the corresponding' + . ' column names here. Column names must be separated by commas' + . ' and not enclosed in quotations.' + ) + ); + $leaf = new TextPropertyItem( + "columns", + __('Column names:') . ' ' . Util::showHint($hint) + ); + $generalOptions->addProperty($leaf); + } + + $leaf = new BoolPropertyItem( + "ignore", + __('Do not abort on INSERT error') + ); + $generalOptions->addProperty($leaf); + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $db, $table, $csv_terminated, $csv_enclosed, $csv_escaped, + $csv_new_line, $csv_columns, $err_url, $import_file_name; + // $csv_replace and $csv_ignore should have been here, + // but we use directly from $_POST + global $error, $timeout_passed, $finished, $message; + + $import_file_name = basename($import_file_name, ".csv"); + $import_file_name = mb_strtolower($import_file_name); + $import_file_name = preg_replace("/[^a-zA-Z0-9_]/", "_", $import_file_name); + + $replacements = [ + '\\n' => "\n", + '\\t' => "\t", + '\\r' => "\r", + ]; + $csv_terminated = strtr($csv_terminated, $replacements); + $csv_enclosed = strtr($csv_enclosed, $replacements); + $csv_escaped = strtr($csv_escaped, $replacements); + $csv_new_line = strtr($csv_new_line, $replacements); + + $param_error = false; + if (strlen($csv_terminated) === 0) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Columns terminated with')); + $error = true; + $param_error = true; + // The default dialog of MS Excel when generating a CSV produces a + // semi-colon-separated file with no chance of specifying the + // enclosing character. Thus, users who want to import this file + // tend to remove the enclosing character on the Import dialog. + // I could not find a test case where having no enclosing characters + // confuses this script. + // But the parser won't work correctly with strings so we allow just + // one character. + } elseif (mb_strlen($csv_enclosed) > 1) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Columns enclosed with')); + $error = true; + $param_error = true; + // I could not find a test case where having no escaping characters + // confuses this script. + // But the parser won't work correctly with strings so we allow just + // one character. + } elseif (mb_strlen($csv_escaped) > 1) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Columns escaped with')); + $error = true; + $param_error = true; + } elseif (mb_strlen($csv_new_line) != 1 + && $csv_new_line != 'auto' + ) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Lines terminated with')); + $error = true; + $param_error = true; + } + + // If there is an error in the parameters entered, + // indicate that immediately. + if ($param_error) { + Util::mysqlDie( + $message->getMessage(), + '', + false, + $err_url + ); + } + + $buffer = ''; + $required_fields = 0; + $sql_template = ''; + $fields = []; + if (! $this->_getAnalyze()) { + $sql_template = 'INSERT'; + if (isset($_POST['csv_ignore'])) { + $sql_template .= ' IGNORE'; + } + $sql_template .= ' INTO ' . Util::backquote($table); + + $tmp_fields = $GLOBALS['dbi']->getColumns($db, $table); + + if (empty($csv_columns)) { + $fields = $tmp_fields; + } else { + $sql_template .= ' ('; + $fields = []; + $tmp = preg_split('/,( ?)/', $csv_columns); + foreach ($tmp as $key => $val) { + if (count($fields) > 0) { + $sql_template .= ', '; + } + /* Trim also `, if user already included backquoted fields */ + $val = trim($val, " \t\r\n\0\x0B`"); + $found = false; + foreach ($tmp_fields as $field) { + if ($field['Field'] == $val) { + $found = true; + break; + } + } + if (! $found) { + $message = Message::error( + __( + 'Invalid column (%s) specified! Ensure that columns' + . ' names are spelled correctly, separated by commas' + . ', and not enclosed in quotes.' + ) + ); + $message->addParam($val); + $error = true; + break; + } + if (isset($field)) { + $fields[] = $field; + } + $sql_template .= Util::backquote($val); + } + $sql_template .= ') '; + } + + $required_fields = count($fields); + + $sql_template .= ' VALUES ('; + } + + // Defaults for parser + $i = 0; + $len = 0; + $lastlen = null; + $line = 1; + $lasti = -1; + $values = []; + $csv_finish = false; + $max_lines = 0; // defaults to 0 (get all the lines) + + // If we get a negative value, probably someone changed min value attribute in DOM or there is an integer overflow, whatever be the case, get all the lines + if (isset($_REQUEST['csv_partial_import']) && $_REQUEST['csv_partial_import'] > 0) { + $max_lines = $_REQUEST['csv_partial_import']; + } + $max_lines_constraint = $max_lines+1; + // if the first row has to be counted as column names, include one more row in the max lines + if (isset($_REQUEST['csv_col_names'])) { + $max_lines_constraint++; + } + + $tempRow = []; + $rows = []; + $col_names = []; + $tables = []; + + $col_count = 0; + $max_cols = 0; + $csv_terminated_len = mb_strlen($csv_terminated); + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + // subtract data we didn't handle yet and stop processing + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data !== true) { + // Append new data to buffer + $buffer .= $data; + unset($data); + + // Force a trailing new line at EOF to prevent parsing problems + if ($finished && $buffer) { + $finalch = mb_substr($buffer, -1); + if ($csv_new_line == 'auto' + && $finalch != "\r" + && $finalch != "\n" + ) { + $buffer .= "\n"; + } elseif ($csv_new_line != 'auto' + && $finalch != $csv_new_line + ) { + $buffer .= $csv_new_line; + } + } + + // Do not parse string when we're not at the end + // and don't have new line inside + if (($csv_new_line == 'auto' + && mb_strpos($buffer, "\r") === false + && mb_strpos($buffer, "\n") === false) + || ($csv_new_line != 'auto' + && mb_strpos($buffer, $csv_new_line) === false) + ) { + continue; + } + } + + // Current length of our buffer + $len = mb_strlen($buffer); + // Currently parsed char + + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + while ($i < $len) { + // Deadlock protection + if ($lasti == $i && $lastlen == $len) { + $message = Message::error( + __('Invalid format of CSV input on line %d.') + ); + $message->addParam($line); + $error = true; + break; + } + $lasti = $i; + $lastlen = $len; + + // This can happen with auto EOL and \r at the end of buffer + if (! $csv_finish) { + // Grab empty field + if ($ch == $csv_terminated) { + if ($i == $len - 1) { + break; + } + $values[] = ''; + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + continue; + } + + // Grab one field + $fallbacki = $i; + if ($ch == $csv_enclosed) { + if ($i == $len - 1) { + break; + } + $need_end = true; + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } else { + $need_end = false; + } + $fail = false; + $value = ''; + while (($need_end + && ($ch != $csv_enclosed + || $csv_enclosed == $csv_escaped)) + || (! $need_end + && ! ($ch == $csv_terminated + || $ch == $csv_new_line + || ($csv_new_line == 'auto' + && ($ch == "\r" || $ch == "\n")))) + ) { + if ($ch == $csv_escaped) { + if ($i == $len - 1) { + $fail = true; + break; + } + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + if ($csv_enclosed == $csv_escaped + && ($ch == $csv_terminated + || $ch == $csv_new_line + || ($csv_new_line == 'auto' + && ($ch == "\r" || $ch == "\n"))) + ) { + break; + } + } + $value .= $ch; + if ($i == $len - 1) { + if (! $finished) { + $fail = true; + } + break; + } + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } + + // unquoted NULL string + if (false === $need_end && $value === 'NULL') { + $value = null; + } + + if ($fail) { + $i = $fallbacki; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $i += $csv_terminated_len - 1; + } + break; + } + // Need to strip trailing enclosing char? + if ($need_end && $ch == $csv_enclosed) { + if ($finished && $i == $len - 1) { + $ch = null; + } elseif ($i == $len - 1) { + $i = $fallbacki; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $i += $csv_terminated_len - 1; + } + break; + } else { + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } + } + // Are we at the end? + if ($ch == $csv_new_line + || ($csv_new_line == 'auto' && ($ch == "\r" || $ch == "\n")) + || ($finished && $i == $len - 1) + ) { + $csv_finish = true; + } + // Go to next char + if ($ch == $csv_terminated) { + if ($i == $len - 1) { + $i = $fallbacki; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $i += $csv_terminated_len - 1; + } + break; + } + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } + // If everything went okay, store value + $values[] = $value; + } + + // End of line + if ($csv_finish + || $ch == $csv_new_line + || ($csv_new_line == 'auto' && ($ch == "\r" || $ch == "\n")) + ) { + if ($csv_new_line == 'auto' && $ch == "\r") { // Handle "\r\n" + if ($i >= ($len - 2) && ! $finished) { + break; // We need more data to decide new line + } + if (mb_substr($buffer, $i + 1, 1) == "\n") { + $i++; + } + } + // We didn't parse value till the end of line, so there was + // empty one + if (! $csv_finish) { + $values[] = ''; + } + + if ($this->_getAnalyze()) { + foreach ($values as $val) { + $tempRow[] = $val; + ++$col_count; + } + + if ($col_count > $max_cols) { + $max_cols = $col_count; + } + $col_count = 0; + + $rows[] = $tempRow; + $tempRow = []; + } else { + // Do we have correct count of values? + if (count($values) != $required_fields) { + // Hack for excel + if ($values[count($values) - 1] == ';') { + unset($values[count($values) - 1]); + } else { + $message = Message::error( + __( + 'Invalid column count in CSV input' + . ' on line %d.' + ) + ); + $message->addParam($line); + $error = true; + break; + } + } + + $first = true; + $sql = $sql_template; + foreach ($values as $key => $val) { + if (! $first) { + $sql .= ', '; + } + if ($val === null) { + $sql .= 'NULL'; + } else { + $sql .= '\'' + . $GLOBALS['dbi']->escapeString($val) + . '\''; + } + + $first = false; + } + $sql .= ')'; + if (isset($_POST['csv_replace'])) { + $sql .= " ON DUPLICATE KEY UPDATE "; + foreach ($fields as $field) { + $fieldName = Util::backquote( + $field['Field'] + ); + $sql .= $fieldName . " = VALUES(" . $fieldName + . "), "; + } + $sql = rtrim($sql, ', '); + } + + /** + * @todo maybe we could add original line to verbose + * SQL in comment + */ + $this->import->runQuery($sql, $sql, $sql_data); + } + + $line++; + $csv_finish = false; + $values = []; + $buffer = mb_substr($buffer, $i + 1); + $len = mb_strlen($buffer); + $i = 0; + $lasti = -1; + $ch = mb_substr($buffer, 0, 1); + if ($max_lines > 0 && $line == $max_lines_constraint) { + $finished = 1; + break; + } + } + } // End of parser loop + if ($max_lines > 0 && $line == $max_lines_constraint) { + $finished = 1; + break; + } + } // End of import loop + + if ($this->_getAnalyze()) { + /* Fill out all rows */ + $num_rows = count($rows); + for ($i = 0; $i < $num_rows; ++$i) { + for ($j = count($rows[$i]); $j < $max_cols; ++$j) { + $rows[$i][] = 'NULL'; + } + } + + if (isset($_REQUEST['csv_col_names'])) { + $col_names = array_splice($rows, 0, 1); + $col_names = $col_names[0]; + // MySQL column names can't end with a space character. + foreach ($col_names as $key => $col_name) { + $col_names[$key] = rtrim($col_name); + } + } + + if ((isset($col_names) && count($col_names) != $max_cols) + || ! isset($col_names) + ) { + // Fill out column names + for ($i = 0; $i < $max_cols; ++$i) { + $col_names[] = 'COL ' . ($i + 1); + } + } + + // get new table name, if user didn't provide one, set the default name + if (isset($_REQUEST['csv_new_tbl_name']) + && strlen($_REQUEST['csv_new_tbl_name']) > 0 + ) { + $tbl_name = $_REQUEST['csv_new_tbl_name']; + } elseif (mb_strlen((string) $db)) { + $result = $GLOBALS['dbi']->fetchResult('SHOW TABLES'); + + // logic to get table name from filename + // if no table then use filename as table name + if (count($result) === 0) { + $tbl_name = $import_file_name; + } else { + // check to see if {filename} as table exist + $name_array = preg_grep("/{$import_file_name}/isU", $result); + // if no use filename as table name + if (count($name_array) === 0) { + $tbl_name = $import_file_name; + } else { + // check if {filename}_ as table exist + $name_array = preg_grep("/{$import_file_name}_/isU", $result); + $tbl_name = $import_file_name . "_" . (count($name_array) + 1); + } + } + } else { + $tbl_name = $import_file_name; + } + + $tables[] = [ + $tbl_name, + $col_names, + $rows, + ]; + + /* Obtain the best-fit MySQL types for each column */ + $analyses = []; + $analyses[] = $this->import->analyzeTable($tables[0]); + + /** + * string $db_name (no backquotes) + * + * array $table = array(table_name, array() column_names, array()() rows) + * array $tables = array of "$table"s + * + * array $analysis = array(array() column_types, array() column_sizes) + * array $analyses = array of "$analysis"s + * + * array $create = array of SQL strings + * + * array $options = an associative array of options + */ + + /* Set database name to the currently selected one, if applicable, + * Otherwise, check if user provided the database name in the request, + * if not, set the default name + */ + if (isset($_REQUEST['csv_new_db_name']) + && strlen($_REQUEST['csv_new_db_name']) > 0 + ) { + $newDb = $_REQUEST['csv_new_db_name']; + } else { + $result = $GLOBALS['dbi']->fetchResult('SHOW DATABASES'); + if (! is_array($result)) { + $result = []; + } + $newDb = 'CSV_DB ' . (count($result) + 1); + } + list($db_name, $options) = $this->getDbnameAndOptions($db, $newDb); + + /* Non-applicable parameters */ + $create = null; + + /* Created and execute necessary SQL statements from data */ + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + + unset($tables); + unset($analyses); + } + + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + + if (count($values) != 0 && ! $error) { + $message = Message::error( + __('Invalid format of CSV input on line %d.') + ); + $message->addParam($line); + $error = true; + } + } + + /** + * Read the expected column_separated_with String of length + * $csv_terminated_len from the $buffer + * into variable $ch and return the read string $ch + * + * @param string $buffer The original string buffer read from + * csv file + * @param string $ch Partially read "column Separated with" + * string, also used to return after + * reading length equal $csv_terminated_len + * @param int $i Current read counter of buffer string + * @param int $csv_terminated_len The length of "column separated with" + * String + * + * @return string + */ + public function readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len) + { + for ($j = 0; $j < $csv_terminated_len - 1; $j++) { + $i++; + $ch .= mb_substr($buffer, $i, 1); + } + + return $ch; + } + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Returns true if the table should be analyzed, false otherwise + * + * @return bool + */ + private function _getAnalyze() + { + return $this->_analyze; + } + + /** + * Sets to true if the table should be analyzed, false otherwise + * + * @param bool $analyze status + * + * @return void + */ + private function _setAnalyze($analyze) + { + $this->_analyze = $analyze; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php new file mode 100644 index 0000000..91260c5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php @@ -0,0 +1,176 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * CSV import plugin for phpMyAdmin using LOAD DATA + * + * @package PhpMyAdmin-Import + * @subpackage LDI + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Import; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\Import\AbstractImportCsv; +use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; +use PhpMyAdmin\Properties\Options\Items\TextPropertyItem; +use PhpMyAdmin\Util; + +// We need relations enabled and we work only on database +if (! isset($GLOBALS['plugin_param']) || $GLOBALS['plugin_param'] !== 'table') { + $GLOBALS['skip_import'] = true; + + return; +} + +/** + * Handles the import for the CSV format using load data + * + * @package PhpMyAdmin-Import + * @subpackage LDI + */ +class ImportLdi extends AbstractImportCsv +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + if ($GLOBALS['cfg']['Import']['ldi_local_option'] == 'auto') { + $GLOBALS['cfg']['Import']['ldi_local_option'] = false; + + $result = $GLOBALS['dbi']->tryQuery( + 'SELECT @@local_infile;' + ); + if ($result != false && $GLOBALS['dbi']->numRows($result) > 0) { + $tmp = $GLOBALS['dbi']->fetchRow($result); + if ($tmp[0] == 'ON') { + $GLOBALS['cfg']['Import']['ldi_local_option'] = true; + } + } + $GLOBALS['dbi']->freeResult($result); + unset($result); + } + + $generalOptions = parent::setProperties(); + $this->properties->setText('CSV using LOAD DATA'); + $this->properties->setExtension('ldi'); + + $leaf = new TextPropertyItem( + "columns", + __('Column names: ') + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "ignore", + __('Do not abort on INSERT error') + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "local_option", + __('Use LOCAL keyword') + ); + $generalOptions->addProperty($leaf); + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $finished, $import_file, $charset_conversion, $table; + global $ldi_local_option, $ldi_replace, $ldi_ignore, $ldi_terminated, + $ldi_enclosed, $ldi_escaped, $ldi_new_line, $skip_queries, $ldi_columns; + + $compression = $GLOBALS['import_handle']->getCompression(); + + if ($import_file == 'none' + || $compression != 'none' + || $charset_conversion + ) { + // We handle only some kind of data! + $GLOBALS['message'] = Message::error( + __('This plugin does not support compressed imports!') + ); + $GLOBALS['error'] = true; + + return; + } + + $sql = 'LOAD DATA'; + if (isset($ldi_local_option)) { + $sql .= ' LOCAL'; + } + $sql .= ' INFILE \'' . $GLOBALS['dbi']->escapeString($import_file) + . '\''; + if (isset($ldi_replace)) { + $sql .= ' REPLACE'; + } elseif (isset($ldi_ignore)) { + $sql .= ' IGNORE'; + } + $sql .= ' INTO TABLE ' . Util::backquote($table); + + if (strlen((string) $ldi_terminated) > 0) { + $sql .= ' FIELDS TERMINATED BY \'' . $ldi_terminated . '\''; + } + if (strlen((string) $ldi_enclosed) > 0) { + $sql .= ' ENCLOSED BY \'' + . $GLOBALS['dbi']->escapeString($ldi_enclosed) . '\''; + } + if (strlen((string) $ldi_escaped) > 0) { + $sql .= ' ESCAPED BY \'' + . $GLOBALS['dbi']->escapeString($ldi_escaped) . '\''; + } + if (strlen((string) $ldi_new_line) > 0) { + if ($ldi_new_line == 'auto') { + $ldi_new_line + = (PHP_EOL == "\n") + ? '\n' + : '\r\n'; + } + $sql .= ' LINES TERMINATED BY \'' . $ldi_new_line . '\''; + } + if ($skip_queries > 0) { + $sql .= ' IGNORE ' . $skip_queries . ' LINES'; + $skip_queries = 0; + } + if (strlen((string) $ldi_columns) > 0) { + $sql .= ' ('; + $tmp = preg_split('/,( ?)/', $ldi_columns); + $cnt_tmp = count($tmp); + for ($i = 0; $i < $cnt_tmp; $i++) { + if ($i > 0) { + $sql .= ', '; + } + /* Trim also `, if user already included backquoted fields */ + $sql .= Util::backquote( + trim($tmp[$i], " \t\r\n\0\x0B`") + ); + } // end for + $sql .= ')'; + } + + $this->import->runQuery($sql, $sql, $sql_data); + $this->import->runQuery('', '', $sql_data); + $finished = true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php new file mode 100644 index 0000000..9723daf --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php @@ -0,0 +1,604 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * MediaWiki import plugin for phpMyAdmin + * + * @package PhpMyAdmin-Import + * @subpackage MediaWiki + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Import; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; + +/** + * Handles the import for the MediaWiki format + * + * @package PhpMyAdmin-Import + * @subpackage MediaWiki + */ +class ImportMediawiki extends ImportPlugin +{ + /** + * Whether to analyze tables + * + * @var bool + */ + private $_analyze; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $this->_setAnalyze(false); + if ($GLOBALS['plugin_param'] !== 'table') { + $this->_setAnalyze(true); + } + + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText(__('MediaWiki Table')); + $importPluginProperties->setExtension('txt'); + $importPluginProperties->setMimeType('text/plain'); + $importPluginProperties->setOptions([]); + $importPluginProperties->setOptionsText(__('Options')); + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $error, $timeout_passed, $finished; + + // Defaults for parser + + // The buffer that will be used to store chunks read from the imported file + $buffer = ''; + + // Used as storage for the last part of the current chunk data + // Will be appended to the first line of the next chunk, if there is one + $last_chunk_line = ''; + + // Remembers whether the current buffer line is part of a comment + $inside_comment = false; + // Remembers whether the current buffer line is part of a data comment + $inside_data_comment = false; + // Remembers whether the current buffer line is part of a structure comment + $inside_structure_comment = false; + + // MediaWiki only accepts "\n" as row terminator + $mediawiki_new_line = "\n"; + + // Initialize the name of the current table + $cur_table_name = ""; + + while (! $finished && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + + if ($data === false) { + // Subtract data we didn't handle yet and stop processing + $GLOBALS['offset'] -= mb_strlen($buffer); + break; + } elseif ($data !== true) { + // Append new data to buffer + $buffer = $data; + unset($data); + // Don't parse string if we're not at the end + // and don't have a new line inside + if (mb_strpos($buffer, $mediawiki_new_line) === false) { + continue; + } + } + + // Because of reading chunk by chunk, the first line from the buffer + // contains only a portion of an actual line from the imported file. + // Therefore, we have to append it to the last line from the previous + // chunk. If we are at the first chunk, $last_chunk_line should be empty. + $buffer = $last_chunk_line . $buffer; + + // Process the buffer line by line + $buffer_lines = explode($mediawiki_new_line, $buffer); + + $full_buffer_lines_count = count($buffer_lines); + // If the reading is not finalised, the final line of the current chunk + // will not be complete + if (! $finished) { + $last_chunk_line = $buffer_lines[--$full_buffer_lines_count]; + } + + for ($line_nr = 0; $line_nr < $full_buffer_lines_count; ++$line_nr) { + $cur_buffer_line = trim($buffer_lines[$line_nr]); + + // If the line is empty, go to the next one + if ($cur_buffer_line === '') { + continue; + } + + $first_character = $cur_buffer_line[0]; + $matches = []; + + // Check beginning of comment + if (! strcmp(mb_substr($cur_buffer_line, 0, 4), "<!--")) { + $inside_comment = true; + continue; + } elseif ($inside_comment) { + // Check end of comment + if (! strcmp(mb_substr($cur_buffer_line, 0, 4), "-->") + ) { + // Only data comments are closed. The structure comments + // will be closed when a data comment begins (in order to + // skip structure tables) + if ($inside_data_comment) { + $inside_data_comment = false; + } + + // End comments that are not related to table structure + if (! $inside_structure_comment) { + $inside_comment = false; + } + } else { + // Check table name + $match_table_name = []; + if (preg_match( + "/^Table data for `(.*)`$/", + $cur_buffer_line, + $match_table_name + ) + ) { + $cur_table_name = $match_table_name[1]; + $inside_data_comment = true; + + $inside_structure_comment + = $this->_mngInsideStructComm( + $inside_structure_comment + ); + } elseif (preg_match( + "/^Table structure for `(.*)`$/", + $cur_buffer_line, + $match_table_name + ) + ) { + // The structure comments will be ignored + $inside_structure_comment = true; + } + } + continue; + } elseif (preg_match('/^\{\|(.*)$/', $cur_buffer_line, $matches)) { + // Check start of table + + // This will store all the column info on all rows from + // the current table read from the buffer + $cur_temp_table = []; + + // Will be used as storage for the current row in the buffer + // Once all its columns are read, it will be added to + // $cur_temp_table and then it will be emptied + $cur_temp_line = []; + + // Helps us differentiate the header columns + // from the normal columns + $in_table_header = false; + // End processing because the current line does not + // contain any column information + } elseif (mb_substr($cur_buffer_line, 0, 2) === '|-' + || mb_substr($cur_buffer_line, 0, 2) === '|+' + || mb_substr($cur_buffer_line, 0, 2) === '|}' + ) { + // Check begin row or end table + + // Add current line to the values storage + if (! empty($cur_temp_line)) { + // If the current line contains header cells + // ( marked with '!' ), + // it will be marked as table header + if ($in_table_header) { + // Set the header columns + $cur_temp_table_headers = $cur_temp_line; + } else { + // Normal line, add it to the table + $cur_temp_table[] = $cur_temp_line; + } + } + + // Empty the temporary buffer + $cur_temp_line = []; + + // No more processing required at the end of the table + if (mb_substr($cur_buffer_line, 0, 2) === '|}') { + $current_table = [ + $cur_table_name, + $cur_temp_table_headers, + $cur_temp_table, + ]; + + // Import the current table data into the database + $this->_importDataOneTable($current_table, $sql_data); + + // Reset table name + $cur_table_name = ""; + } + // What's after the row tag is now only attributes + } elseif (($first_character === '|') || ($first_character === '!')) { + // Check cell elements + + // Header cells + if ($first_character === '!') { + // Mark as table header, but treat as normal row + $cur_buffer_line = str_replace('!!', '||', $cur_buffer_line); + // Will be used to set $cur_temp_line as table header + $in_table_header = true; + } else { + $in_table_header = false; + } + + // Loop through each table cell + $cells = $this->_explodeMarkup($cur_buffer_line); + foreach ($cells as $cell) { + $cell = $this->_getCellData($cell); + + // Delete the beginning of the column, if there is one + $cell = trim($cell); + $col_start_chars = [ + "|", + "!", + ]; + foreach ($col_start_chars as $col_start_char) { + $cell = $this->_getCellContent($cell, $col_start_char); + } + + // Add the cell to the row + $cur_temp_line[] = $cell; + } // foreach $cells + } else { + // If it's none of the above, then the current line has a bad + // format + $message = Message::error( + __('Invalid format of mediawiki input on line: <br>%s.') + ); + $message->addParam($cur_buffer_line); + $error = true; + } + } // End treating full buffer lines + } // while - finished parsing buffer + } + + /** + * Imports data from a single table + * + * @param array $table containing all table info: + * <code> $table[0] - string + * containing table name + * $table[1] - array[] of + * table headers $table[2] - + * array[][] of table content + * rows </code> + * + * @param array $sql_data 2-element array with sql data + * + * @global bool $analyze whether to scan for column types + * + * @return void + */ + private function _importDataOneTable(array $table, array &$sql_data) + { + $analyze = $this->_getAnalyze(); + if ($analyze) { + // Set the table name + $this->_setTableName($table[0]); + + // Set generic names for table headers if they don't exist + $this->_setTableHeaders($table[1], $table[2][0]); + + // Create the tables array to be used in Import::buildSql() + $tables = []; + $tables[] = [ + $table[0], + $table[1], + $table[2], + ]; + + // Obtain the best-fit MySQL types for each column + $analyses = []; + $analyses[] = $this->import->analyzeTable($tables[0]); + + $this->_executeImportTables($tables, $analyses, $sql_data); + } + + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + } + + /** + * Sets the table name + * + * @param string $table_name reference to the name of the table + * + * @return void + */ + private function _setTableName(&$table_name) + { + if (empty($table_name)) { + $result = $GLOBALS['dbi']->fetchResult('SHOW TABLES'); + // todo check if the name below already exists + $table_name = 'TABLE ' . (count($result) + 1); + } + } + + /** + * Set generic names for table headers, if they don't exist + * + * @param array $table_headers reference to the array containing the headers + * of a table + * @param array $table_row array containing the first content row + * + * @return void + */ + private function _setTableHeaders(array &$table_headers, array $table_row) + { + if (empty($table_headers)) { + // The first table row should contain the number of columns + // If they are not set, generic names will be given (COL 1, COL 2, etc) + $num_cols = count($table_row); + for ($i = 0; $i < $num_cols; ++$i) { + $table_headers[$i] = 'COL ' . ($i + 1); + } + } + } + + /** + * Sets the database name and additional options and calls Import::buildSql() + * Used in PMA_importDataAllTables() and $this->_importDataOneTable() + * + * @param array $tables structure: + * array( + * array(table_name, array() column_names, array()() + * rows) + * ) + * @param array $analyses structure: + * $analyses = array( + * array(array() column_types, array() column_sizes) + * ) + * @param array $sql_data 2-element array with sql data + * + * @global string $db name of the database to import in + * + * @return void + */ + private function _executeImportTables(array &$tables, array &$analyses, array &$sql_data) + { + global $db; + + // $db_name : The currently selected database name, if applicable + // No backquotes + // $options : An associative array of options + list($db_name, $options) = $this->getDbnameAndOptions($db, 'mediawiki_DB'); + + // Array of SQL strings + // Non-applicable parameters + $create = null; + + // Create and execute necessary SQL statements from data + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + } + + /** + * Replaces all instances of the '||' separator between delimiters + * in a given string + * + * @param string $replace the string to be replaced with + * @param string $subject the text to be replaced + * + * @return string with replacements + */ + private function _delimiterReplace($replace, $subject) + { + // String that will be returned + $cleaned = ""; + // Possible states of current character + $inside_tag = false; + $inside_attribute = false; + // Attributes can be declared with either " or ' + $start_attribute_character = false; + + // The full separator is "||"; + // This remembers if the previous character was '|' + $partial_separator = false; + + // Parse text char by char + for ($i = 0, $iMax = strlen($subject); $i < $iMax; $i++) { + $cur_char = $subject[$i]; + // Check for separators + if ($cur_char == '|') { + // If we're not inside a tag, then this is part of a real separator, + // so we append it to the current segment + if (! $inside_attribute) { + $cleaned .= $cur_char; + if ($partial_separator) { + $inside_tag = false; + $inside_attribute = false; + } + } elseif ($partial_separator) { + // If we are inside a tag, we replace the current char with + // the placeholder and append that to the current segment + $cleaned .= $replace; + } + + // If the previous character was also '|', then this ends a + // full separator. If not, this may be the beginning of one + $partial_separator = ! $partial_separator; + } else { + // If we're inside a tag attribute and the current character is + // not '|', but the previous one was, it means that the single '|' + // was not appended, so we append it now + if ($partial_separator && $inside_attribute) { + $cleaned .= "|"; + } + // If the char is different from "|", no separator can be formed + $partial_separator = false; + + // any other character should be appended to the current segment + $cleaned .= $cur_char; + + if ($cur_char == '<' && ! $inside_attribute) { + // start of a tag + $inside_tag = true; + } elseif ($cur_char == '>' && ! $inside_attribute) { + // end of a tag + $inside_tag = false; + } elseif (($cur_char == '"' || $cur_char == "'") && $inside_tag) { + // start or end of an attribute + if (! $inside_attribute) { + $inside_attribute = true; + // remember the attribute`s declaration character (" or ') + $start_attribute_character = $cur_char; + } else { + if ($cur_char == $start_attribute_character) { + $inside_attribute = false; + // unset attribute declaration character + $start_attribute_character = false; + } + } + } + } + } // end for each character in $subject + + return $cleaned; + } + + /** + * Separates a string into items, similarly to explode + * Uses the '||' separator (which is standard in the mediawiki format) + * and ignores any instances of it inside markup tags + * Used in parsing buffer lines containing data cells + * + * @param string $text text to be split + * + * @return array + */ + private function _explodeMarkup($text) + { + $separator = "||"; + $placeholder = "\x00"; + + // Remove placeholder instances + $text = str_replace($placeholder, '', $text); + + // Replace instances of the separator inside HTML-like + // tags with the placeholder + $cleaned = $this->_delimiterReplace($placeholder, $text); + // Explode, then put the replaced separators back in + $items = explode($separator, $cleaned); + foreach ($items as $i => $str) { + $items[$i] = str_replace($placeholder, $separator, $str); + } + + return $items; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Returns true if the table should be analyzed, false otherwise + * + * @return bool + */ + private function _getAnalyze() + { + return $this->_analyze; + } + + /** + * Sets to true if the table should be analyzed, false otherwise + * + * @param bool $analyze status + * + * @return void + */ + private function _setAnalyze($analyze) + { + $this->_analyze = $analyze; + } + + /** + * Get cell + * + * @param string $cell Cell + * + * @return mixed + */ + private function _getCellData($cell) + { + // A cell could contain both parameters and data + $cell_data = explode('|', $cell, 2); + + // A '|' inside an invalid link should not + // be mistaken as delimiting cell parameters + if (mb_strpos($cell_data[0], '[[') === false) { + return $cell; + } + + if (count($cell_data) === 1) { + return $cell_data[0]; + } + + return $cell_data[1]; + } + + /** + * Manage $inside_structure_comment + * + * @param boolean $inside_structure_comment Value to test + * + * @return bool + */ + private function _mngInsideStructComm($inside_structure_comment) + { + // End ignoring structure rows + if ($inside_structure_comment) { + $inside_structure_comment = false; + } + + return $inside_structure_comment; + } + + /** + * Get cell content + * + * @param string $cell Cell + * @param string $col_start_char Start char + * + * @return string + */ + private function _getCellContent($cell, $col_start_char) + { + if (mb_strpos($cell, $col_start_char) === 0) { + $cell = trim(mb_substr($cell, 1)); + } + + return $cell; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php new file mode 100644 index 0000000..8e68692 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php @@ -0,0 +1,427 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * OpenDocument Spreadsheet import plugin for phpMyAdmin + * + * @todo Pretty much everything + * @todo Importing of accented characters seems to fail + * @package PhpMyAdmin-Import + * @subpackage ODS + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Import; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup; +use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup; +use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; +use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; +use SimpleXMLElement; + +/** + * Handles the import for the ODS format + * + * @package PhpMyAdmin-Import + * @subpackage ODS + */ +class ImportOds extends ImportPlugin +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText('OpenDocument Spreadsheet'); + $importPluginProperties->setExtension('ods'); + $importPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "col_names", + __( + 'The first line of the file contains the table column names' + . ' <i>(if this is unchecked, the first line will become part' + . ' of the data)</i>' + ) + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "empty_rows", + __('Do not import empty rows') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "recognize_percentages", + __( + 'Import percentages as proper decimals <i>(ex. 12.00% to .12)</i>' + ) + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "recognize_currency", + __('Import currencies <i>(ex. $5.00 to 5.00)</i>') + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $db, $error, $timeout_passed, $finished; + + $i = 0; + $len = 0; + $buffer = ""; + + /** + * Read in the file via Import::getNextChunk so that + * it can process compressed files + */ + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + /* subtract data we didn't handle yet and stop processing */ + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data !== true) { + /* Append new data to buffer */ + $buffer .= $data; + unset($data); + } + } + + unset($data); + + /** + * Disable loading of external XML entities. + */ + libxml_disable_entity_loader(); + + /** + * Load the XML string + * + * The option LIBXML_COMPACT is specified because it can + * result in increased performance without the need to + * alter the code in any way. It's basically a freebee. + */ + $xml = @simplexml_load_string($buffer, "SimpleXMLElement", LIBXML_COMPACT); + + unset($buffer); + + if ($xml === false) { + $sheets = []; + $GLOBALS['message'] = Message::error( + __( + 'The XML file specified was either malformed or incomplete.' + . ' Please correct the issue and try again.' + ) + ); + $GLOBALS['error'] = true; + } else { + /** @var SimpleXMLElement $root */ + $root = $xml->children('office', true)->{'body'}->{'spreadsheet'}; + if (empty($root)) { + $sheets = []; + $GLOBALS['message'] = Message::error( + __('Could not parse OpenDocument Spreadsheet!') + ); + $GLOBALS['error'] = true; + } else { + $sheets = $root->children('table', true); + } + } + + $tables = []; + + $max_cols = 0; + + $col_count = 0; + $col_names = []; + + $tempRow = []; + $tempRows = []; + $rows = []; + + /* Iterate over tables */ + /** @var SimpleXMLElement $sheet */ + foreach ($sheets as $sheet) { + $col_names_in_first_row = isset($_REQUEST['ods_col_names']); + + /* Iterate over rows */ + /** @var SimpleXMLElement $row */ + foreach ($sheet as $row) { + $type = $row->getName(); + if (strcmp('table-row', $type)) { + continue; + } + /* Iterate over columns */ + $cellCount = count($row); + $a = 0; + /** @var SimpleXMLElement $cell */ + foreach ($row as $cell) { + $a++; + $text = $cell->children('text', true); + $cell_attrs = $cell->attributes('office', true); + + if (count($text) != 0) { + $attr = $cell->attributes('table', true); + $num_repeat = (int) $attr['number-columns-repeated']; + $num_iterations = $num_repeat ?: 1; + + for ($k = 0; $k < $num_iterations; $k++) { + $value = $this->getValue($cell_attrs, $text); + if (! $col_names_in_first_row) { + $tempRow[] = $value; + } else { + // MySQL column names can't end with a space + // character. + $col_names[] = rtrim($value); + } + + ++$col_count; + } + continue; + } + + // skip empty repeats in the last row + if ($a == $cellCount) { + continue; + } + + $attr = $cell->attributes('table', true); + $num_null = (int) $attr['number-columns-repeated']; + + if ($num_null) { + if (! $col_names_in_first_row) { + for ($i = 0; $i < $num_null; ++$i) { + $tempRow[] = 'NULL'; + ++$col_count; + } + } else { + for ($i = 0; $i < $num_null; ++$i) { + $col_names[] = $this->import->getColumnAlphaName( + $col_count + 1 + ); + ++$col_count; + } + } + } else { + if (! $col_names_in_first_row) { + $tempRow[] = 'NULL'; + } else { + $col_names[] = $this->import->getColumnAlphaName( + $col_count + 1 + ); + } + + ++$col_count; + } + } //Endforeach + + /* Find the widest row */ + if ($col_count > $max_cols) { + $max_cols = $col_count; + } + + /* Don't include a row that is full of NULL values */ + if (! $col_names_in_first_row) { + if ($_REQUEST['ods_empty_rows']) { + foreach ($tempRow as $cell) { + if (strcmp('NULL', $cell)) { + $tempRows[] = $tempRow; + break; + } + } + } else { + $tempRows[] = $tempRow; + } + } + + $col_count = 0; + $col_names_in_first_row = false; + $tempRow = []; + } + + /* Skip over empty sheets */ + if (count($tempRows) == 0 || count($tempRows[0]) === 0) { + $col_names = []; + $tempRow = []; + $tempRows = []; + continue; + } + + /** + * Fill out each row as necessary to make + * every one exactly as wide as the widest + * row. This included column names. + */ + + /* Fill out column names */ + for ($i = count($col_names); $i < $max_cols; ++$i) { + $col_names[] = $this->import->getColumnAlphaName($i + 1); + } + + /* Fill out all rows */ + $num_rows = count($tempRows); + for ($i = 0; $i < $num_rows; ++$i) { + for ($j = count($tempRows[$i]); $j < $max_cols; ++$j) { + $tempRows[$i][] = 'NULL'; + } + } + + /* Store the table name so we know where to place the row set */ + $tbl_attr = $sheet->attributes('table', true); + $tables[] = [(string) $tbl_attr['name']]; + + /* Store the current sheet in the accumulator */ + $rows[] = [ + (string) $tbl_attr['name'], + $col_names, + $tempRows, + ]; + $tempRows = []; + $col_names = []; + $max_cols = 0; + } + + unset($tempRow); + unset($tempRows); + unset($col_names); + unset($sheets); + unset($xml); + + /** + * Bring accumulated rows into the corresponding table + */ + $num_tables = count($tables); + for ($i = 0; $i < $num_tables; ++$i) { + $num_rows = count($rows); + for ($j = 0; $j < $num_rows; ++$j) { + if (strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) { + continue; + } + + if (! isset($tables[$i][Import::COL_NAMES])) { + $tables[$i][] = $rows[$j][Import::COL_NAMES]; + } + + $tables[$i][Import::ROWS] = $rows[$j][Import::ROWS]; + } + } + + /* No longer needed */ + unset($rows); + + /* Obtain the best-fit MySQL types for each column */ + $analyses = []; + + $len = count($tables); + for ($i = 0; $i < $len; ++$i) { + $analyses[] = $this->import->analyzeTable($tables[$i]); + } + + /** + * string $db_name (no backquotes) + * + * array $table = array(table_name, array() column_names, array()() rows) + * array $tables = array of "$table"s + * + * array $analysis = array(array() column_types, array() column_sizes) + * array $analyses = array of "$analysis"s + * + * array $create = array of SQL strings + * + * array $options = an associative array of options + */ + + /* Set database name to the currently selected one, if applicable */ + list($db_name, $options) = $this->getDbnameAndOptions($db, 'ODS_DB'); + + /* Non-applicable parameters */ + $create = null; + + /* Created and execute necessary SQL statements from data */ + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + + unset($tables); + unset($analyses); + + /* Commit any possible data in buffers */ + $this->import->runQuery('', '', $sql_data); + } + + /** + * Get value + * + * @param array $cell_attrs Cell attributes + * @param array $text Texts + * + * @return float|string + */ + protected function getValue($cell_attrs, $text) + { + if ($_REQUEST['ods_recognize_percentages'] + && ! strcmp( + 'percentage', + (string) $cell_attrs['value-type'] + ) + ) { + $value = (double) $cell_attrs['value']; + + return $value; + } elseif ($_REQUEST['ods_recognize_currency'] + && ! strcmp('currency', (string) $cell_attrs['value-type']) + ) { + $value = (double) $cell_attrs['value']; + + return $value; + } + + /* We need to concatenate all paragraphs */ + $values = []; + foreach ($text as $paragraph) { + $values[] = (string) $paragraph; + } + $value = implode("\n", $values); + + return $value; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php new file mode 100644 index 0000000..614cb9e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php @@ -0,0 +1,335 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * ESRI Shape file import plugin for phpMyAdmin + * + * @package PhpMyAdmin-Import + * @subpackage ESRI_Shape + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Gis\GisFactory; +use PhpMyAdmin\Gis\GisMultiLineString; +use PhpMyAdmin\Gis\GisMultiPoint; +use PhpMyAdmin\Gis\GisPoint; +use PhpMyAdmin\Gis\GisPolygon; +use PhpMyAdmin\Import; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; +use PhpMyAdmin\Sanitize; +use PhpMyAdmin\ZipExtension; + +/** + * Handles the import for ESRI Shape files + * + * @package PhpMyAdmin-Import + * @subpackage ESRI_Shape + */ +class ImportShp extends ImportPlugin +{ + /** + * @var ZipExtension + */ + private $zipExtension; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + if (extension_loaded('zip')) { + $this->zipExtension = new ZipExtension(); + } + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText(__('ESRI Shape File')); + $importPluginProperties->setExtension('shp'); + $importPluginProperties->setOptions([]); + $importPluginProperties->setOptionsText(__('Options')); + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $db, $error, $finished, + $import_file, $local_import_file, $message; + + $GLOBALS['finished'] = false; + + $compression = $GLOBALS['import_handle']->getCompression(); + + $shp = new ShapeFileImport(1); + // If the zip archive has more than one file, + // get the correct content to the buffer from .shp file. + if ($compression == 'application/zip' + && $this->zipExtension->getNumberOfFiles($import_file) > 1 + ) { + if ($GLOBALS['import_handle']->openZip('/^.*\.shp$/i') === false) { + $message = Message::error( + __('There was an error importing the ESRI shape file: "%s".') + ); + $message->addParam($GLOBALS['import_handle']->getError()); + + return; + } + } + + $temp_dbf_file = false; + // We need dbase extension to handle .dbf file + if (extension_loaded('dbase')) { + $temp = $GLOBALS['PMA_Config']->getTempDir('shp'); + // If we can extract the zip archive to 'TempDir' + // and use the files in it for import + if ($compression == 'application/zip' && $temp !== null) { + $dbf_file_name = $this->zipExtension->findFile( + $import_file, + '/^.*\.dbf$/i' + ); + // If the corresponding .dbf file is in the zip archive + if ($dbf_file_name) { + // Extract the .dbf file and point to it. + $extracted = $this->zipExtension->extract( + $import_file, + $dbf_file_name + ); + if ($extracted !== false) { + // remove filename extension, e.g. + // dresden_osm.shp/gis.osm_transport_a_v06.dbf + // to + // dresden_osm.shp/gis.osm_transport_a_v06 + $path_parts = pathinfo($dbf_file_name); + $dbf_file_name = $path_parts['dirname'] . '/' . $path_parts['filename']; + + // sanitize filename + $dbf_file_name = Sanitize::sanitizeFilename($dbf_file_name, true); + + // concat correct filename and extension + $dbf_file_path = $temp . '/' . $dbf_file_name . '.dbf'; + + if (file_put_contents($dbf_file_path, $extracted, LOCK_EX) !== false) { + $temp_dbf_file = true; + + // Replace the .dbf with .*, as required by the bsShapeFiles library. + $shp->FileName = substr($dbf_file_path, 0, -4) . '.*'; + } + } + } + } elseif (! empty($local_import_file) + && ! empty($GLOBALS['cfg']['UploadDir']) + && $compression == 'none' + ) { + // If file is in UploadDir, use .dbf file in the same UploadDir + // to load extra data. + // Replace the .shp with .*, + // so the bsShapeFiles library correctly locates .dbf file. + $file_name = mb_substr( + $import_file, + 0, + mb_strlen($import_file) - 4 + ) . '.*'; + $shp->FileName = $file_name; + } + } + + // It should load data before file being deleted + $shp->loadFromFile(''); + + // Delete the .dbf file extracted to 'TempDir' + if ($temp_dbf_file + && isset($dbf_file_path) + && @file_exists($dbf_file_path) + ) { + unlink($dbf_file_path); + } + + if ($shp->lastError != '') { + $error = true; + $message = Message::error( + __('There was an error importing the ESRI shape file: "%s".') + ); + $message->addParam($shp->lastError); + + return; + } + + switch ($shp->shapeType) { + // ESRI Null Shape + case 0: + break; + // ESRI Point + case 1: + $gis_type = 'point'; + break; + // ESRI PolyLine + case 3: + $gis_type = 'multilinestring'; + break; + // ESRI Polygon + case 5: + $gis_type = 'multipolygon'; + break; + // ESRI MultiPoint + case 8: + $gis_type = 'multipoint'; + break; + default: + $error = true; + $message = Message::error( + __('MySQL Spatial Extension does not support ESRI type "%s".') + ); + $message->addParam($shp->getShapeName()); + return; + } + + if (isset($gis_type)) { + /** @var GisMultiLineString|GisMultiPoint|GisPoint|GisPolygon $gis_obj */ + $gis_obj = GisFactory::factory($gis_type); + } else { + $gis_obj = null; + } + + $num_rows = count($shp->records); + // If .dbf file is loaded, the number of extra data columns + $num_data_cols = $shp->getDBFHeader() !== null ? count($shp->getDBFHeader()) : 0; + + $rows = []; + $col_names = []; + if ($num_rows != 0) { + foreach ($shp->records as $record) { + $tempRow = []; + if ($gis_obj == null) { + $tempRow[] = null; + } else { + $tempRow[] = "GeomFromText('" + . $gis_obj->getShape($record->SHPData) . "')"; + } + + if ($shp->getDBFHeader() !== null) { + foreach ($shp->getDBFHeader() as $c) { + $cell = trim((string) $record->DBFData[$c[0]]); + + if (! strcmp($cell, '')) { + $cell = 'NULL'; + } + + $tempRow[] = $cell; + } + } + $rows[] = $tempRow; + } + } + + if (count($rows) === 0) { + $error = true; + $message = Message::error( + __('The imported file does not contain any data!') + ); + + return; + } + + // Column names for spatial column and the rest of the columns, + // if they are available + $col_names[] = 'SPATIAL'; + for ($n = 0; $n < $num_data_cols; $n++) { + $col_names[] = $shp->getDBFHeader()[$n][0]; + } + + // Set table name based on the number of tables + if (strlen((string) $db) > 0) { + $result = $GLOBALS['dbi']->fetchResult('SHOW TABLES'); + $table_name = 'TABLE ' . (count($result) + 1); + } else { + $table_name = 'TBL_NAME'; + } + $tables = [ + [ + $table_name, + $col_names, + $rows, + ], + ]; + + // Use data from shape file to chose best-fit MySQL types for each column + $analyses = []; + $analyses[] = $this->import->analyzeTable($tables[0]); + + $table_no = 0; + $spatial_col = 0; + $analyses[$table_no][Import::TYPES][$spatial_col] = Import::GEOMETRY; + $analyses[$table_no][Import::FORMATTEDSQL][$spatial_col] = true; + + // Set database name to the currently selected one, if applicable + if (strlen((string) $db) > 0) { + $db_name = $db; + $options = ['create_db' => false]; + } else { + $db_name = 'SHP_DB'; + $options = null; + } + + // Created and execute necessary SQL statements from data + $null_param = null; + $this->import->buildSql($db_name, $tables, $analyses, $null_param, $options, $sql_data); + + unset($tables); + unset($analyses); + + $finished = true; + $error = false; + + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + } + + /** + * Returns specified number of bytes from the buffer. + * Buffer automatically fetches next chunk of data when the buffer + * falls short. + * Sets $eof when $GLOBALS['finished'] is set and the buffer falls short. + * + * @param int $length number of bytes + * + * @return string + */ + public static function readFromBuffer($length) + { + global $buffer, $eof; + + $import = new Import(); + + if (strlen((string) $buffer) < $length) { + if ($GLOBALS['finished']) { + $eof = true; + } else { + $buffer .= $import->getNextChunk(); + } + } + $result = substr($buffer, 0, $length); + $buffer = substr($buffer, $length); + + return $result; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php new file mode 100644 index 0000000..599db37 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php @@ -0,0 +1,200 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * SQL import plugin for phpMyAdmin + * + * @package PhpMyAdmin-Import + * @subpackage SQL + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Import; +use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup; +use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup; +use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; +use PhpMyAdmin\Properties\Options\Items\SelectPropertyItem; +use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; +use PhpMyAdmin\SqlParser\Utils\BufferedQuery; + +/** + * Handles the import for the SQL format + * + * @package PhpMyAdmin-Import + * @subpackage SQL + */ +class ImportSql extends ImportPlugin +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText('SQL'); + $importPluginProperties->setExtension('sql'); + $importPluginProperties->setOptionsText(__('Options')); + + $compats = $GLOBALS['dbi']->getCompatibilities(); + if (count($compats) > 0) { + $values = []; + foreach ($compats as $val) { + $values[$val] = $val; + } + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new SelectPropertyItem( + "compatibility", + __('SQL compatibility mode:') + ); + $leaf->setValues($values); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'Server_SQL_mode', + ] + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "no_auto_value_on_zero", + __('Do not use <code>AUTO_INCREMENT</code> for zero values') + ); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'Server_SQL_mode', + 'sqlmode_no_auto_value_on_zero', + ] + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + } + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $error, $timeout_passed; + + // Handle compatibility options. + $this->_setSQLMode($GLOBALS['dbi'], $_REQUEST); + + $bq = new BufferedQuery(); + if (isset($_POST['sql_delimiter'])) { + $bq->setDelimiter($_POST['sql_delimiter']); + } + + /** + * Will be set in Import::getNextChunk(). + * + * @global bool $GLOBALS ['finished'] + */ + $GLOBALS['finished'] = false; + + while ((! $error) && (! $timeout_passed)) { + // Getting the first statement, the remaining data and the last + // delimiter. + $statement = $bq->extract(); + + // If there is no full statement, we are looking for more data. + if (empty($statement)) { + // Importing new data. + $newData = $this->import->getNextChunk(); + + // Subtract data we didn't handle yet and stop processing. + if ($newData === false) { + $GLOBALS['offset'] -= mb_strlen($bq->query); + break; + } + + // Checking if the input buffer has finished. + if ($newData === true) { + $GLOBALS['finished'] = true; + break; + } + + // Convert CR (but not CRLF) to LF otherwise all queries may + // not get executed on some platforms. + $bq->query .= preg_replace("/\r($|[^\n])/", "\n$1", $newData); + + continue; + } + + // Executing the query. + $this->import->runQuery($statement, $statement, $sql_data); + } + + // Extracting remaining statements. + while (! $error && ! $timeout_passed && ! empty($bq->query)) { + $statement = $bq->extract(true); + if (! empty($statement)) { + $this->import->runQuery($statement, $statement, $sql_data); + } + } + + // Finishing. + $this->import->runQuery('', '', $sql_data); + } + + /** + * Handle compatibility options + * + * @param DatabaseInterface $dbi Database interface + * @param array $request Request array + * + * @return void + */ + private function _setSQLMode($dbi, array $request) + { + $sql_modes = []; + if (isset($request['sql_compatibility']) + && 'NONE' != $request['sql_compatibility'] + ) { + $sql_modes[] = $request['sql_compatibility']; + } + if (isset($request['sql_no_auto_value_on_zero'])) { + $sql_modes[] = 'NO_AUTO_VALUE_ON_ZERO'; + } + if (count($sql_modes) > 0) { + $dbi->tryQuery( + 'SET SQL_MODE="' . implode(',', $sql_modes) . '"' + ); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php new file mode 100644 index 0000000..833d4e4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php @@ -0,0 +1,375 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * XML import plugin for phpMyAdmin + * + * @todo Improve efficiency + * @package PhpMyAdmin-Import + * @subpackage XML + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Import; +use PhpMyAdmin\Message; +use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; +use PhpMyAdmin\Util; +use SimpleXMLElement; + +/** + * Handles the import for the XML format + * + * @package PhpMyAdmin-Import + * @subpackage XML + */ +class ImportXml extends ImportPlugin +{ + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText(__('XML')); + $importPluginProperties->setExtension('xml'); + $importPluginProperties->setMimeType('text/xml'); + $importPluginProperties->setOptions([]); + $importPluginProperties->setOptionsText(__('Options')); + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $error, $timeout_passed, $finished, $db; + + $i = 0; + $len = 0; + $buffer = ""; + + /** + * Read in the file via Import::getNextChunk so that + * it can process compressed files + */ + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + /* subtract data we didn't handle yet and stop processing */ + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data !== true) { + /* Append new data to buffer */ + $buffer .= $data; + unset($data); + } + } + + unset($data); + + /** + * Disable loading of external XML entities. + */ + libxml_disable_entity_loader(); + + /** + * Load the XML string + * + * The option LIBXML_COMPACT is specified because it can + * result in increased performance without the need to + * alter the code in any way. It's basically a freebee. + */ + $xml = @simplexml_load_string($buffer, "SimpleXMLElement", LIBXML_COMPACT); + + unset($buffer); + + /** + * The XML was malformed + */ + if ($xml === false) { + Message::error( + __( + 'The XML file specified was either malformed or incomplete.' + . ' Please correct the issue and try again.' + ) + ) + ->display(); + unset($xml); + $GLOBALS['finished'] = false; + + return; + } + + /** + * Table accumulator + */ + $tables = []; + /** + * Row accumulator + */ + $rows = []; + + /** + * Temp arrays + */ + $tempRow = []; + $tempCells = []; + + /** + * CREATE code included (by default: no) + */ + $struct_present = false; + + /** + * Analyze the data in each table + */ + $namespaces = $xml->getNamespaces(true); + + /** + * Get the database name, collation and charset + */ + $db_attr = $xml->children(isset($namespaces['pma']) ? $namespaces['pma'] : null) + ->{'structure_schemas'}->{'database'}; + + if ($db_attr instanceof SimpleXMLElement) { + $db_attr = $db_attr->attributes(); + $db_name = (string) $db_attr['name']; + $collation = (string) $db_attr['collation']; + $charset = (string) $db_attr['charset']; + } else { + /** + * If the structure section is not present + * get the database name from the data section + */ + $db_attr = $xml->children() + ->attributes(); + $db_name = (string) $db_attr['name']; + $collation = null; + $charset = null; + } + + /** + * The XML was malformed + */ + if ($db_name === null) { + Message::error( + __( + 'The XML file specified was either malformed or incomplete.' + . ' Please correct the issue and try again.' + ) + ) + ->display(); + unset($xml); + $GLOBALS['finished'] = false; + + return; + } + + /** + * Retrieve the structure information + */ + if (isset($namespaces['pma'])) { + /** + * Get structures for all tables + * + * @var SimpleXMLElement $struct + */ + $struct = $xml->children($namespaces['pma']); + + $create = []; + + /** @var SimpleXMLElement $val1 */ + foreach ($struct as $val1) { + /** @var SimpleXMLElement $val2 */ + foreach ($val1 as $val2) { + // Need to select the correct database for the creation of + // tables, views, triggers, etc. + /** + * @todo Generating a USE here blocks importing of a table + * into another database. + */ + $attrs = $val2->attributes(); + $create[] = "USE " + . Util::backquote( + $attrs["name"] + ); + + foreach ($val2 as $val3) { + /** + * Remove the extra cosmetic spacing + */ + $val3 = str_replace(" ", "", (string) $val3); + $create[] = $val3; + } + } + } + + $struct_present = true; + } + + /** + * Move down the XML tree to the actual data + */ + $xml = $xml->children() + ->children(); + + $data_present = false; + + /** + * Only attempt to analyze/collect data if there is data present + */ + if ($xml && @count($xml->children())) { + $data_present = true; + + /** + * Process all database content + */ + foreach ($xml as $v1) { + $tbl_attr = $v1->attributes(); + + $isInTables = false; + $num_tables = count($tables); + for ($i = 0; $i < $num_tables; ++$i) { + if (! strcmp($tables[$i][Import::TBL_NAME], (string) $tbl_attr['name'])) { + $isInTables = true; + break; + } + } + + if (! $isInTables) { + $tables[] = [(string) $tbl_attr['name']]; + } + + foreach ($v1 as $v2) { + $row_attr = $v2->attributes(); + if (! array_search((string) $row_attr['name'], $tempRow)) { + $tempRow[] = (string) $row_attr['name']; + } + $tempCells[] = (string) $v2; + } + + $rows[] = [ + (string) $tbl_attr['name'], + $tempRow, + $tempCells, + ]; + + $tempRow = []; + $tempCells = []; + } + + unset($tempRow); + unset($tempCells); + unset($xml); + + /** + * Bring accumulated rows into the corresponding table + */ + $num_tables = count($tables); + for ($i = 0; $i < $num_tables; ++$i) { + $num_rows = count($rows); + for ($j = 0; $j < $num_rows; ++$j) { + if (! strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) { + if (! isset($tables[$i][Import::COL_NAMES])) { + $tables[$i][] = $rows[$j][Import::COL_NAMES]; + } + + $tables[$i][Import::ROWS][] = $rows[$j][Import::ROWS]; + } + } + } + + unset($rows); + + if (! $struct_present) { + $analyses = []; + + $len = count($tables); + for ($i = 0; $i < $len; ++$i) { + $analyses[] = $this->import->analyzeTable($tables[$i]); + } + } + } + + unset($xml); + unset($tempCells); + unset($rows); + + /** + * Only build SQL from data if there is data present + */ + if ($data_present) { + /** + * Set values to NULL if they were not present + * to maintain Import::buildSql() call integrity + */ + if (! isset($analyses)) { + $analyses = null; + if (! $struct_present) { + $create = null; + } + } + } + + /** + * string $db_name (no backquotes) + * + * array $table = array(table_name, array() column_names, array()() rows) + * array $tables = array of "$table"s + * + * array $analysis = array(array() column_types, array() column_sizes) + * array $analyses = array of "$analysis"s + * + * array $create = array of SQL strings + * + * array $options = an associative array of options + */ + + /* Set database name to the currently selected one, if applicable */ + if (strlen((string) $db)) { + /* Override the database name in the XML file, if one is selected */ + $db_name = $db; + $options = ['create_db' => false]; + } else { + if ($db_name === null) { + $db_name = 'XML_DB'; + } + + /* Set database collation/charset */ + $options = [ + 'db_collation' => $collation, + 'db_charset' => $charset, + ]; + } + + /* Created and execute necessary SQL statements from data */ + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + + unset($analyses); + unset($tables); + unset($create); + + /* Commit any possible data in buffers */ + $this->import->runQuery('', '', $sql_data); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/README b/srcs/phpmyadmin/libraries/classes/Plugins/Import/README new file mode 100644 index 0000000..e240556 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/README @@ -0,0 +1,156 @@ +This directory holds import plugins for phpMyAdmin. Any new plugin should +basically follow the structure presented here. The messages must use our +gettext mechanism, see https://wiki.phpmyadmin.net/pma/Gettext_for_developers. + +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * [Name] import plugin for phpMyAdmin + * + * @package PhpMyAdmin-Import + * @subpackage [Name] + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\Import; +use PhpMyAdmin\Plugins\ImportPlugin; + +/** + * Handles the import for the [Name] format + * + * @package PhpMyAdmin-Import + */ +class Import[Name] extends ImportPlugin +{ + /** + * optional - declare variables and descriptions + * + * @var type + */ + private $_myOptionalVariable; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new PhpMyAdmin\Properties\Plugins\ImportPluginProperties(); + $importPluginProperties->setText('[name]'); // the name of your plug-in + $importPluginProperties->setExtension('[ext]'); // extension this plug-in can handle + $importPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new + PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup( + "general_opts" + ); + + // optional : + // create primary items and add them to the group + // type - one of the classes listed in libraries/properties/options/items/ + // name - form element name + // text - description in GUI + // size - size of text element + // len - maximal size of input + // values - possible values of the item + $leaf = new PhpMyAdmin\Properties\Options\Items\RadioPropertyItem( + "structure_or_data" + ); + $leaf->setValues( + array( + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data') + ) + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array &$sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(&$sql_data = array()) + { + // get globals (others are optional) + global $error, $timeout_passed, $finished; + + $buffer = ''; + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + // subtract data we didn't handle yet and stop processing + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data === true) { + // Handle rest of buffer + } else { + // Append new data to buffer + $buffer .= $data; + } + // PARSE $buffer here, post sql queries using: + $this->import->runQuery($sql, $verbose_sql_with_comments, $sql_data); + } // End of import loop + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + } + + + // optional: + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + + /** + * Getter description + * + * @return type + */ + private function _getMyOptionalVariable() + { + return $this->_myOptionalVariable; + } + + /** + * Setter description + * + * @param type $my_optional_variable description + * + * @return void + */ + private function _setMyOptionalVariable($my_optional_variable) + { + $this->_myOptionalVariable = $my_optional_variable; + } +} +?> diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php new file mode 100644 index 0000000..8af3f90 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php @@ -0,0 +1,46 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * This class extends ShapeFile class to cater the following phpMyAdmin + * specific requirements. + * + * @package PhpMyAdmin-Import + * @subpackage ESRI_Shape + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import; + +use PhpMyAdmin\ShapeFile\ShapeFile; + +/** + * ShapeFileImport class + * + * @package PhpMyAdmin + */ +class ShapeFileImport extends ShapeFile +{ + /** + * Reads given number of bytes from SHP file + * + * @param int $bytes number of bytes + * + * @return string|false + */ + public function readSHP($bytes) + { + return ImportShp::readFromBuffer($bytes); + } + + /** + * Checks whether file is at EOF + * + * @return bool + */ + public function eofSHP() + { + global $eof; + + return $eof; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadApc.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadApc.php new file mode 100644 index 0000000..6e7e089 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadApc.php @@ -0,0 +1,83 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Provides upload functionalities for the import plugins + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import\Upload; + +use PhpMyAdmin\Display\ImportAjax; +use PhpMyAdmin\Plugins\UploadInterface; + +/** + * Implementation for the APC extension + * + * @package PhpMyAdmin + */ +class UploadApc implements UploadInterface +{ + /** + * Gets the specific upload ID Key + * + * @return string ID Key + */ + public static function getIdKey() + { + return 'APC_UPLOAD_PROGRESS'; + } + + /** + * Returns upload status. + * + * This is implementation for APC extension. + * + * @param string $id upload id + * + * @return array|null + */ + public static function getUploadStatus($id) + { + global $SESSION_KEY; + + if (trim($id) == "") { + return null; + } + if (! array_key_exists($id, $_SESSION[$SESSION_KEY])) { + $_SESSION[$SESSION_KEY][$id] = [ + 'id' => $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadApc::getIdKey(), + ]; + } + $ret = $_SESSION[$SESSION_KEY][$id]; + + if (! ImportAjax::apcCheck() || $ret['finished']) { + return $ret; + } + $status = apc_fetch('upload_' . $id); + + if ($status) { + $ret['finished'] = (bool) $status['done']; + $ret['total'] = $status['total']; + $ret['complete'] = $status['current']; + + if ($ret['total'] > 0) { + $ret['percent'] = $ret['complete'] / $ret['total'] * 100; + } + + if ($ret['percent'] == 100) { + $ret['finished'] = (bool) true; + } + + $_SESSION[$SESSION_KEY][$id] = $ret; + } + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php new file mode 100644 index 0000000..087b1e1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php @@ -0,0 +1,60 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Provides upload functionalities for the import plugins + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import\Upload; + +use PhpMyAdmin\Plugins\UploadInterface; + +/** + * Implementation for no plugin + * + * @package PhpMyAdmin + */ +class UploadNoplugin implements UploadInterface +{ + /** + * Gets the specific upload ID Key + * + * @return string ID Key + */ + public static function getIdKey() + { + return 'noplugin'; + } + + /** + * Returns upload status. + * + * This is implementation when no webserver support exists, + * so it returns just zeroes. + * + * @param string $id upload id + * + * @return array|null + */ + public static function getUploadStatus($id) + { + global $SESSION_KEY; + + if (trim($id) == "") { + return null; + } + if (! array_key_exists($id, $_SESSION[$SESSION_KEY])) { + $_SESSION[$SESSION_KEY][$id] = [ + 'id' => $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadNoplugin::getIdKey(), + ]; + } + return $_SESSION[$SESSION_KEY][$id]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php new file mode 100644 index 0000000..13578e0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php @@ -0,0 +1,97 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Provides upload functionalities for the import plugins + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import\Upload; + +use PhpMyAdmin\Display\ImportAjax; +use PhpMyAdmin\Plugins\UploadInterface; + +/** + * Implementation for upload progress + * + * @package PhpMyAdmin + */ +class UploadProgress implements UploadInterface +{ + /** + * Gets the specific upload ID Key + * + * @return string ID Key + */ + public static function getIdKey() + { + return 'UPLOAD_IDENTIFIER'; + } + + /** + * Returns upload status. + * + * This is implementation for upload progress + * + * @param string $id upload id + * + * @return array|null + */ + public static function getUploadStatus($id) + { + global $SESSION_KEY; + + if (trim($id) == "") { + return null; + } + + if (! array_key_exists($id, $_SESSION[$SESSION_KEY])) { + $_SESSION[$SESSION_KEY][$id] = [ + 'id' => $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadProgress::getIdKey(), + ]; + } + $ret = $_SESSION[$SESSION_KEY][$id]; + + if (! ImportAjax::progressCheck() || $ret['finished']) { + return $ret; + } + + $status = null; + if (function_exists('uploadprogress_get_info')) { + $status = uploadprogress_get_info($id); + } + + if ($status) { + if ($status['bytes_uploaded'] == $status['bytes_total']) { + $ret['finished'] = true; + } else { + $ret['finished'] = false; + } + $ret['total'] = $status['bytes_total']; + $ret['complete'] = $status['bytes_uploaded']; + + if ($ret['total'] > 0) { + $ret['percent'] = $ret['complete'] / $ret['total'] * 100; + } + } else { + $ret = [ + 'id' => $id, + 'finished' => true, + 'percent' => 100, + 'total' => $ret['total'], + 'complete' => $ret['total'], + 'plugin' => UploadProgress::getIdKey(), + ]; + } + + $_SESSION[$SESSION_KEY][$id] = $ret; + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php new file mode 100644 index 0000000..133b11a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php @@ -0,0 +1,95 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Provides upload functionalities for the import plugins + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins\Import\Upload; + +use PhpMyAdmin\Display\ImportAjax; +use PhpMyAdmin\Plugins\UploadInterface; + +/** + * Implementation for session + * + * @package PhpMyAdmin + */ +class UploadSession implements UploadInterface +{ + /** + * Gets the specific upload ID Key + * + * @return string ID Key + */ + public static function getIdKey() + { + return ini_get('session.upload_progress.name'); + } + + /** + * Returns upload status. + * + * This is implementation for session.upload_progress in PHP 5.4+. + * + * @param string $id upload id + * + * @return array|null + */ + public static function getUploadStatus($id) + { + global $SESSION_KEY; + + if (trim($id) == '') { + return null; + } + + if (! array_key_exists($id, $_SESSION[$SESSION_KEY])) { + $_SESSION[$SESSION_KEY][$id] = [ + 'id' => $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadSession::getIdKey(), + ]; + } + $ret = $_SESSION[$SESSION_KEY][$id]; + + if (! ImportAjax::sessionCheck() || $ret['finished']) { + return $ret; + } + + $status = false; + $sessionkey = ini_get('session.upload_progress.prefix') . $id; + + if (isset($_SESSION[$sessionkey])) { + $status = $_SESSION[$sessionkey]; + } + + if ($status) { + $ret['finished'] = $status['done']; + $ret['total'] = $status['content_length']; + $ret['complete'] = $status['bytes_processed']; + + if ($ret['total'] > 0) { + $ret['percent'] = $ret['complete'] / $ret['total'] * 100; + } + } else { + $ret = [ + 'id' => $id, + 'finished' => true, + 'percent' => 100, + 'total' => $ret['total'], + 'complete' => $ret['total'], + 'plugin' => UploadSession::getIdKey(), + ]; + } + + $_SESSION[$SESSION_KEY][$id] = $ret; + + return $ret; + } +} |
