Test Failed
Push — master ( a7ba1f...918038 )
by Maurício
03:22
created

ShapeFile::supportsDbase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 - 2017 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 = [
43
        'xmin' => 0.0,
44
        'ymin' => 0.0,
45
        'xmax' => 0.0,
46
        'ymax' => 0.0,
47
    ];
48
    private $fileLength = 0;
49
    public $shapeType = 0;
50
51
    public $records = [];
52
53 21
    /**
54
     * Checks whether dbase manipuations are supported.
55 21
     *
56
     * @return bool
57
     */
58
    public static function supportsDbase()
59
    {
60
        return extension_loaded('dbase');
61
    }
62
63 24
    /**
64
     * @param int        $shapeType   File shape type, should be same as all records
65 24
     * @param array      $boundingBox File bounding box
66 24
     * @param null|mixed $FileName    File name
67 24
     */
68 24
    public function __construct(
69 24
        $shapeType,
70
        $boundingBox = [
71
            'xmin' => 0.0,
72
            'ymin' => 0.0,
73
            'xmax' => 0.0,
74
            'ymax' => 0.0,
75
        ],
76 22
        $FileName = null
77
    ) {
78 22
        $this->shapeType = $shapeType;
79 22
        $this->boundingBox = $boundingBox;
80 22
        $this->FileName = $FileName;
81 22
        $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).
82
    }
83
84
    /**
85
     * Loads shapefile and dbase (if supported).
86 22
     *
87 20
     * @param string $FileName File mask to load (eg. example.*)
88 1
     */
89 1
    public function loadFromFile($FileName)
90
    {
91 1
        if (! empty($FileName)) {
92
            $this->FileName = $FileName;
93 19
            $result = $this->_openSHPFile();
94
        } else {
95
            /* We operate on buffer emulated by readSHP / eofSHP */
96
            $result = true;
97
        }
98
99 19
        if ($result && ($this->_openDBFFile())) {
100 19
            if (! $this->_loadHeaders()) {
101
                $this->_closeSHPFile();
102 19
                $this->_closeDBFFile();
103
104
                return false;
105 2
            }
106
            if (! $this->_loadRecords()) {
107
                $this->_closeSHPFile();
108
                $this->_closeDBFFile();
109
110
                return false;
111
            }
112
            $this->_closeSHPFile();
113 13
            $this->_closeDBFFile();
114
115 13
            return true;
116 13
        }
117 13
118
        return false;
119 13
    }
120 13
121 13
    /**
122 13
     * Saves shapefile.
123 13
     *
124 13
     * @param string|null $FileName Name of file, otherwise existing is used
125 13
     */
126
    public function saveToFile($FileName = null)
127
    {
128 13
        if (! is_null($FileName)) {
129
            $this->FileName = $FileName;
130
        }
131
132
        if (($this->_openSHPFile(true)) && ($this->_openSHXFile(true)) && ($this->_createDBFFile())) {
133
            $this->_saveHeaders();
134
            $this->_saveRecords();
135
            $this->_closeSHPFile();
136
            $this->_closeSHXFile();
137 23
            $this->_closeDBFFile();
138
        } else {
139 23
            return false;
140
        }
141
    }
142
143
    /**
144
     * Generates filename with given extension.
145
     *
146
     * @param string $extension Extension to use (including dot)
147
     *
148 12
     * @return string
149
     */
150 12
    private function _getFilename($extension)
151 12
    {
152
        return str_replace('.*', $extension, $this->FileName);
153 12
    }
154 12
155 12
    /**
156 12
     * Updates bounding box based on SHPData.
157 12
     *
158 12
     * @param string $type Type of box
159 12
     * @param array  $data ShapeRecord SHPData
160
     */
161
    private function updateBBox($type, $data)
162
    {
163
        $min = $type . 'min';
164
        $max = $type . 'max';
165
166
        if (! isset($this->boundingBox[$min]) || $this->boundingBox[$min] == 0.0 || ($this->boundingBox[$min] > $data[$min])) {
167
            $this->boundingBox[$min] = $data[$min];
168 12
        }
169
        if (! isset($this->boundingBox[$max]) || $this->boundingBox[$max] == 0.0 || ($this->boundingBox[$max] < $data[$max])) {
170 12
            $this->boundingBox[$max] = $data[$max];
171 12
        }
172 12
    }
173
174 12
    /**
175 12
     * Adds record to shape file.
176 12
     *
177
     * @param ShapeRecord $record
178 12
     *
179 12
     * @return int Number of added record
180
     */
181 12
    public function addRecord($record)
182 8
    {
183 8
        if ((isset($this->DBFHeader)) && (is_array($this->DBFHeader))) {
184
            $record->updateDBFInfo($this->DBFHeader);
185 12
        }
186 4
187 4
        $this->fileLength += ($record->getContentLength() + 4);
188
        $this->records[] = $record;
189 12
        $this->records[count($this->records) - 1]->recordNumber = count($this->records);
190
191
        $this->updateBBox('x', $record->SHPData);
192
        $this->updateBBox('y', $record->SHPData);
193
194
        if (in_array($this->shapeType, [11, 13, 15, 18, 21, 23, 25, 28])) {
195
            $this->updateBBox('m', $record->SHPData);
196
        }
197
198
        if (in_array($this->shapeType, [11, 13, 15, 18])) {
199
            $this->updateBBox('z', $record->SHPData);
200
        }
201
202
        return count($this->records) - 1;
203
    }
204
205
    /**
206
     * Deletes record from shapefile.
207
     *
208
     * @param int $index
209
     */
210
    public function deleteRecord($index)
211
    {
212
        if (isset($this->records[$index])) {
213
            $this->fileLength -= ($this->records[$index]->getContentLength() + 4);
214
            $count = count($this->records) - 1;
215
            for ($i = $index; $i < $count; ++$i) {
216
                $this->records[$i] = $this->records[$i + 1];
217
            }
218
            unset($this->records[count($this->records) - 1]);
219
            $this->_deleteRecordFromDBF($index);
220
        }
221
    }
222
223
    /**
224
     * Returns array defining fields in DBF file.
225
     *
226
     * @return array see setDBFHeader for more information
227
     */
228
    public function getDBFHeader()
229 12
    {
230
        return $this->DBFHeader;
231 12
    }
232
233 12
    /**
234 12
     * Changes array defining fields in DBF file, used in dbase_create call.
235
     *
236
     * @param array $header An array of arrays, each array describing the
237 12
     *                      format of one field of the database. Each
238
     *                      field consists of a name, a character indicating
239
     *                      the field type, and optionally, a length,
240
     *                      a precision and a nullable flag.
241
     */
242
    public function setDBFHeader($header)
243
    {
244
        $this->DBFHeader = $header;
245
246
        $count = count($this->records);
247 1
        for ($i = 0; $i < $count; ++$i) {
248
            $this->records[$i]->updateDBFInfo($header);
249 1
        }
250 1
    }
251
252 1
    /**
253
     * Lookups value in the DBF file and returs index.
254
     *
255 1
     * @param string $field Field to match
256
     * @param mixed  $value Value to match
257 1
     *
258
     * @return int
259
     */
260
    public function getIndexFromDBFData($field, $value)
261
    {
262
        foreach ($this->records as $index => $record) {
263
            if (isset($record->DBFData[$field]) &&
264
                (trim(strtoupper($record->DBFData[$field])) == strtoupper($value))
265
            ) {
266
                return $index;
267
            }
268
        }
269
270
        return -1;
271
    }
272
273
    /**
274
     * Loads DBF metadata.
275
     */
276
    private function _loadDBFHeader()
277
    {
278
        $DBFFile = fopen($this->_getFilename('.dbf'), 'r');
279
280
        $result = [];
281
        $i = 1;
282
        $inHeader = true;
283
284
        while ($inHeader) {
285
            if (! feof($DBFFile)) {
286
                $buff32 = fread($DBFFile, 32);
287
                if ($i > 1) {
288
                    if (substr($buff32, 0, 1) == chr(13)) {
289
                        $inHeader = false;
290
                    } else {
291
                        $pos = strpos(substr($buff32, 0, 10), chr(0));
292
                        $pos = ($pos == 0 ? 10 : $pos);
293
294
                        $fieldName = substr($buff32, 0, $pos);
295
                        $fieldType = substr($buff32, 11, 1);
296
                        $fieldLen = ord(substr($buff32, 16, 1));
297
                        $fieldDec = ord(substr($buff32, 17, 1));
298
299
                        array_push($result, [$fieldName, $fieldType, $fieldLen, $fieldDec]);
300
                    }
301
                }
302
                ++$i;
303
            } else {
304
                $inHeader = false;
305
            }
306
        }
307
308
        fclose($DBFFile);
309
310
        return $result;
311
    }
312
313
    /**
314
     * Deletes record from the DBF file.
315
     *
316
     * @param int $index
317 20
     */
318
    private function _deleteRecordFromDBF($index)
319 20
    {
320 1
        if (@dbase_delete_record($this->DBFFile, $index)) {
321
            dbase_pack($this->DBFFile);
322 1
        }
323
    }
324
325
    /**
326 19
     * Loads SHP file metadata.
327
     *
328 19
     * @return bool
329
     */
330
    private function _loadHeaders()
331 19
    {
332
        if (Util::loadData('N', $this->readSHP(4)) != self::MAGIC) {
333 19
            $this->setError('Not a SHP file (file code mismatch)');
334
335 19
            return false;
336 19
        }
337 19
338 19
        /* Skip 20 unused bytes */
339 19
        $this->readSHP(20);
340 19
341 19
        $this->fileLength = Util::loadData('N', $this->readSHP(4));
342 19
343 19
        /* We currently ignore version */
344
        $this->readSHP(4);
345 19
346
        $this->shapeType = Util::loadData('V', $this->readSHP(4));
347
348
        $this->boundingBox = [];
349 19
        $this->boundingBox['xmin'] = Util::loadData('d', $this->readSHP(8));
350
        $this->boundingBox['ymin'] = Util::loadData('d', $this->readSHP(8));
351
        $this->boundingBox['xmax'] = Util::loadData('d', $this->readSHP(8));
352
        $this->boundingBox['ymax'] = Util::loadData('d', $this->readSHP(8));
353
        $this->boundingBox['zmin'] = Util::loadData('d', $this->readSHP(8));
354
        $this->boundingBox['zmax'] = Util::loadData('d', $this->readSHP(8));
355
        $this->boundingBox['mmin'] = Util::loadData('d', $this->readSHP(8));
356
        $this->boundingBox['mmax'] = Util::loadData('d', $this->readSHP(8));
357
358 13
        if (self::supportsDbase()) {
359
            $this->DBFHeader = $this->_loadDBFHeader();
360 13
        }
361 13
362 13
        return true;
363 13
    }
364
365
    /**
366
     * Saves bounding box record, possibly using 0 instead of not set values.
367
     *
368
     * @param file   $file File object
369
     * @param string $type Bounding box dimension (eg. xmax, mmin...)
370 13
     */
371
    private function _saveBBoxRecord($file, $type)
372 13
    {
373 13
        fwrite($file, Util::packDouble(
374 13
            isset($this->boundingBox[$type]) ? $this->boundingBox[$type] : 0
375 13
        ));
376 13
    }
377 13
378 13
    /**
379 13
     * Saves bounding box to a file.
380 13
     *
381
     * @param file $file File object
382
     */
383
    private function _saveBBox($file)
384
    {
385 13
        $this->_saveBBoxRecord($file, 'xmin');
386
        $this->_saveBBoxRecord($file, 'ymin');
387 13
        $this->_saveBBoxRecord($file, 'xmax');
388 13
        $this->_saveBBoxRecord($file, 'ymax');
389 13
        $this->_saveBBoxRecord($file, 'zmin');
390 13
        $this->_saveBBoxRecord($file, 'zmax');
391 13
        $this->_saveBBoxRecord($file, 'mmin');
392
        $this->_saveBBoxRecord($file, 'mmax');
393 13
    }
394 13
395 13
    /**
396 13
     * Saves SHP and SHX file metadata.
397 13
     */
398 13
    private function _saveHeaders()
399
    {
400
        fwrite($this->SHPFile, pack('NNNNNN', self::MAGIC, 0, 0, 0, 0, 0));
401
        fwrite($this->SHPFile, pack('N', $this->fileLength));
402
        fwrite($this->SHPFile, pack('V', 1000));
403
        fwrite($this->SHPFile, pack('V', $this->shapeType));
404
        $this->_saveBBox($this->SHPFile);
0 ignored issues
show
Bug introduced by
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...
405 19
406
        fwrite($this->SHXFile, pack('NNNNNN', self::MAGIC, 0, 0, 0, 0, 0));
407
        fwrite($this->SHXFile, pack('N', 50 + 4 * count($this->records)));
408 19
        fwrite($this->SHXFile, pack('V', 1000));
409 19
        fwrite($this->SHXFile, pack('V', $this->shapeType));
410 19
        $this->_saveBBox($this->SHXFile);
0 ignored issues
show
Bug introduced by
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...
411 19
    }
412
413
    /**
414
     * Loads records from SHP file (and DBF).
415
     *
416 19
     * @return bool
417 19
     */
418
    private function _loadRecords()
419
    {
420 19
        /* Need to start at offset 100 */
421 19
        while (! $this->eofSHP()) {
422
            $record = new ShapeRecord(-1);
423 19
            $record->loadFromFile($this, $this->SHPFile, $this->DBFFile);
424
            if ($record->lastError != '') {
425
                $this->setError($record->lastError);
426
427
                return false;
428
            }
429 13
            if (($record->shapeType === false || $record->shapeType === '') && $this->eofSHP()) {
430
                break;
431 13
            }
432 13
433 12
            $this->records[] = $record;
434
        }
435 12
436
        return true;
437
    }
438 12
439 12
    /**
440 12
     * Saves records to SHP and SHX files.
441 12
     */
442 12
    private function _saveRecords()
443 13
    {
444
        $offset = 50;
445
        if (is_array($this->records) && (count($this->records) > 0)) {
446
            foreach ($this->records as $index => $record) {
447
                //Save the record to the .shp file
448
                $record->saveToFile($this->SHPFile, $this->DBFFile, $index + 1);
449
450
                //Save the record to the .shx file
451
                fwrite($this->SHXFile, pack('N', $offset));
452
                fwrite($this->SHXFile, pack('N', $record->getContentLength()));
453
                $offset += (4 + $record->getContentLength());
454 23
            }
455
        }
456 23
    }
457 23
458 23
    /**
459 2
     * Generic interface to open files.
460
     *
461 2
     * @param bool   $toWrite   Whether file should be opened for writing
462
     * @param string $extension File extension
463
     * @param string $name      Verbose file name to report errors
464 21
     *
465
     * @return file|false File handle
466
     */
467
    private function _openFile($toWrite, $extension, $name)
468
    {
469
        $shp_name = $this->_getFilename($extension);
470
        $result = @fopen($shp_name, ($toWrite ? 'wb+' : 'rb'));
471
        if (! $result) {
472
            $this->setError(sprintf('It wasn\'t possible to open the %s file "%s"', $name, $shp_name));
473
474 23
            return false;
475
        }
476 23
477 23
        return $result;
478 2
    }
479
480
    /**
481 21
     * Opens SHP file.
482
     *
483
     * @param bool $toWrite Whether file should be opened for writing
484
     *
485
     * @return bool
486
     */
487 21
    private function _openSHPFile($toWrite = false)
488
    {
489 21
        $this->SHPFile = $this->_openFile($toWrite, '.shp', 'Shape');
490 21
        if (! $this->SHPFile) {
491 21
            return false;
492 21
        }
493 21
494
        return true;
495
    }
496
497
    /**
498
     * Closes SHP file.
499
     */
500
    private function _closeSHPFile()
501
    {
502 13
        if ($this->SHPFile) {
503
            fclose($this->SHPFile);
504 13
            $this->SHPFile = null;
505 13
        }
506
    }
507
508
    /**
509 13
     * Opens SHX file.
510
     *
511
     * @param bool $toWrite Whether file should be opened for writing
512
     *
513
     * @return bool
514
     */
515 13
    private function _openSHXFile($toWrite = false)
516
    {
517 13
        $this->SHXFile = $this->_openFile($toWrite, '.shx', 'Index');
518 13
        if (! $this->SHXFile) {
519 13
            return false;
520 13
        }
521 13
522
        return true;
523
    }
524
525
    /**
526
     * Closes SHX file.
527
     */
528 13
    private function _closeSHXFile()
529
    {
530 13
        if ($this->SHXFile) {
531 13
            fclose($this->SHXFile);
532
            $this->SHXFile = null;
533 13
        }
534
    }
535
536
    /**
537
     * Creates DBF file.
538
     *
539
     * @return bool
540
     */
541
    private function _createDBFFile()
542
    {
543
        if (! self::supportsDbase() || ! is_array($this->DBFHeader) || count($this->DBFHeader) == 0) {
544
            $this->DBFFile = null;
545
546
            return true;
547
        }
548
        $dbf_name = $this->_getFilename('.dbf');
549
550
        /* Unlink existing file */
551
        if (file_exists($dbf_name)) {
552
            unlink($dbf_name);
553
        }
554
555
        /* Create new file */
556
        $this->DBFFile = @dbase_create($dbf_name, $this->DBFHeader);
557
        if ($this->DBFFile === false) {
558 20
            $this->setError(sprintf('It wasn\'t possible to create the DBase file "%s"', $dbf_name));
559
560 20
            return false;
561 20
        }
562
563 20
        return true;
564
    }
565
566
    /**
567
     * Loads DBF file if supported.
568
     *
569
     * @return bool
570
     */
571
    private function _openDBFFile()
572
    {
573
        if (! self::supportsDbase()) {
574
            $this->DBFFile = null;
575
576
            return true;
577
        }
578
        $dbf_name = $this->_getFilename('.dbf');
579
        if (is_readable($dbf_name)) {
580
            $this->DBFFile = @dbase_open($dbf_name, 0);
581
            if (! $this->DBFFile) {
582
                $this->setError(sprintf('It wasn\'t possible to open the DBase file "%s"', $dbf_name));
583
584
                return false;
585 21
            }
586
        } else {
587 21
            $this->setError(sprintf('It wasn\'t possible to find the DBase file "%s"', $dbf_name));
588
589
            return false;
590
        }
591 21
592
        return true;
593
    }
594
595
    /**
596
     * Closes DBF file.
597
     */
598 3
    private function _closeDBFFile()
599
    {
600 3
        if ($this->DBFFile) {
601 3
            dbase_close($this->DBFFile);
602
            $this->DBFFile = null;
603
        }
604
    }
605
606
    /**
607
     * Sets error message.
608
     *
609
     * @param string $error
610 20
     */
611
    public function setError($error)
612 20
    {
613
        $this->lastError = $error;
614
    }
615
616
    /**
617
     * Reads given number of bytes from SHP file.
618
     *
619
     * @param int $bytes
620 19
     *
621
     * @return string
622 19
     */
623
    public function readSHP($bytes)
624
    {
625
        return fread($this->SHPFile, $bytes);
626
    }
627
628
    /**
629
     * Checks whether file is at EOF.
630 1
     *
631
     * @return bool
632 1
     */
633
    public function eofSHP()
634
    {
635
        return feof($this->SHPFile);
636
    }
637
638
    /**
639
     * Returns shape name.
640
     *
641
     * @return string
642
     */
643 8
    public function getShapeName()
644
    {
645 8
        return Util::nameShape($this->shapeType);
646
    }
647
648
    /**
649
     * Check whether file contains measure data.
650
     *
651
     * For some reason this is distinguished by zero bounding box in the
652
     * specification.
653
     *
654
     * @return bool
655
     */
656
    public function hasMeasure()
657
    {
658
        return $this->boundingBox['mmin'] != 0 || $this->boundingBox['mmax'] != 0;
659
    }
660
}
661