ImportXml.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. /**
  3. * XML import plugin for phpMyAdmin
  4. *
  5. * @todo Improve efficiency
  6. */
  7. declare(strict_types=1);
  8. namespace PhpMyAdmin\Plugins\Import;
  9. use PhpMyAdmin\File;
  10. use PhpMyAdmin\Import;
  11. use PhpMyAdmin\Message;
  12. use PhpMyAdmin\Plugins\ImportPlugin;
  13. use PhpMyAdmin\Properties\Plugins\ImportPluginProperties;
  14. use PhpMyAdmin\Util;
  15. use SimpleXMLElement;
  16. use function __;
  17. use function count;
  18. use function in_array;
  19. use function libxml_disable_entity_loader;
  20. use function simplexml_load_string;
  21. use function str_replace;
  22. use function strcmp;
  23. use function strlen;
  24. use const LIBXML_COMPACT;
  25. use const PHP_VERSION_ID;
  26. /**
  27. * Handles the import for the XML format
  28. */
  29. class ImportXml extends ImportPlugin
  30. {
  31. /**
  32. * @psalm-return non-empty-lowercase-string
  33. */
  34. public function getName(): string
  35. {
  36. return 'xml';
  37. }
  38. protected function setProperties(): ImportPluginProperties
  39. {
  40. $importPluginProperties = new ImportPluginProperties();
  41. $importPluginProperties->setText(__('XML'));
  42. $importPluginProperties->setExtension('xml');
  43. $importPluginProperties->setMimeType('text/xml');
  44. $importPluginProperties->setOptionsText(__('Options'));
  45. return $importPluginProperties;
  46. }
  47. /**
  48. * Handles the whole import logic
  49. *
  50. * @param array $sql_data 2-element array with sql data
  51. */
  52. public function doImport(?File $importHandle = null, array &$sql_data = []): void
  53. {
  54. global $error, $timeout_passed, $finished, $db;
  55. $buffer = '';
  56. /**
  57. * Read in the file via Import::getNextChunk so that
  58. * it can process compressed files
  59. */
  60. while (! $finished && ! $error && ! $timeout_passed) {
  61. $data = $this->import->getNextChunk($importHandle);
  62. if ($data === false) {
  63. /* subtract data we didn't handle yet and stop processing */
  64. $GLOBALS['offset'] -= strlen($buffer);
  65. break;
  66. }
  67. if ($data === true) {
  68. continue;
  69. }
  70. /* Append new data to buffer */
  71. $buffer .= $data;
  72. }
  73. /**
  74. * Disable loading of external XML entities for PHP versions below 8.0.
  75. */
  76. if (PHP_VERSION_ID < 80000) {
  77. // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
  78. libxml_disable_entity_loader();
  79. }
  80. /**
  81. * Load the XML string
  82. *
  83. * The option LIBXML_COMPACT is specified because it can
  84. * result in increased performance without the need to
  85. * alter the code in any way. It's basically a freebee.
  86. */
  87. $xml = @simplexml_load_string($buffer, SimpleXMLElement::class, LIBXML_COMPACT);
  88. unset($buffer);
  89. /**
  90. * The XML was malformed
  91. */
  92. if ($xml === false) {
  93. echo Message::error(
  94. __(
  95. 'The XML file specified was either malformed or incomplete. Please correct the issue and try again.'
  96. )
  97. )->getDisplay();
  98. unset($xml);
  99. $GLOBALS['finished'] = false;
  100. return;
  101. }
  102. /**
  103. * Table accumulator
  104. */
  105. $tables = [];
  106. /**
  107. * Row accumulator
  108. */
  109. $rows = [];
  110. /**
  111. * Temp arrays
  112. */
  113. $tempRow = [];
  114. $tempCells = [];
  115. /**
  116. * CREATE code included (by default: no)
  117. */
  118. $struct_present = false;
  119. /**
  120. * Analyze the data in each table
  121. */
  122. $namespaces = $xml->getNamespaces(true);
  123. /**
  124. * Get the database name, collation and charset
  125. */
  126. $db_attr = $xml->children($namespaces['pma'] ?? null)
  127. ->{'structure_schemas'}->{'database'};
  128. if ($db_attr instanceof SimpleXMLElement) {
  129. $db_attr = $db_attr->attributes();
  130. $db_name = (string) $db_attr['name'];
  131. $collation = (string) $db_attr['collation'];
  132. $charset = (string) $db_attr['charset'];
  133. } else {
  134. /**
  135. * If the structure section is not present
  136. * get the database name from the data section
  137. */
  138. $db_attr = $xml->children()
  139. ->attributes();
  140. $db_name = (string) $db_attr['name'];
  141. $collation = null;
  142. $charset = null;
  143. }
  144. /**
  145. * The XML was malformed
  146. */
  147. if ($db_name === '') {
  148. echo Message::error(
  149. __(
  150. 'The XML file specified was either malformed or incomplete. Please correct the issue and try again.'
  151. )
  152. )->getDisplay();
  153. unset($xml);
  154. $GLOBALS['finished'] = false;
  155. return;
  156. }
  157. /**
  158. * Retrieve the structure information
  159. */
  160. if (isset($namespaces['pma'])) {
  161. /**
  162. * Get structures for all tables
  163. *
  164. * @var SimpleXMLElement $struct
  165. */
  166. $struct = $xml->children($namespaces['pma']);
  167. $create = [];
  168. foreach ($struct as $val1) {
  169. foreach ($val1 as $val2) {
  170. // Need to select the correct database for the creation of
  171. // tables, views, triggers, etc.
  172. /**
  173. * @todo Generating a USE here blocks importing of a table
  174. * into another database.
  175. */
  176. $attrs = $val2->attributes();
  177. $create[] = 'USE ' . Util::backquote((string) $attrs['name']);
  178. foreach ($val2 as $val3) {
  179. /**
  180. * Remove the extra cosmetic spacing
  181. */
  182. $val3 = str_replace(' ', '', (string) $val3);
  183. $create[] = $val3;
  184. }
  185. }
  186. }
  187. $struct_present = true;
  188. }
  189. /**
  190. * Move down the XML tree to the actual data
  191. */
  192. $xml = $xml->children()
  193. ->children();
  194. $data_present = false;
  195. /**
  196. * Only attempt to analyze/collect data if there is data present
  197. */
  198. if ($xml && $xml->children()->count()) {
  199. $data_present = true;
  200. /**
  201. * Process all database content
  202. */
  203. foreach ($xml as $v1) {
  204. /** @psalm-suppress PossiblyNullReference */
  205. $tbl_attr = $v1->attributes();
  206. $isInTables = false;
  207. $num_tables = count($tables);
  208. for ($i = 0; $i < $num_tables; ++$i) {
  209. if (! strcmp($tables[$i][Import::TBL_NAME], (string) $tbl_attr['name'])) {
  210. $isInTables = true;
  211. break;
  212. }
  213. }
  214. if (! $isInTables) {
  215. $tables[] = [(string) $tbl_attr['name']];
  216. }
  217. foreach ($v1 as $v2) {
  218. /** @psalm-suppress PossiblyNullReference */
  219. $row_attr = $v2->attributes();
  220. if (! in_array((string) $row_attr['name'], $tempRow)) {
  221. $tempRow[] = (string) $row_attr['name'];
  222. }
  223. $tempCells[] = (string) $v2;
  224. }
  225. $rows[] = [
  226. (string) $tbl_attr['name'],
  227. $tempRow,
  228. $tempCells,
  229. ];
  230. $tempRow = [];
  231. $tempCells = [];
  232. }
  233. unset($tempRow, $tempCells, $xml);
  234. /**
  235. * Bring accumulated rows into the corresponding table
  236. */
  237. $num_tables = count($tables);
  238. for ($i = 0; $i < $num_tables; ++$i) {
  239. $num_rows = count($rows);
  240. for ($j = 0; $j < $num_rows; ++$j) {
  241. if (strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) {
  242. continue;
  243. }
  244. if (! isset($tables[$i][Import::COL_NAMES])) {
  245. $tables[$i][] = $rows[$j][Import::COL_NAMES];
  246. }
  247. $tables[$i][Import::ROWS][] = $rows[$j][Import::ROWS];
  248. }
  249. }
  250. unset($rows);
  251. if (! $struct_present) {
  252. $analyses = [];
  253. $len = count($tables);
  254. for ($i = 0; $i < $len; ++$i) {
  255. $analyses[] = $this->import->analyzeTable($tables[$i]);
  256. }
  257. }
  258. }
  259. unset($xml, $tempCells, $rows);
  260. /**
  261. * Only build SQL from data if there is data present
  262. */
  263. if ($data_present) {
  264. /**
  265. * Set values to NULL if they were not present
  266. * to maintain Import::buildSql() call integrity
  267. */
  268. if (! isset($analyses)) {
  269. $analyses = null;
  270. if (! $struct_present) {
  271. $create = null;
  272. }
  273. }
  274. }
  275. /**
  276. * string $db_name (no backquotes)
  277. *
  278. * array $table = array(table_name, array() column_names, array()() rows)
  279. * array $tables = array of "$table"s
  280. *
  281. * array $analysis = array(array() column_types, array() column_sizes)
  282. * array $analyses = array of "$analysis"s
  283. *
  284. * array $create = array of SQL strings
  285. *
  286. * array $options = an associative array of options
  287. */
  288. /* Set database name to the currently selected one, if applicable */
  289. if (strlen((string) $db)) {
  290. /* Override the database name in the XML file, if one is selected */
  291. $db_name = $db;
  292. $options = ['create_db' => false];
  293. } else {
  294. /* Set database collation/charset */
  295. $options = [
  296. 'db_collation' => $collation,
  297. 'db_charset' => $charset,
  298. ];
  299. }
  300. /* Created and execute necessary SQL statements from data */
  301. $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data);
  302. unset($analyses, $tables, $create);
  303. /* Commit any possible data in buffers */
  304. $this->import->runQuery('', '', $sql_data);
  305. }
  306. }