Completed
Push — master ( 67a9e5...5289c4 )
by Michal
03:56
created

src/ShapeFile.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * phpMyAdmin ShapeFile library
4
 * <https://github.com/phpmyadmin/shapefile/>.
5
 *
6
 * Copyright 2006-2007 Ovidio <ovidio AT users.sourceforge.net>
7
 * Copyright 2016 Michal Čihař <[email protected]>
8
 *
9
 * This program is free software; you can redistribute it and/or
10
 * modify it under the terms of the GNU General Public License
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, you can download one from
20
 * https://www.gnu.org/copyleft/gpl.html.
21
 */
22
23
namespace PhpMyAdmin\ShapeFile;
24
25
/**
26
 * ShapeFile class.
27
 */
28
class ShapeFile
29
{
30
    const MAGIC = 0x270a;
31
32
    public $FileName;
33
34
    private $SHPFile = null;
35
    private $SHXFile = null;
36
    private $DBFFile = null;
37
38
    private $DBFHeader;
39
40
    public $lastError = '';
41
42
    public $boundingBox = array('xmin' => 0.0, 'ymin' => 0.0, 'xmax' => 0.0, 'ymax' => 0.0);
43
    private $fileLength = 0;
44
    public $shapeType = 0;
45
46
    public $records = array();
47
48
    /**
49
     * Checks whether dbase manipuations are supported.
50
     *
51
     * @return bool
52
     */
53 21
    public static function supports_dbase()
54
    {
55 21
        return extension_loaded('dbase');
56
    }
57
58
    /**
59
     * @param int $shapeType
60
     */
61 24
    public function __construct($shapeType, $boundingBox = array('xmin' => 0.0, 'ymin' => 0.0, 'xmax' => 0.0, 'ymax' => 0.0), $FileName = null)
62
    {
63 24
        $this->shapeType = $shapeType;
64 24
        $this->boundingBox = $boundingBox;
65 24
        $this->FileName = $FileName;
66 24
        $this->fileLength = 50; // The value for file length is the total length of the file in 16-bit words (including the fifty 16-bit words that make up the header).
67 24
    }
68
69
    /**
70
     * Loads shapefile and dbase (if supported).
71
     *
72
     * @param string $FileName File mask to load (eg. example.*)
73
     */
74 22
    public function loadFromFile($FileName)
75
    {
76 22
        if (!empty($FileName)) {
77 22
            $this->FileName = $FileName;
78 22
            $result = $this->_openSHPFile();
79 22
        } else {
80
            /* We operate on buffer emulated by readSHP / eofSHP */
81
            $result = true;
82
        }
83
84 22
        if ($result && ($this->_openDBFFile())) {
85 20
            if (!$this->_loadHeaders()) {
86 1
                $this->_closeSHPFile();
87 1
                $this->_closeDBFFile();
88
89 1
                return false;
90
            }
91 19
            if (!$this->_loadRecords()) {
92
                $this->_closeSHPFile();
93
                $this->_closeDBFFile();
94
95
                return false;
96
            }
97 19
            $this->_closeSHPFile();
98 19
            $this->_closeDBFFile();
99
100 19
            return true;
101
        } else {
102 2
            return false;
103
        }
104
    }
105
106
    /**
107
     * Saves shapefile.
108
     *
109
     * @param string|null $FileName Name of file, otherwise existing is used
110
     */
111 13
    public function saveToFile($FileName = null)
112
    {
113 13
        if (!is_null($FileName)) {
114 13
            $this->FileName = $FileName;
115 13
        }
116
117 13
        if (($this->_openSHPFile(true)) && ($this->_openSHXFile(true)) && ($this->_createDBFFile())) {
118 13
            $this->_saveHeaders();
119 13
            $this->_saveRecords();
120 13
            $this->_closeSHPFile();
121 13
            $this->_closeSHXFile();
122 13
            $this->_closeDBFFile();
123 13
        } else {
124
            return false;
125
        }
126 13
    }
127
128
    /**
129
     * Generates filename with given extension.
130
     *
131
     * @param string $extension Extension to use (including dot)
132
     *
133
     * @return string
134
     */
135 23
    private function _getFilename($extension)
136
    {
137 23
        return str_replace('.*', $extension, $this->FileName);
138
    }
139
140
    /**
141
     * Updates bounding box based on SHPData.
142
     *
143
     * @param string $type Type of box
144
     * @param array  $data ShapeRecord SHPData
145
     */
146 12
    private function updateBBox($type, $data)
147
    {
148 12
        $min = $type . 'min';
149 12
        $max = $type . 'max';
150
151 12
        if (!isset($this->boundingBox[$min]) || $this->boundingBox[$min] == 0.0 || ($this->boundingBox[$min] > $data[$min])) {
152 12
            $this->boundingBox[$min] = $data[$min];
153 12
        }
154 12
        if (!isset($this->boundingBox[$max]) || $this->boundingBox[$max] == 0.0 || ($this->boundingBox[$max] < $data[$max])) {
155 12
            $this->boundingBox[$max] = $data[$max];
156 12
        }
157 12
    }
158
159
    /**
160
     * Adds record to shape file.
161
     *
162
     * @param ShapeRecord $record
163
     *
164
     * @return int Number of added record
165
     */
166 12
    public function addRecord($record)
167
    {
168 12
        if ((isset($this->DBFHeader)) && (is_array($this->DBFHeader))) {
169 12
            $record->updateDBFInfo($this->DBFHeader);
170 12
        }
171
172 12
        $this->fileLength += ($record->getContentLength() + 4);
173 12
        $this->records[] = $record;
174 12
        $this->records[count($this->records) - 1]->recordNumber = count($this->records);
175
176 12
        $this->updateBBox('x', $record->SHPData);
177 12
        $this->updateBBox('y', $record->SHPData);
178
179 12
        if (in_array($this->shapeType, array(11, 13, 15, 18, 21, 23, 25, 28))) {
180 8
            $this->updateBBox('m', $record->SHPData);
181 8
        }
182
183 12
        if (in_array($this->shapeType, array(11, 13, 15, 18))) {
184 4
            $this->updateBBox('z', $record->SHPData);
185 4
        }
186
187 12
        return count($this->records) - 1;
188
    }
189
190
    /**
191
     * Deletes record from shapefile.
192
     *
193
     * @param int $index
194
     */
195
    public function deleteRecord($index)
196
    {
197
        if (isset($this->records[$index])) {
198
            $this->fileLength -= ($this->records[$index]->getContentLength() + 4);
199
            $count = count($this->records) - 1;
200
            for ($i = $index; $i < $count; ++$i) {
201
                $this->records[$i] = $this->records[$i + 1];
202
            }
203
            unset($this->records[count($this->records) - 1]);
204
            $this->_deleteRecordFromDBF($index);
205
        }
206
    }
207
208
    /**
209
     * Returns array defining fields in DBF file.
210
     *
211
     * @return array see setDBFHeader for more information
212
     */
213
    public function getDBFHeader()
214
    {
215
        return $this->DBFHeader;
216
    }
217
218
    /**
219
     * Changes array defining fields in DBF file, used in dbase_create call.
220
     *
221
     * @param array $header An array of arrays, each array describing the
222
     *                      format of one field of the database. Each
223
     *                      field consists of a name, a character indicating
224
     *                      the field type, and optionally, a length,
225
     *                      a precision and a nullable flag.
226
     */
227 12
    public function setDBFHeader($header)
228
    {
229 12
        $this->DBFHeader = $header;
230
231 12
        $count = count($this->records);
232 12
        for ($i = 0; $i < $count; ++$i) {
233
            $this->records[$i]->updateDBFInfo($header);
234
        }
235 12
    }
236
237
    /**
238
     * Lookups value in the DBF file and returs index.
239
     *
240
     * @param string $field Field to match
241
     * @param mixed  $value Value to match
242
     *
243
     * @return int
244
     */
245 1
    public function getIndexFromDBFData($field, $value)
246
    {
247 1
        foreach ($this->records as $index => $record) {
248 1
            if (isset($record->DBFData[$field]) &&
249
                (trim(strtoupper($record->DBFData[$field])) == strtoupper($value))
250 1
            ) {
251
                return $index;
252
            }
253 1
        }
254
255 1
        return -1;
256
    }
257
258
    /**
259
     * Loads DBF metadata.
260
     */
261
    private function _loadDBFHeader()
262
    {
263
        $DBFFile = fopen($this->_getFilename('.dbf'), 'r');
264
265
        $result = array();
266
        $i = 1;
267
        $inHeader = true;
268
269
        while ($inHeader) {
270
            if (!feof($DBFFile)) {
271
                $buff32 = fread($DBFFile, 32);
272
                if ($i > 1) {
273
                    if (substr($buff32, 0, 1) == chr(13)) {
274
                        $inHeader = false;
275
                    } else {
276
                        $pos = strpos(substr($buff32, 0, 10), chr(0));
277
                        $pos = ($pos == 0 ? 10 : $pos);
278
279
                        $fieldName = substr($buff32, 0, $pos);
280
                        $fieldType = substr($buff32, 11, 1);
281
                        $fieldLen = ord(substr($buff32, 16, 1));
282
                        $fieldDec = ord(substr($buff32, 17, 1));
283
284
                        array_push($result, array($fieldName, $fieldType, $fieldLen, $fieldDec));
285
                    }
286
                }
287
                ++$i;
288
            } else {
289
                $inHeader = false;
290
            }
291
        }
292
293
        fclose($DBFFile);
294
295
        return $result;
296
    }
297
298
    /**
299
     * Deletes record from the DBF file.
300
     *
301
     * @param int $index
302
     */
303
    private function _deleteRecordFromDBF($index)
304
    {
305
        if (@dbase_delete_record($this->DBFFile, $index)) {
306
            dbase_pack($this->DBFFile);
307
        }
308
    }
309
310
    /**
311
     * Loads SHP file metadata.
312
     *
313
     * @return bool
314
     */
315 20
    private function _loadHeaders()
316
    {
317 20
        if (Util::loadData('N', $this->readSHP(4)) != self::MAGIC) {
318 1
            $this->setError('Not a SHP file (file code mismatch)');
319
320 1
            return false;
321
        }
322
323
        /* Skip 20 unused bytes */
324 19
        $this->readSHP(20);
325
326 19
        $this->fileLength = Util::loadData('N', $this->readSHP(4));
327
328
        /* We currently ignore version */
329 19
        $this->readSHP(4);
330
331 19
        $this->shapeType = Util::loadData('V', $this->readSHP(4));
332
333 19
        $this->boundingBox = array();
334 19
        $this->boundingBox['xmin'] = Util::loadData('d', $this->readSHP(8));
335 19
        $this->boundingBox['ymin'] = Util::loadData('d', $this->readSHP(8));
336 19
        $this->boundingBox['xmax'] = Util::loadData('d', $this->readSHP(8));
337 19
        $this->boundingBox['ymax'] = Util::loadData('d', $this->readSHP(8));
338 19
        $this->boundingBox['zmin'] = Util::loadData('d', $this->readSHP(8));
339 19
        $this->boundingBox['zmax'] = Util::loadData('d', $this->readSHP(8));
340 19
        $this->boundingBox['mmin'] = Util::loadData('d', $this->readSHP(8));
341 19
        $this->boundingBox['mmax'] = Util::loadData('d', $this->readSHP(8));
342
343 19
        if (self::supports_dbase()) {
344
            $this->DBFHeader = $this->_loadDBFHeader();
345
        }
346
347 19
        return true;
348
    }
349
350
    /**
351
     * Saves bounding box record, possibly using 0 instead of not set values.
352
     *
353
     * @param file   $file File object
354
     * @param string $type Bounding box dimension (eg. xmax, mmin...)
355
     */
356 13
    private function _saveBBoxRecord($file, $type)
357
    {
358 13
        fwrite($file, Util::packDouble(
359 13
            isset($this->boundingBox[$type]) ? $this->boundingBox[$type] : 0)
360 13
        );
361 13
    }
362
363
    /**
364
     * Saves bounding box to a file.
365
     *
366
     * @param file $file File object
367
     */
368 13
    private function _saveBBox($file)
369
    {
370 13
        $this->_saveBBoxRecord($file, 'xmin');
371 13
        $this->_saveBBoxRecord($file, 'ymin');
372 13
        $this->_saveBBoxRecord($file, 'xmax');
373 13
        $this->_saveBBoxRecord($file, 'ymax');
374 13
        $this->_saveBBoxRecord($file, 'zmin');
375 13
        $this->_saveBBoxRecord($file, 'zmax');
376 13
        $this->_saveBBoxRecord($file, 'mmin');
377 13
        $this->_saveBBoxRecord($file, 'mmax');
378 13
    }
379
380
    /**
381
     * Saves SHP and SHX file metadata.
382
     */
383 13
    private function _saveHeaders()
384
    {
385 13
        fwrite($this->SHPFile, pack('NNNNNN', self::MAGIC, 0, 0, 0, 0, 0));
386 13
        fwrite($this->SHPFile, pack('N', $this->fileLength));
387 13
        fwrite($this->SHPFile, pack('V', 1000));
388 13
        fwrite($this->SHPFile, pack('V', $this->shapeType));
389 13
        $this->_saveBBox($this->SHPFile);
0 ignored issues
show
It seems like $this->SHPFile can also be of type false or null; however, PhpMyAdmin\ShapeFile\ShapeFile::_saveBBox() does only seem to accept object<PhpMyAdmin\ShapeFile\file>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
390
391 13
        fwrite($this->SHXFile, pack('NNNNNN', self::MAGIC, 0, 0, 0, 0, 0));
392 13
        fwrite($this->SHXFile, pack('N', 50 + 4 * count($this->records)));
393 13
        fwrite($this->SHXFile, pack('V', 1000));
394 13
        fwrite($this->SHXFile, pack('V', $this->shapeType));
395 13
        $this->_saveBBox($this->SHXFile);
0 ignored issues
show
It seems like $this->SHXFile can also be of type false or null; however, PhpMyAdmin\ShapeFile\ShapeFile::_saveBBox() does only seem to accept object<PhpMyAdmin\ShapeFile\file>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
396 13
    }
397
398
    /**
399
     * Loads records from SHP file (and DBF).
400
     *
401
     * @return bool
402
     */
403 19
    private function _loadRecords()
404
    {
405
        /* Need to start at offset 100 */
406 19
        while (!$this->eofSHP()) {
407 19
            $record = new ShapeRecord(-1);
408 19
            $record->loadFromFile($this, $this->SHPFile, $this->DBFFile);
409 19
            if ($record->lastError != '') {
410
                $this->setError($record->lastError);
411
412
                return false;
413
            }
414 19
            if (($record->shapeType === false || $record->shapeType === '') && $this->eofSHP()) {
415 19
                break;
416
            }
417
418 19
            $this->records[] = $record;
419 19
        }
420
421 19
        return true;
422
    }
423
424
    /**
425
     * Saves records to SHP and SHX files.
426
     */
427 13
    private function _saveRecords()
428
    {
429 13
        $offset = 50;
430 13
        if (is_array($this->records) && (count($this->records) > 0)) {
431 12
            foreach ($this->records as $index => $record) {
432
                //Save the record to the .shp file
433 12
                $record->saveToFile($this->SHPFile, $this->DBFFile, $index + 1);
434
435
                //Save the record to the .shx file
436 12
                fwrite($this->SHXFile, pack('N', $offset));
437 12
                fwrite($this->SHXFile, pack('N', $record->getContentLength()));
438 12
                $offset += (4 + $record->getContentLength());
439 12
            }
440 12
        }
441 13
    }
442
443
    /**
444
     * Generic interface to open files.
445
     *
446
     * @param bool   $toWrite   Whether file should be opened for writing
447
     * @param string $extension File extension
448
     * @param string $name      Verbose file name to report errors
449
     *
450
     * @return file|false File handle
451
     */
452 23
    private function _openFile($toWrite, $extension, $name)
453
    {
454 23
        $shp_name = $this->_getFilename($extension);
455 23
        $result = @fopen($shp_name, ($toWrite ? 'wb+' : 'rb'));
456 23
        if (!$result) {
457 2
            $this->setError(sprintf('It wasn\'t possible to open the %s file "%s"', $name, $shp_name));
458
459 2
            return false;
460
        }
461
462 21
        return $result;
463
    }
464
465
    /**
466
     * Opens SHP file.
467
     *
468
     * @param bool $toWrite Whether file should be opened for writing
469
     *
470
     * @return bool
471
     */
472 23
    private function _openSHPFile($toWrite = false)
473
    {
474 23
        $this->SHPFile = $this->_openFile($toWrite, '.shp', 'Shape');
475 23
        if (!$this->SHPFile) {
476 2
            return false;
477
        }
478
479 21
        return true;
480
    }
481
482
    /**
483
     * Closes SHP file.
484
     */
485 21
    private function _closeSHPFile()
486
    {
487 21
        if ($this->SHPFile) {
488 21
            fclose($this->SHPFile);
489 21
            $this->SHPFile = null;
490 21
        }
491 21
    }
492
493
    /**
494
     * Opens SHX file.
495
     *
496
     * @param bool $toWrite Whether file should be opened for writing
497
     *
498
     * @return bool
499
     */
500 13
    private function _openSHXFile($toWrite = false)
501
    {
502 13
        $this->SHXFile = $this->_openFile($toWrite, '.shx', 'Index');
503 13
        if (!$this->SHXFile) {
504
            return false;
505
        }
506
507 13
        return true;
508
    }
509
510
    /**
511
     * Closes SHX file.
512
     */
513 13
    private function _closeSHXFile()
514
    {
515 13
        if ($this->SHXFile) {
516 13
            fclose($this->SHXFile);
517 13
            $this->SHXFile = null;
518 13
        }
519 13
    }
520
521
    /**
522
     * Creates DBF file.
523
     *
524
     * @return bool
525
     */
526 13
    private function _createDBFFile()
527
    {
528 13
        if (!self::supports_dbase() || !is_array($this->DBFHeader) || count($this->DBFHeader) == 0) {
529 13
            $this->DBFFile = null;
530
531 13
            return true;
532
        }
533
        $dbf_name = $this->_getFilename('.dbf');
534
535
        /* Unlink existing file */
536
        if (file_exists($dbf_name)) {
537
            unlink($dbf_name);
538
        }
539
540
        /* Create new file */
541
        $this->DBFFile = @dbase_create($dbf_name, $this->DBFHeader);
542
        if ($this->DBFFile === false) {
543
            $this->setError(sprintf('It wasn\'t possible to create the DBase file "%s"', $dbf_name));
544
545
            return false;
546
        }
547
548
        return true;
549
    }
550
551
    /**
552
     * Loads DBF file if supported.
553
     *
554
     * @return bool
555
     */
556 20
    private function _openDBFFile()
557
    {
558 20
        if (!self::supports_dbase()) {
559 20
            $this->DBFFile = null;
560
561 20
            return true;
562
        }
563
        $dbf_name = $this->_getFilename('.dbf');
564
        if (is_readable($dbf_name)) {
565
            $this->DBFFile = @dbase_open($dbf_name, 0);
566
            if (!$this->DBFFile) {
567
                $this->setError(sprintf('It wasn\'t possible to open the DBase file "%s"', $dbf_name));
568
569
                return false;
570
            }
571
        } else {
572
            $this->setError(sprintf('It wasn\'t possible to find the DBase file "%s"', $dbf_name));
573
574
            return false;
575
        }
576
577
        return true;
578
    }
579
580
    /**
581
     * Closes DBF file.
582
     */
583 21
    private function _closeDBFFile()
584
    {
585 21
        if ($this->DBFFile) {
586
            dbase_close($this->DBFFile);
587
            $this->DBFFile = null;
588
        }
589 21
    }
590
591
    /**
592
     * Sets error message.
593
     *
594
     * @param string $error
595
     */
596 3
    public function setError($error)
597
    {
598 3
        $this->lastError = $error;
599 3
    }
600
601
    /**
602
     * Reads given number of bytes from SHP file.
603
     *
604
     * @param int $bytes
605
     *
606
     * @return string
607
     */
608 20
    public function readSHP($bytes)
609
    {
610 20
        return fread($this->SHPFile, $bytes);
611
    }
612
613
    /**
614
     * Checks whether file is at EOF.
615
     *
616
     * @return bool
617
     */
618 19
    public function eofSHP()
619
    {
620 19
        return feof($this->SHPFile);
621
    }
622
623
    /**
624
     * Returns shape name.
625
     *
626
     * @return string
627
     */
628 1
    public function getShapeName()
629
    {
630 1
        return Util::nameShape($this->shapeType);
631
    }
632
633
    /**
634
     * Check whether file contains measure data.
635
     *
636
     * For some reason this is distinguished by zero bouding box in the
637
     * specification.
638
     *
639
     * @return bool
640
     */
641 8
    public function hasMeasure()
642
    {
643 8
        return $this->boundingBox['mmin'] != 0 || $this->boundingBox['mmax'] != 0;
644
    }
645
}
646