ImportShp.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <?php
  2. /**
  3. * ESRI Shape file import plugin for phpMyAdmin
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Plugins\Import;
  7. use PhpMyAdmin\File;
  8. use PhpMyAdmin\Gis\GisFactory;
  9. use PhpMyAdmin\Gis\GisMultiLineString;
  10. use PhpMyAdmin\Gis\GisMultiPoint;
  11. use PhpMyAdmin\Gis\GisPoint;
  12. use PhpMyAdmin\Gis\GisPolygon;
  13. use PhpMyAdmin\Import;
  14. use PhpMyAdmin\Message;
  15. use PhpMyAdmin\Plugins\ImportPlugin;
  16. use PhpMyAdmin\Properties\Plugins\ImportPluginProperties;
  17. use PhpMyAdmin\Sanitize;
  18. use PhpMyAdmin\ZipExtension;
  19. use ZipArchive;
  20. use const LOCK_EX;
  21. use function count;
  22. use function extension_loaded;
  23. use function file_exists;
  24. use function file_put_contents;
  25. use function mb_strlen;
  26. use function mb_substr;
  27. use function pathinfo;
  28. use function strcmp;
  29. use function strlen;
  30. use function substr;
  31. use function trim;
  32. use function unlink;
  33. /**
  34. * Handles the import for ESRI Shape files
  35. */
  36. class ImportShp extends ImportPlugin
  37. {
  38. /** @var ZipExtension */
  39. private $zipExtension;
  40. public function __construct()
  41. {
  42. parent::__construct();
  43. $this->setProperties();
  44. if (! extension_loaded('zip')) {
  45. return;
  46. }
  47. $this->zipExtension = new ZipExtension(new ZipArchive());
  48. }
  49. /**
  50. * Sets the import plugin properties.
  51. * Called in the constructor.
  52. *
  53. * @return void
  54. */
  55. protected function setProperties()
  56. {
  57. $importPluginProperties = new ImportPluginProperties();
  58. $importPluginProperties->setText(__('ESRI Shape File'));
  59. $importPluginProperties->setExtension('shp');
  60. $importPluginProperties->setOptions([]);
  61. $importPluginProperties->setOptionsText(__('Options'));
  62. $this->properties = $importPluginProperties;
  63. }
  64. /**
  65. * Handles the whole import logic
  66. *
  67. * @param array $sql_data 2-element array with sql data
  68. *
  69. * @return void
  70. */
  71. public function doImport(?File $importHandle = null, array &$sql_data = [])
  72. {
  73. global $db, $error, $finished, $import_file, $local_import_file, $message, $dbi;
  74. $GLOBALS['finished'] = false;
  75. if ($importHandle === null) {
  76. return;
  77. }
  78. /** @see ImportShp::readFromBuffer() */
  79. $GLOBALS['importHandle'] = $importHandle;
  80. $compression = $importHandle->getCompression();
  81. $shp = new ShapeFileImport(1);
  82. // If the zip archive has more than one file,
  83. // get the correct content to the buffer from .shp file.
  84. if ($compression === 'application/zip'
  85. && $this->zipExtension->getNumberOfFiles($import_file) > 1
  86. ) {
  87. if ($importHandle->openZip('/^.*\.shp$/i') === false) {
  88. $message = Message::error(
  89. __('There was an error importing the ESRI shape file: "%s".')
  90. );
  91. $message->addParam($importHandle->getError());
  92. return;
  93. }
  94. }
  95. $temp_dbf_file = false;
  96. // We need dbase extension to handle .dbf file
  97. if (extension_loaded('dbase')) {
  98. $temp = $GLOBALS['PMA_Config']->getTempDir('shp');
  99. // If we can extract the zip archive to 'TempDir'
  100. // and use the files in it for import
  101. if ($compression === 'application/zip' && $temp !== null) {
  102. $dbf_file_name = $this->zipExtension->findFile(
  103. $import_file,
  104. '/^.*\.dbf$/i'
  105. );
  106. // If the corresponding .dbf file is in the zip archive
  107. if ($dbf_file_name) {
  108. // Extract the .dbf file and point to it.
  109. $extracted = $this->zipExtension->extract(
  110. $import_file,
  111. $dbf_file_name
  112. );
  113. if ($extracted !== false) {
  114. // remove filename extension, e.g.
  115. // dresden_osm.shp/gis.osm_transport_a_v06.dbf
  116. // to
  117. // dresden_osm.shp/gis.osm_transport_a_v06
  118. $path_parts = pathinfo($dbf_file_name);
  119. $dbf_file_name = $path_parts['dirname'] . '/' . $path_parts['filename'];
  120. // sanitize filename
  121. $dbf_file_name = Sanitize::sanitizeFilename($dbf_file_name, true);
  122. // concat correct filename and extension
  123. $dbf_file_path = $temp . '/' . $dbf_file_name . '.dbf';
  124. if (file_put_contents($dbf_file_path, $extracted, LOCK_EX) !== false) {
  125. $temp_dbf_file = true;
  126. // Replace the .dbf with .*, as required by the bsShapeFiles library.
  127. $shp->FileName = substr($dbf_file_path, 0, -4) . '.*';
  128. }
  129. }
  130. }
  131. } elseif (! empty($local_import_file)
  132. && ! empty($GLOBALS['cfg']['UploadDir'])
  133. && $compression === 'none'
  134. ) {
  135. // If file is in UploadDir, use .dbf file in the same UploadDir
  136. // to load extra data.
  137. // Replace the .shp with .*,
  138. // so the bsShapeFiles library correctly locates .dbf file.
  139. $file_name = mb_substr(
  140. $import_file,
  141. 0,
  142. mb_strlen($import_file) - 4
  143. ) . '.*';
  144. $shp->FileName = $file_name;
  145. }
  146. }
  147. // It should load data before file being deleted
  148. $shp->loadFromFile('');
  149. // Delete the .dbf file extracted to 'TempDir'
  150. if ($temp_dbf_file
  151. && isset($dbf_file_path)
  152. && @file_exists($dbf_file_path)
  153. ) {
  154. unlink($dbf_file_path);
  155. }
  156. if ($shp->lastError != '') {
  157. $error = true;
  158. $message = Message::error(
  159. __('There was an error importing the ESRI shape file: "%s".')
  160. );
  161. $message->addParam($shp->lastError);
  162. return;
  163. }
  164. switch ($shp->shapeType) {
  165. // ESRI Null Shape
  166. case 0:
  167. break;
  168. // ESRI Point
  169. case 1:
  170. $gis_type = 'point';
  171. break;
  172. // ESRI PolyLine
  173. case 3:
  174. $gis_type = 'multilinestring';
  175. break;
  176. // ESRI Polygon
  177. case 5:
  178. $gis_type = 'multipolygon';
  179. break;
  180. // ESRI MultiPoint
  181. case 8:
  182. $gis_type = 'multipoint';
  183. break;
  184. default:
  185. $error = true;
  186. $message = Message::error(
  187. __('MySQL Spatial Extension does not support ESRI type "%s".')
  188. );
  189. $message->addParam($shp->getShapeName());
  190. return;
  191. }
  192. if (isset($gis_type)) {
  193. /** @var GisMultiLineString|GisMultiPoint|GisPoint|GisPolygon $gis_obj */
  194. $gis_obj = GisFactory::factory($gis_type);
  195. } else {
  196. $gis_obj = null;
  197. }
  198. $num_rows = count($shp->records);
  199. // If .dbf file is loaded, the number of extra data columns
  200. $num_data_cols = $shp->getDBFHeader() !== null ? count($shp->getDBFHeader()) : 0;
  201. $rows = [];
  202. $col_names = [];
  203. if ($num_rows != 0) {
  204. foreach ($shp->records as $record) {
  205. $tempRow = [];
  206. if ($gis_obj == null) {
  207. $tempRow[] = null;
  208. } else {
  209. $tempRow[] = "GeomFromText('"
  210. . $gis_obj->getShape($record->SHPData) . "')";
  211. }
  212. if ($shp->getDBFHeader() !== null) {
  213. foreach ($shp->getDBFHeader() as $c) {
  214. $cell = trim((string) $record->DBFData[$c[0]]);
  215. if (! strcmp($cell, '')) {
  216. $cell = 'NULL';
  217. }
  218. $tempRow[] = $cell;
  219. }
  220. }
  221. $rows[] = $tempRow;
  222. }
  223. }
  224. if (count($rows) === 0) {
  225. $error = true;
  226. $message = Message::error(
  227. __('The imported file does not contain any data!')
  228. );
  229. return;
  230. }
  231. // Column names for spatial column and the rest of the columns,
  232. // if they are available
  233. $col_names[] = 'SPATIAL';
  234. for ($n = 0; $n < $num_data_cols; $n++) {
  235. $col_names[] = $shp->getDBFHeader()[$n][0];
  236. }
  237. // Set table name based on the number of tables
  238. if (strlen((string) $db) > 0) {
  239. $result = $dbi->fetchResult('SHOW TABLES');
  240. $table_name = 'TABLE ' . (count($result) + 1);
  241. } else {
  242. $table_name = 'TBL_NAME';
  243. }
  244. $tables = [
  245. [
  246. $table_name,
  247. $col_names,
  248. $rows,
  249. ],
  250. ];
  251. // Use data from shape file to chose best-fit MySQL types for each column
  252. $analyses = [];
  253. $analyses[] = $this->import->analyzeTable($tables[0]);
  254. $table_no = 0;
  255. $spatial_col = 0;
  256. $analyses[$table_no][Import::TYPES][$spatial_col] = Import::GEOMETRY;
  257. $analyses[$table_no][Import::FORMATTEDSQL][$spatial_col] = true;
  258. // Set database name to the currently selected one, if applicable
  259. if (strlen((string) $db) > 0) {
  260. $db_name = $db;
  261. $options = ['create_db' => false];
  262. } else {
  263. $db_name = 'SHP_DB';
  264. $options = null;
  265. }
  266. // Created and execute necessary SQL statements from data
  267. $null_param = null;
  268. $this->import->buildSql($db_name, $tables, $analyses, $null_param, $options, $sql_data);
  269. unset($tables, $analyses);
  270. $finished = true;
  271. $error = false;
  272. // Commit any possible data in buffers
  273. $this->import->runQuery('', '', $sql_data);
  274. }
  275. /**
  276. * Returns specified number of bytes from the buffer.
  277. * Buffer automatically fetches next chunk of data when the buffer
  278. * falls short.
  279. * Sets $eof when $GLOBALS['finished'] is set and the buffer falls short.
  280. *
  281. * @param int $length number of bytes
  282. *
  283. * @return string
  284. */
  285. public static function readFromBuffer($length)
  286. {
  287. global $buffer, $eof, $importHandle;
  288. $import = new Import();
  289. if (strlen((string) $buffer) < $length) {
  290. if ($GLOBALS['finished']) {
  291. $eof = true;
  292. } else {
  293. $buffer .= $import->getNextChunk($importHandle);
  294. }
  295. }
  296. $result = substr($buffer, 0, $length);
  297. $buffer = substr($buffer, $length);
  298. return $result;
  299. }
  300. }