ImportOds.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. namespace PhpMyAdmin\Plugins\Import;
  12. use PhpMyAdmin\Import;
  13. use PhpMyAdmin\Message;
  14. use PhpMyAdmin\Plugins\ImportPlugin;
  15. use PhpMyAdmin\Properties\Plugins\ImportPluginProperties;
  16. use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup;
  17. use PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup;
  18. use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem;
  19. use SimpleXMLElement;
  20. /**
  21. * Handles the import for the ODS format
  22. *
  23. * @package PhpMyAdmin-Import
  24. * @subpackage ODS
  25. */
  26. class ImportOds extends ImportPlugin
  27. {
  28. /**
  29. * Constructor
  30. */
  31. public function __construct()
  32. {
  33. $this->setProperties();
  34. }
  35. /**
  36. * Sets the import plugin properties.
  37. * Called in the constructor.
  38. *
  39. * @return void
  40. */
  41. protected function setProperties()
  42. {
  43. $importPluginProperties = new ImportPluginProperties();
  44. $importPluginProperties->setText('OpenDocument Spreadsheet');
  45. $importPluginProperties->setExtension('ods');
  46. $importPluginProperties->setOptionsText(__('Options'));
  47. // create the root group that will be the options field for
  48. // $importPluginProperties
  49. // this will be shown as "Format specific options"
  50. $importSpecificOptions = new OptionsPropertyRootGroup(
  51. "Format Specific Options"
  52. );
  53. // general options main group
  54. $generalOptions = new OptionsPropertyMainGroup("general_opts");
  55. // create primary items and add them to the group
  56. $leaf = new BoolPropertyItem(
  57. "col_names",
  58. __(
  59. 'The first line of the file contains the table column names'
  60. . ' <i>(if this is unchecked, the first line will become part'
  61. . ' of the data)</i>'
  62. )
  63. );
  64. $generalOptions->addProperty($leaf);
  65. $leaf = new BoolPropertyItem(
  66. "empty_rows",
  67. __('Do not import empty rows')
  68. );
  69. $generalOptions->addProperty($leaf);
  70. $leaf = new BoolPropertyItem(
  71. "recognize_percentages",
  72. __(
  73. 'Import percentages as proper decimals <i>(ex. 12.00% to .12)</i>'
  74. )
  75. );
  76. $generalOptions->addProperty($leaf);
  77. $leaf = new BoolPropertyItem(
  78. "recognize_currency",
  79. __('Import currencies <i>(ex. $5.00 to 5.00)</i>')
  80. );
  81. $generalOptions->addProperty($leaf);
  82. // add the main group to the root group
  83. $importSpecificOptions->addProperty($generalOptions);
  84. // set the options for the import plugin property item
  85. $importPluginProperties->setOptions($importSpecificOptions);
  86. $this->properties = $importPluginProperties;
  87. }
  88. /**
  89. * Handles the whole import logic
  90. *
  91. * @param array &$sql_data 2-element array with sql data
  92. *
  93. * @return void
  94. */
  95. public function doImport(array &$sql_data = array())
  96. {
  97. global $db, $error, $timeout_passed, $finished;
  98. $i = 0;
  99. $len = 0;
  100. $buffer = "";
  101. /**
  102. * Read in the file via Import::getNextChunk so that
  103. * it can process compressed files
  104. */
  105. while (!($finished && $i >= $len) && !$error && !$timeout_passed) {
  106. $data = Import::getNextChunk();
  107. if ($data === false) {
  108. /* subtract data we didn't handle yet and stop processing */
  109. $GLOBALS['offset'] -= strlen($buffer);
  110. break;
  111. } elseif ($data === true) {
  112. /* Handle rest of buffer */
  113. } else {
  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 = array();
  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 = array();
  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 = array();
  156. $max_cols = 0;
  157. $col_count = 0;
  158. $col_names = array();
  159. $tempRow = array();
  160. $tempRows = array();
  161. $rows = array();
  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 ? $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[] = 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[] = 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 = array();
  249. }
  250. /* Skip over empty sheets */
  251. if (count($tempRows) == 0 || count($tempRows[0]) == 0) {
  252. $col_names = array();
  253. $tempRow = array();
  254. $tempRows = array();
  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[] = 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[] = array((string)$tbl_attr['name']);
  276. /* Store the current sheet in the accumulator */
  277. $rows[] = array((string)$tbl_attr['name'], $col_names, $tempRows);
  278. $tempRows = array();
  279. $col_names = array();
  280. $max_cols = 0;
  281. }
  282. unset($tempRow);
  283. unset($tempRows);
  284. unset($col_names);
  285. unset($sheets);
  286. unset($xml);
  287. /**
  288. * Bring accumulated rows into the corresponding table
  289. */
  290. $num_tables = count($tables);
  291. for ($i = 0; $i < $num_tables; ++$i) {
  292. $num_rows = count($rows);
  293. for ($j = 0; $j < $num_rows; ++$j) {
  294. if (strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) {
  295. continue;
  296. }
  297. if (!isset($tables[$i][Import::COL_NAMES])) {
  298. $tables[$i][] = $rows[$j][Import::COL_NAMES];
  299. }
  300. $tables[$i][Import::ROWS] = $rows[$j][Import::ROWS];
  301. }
  302. }
  303. /* No longer needed */
  304. unset($rows);
  305. /* Obtain the best-fit MySQL types for each column */
  306. $analyses = array();
  307. $len = count($tables);
  308. for ($i = 0; $i < $len; ++$i) {
  309. $analyses[] = Import::analyzeTable($tables[$i]);
  310. }
  311. /**
  312. * string $db_name (no backquotes)
  313. *
  314. * array $table = array(table_name, array() column_names, array()() rows)
  315. * array $tables = array of "$table"s
  316. *
  317. * array $analysis = array(array() column_types, array() column_sizes)
  318. * array $analyses = array of "$analysis"s
  319. *
  320. * array $create = array of SQL strings
  321. *
  322. * array $options = an associative array of options
  323. */
  324. /* Set database name to the currently selected one, if applicable */
  325. list($db_name, $options) = $this->getDbnameAndOptions($db, 'ODS_DB');
  326. /* Non-applicable parameters */
  327. $create = null;
  328. /* Created and execute necessary SQL statements from data */
  329. Import::buildSql($db_name, $tables, $analyses, $create, $options, $sql_data);
  330. unset($tables);
  331. unset($analyses);
  332. /* Commit any possible data in buffers */
  333. Import::runQuery('', '', $sql_data);
  334. }
  335. /**
  336. * Get value
  337. *
  338. * @param array $cell_attrs Cell attributes
  339. * @param array $text Texts
  340. *
  341. * @return float|string
  342. */
  343. protected function getValue($cell_attrs, $text)
  344. {
  345. if ($_REQUEST['ods_recognize_percentages']
  346. && !strcmp(
  347. 'percentage',
  348. $cell_attrs['value-type']
  349. )
  350. ) {
  351. $value = (double)$cell_attrs['value'];
  352. return $value;
  353. } elseif ($_REQUEST['ods_recognize_currency']
  354. && !strcmp('currency', $cell_attrs['value-type'])
  355. ) {
  356. $value = (double)$cell_attrs['value'];
  357. return $value;
  358. }
  359. /* We need to concatenate all paragraphs */
  360. $values = array();
  361. foreach ($text as $paragraph) {
  362. $values[] = (string)$paragraph;
  363. }
  364. $value = implode("\n", $values);
  365. return $value;
  366. }
  367. }