Skip to content

Commit 6904e7c

Browse files
Add KML Validator
1 parent 0761e93 commit 6904e7c

File tree

9 files changed

+301
-14
lines changed

9 files changed

+301
-14
lines changed

src/Enums/GeometryType.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace PlinCode\KmlParser\Enums;
4+
5+
enum GeometryType: string
6+
{
7+
case POINT = 'Point';
8+
case LINE_STRING = 'LineString';
9+
case POLYGON = 'Polygon';
10+
case MULTI_GEOMETRY = 'MultiGeometry';
11+
12+
/**
13+
* Get all values as array
14+
*
15+
* @return array<string>
16+
*/
17+
public static function values(): array
18+
{
19+
return array_map(fn (self $type) => $type->value, self::cases());
20+
}
21+
}

src/Enums/RequiredElement.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace PlinCode\KmlParser\Enums;
4+
5+
enum RequiredElement: string
6+
{
7+
case KML = 'kml';
8+
case DOCUMENT = 'Document';
9+
10+
/**
11+
* Get all values as array
12+
*
13+
* @return array<string>
14+
*/
15+
public static function values(): array
16+
{
17+
return array_map(fn (self $element) => $element->value, self::cases());
18+
}
19+
}

src/Exceptions/KmlException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
class KmlException extends Exception
88
{
99
//
10-
}
10+
}

src/Exceptions/KmlParserException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ public static function failedToParse(string $message): self
2323
{
2424
return new self("Failed to parse KML content: {$message}");
2525
}
26-
}
26+
}

src/Exceptions/KmzExtractorException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ public static function invalidZipFile(string $path): self
2323
{
2424
return new self("Invalid KMZ file: {$path}");
2525
}
26-
}
26+
}

src/KmlParser.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Exception;
66
use PlinCode\KmlParser\Exceptions\KmlParserException;
77
use PlinCode\KmlParser\Traits\ParsesCoordinates;
8+
use PlinCode\KmlParser\Validators\KmlValidator;
89
use SimpleXMLElement;
910

1011
class KmlParser
@@ -15,9 +16,12 @@ class KmlParser
1516

1617
protected string $namespace = 'http://www.opengis.net/kml/2.2';
1718

19+
protected KmlValidator $validator;
20+
1821
public function __construct()
1922
{
2023
$this->namespace = config('kml-parser.namespace', $this->namespace);
24+
$this->validator = new KmlValidator;
2125
}
2226

2327
/**
@@ -41,9 +45,9 @@ public function loadFromFile(string $path): self
4145
*/
4246
public function loadFromString(string $content): self
4347
{
44-
libxml_use_internal_errors(true);
45-
4648
try {
49+
$this->validator->validate($content);
50+
libxml_use_internal_errors(true);
4751
$this->xml = new SimpleXMLElement($content);
4852

4953
$errors = libxml_get_errors();

src/Validators/KmlValidator.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace PlinCode\KmlParser\Validators;
4+
5+
use PlinCode\KmlParser\Enums\GeometryType;
6+
use PlinCode\KmlParser\Exceptions\KmlException;
7+
use SimpleXMLElement;
8+
9+
class KmlValidator
10+
{
11+
protected string $namespace = 'http://www.opengis.net/kml/2.2';
12+
13+
protected SimpleXMLElement $xml;
14+
15+
public function validate(string $content): void
16+
{
17+
libxml_use_internal_errors(true);
18+
19+
try {
20+
$this->xml = new SimpleXMLElement($content);
21+
22+
$namespaces = $this->xml->getDocNamespaces();
23+
if (! isset($namespaces['']) || $namespaces[''] !== $this->namespace) {
24+
throw new KmlException('Invalid or missing KML namespace');
25+
}
26+
27+
$this->xml->registerXPathNamespace('kml', $this->namespace);
28+
29+
if (empty($this->xml->Document)) {
30+
throw new KmlException('Missing required element: Document');
31+
}
32+
33+
$placemarks = $this->xml->xpath('//kml:Placemark');
34+
if (! empty($placemarks)) {
35+
foreach ($placemarks as $placemark) {
36+
$this->validatePlacemark($placemark);
37+
}
38+
}
39+
} catch (KmlException $e) {
40+
throw $e;
41+
} catch (\Exception $e) {
42+
throw new KmlException('Invalid KML content: '.$e->getMessage());
43+
} finally {
44+
libxml_clear_errors();
45+
}
46+
}
47+
48+
protected function validatePlacemark(SimpleXMLElement $placemark): void
49+
{
50+
$hasGeometry = false;
51+
foreach (GeometryType::cases() as $type) {
52+
if ($placemark->{$type->value}) {
53+
$hasGeometry = true;
54+
$this->validateGeometryCoordinates($placemark->{$type->value}, $type->value);
55+
break;
56+
}
57+
}
58+
59+
if (! $hasGeometry) {
60+
throw new KmlException('Found Placemark without valid geometry');
61+
}
62+
}
63+
64+
protected function validateGeometryCoordinates(SimpleXMLElement $geometry, string $type): void
65+
{
66+
if ($type === 'Polygon') {
67+
if (empty($geometry->outerBoundaryIs->LinearRing->coordinates)) {
68+
throw new KmlException('Empty coordinates in Polygon geometry');
69+
}
70+
$coordinates = (string) $geometry->outerBoundaryIs->LinearRing->coordinates;
71+
} else {
72+
if (empty($geometry->coordinates)) {
73+
throw new KmlException('Empty coordinates in geometry');
74+
}
75+
$coordinates = (string) $geometry->coordinates;
76+
}
77+
78+
if (empty(trim($coordinates))) {
79+
throw new KmlException('Empty coordinates in geometry');
80+
}
81+
82+
$coords = preg_split('/\s+/', trim($coordinates));
83+
foreach ($coords as $coord) {
84+
if (empty(trim($coord))) {
85+
continue;
86+
}
87+
88+
$parts = explode(',', trim($coord));
89+
if (count($parts) < 2 || count($parts) > 3) {
90+
throw new KmlException('Invalid coordinate format');
91+
}
92+
93+
if (! is_numeric($parts[0]) || $parts[0] < -180 || $parts[0] > 180) {
94+
throw new KmlException("Invalid longitude value: {$parts[0]}");
95+
}
96+
97+
if (! is_numeric($parts[1]) || $parts[1] < -90 || $parts[1] > 90) {
98+
throw new KmlException("Invalid latitude value: {$parts[1]}");
99+
}
100+
101+
if (isset($parts[2]) && ! is_numeric($parts[2])) {
102+
throw new KmlException("Invalid altitude value: {$parts[2]}");
103+
}
104+
}
105+
}
106+
}

tests/ExceptionsTest.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,51 @@
77

88
it('throws exception when KML file not found', function () {
99
$parser = new KmlParser;
10-
10+
1111
$parser->loadFromFile('non-existent.kml');
1212
})->throws(KmlParserException::class, 'KML file not found: non-existent.kml');
1313

1414
it('throws exception when no KML data loaded', function () {
1515
$parser = new KmlParser;
16-
16+
1717
$parser->getPlacemarks();
1818
})->throws(KmlParserException::class, 'No KML data loaded');
1919

2020
it('throws exception on invalid XML', function () {
2121
$parser = new KmlParser;
22-
22+
2323
$parser->loadFromString('invalid xml content');
2424
})->throws(KmlParserException::class, 'Failed to parse KML content');
2525

2626
it('throws exception when KMZ file not found', function () {
2727
$extractor = new KmzExtractor;
28-
28+
2929
$extractor->extractKmlContent('non-existent.kmz');
3030
})->throws(KmzExtractorException::class, 'KMZ file not found: non-existent.kmz');
3131

3232
it('throws exception when KMZ file is invalid', function () {
3333
$extractor = new KmzExtractor;
34-
34+
3535
// Create an empty file
3636
file_put_contents(__DIR__.'/files/invalid.kmz', 'not a zip file');
37-
37+
3838
$extractor->extractKmlContent(__DIR__.'/files/invalid.kmz');
3939
})->throws(KmzExtractorException::class, 'Invalid KMZ file');
4040

4141
it('throws exception when no KML found in KMZ', function () {
4242
$extractor = new KmzExtractor;
43-
43+
4444
// Create a valid zip file without KML
4545
$zip = new ZipArchive;
4646
$zip->open(__DIR__.'/files/no-kml.kmz', ZipArchive::CREATE);
4747
$zip->addFromString('test.txt', 'test content');
4848
$zip->close();
49-
49+
5050
$extractor->extractKmlContent(__DIR__.'/files/no-kml.kmz');
5151
})->throws(KmzExtractorException::class, 'No KML file found in KMZ archive');
5252

5353
afterEach(function () {
5454
// Cleanup test files
5555
@unlink(__DIR__.'/files/invalid.kmz');
5656
@unlink(__DIR__.'/files/no-kml.kmz');
57-
});
57+
});

0 commit comments

Comments
 (0)