ImportOds.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * OpenDocument Spreadsheet import plugin for phpMyAdmin
  5. *
  6. * @todo Pretty much everything
  7. * @todo Importing of accented characters seems to fail
  8. * @package PhpMyAdmin-Import
  9. * @subpackage ODS
  10. */
  11. declare(strict_types=1);
  12. namespace PhpMyAdmin\Plugins\Import;
  13. use PhpMyAdmin\Import;
  14. use PhpMyAdmin\Message;
  15. use PhpMyAdmin\Plugins\ImportPlugin;
  16. use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup;
  17. use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup;
  18. use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem;
  19. use PhpMyAdmin\Properties\Plugins\ImportPluginProperties;
  20. use SimpleXMLElement;
  21. /**
  22. * Handles the import for the ODS format
  23. *
  24. * @package PhpMyAdmin-Import
  25. * @subpackage ODS
  26. */
  27. class ImportOds extends ImportPlugin
  28. {
  29. /**
  30. * Constructor
  31. */
  32. public function __construct()
  33. {
  34. parent::__construct();
  35. $this->setProperties();
  36. }
  37. /**
  38. * Sets the import plugin properties.
  39. * Called in the constructor.
  40. *
  41. * @return void
  42. */
  43. protected function setProperties()
  44. {
  45. $importPluginProperties = new ImportPluginProperties();
  46. $importPluginProperties->setText('OpenDocument Spreadsheet');
  47. $importPluginProperties->setExtension('ods');
  48. $importPluginProperties->setOptionsText(__('Options'));
  49. // create the root group that will be the options field for
  50. // $importPluginProperties
  51. // this will be shown as "Format specific options"
  52. $importSpecificOptions = new OptionsPropertyRootGroup(
  53. "Format Specific Options"
  54. );
  55. // general options main group
  56. $generalOptions = new OptionsPropertyMainGroup("general_opts");
  57. // create primary items and add them to the group
  58. $leaf = new BoolPropertyItem(
  59. "col_names",
  60. __(
  61. 'The first line of the file contains the table column names'
  62. . ' <i>(if this is unchecked, the first line will become part'
  63. . ' of the data)</i>'
  64. )
  65. );
  66. $generalOptions->addProperty($leaf);
  67. $leaf = new BoolPropertyItem(
  68. "empty_rows",
  69. __('Do not import empty rows')
  70. );
  71. $generalOptions->addProperty($leaf);
  72. $leaf = new BoolPropertyItem(
  73. "recognize_percentages",
  74. __(
  75. 'Import percentages as proper decimals <i>(ex. 12.00% to .12)</i>'
  76. )
  77. );
  78. $generalOptions->addProperty($leaf);
  79. $leaf = new BoolPropertyItem(
  80. "recognize_currency",
  81. __('Import currencies <i>(ex. $5.00 to 5.00)</i>')
  82. );
  83. $generalOptions->addProperty($leaf);
  84. // add the main group to the root group
  85. $importSpecificOptions->addProperty($generalOptions);
  86. // set the options for the import plugin property item
  87. $importPluginProperties->setOptions($importSpecificOptions);
  88. $this->properties = $importPluginProperties;
  89. }
  90. /**
  91. * Handles the whole import logic
  92. *
  93. * @param array $sql_data 2-element array with sql data
  94. *
  95. * @return void
  96. */
  97. public function doImport(array &$sql_data = [])
  98. {
  99. global $db, $error, $timeout_passed, $finished;
  100. $i = 0;
  101. $len = 0;
  102. $buffer = "";
  103. /**
  104. * Read in the file via Import::getNextChunk so that
  105. * it can process compressed files
  106. */
  107. while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) {
  108. $data = $this->import->getNextChunk();
  109. if ($data === false) {
  110. /* subtract data we didn't handle yet and stop processing */
  111. $GLOBALS['offset'] -= strlen($buffer);
  112. break;
  113. } elseif ($data !== true) {
  114. /* Append new data to buffer */
  115. $buffer .= $data;
  116. unset($data);
  117. }
  118. }
  119. unset($data);
  120. /**
  121. * Disable loading of external XML entities.
  122. */
  123. libxml_disable_entity_loader();
  124. /**
  125. * Load the XML string
  126. *
  127. * The option LIBXML_COMPACT is specified because it can
  128. * result in increased performance without the need to
  129. * alter the code in any way. It's basically a freebee.
  130. */
  131. $xml = @simplexml_load_string($buffer, "SimpleXMLElement", LIBXML_COMPACT);
  132. unset($buffer);
  133. if ($xml === false) {
  134. $sheets = [];
  135. $GLOBALS['message'] = Message::error(
  136. __(
  137. 'The XML file specified was either malformed or incomplete.'
  138. . ' Please correct the issue and try again.'
  139. )
  140. );
  141. $GLOBALS['error'] = true;
  142. } else {
  143. /** @var SimpleXMLElement $root */
  144. $root = $xml->children('office', true)->{'body'}->{'spreadsheet'};
  145. if (empty($root)) {
  146. $sheets = [];
  147. $GLOBALS['message'] = Message::error(
  148. __('Could not parse OpenDocument Spreadsheet!')
  149. );
  150. $GLOBALS['error'] = true;
  151. } else {
  152. $sheets = $root->children('table', true);
  153. }
  154. }
  155. $tables = [];
  156. $max_cols = 0;
  157. $col_count = 0;
  158. $col_names = [];
  159. $tempRow = [];
  160. $tempRows = [];
  161. $rows = [];
  162. /* Iterate over tables */
  163. /** @var SimpleXMLElement $sheet */
  164. foreach ($sheets as $sheet) {
  165. $col_names_in_first_row = isset($_REQUEST['ods_col_names']);
  166. /* Iterate over rows */
  167. /** @var SimpleXMLElement $row */
  168. foreach ($sheet as $row) {
  169. $type = $row->getName();
  170. if (strcmp('table-row', $type)) {
  171. continue;
  172. }
  173. /* Iterate over columns */
  174. $cellCount = count($row);
  175. $a = 0;
  176. /** @var SimpleXMLElement $cell */
  177. foreach ($row as $cell) {
  178. $a++;
  179. $text = $cell->children('text', true);
  180. $cell_attrs = $cell->attributes('office', true);
  181. if (count($text) != 0) {
  182. $attr = $cell->attributes('table', true);
  183. $num_repeat = (int) $attr['number-columns-repeated'];
  184. $num_iterations = $num_repeat ?: 1;
  185. for ($k = 0; $k < $num_iterations; $k++) {
  186. $value = $this->getValue($cell_attrs, $text);
  187. if (! $col_names_in_first_row) {
  188. $tempRow[] = $value;
  189. } else {
  190. // MySQL column names can't end with a space
  191. // character.
  192. $col_names[] = rtrim($value);
  193. }
  194. ++$col_count;
  195. }
  196. continue;
  197. }
  198. // skip empty repeats in the last row
  199. if ($a == $cellCount) {
  200. continue;
  201. }
  202. $attr = $cell->attributes('table', true);
  203. $num_null = (int) $attr['number-columns-repeated'];
  204. if ($num_null) {
  205. if (! $col_names_in_first_row) {
  206. for ($i = 0; $i < $num_null; ++$i) {
  207. $tempRow[] = 'NULL';
  208. ++$col_count;
  209. }
  210. } else {
  211. for ($i = 0; $i < $num_null; ++$i) {
  212. $col_names[] = $this->import->getColumnAlphaName(
  213. $col_count + 1
  214. );
  215. ++$col_count;
  216. }
  217. }
  218. } else {
  219. if (! $col_names_in_first_row) {
  220. $tempRow[] = 'NULL';
  221. } else {
  222. $col_names[] = $this->import->getColumnAlphaName(
  223. $col_count + 1
  224. );
  225. }
  226. ++$col_count;
  227. }
  228. } //Endforeach
  229. /* Find the widest row */
  230. if ($col_count > $max_cols) {
  231. $max_cols = $col_count;
  232. }
  233. /* Don't include a row that is full of NULL values */
  234. if (! $col_names_in_first_row) {
  235. if ($_REQUEST['ods_empty_rows']) {
  236. foreach ($tempRow as $cell) {
  237. if (strcmp('NULL', $cell)) {
  238. $tempRows[] = $tempRow;
  239. break;
  240. }
  241. }
  242. } else {
  243. $tempRows[] = $tempRow;
  244. }
  245. }
  246. $col_count = 0;
  247. $col_names_in_first_row = false;
  248. $tempRow = [];
  249. }
  250. /* Skip over empty sheets */
  251. if (count($tempRows) == 0 || count($tempRows[0]) === 0) {
  252. $col_names = [];
  253. $tempRow = [];
  254. $tempRows = [];
  255. continue;
  256. }
  257. /**
  258. * Fill out each row as necessary to make
  259. * every one exactly as wide as the widest
  260. * row. This included column names.
  261. */
  262. /* Fill out column names */
  263. for ($i = count($col_names); $i < $max_cols; ++$i) {
  264. $col_names[] = $this->import->getColumnAlphaName($i + 1);
  265. }
  266. /* Fill out all rows */
  267. $num_rows = count($tempRows);
  268. for ($i = 0; $i < $num_rows; ++$i) {
  269. for ($j = count($tempRows[$i]); $j < $max_cols; ++$j) {
  270. $tempRows[$i][] = 'NULL';
  271. }
  272. }
  273. /* Store the table name so we know where to place the row set */
  274. $tbl_attr = $sheet->attributes('table', true);
  275. $tables[] = [(string) $tbl_attr['name']];
  276. /* Store the current sheet in the accumulator */
  277. $rows[] = [
  278. (string) $tbl_attr['name'],
  279. $col_names,
  280. $tempRows,
  281. ];
  282. $tempRows = [];
  283. $col_names = [];
  284. $max_cols = 0;
  285. }
  286. unset($tempRow);
  287. unset($tempRows);
  288. unset($col_names);
  289. unset($sheets);
  290. unset($xml);
  291. /**
  292. * Bring accumulated rows into the corresponding table
  293. */
  294. $num_tables = count($tables);
  295. for ($i = 0; $i < $num_tables; ++$i) {
  296. $num_rows = count($rows);
  297. for ($j = 0; $j < $num_rows; ++$j) {
  298. if (strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) {
  299. continue;
  300. }
  301. if (! isset($tables[$i][Import::COL_NAMES])) {
  302. $tables[$i][] = $rows[$j][Import::COL_NAMES];
  303. }
  304. $tables[$i][Import::ROWS] = $rows[$j][Import::ROWS];
  305. }
  306. }
  307. /* No longer needed */
  308. unset($rows);
  309. /* Obtain the best-fit MySQL types for each column */
  310. $analyses = [];
  311. $len = count($tables);
  312. for ($i = 0; $i < $len; ++$i) {
  313. $analyses[] = $this->import->analyzeTable($tables[$i]);
  314. }
  315. /**
  316. * string $db_name (no backquotes)
  317. *
  318. * array $table = array(table_name, array() column_names, array()() rows)
  319. * array $tables = array of "$table"s
  320. *
  321. * array $analysis = array(array() column_types, array() column_sizes)
  322. * array $analyses = array of "$analysis"s
  323. *
  324. * array $create = array of SQL strings
  325. *
  326. * array $options = an associative array of options
  327. */
  328. /* Set database name to the currently selected one, if applicable */
  329. list($db_name, $options) = $this->getDbnameAndOptions($db, 'ODS_DB');
  330. /* Non-applicable parameters */
  331. $create = null;
  332. /* Created and execute necessary SQL statements from data */
  333. $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data);
  334. unset($tables);
  335. unset($analyses);
  336. /* Commit any possible data in buffers */
  337. $this->import->runQuery('', '', $sql_data);
  338. }
  339. /**
  340. * Get value
  341. *
  342. * @param array $cell_attrs Cell attributes
  343. * @param array $text Texts
  344. *
  345. * @return float|string
  346. */
  347. protected function getValue($cell_attrs, $text)
  348. {
  349. if ($_REQUEST['ods_recognize_percentages']
  350. && ! strcmp(
  351. 'percentage',
  352. (string) $cell_attrs['value-type']
  353. )
  354. ) {
  355. $value = (double) $cell_attrs['value'];
  356. return $value;
  357. } elseif ($_REQUEST['ods_recognize_currency']
  358. && ! strcmp('currency', (string) $cell_attrs['value-type'])
  359. ) {
  360. $value = (double) $cell_attrs['value'];
  361. return $value;
  362. }
  363. /* We need to concatenate all paragraphs */
  364. $values = [];
  365. foreach ($text as $paragraph) {
  366. $values[] = (string) $paragraph;
  367. }
  368. $value = implode("\n", $values);
  369. return $value;
  370. }
  371. }