1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* ESRI Shape file import plugin for phpMyAdmin |
4
|
|
|
*/ |
5
|
|
|
|
6
|
|
|
declare(strict_types=1); |
7
|
|
|
|
8
|
|
|
namespace PhpMyAdmin\Plugins\Import; |
9
|
|
|
|
10
|
|
|
use PhpMyAdmin\Config; |
11
|
|
|
use PhpMyAdmin\Current; |
12
|
|
|
use PhpMyAdmin\File; |
13
|
|
|
use PhpMyAdmin\Gis\GisFactory; |
14
|
|
|
use PhpMyAdmin\Gis\GisMultiLineString; |
15
|
|
|
use PhpMyAdmin\Gis\GisMultiPoint; |
16
|
|
|
use PhpMyAdmin\Gis\GisPoint; |
17
|
|
|
use PhpMyAdmin\Gis\GisPolygon; |
18
|
|
|
use PhpMyAdmin\Http\ServerRequest; |
19
|
|
|
use PhpMyAdmin\Import\ColumnType; |
20
|
|
|
use PhpMyAdmin\Import\Import; |
21
|
|
|
use PhpMyAdmin\Import\ImportSettings; |
22
|
|
|
use PhpMyAdmin\Import\ImportTable; |
23
|
|
|
use PhpMyAdmin\Message; |
24
|
|
|
use PhpMyAdmin\Plugins\ImportPlugin; |
25
|
|
|
use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; |
26
|
|
|
use PhpMyAdmin\Sanitize; |
27
|
|
|
use PhpMyAdmin\ShapeFile\ShapeType; |
28
|
|
|
use PhpMyAdmin\ZipExtension; |
29
|
|
|
use ZipArchive; |
30
|
|
|
|
31
|
|
|
use function __; |
32
|
|
|
use function count; |
33
|
|
|
use function extension_loaded; |
34
|
|
|
use function file_exists; |
35
|
|
|
use function file_put_contents; |
36
|
|
|
use function mb_substr; |
37
|
|
|
use function method_exists; |
38
|
|
|
use function pathinfo; |
39
|
|
|
use function strlen; |
40
|
|
|
use function substr; |
41
|
|
|
use function trim; |
42
|
|
|
use function unlink; |
43
|
|
|
|
44
|
|
|
use const LOCK_EX; |
45
|
|
|
use const PATHINFO_FILENAME; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Handles the import for ESRI Shape files |
49
|
|
|
*/ |
50
|
|
|
class ImportShp extends ImportPlugin |
51
|
|
|
{ |
52
|
|
|
private ZipExtension|null $zipExtension = null; |
53
|
|
|
private static File|null $importHandle = null; |
54
|
|
|
private static string $buffer = ''; |
55
|
|
|
public static bool $eof = false; |
56
|
|
|
|
57
|
12 |
|
protected function init(): void |
58
|
|
|
{ |
59
|
12 |
|
if (! extension_loaded('zip')) { |
60
|
|
|
return; |
61
|
|
|
} |
62
|
|
|
|
63
|
12 |
|
$this->zipExtension = new ZipExtension(new ZipArchive()); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** @psalm-return non-empty-lowercase-string */ |
67
|
|
|
public function getName(): string |
68
|
|
|
{ |
69
|
|
|
return 'shp'; |
70
|
|
|
} |
71
|
|
|
|
72
|
12 |
|
protected function setProperties(): ImportPluginProperties |
73
|
|
|
{ |
74
|
12 |
|
$importPluginProperties = new ImportPluginProperties(); |
75
|
12 |
|
$importPluginProperties->setText(__('ESRI Shape File')); |
76
|
12 |
|
$importPluginProperties->setExtension('shp'); |
77
|
12 |
|
$importPluginProperties->setOptionsText(__('Options')); |
78
|
|
|
|
79
|
12 |
|
return $importPluginProperties; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
public function setImportOptions(ServerRequest $request): void |
83
|
|
|
{ |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Handles the whole import logic |
88
|
|
|
* |
89
|
|
|
* @return string[] |
90
|
|
|
*/ |
91
|
8 |
|
public function doImport(File|null $importHandle = null): array |
92
|
|
|
{ |
93
|
8 |
|
ImportSettings::$finished = false; |
94
|
|
|
|
95
|
8 |
|
if ($importHandle === null || $this->zipExtension === null) { |
96
|
|
|
return []; |
97
|
|
|
} |
98
|
|
|
|
99
|
8 |
|
self::$importHandle = $importHandle; |
100
|
|
|
|
101
|
8 |
|
$compression = $importHandle->getCompression(); |
102
|
|
|
|
103
|
8 |
|
$shp = new ShapeFileImport(ShapeType::Point); |
104
|
|
|
// If the zip archive has more than one file, |
105
|
|
|
// get the correct content to the buffer from .shp file. |
106
|
|
|
if ( |
107
|
8 |
|
$compression === 'application/zip' |
108
|
8 |
|
&& $this->zipExtension->getNumberOfFiles(ImportSettings::$importFile) > 1 |
109
|
|
|
) { |
110
|
8 |
|
if ($importHandle->openZip('/^.*\.shp$/i') === false) { |
111
|
|
|
Current::$message = Message::error( |
112
|
|
|
__('There was an error importing the ESRI shape file: "%s".'), |
113
|
|
|
); |
114
|
|
|
Current::$message->addParam($importHandle->getError()); |
115
|
|
|
|
116
|
|
|
return []; |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
|
120
|
8 |
|
$tempDbfFile = false; |
121
|
|
|
// We need dbase extension to handle .dbf file |
122
|
8 |
|
if (extension_loaded('dbase')) { |
123
|
2 |
|
$config = Config::getInstance(); |
|
|
|
|
124
|
2 |
|
$temp = $config->getTempDir('shp'); |
125
|
|
|
// If we can extract the zip archive to 'TempDir' |
126
|
|
|
// and use the files in it for import |
127
|
2 |
|
if ($compression === 'application/zip' && $temp !== null) { |
128
|
2 |
|
$dbfFileName = $this->zipExtension->findFile(ImportSettings::$importFile, '/^.*\.dbf$/i'); |
129
|
|
|
// If the corresponding .dbf file is in the zip archive |
130
|
2 |
|
if ($dbfFileName !== false) { |
131
|
|
|
// Extract the .dbf file and point to it. |
132
|
2 |
|
$extracted = $this->zipExtension->extract(ImportSettings::$importFile, $dbfFileName); |
133
|
2 |
|
if ($extracted !== false) { |
134
|
|
|
// remove filename extension, e.g. |
135
|
|
|
// dresden_osm.shp/gis.osm_transport_a_v06.dbf |
136
|
|
|
// to |
137
|
|
|
// dresden_osm.shp/gis.osm_transport_a_v06 |
138
|
2 |
|
$pathParts = pathinfo($dbfFileName); |
139
|
2 |
|
$dbfFileName = $pathParts['dirname'] . '/' . $pathParts['filename']; |
140
|
|
|
|
141
|
|
|
// sanitize filename |
142
|
2 |
|
$dbfFileName = Sanitize::sanitizeFilename($dbfFileName, true); |
143
|
|
|
|
144
|
|
|
// concat correct filename and extension |
145
|
2 |
|
$dbfFilePath = $temp . '/' . $dbfFileName . '.dbf'; |
146
|
|
|
|
147
|
2 |
|
if (file_put_contents($dbfFilePath, $extracted, LOCK_EX) !== false) { |
148
|
2 |
|
$tempDbfFile = true; |
149
|
|
|
|
150
|
|
|
// Replace the .dbf with .*, as required by the bsShapeFiles library. |
151
|
2 |
|
$shp->fileName = substr($dbfFilePath, 0, -4) . '.*'; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
} elseif ( |
156
|
|
|
ImportSettings::$localImportFile !== '' |
157
|
|
|
&& ! empty($config->settings['UploadDir']) |
158
|
|
|
&& $compression === 'none' |
159
|
|
|
) { |
160
|
|
|
// If file is in UploadDir, use .dbf file in the same UploadDir |
161
|
|
|
// to load extra data. |
162
|
|
|
// Replace the .shp with .*, |
163
|
|
|
// so the bsShapeFiles library correctly locates .dbf file. |
164
|
|
|
$shp->fileName = mb_substr(ImportSettings::$importFile, 0, -4) . '.*'; |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
// It should load data before file being deleted |
169
|
8 |
|
$shp->loadFromFile(''); |
170
|
|
|
|
171
|
|
|
// Delete the .dbf file extracted to 'TempDir' |
172
|
8 |
|
if ($tempDbfFile && isset($dbfFilePath) && @file_exists($dbfFilePath)) { |
173
|
2 |
|
unlink($dbfFilePath); |
174
|
|
|
} |
175
|
|
|
|
176
|
8 |
|
if ($shp->lastError != '') { |
177
|
|
|
Import::$hasError = true; |
178
|
|
|
Current::$message = Message::error( |
179
|
|
|
__('There was an error importing the ESRI shape file: "%s".'), |
180
|
|
|
); |
181
|
|
|
Current::$message->addParam($shp->lastError); |
182
|
|
|
|
183
|
|
|
return []; |
184
|
|
|
} |
185
|
|
|
|
186
|
8 |
|
switch ($shp->shapeType) { |
187
|
8 |
|
case ShapeType::Null: |
188
|
|
|
break; |
189
|
8 |
|
case ShapeType::Point: |
190
|
4 |
|
$gisType = 'point'; |
191
|
4 |
|
break; |
192
|
4 |
|
case ShapeType::PolyLine: |
193
|
|
|
$gisType = 'multilinestring'; |
194
|
|
|
break; |
195
|
4 |
|
case ShapeType::Polygon: |
196
|
4 |
|
$gisType = 'multipolygon'; |
197
|
4 |
|
break; |
198
|
|
|
case ShapeType::MultiPoint: |
199
|
|
|
$gisType = 'multipoint'; |
200
|
|
|
break; |
201
|
|
|
default: |
202
|
|
|
Import::$hasError = true; |
203
|
|
|
Current::$message = Message::error( |
204
|
|
|
__('MySQL Spatial Extension does not support ESRI type "%s".'), |
205
|
|
|
); |
206
|
|
|
Current::$message->addParam($shp->getShapeName()); |
207
|
|
|
|
208
|
|
|
return []; |
209
|
|
|
} |
210
|
|
|
|
211
|
8 |
|
if (isset($gisType)) { |
212
|
|
|
/** @var GisMultiLineString|GisMultiPoint|GisPoint|GisPolygon $gisObj */ |
213
|
8 |
|
$gisObj = GisFactory::fromType($gisType); |
214
|
|
|
} else { |
215
|
|
|
$gisObj = null; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// If .dbf file is loaded, the number of extra data columns |
219
|
8 |
|
$numDataCols = $shp->getDBFHeader() !== null ? count($shp->getDBFHeader()) : 0; |
220
|
|
|
|
221
|
8 |
|
$rows = []; |
222
|
8 |
|
$colNames = []; |
223
|
8 |
|
foreach ($shp->records as $record) { |
224
|
8 |
|
$tempRow = []; |
225
|
8 |
|
if ($gisObj == null || ! method_exists($gisObj, 'getShape')) { |
226
|
|
|
$tempRow[] = null; |
227
|
|
|
} else { |
228
|
8 |
|
$tempRow[] = "GeomFromText('" . $gisObj->getShape($record->shpData) . "')"; |
229
|
|
|
} |
230
|
|
|
|
231
|
8 |
|
if ($shp->getDBFHeader() !== null) { |
232
|
8 |
|
foreach ($shp->getDBFHeader() as $c) { |
233
|
2 |
|
$cell = trim((string) $record->dbfData[$c[0]]); |
234
|
|
|
|
235
|
2 |
|
if ($cell === '') { |
236
|
1 |
|
$cell = 'NULL'; |
237
|
|
|
} |
238
|
|
|
|
239
|
2 |
|
$tempRow[] = $cell; |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
|
243
|
8 |
|
$rows[] = $tempRow; |
244
|
|
|
} |
245
|
|
|
|
246
|
8 |
|
if ($rows === []) { |
247
|
|
|
Import::$hasError = true; |
248
|
|
|
Current::$message = Message::error( |
249
|
|
|
__('The imported file does not contain any data!'), |
250
|
|
|
); |
251
|
|
|
|
252
|
|
|
return []; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
// Column names for spatial column and the rest of the columns, |
256
|
|
|
// if they are available |
257
|
8 |
|
$colNames[] = 'SPATIAL'; |
258
|
8 |
|
$dbfHeader = $shp->getDBFHeader(); |
259
|
8 |
|
for ($n = 0; $n < $numDataCols; $n++) { |
260
|
2 |
|
if ($dbfHeader === null) { |
261
|
|
|
continue; |
262
|
|
|
} |
263
|
|
|
|
264
|
2 |
|
$colNames[] = $dbfHeader[$n][0]; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
// Set table name based on the number of tables |
268
|
8 |
|
if (Current::$database !== '') { |
269
|
|
|
$tableName = $this->import->getNextAvailableTableName( |
270
|
|
|
Current::$database, |
271
|
|
|
pathinfo(ImportSettings::$importFileName, PATHINFO_FILENAME), |
|
|
|
|
272
|
|
|
); |
273
|
|
|
} else { |
274
|
8 |
|
$tableName = 'TBL_NAME'; |
275
|
|
|
} |
276
|
|
|
|
277
|
8 |
|
$table = new ImportTable($tableName, $colNames, $rows); |
278
|
|
|
|
279
|
|
|
// Use data from shape file to chose best-fit MySQL types for each column |
280
|
8 |
|
$analysis = $this->import->analyzeTable($table); |
281
|
|
|
|
282
|
|
|
// The first column is the SPATIAL column defined earlier |
283
|
|
|
// This column contains SQL functions and should not be formatted by buildSql() |
284
|
8 |
|
$analysis[0]->type = ColumnType::Geometry; |
285
|
8 |
|
$analysis[0]->isFullyFormattedSql = true; |
286
|
|
|
|
287
|
|
|
// Set database name to the currently selected one, if applicable |
288
|
8 |
|
$dbName = Current::$database !== '' ? Current::$database : 'SHP_DB'; |
289
|
8 |
|
$createDb = Current::$database === ''; |
290
|
|
|
|
291
|
|
|
// Created and execute necessary SQL statements from data |
292
|
8 |
|
$sqlStatements = []; |
293
|
8 |
|
if ($createDb) { |
294
|
8 |
|
$sqlStatements = $this->import->createDatabase($dbName, 'utf8', 'utf8_general_ci', []); |
295
|
|
|
} |
296
|
|
|
|
297
|
8 |
|
$this->import->buildSql($dbName, [$table], [$analysis], sqlData: $sqlStatements); |
298
|
|
|
|
299
|
8 |
|
ImportSettings::$finished = true; |
300
|
8 |
|
Import::$hasError = false; |
301
|
|
|
|
302
|
|
|
// Commit any possible data in buffers |
303
|
8 |
|
$this->import->runQuery('', $sqlStatements); |
304
|
|
|
|
305
|
8 |
|
return $sqlStatements; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Returns specified number of bytes from the buffer. |
310
|
|
|
* Buffer automatically fetches next chunk of data when the buffer |
311
|
|
|
* falls short. |
312
|
|
|
* Sets $eof when ImportSettings::$finished is set and the buffer falls short. |
313
|
|
|
* |
314
|
|
|
* @param int $length number of bytes |
315
|
|
|
*/ |
316
|
8 |
|
public static function readFromBuffer(int $length): string |
317
|
|
|
{ |
318
|
8 |
|
$import = new Import(); |
319
|
|
|
|
320
|
8 |
|
if (strlen(self::$buffer) < $length) { |
321
|
8 |
|
if (ImportSettings::$finished) { |
322
|
8 |
|
self::$eof = true; |
323
|
|
|
} else { |
324
|
8 |
|
self::$buffer .= $import->getNextChunk(self::$importHandle); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
8 |
|
$result = substr(self::$buffer, 0, $length); |
329
|
8 |
|
self::$buffer = substr(self::$buffer, $length); |
330
|
|
|
|
331
|
8 |
|
return $result; |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.