Skip to content

Commit 1d06a29

Browse files
feat(validator): accept the legacy KML namespaces
KmlValidator hardcoded the OGC 2.2 namespace and rejected everything else, so any document exported before the OGC took the format over (earth.google.com/kml/2.0, 2.1 and 2.2, still common in the wild) failed validation with "Invalid or missing KML namespace" and could not be parsed at all. The accepted namespaces are now a list, configurable through the new supported_namespaces key, and XPath is registered against the namespace the document actually declares rather than the one we assumed. That last part is what makes a 2.1 document parse end to end instead of validating and then returning no placemarks. The validator also ignored the kml-parser.namespace config entirely, even though KmlParser read it. Setting that key made every document invalid, since validation still demanded 2.2. KmlParser now passes the configured namespaces to the validator, so the key finally does what it says. KmlValidator keeps working standalone, defaulting to the known namespaces without touching the container.
1 parent 2aab5b2 commit 1d06a29

5 files changed

Lines changed: 170 additions & 6 deletions

File tree

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,25 @@ return [
3535
|
3636
*/
3737
'namespace' => 'http://www.opengis.net/kml/2.2',
38-
38+
39+
/*
40+
|--------------------------------------------------------------------------
41+
| Accepted KML Namespaces
42+
|--------------------------------------------------------------------------
43+
|
44+
| A document is rejected unless it declares one of these as its default
45+
| namespace. 2.2 is the OGC standard; the earth.google.com variants come
46+
| from Google Earth and older exporters and are still common in the wild.
47+
| XPath always runs against whichever one the document actually declares.
48+
|
49+
*/
50+
'supported_namespaces' => [
51+
'http://www.opengis.net/kml/2.2',
52+
'http://earth.google.com/kml/2.2',
53+
'http://earth.google.com/kml/2.1',
54+
'http://earth.google.com/kml/2.0',
55+
],
56+
3957
/*
4058
|--------------------------------------------------------------------------
4159
| Temporary Directory

config/kml-parser.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@
1313
*/
1414
'namespace' => 'http://www.opengis.net/kml/2.2',
1515

16+
/*
17+
|--------------------------------------------------------------------------
18+
| Accepted KML Namespaces
19+
|--------------------------------------------------------------------------
20+
|
21+
| A document is rejected unless it declares one of these as its default
22+
| namespace. 2.2 is the OGC standard; the earth.google.com variants come
23+
| from Google Earth and older exporters and are still common in the wild.
24+
| XPath always runs against whichever one the document actually declares.
25+
|
26+
*/
27+
'supported_namespaces' => [
28+
'http://www.opengis.net/kml/2.2',
29+
'http://earth.google.com/kml/2.2',
30+
'http://earth.google.com/kml/2.1',
31+
'http://earth.google.com/kml/2.0',
32+
],
33+
1634
/*
1735
|--------------------------------------------------------------------------
1836
| Temporary Directory

src/KmlParser.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,20 @@ class KmlParser
2121
public function __construct()
2222
{
2323
$this->namespace = config('kml-parser.namespace', $this->namespace);
24-
$this->validator = new KmlValidator;
24+
$this->validator = new KmlValidator($this->supportedNamespaces());
25+
}
26+
27+
/**
28+
* Namespaces a document is allowed to declare: the configured primary one
29+
* plus every variant listed in the config.
30+
*
31+
* @return array<int, string>
32+
*/
33+
protected function supportedNamespaces(): array
34+
{
35+
$supported = config('kml-parser.supported_namespaces', KmlValidator::DEFAULT_NAMESPACES);
36+
37+
return array_values(array_unique(array_merge([$this->namespace], (array) $supported)));
2538
}
2639

2740
/**
@@ -59,7 +72,7 @@ public function loadFromString(string $content): self
5972
$this->validator->validateDocument($xml);
6073

6174
$this->xml = $xml;
62-
$this->xml->registerXPathNamespace('kml', $this->namespace);
75+
$this->xml->registerXPathNamespace('kml', $this->validator->documentNamespace());
6376

6477
return $this;
6578
}

src/Validators/KmlValidator.php

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,47 @@
88

99
class KmlValidator
1010
{
11-
protected string $namespace = 'http://www.opengis.net/kml/2.2';
11+
/**
12+
* The namespaces a KML document is allowed to declare.
13+
*
14+
* 2.2 is the OGC standard. The earth.google.com variants predate the OGC
15+
* taking the format over, and exports carrying them are still in wide
16+
* circulation, so rejecting them outright rejects valid files.
17+
*
18+
* @var array<int, string>
19+
*/
20+
public const DEFAULT_NAMESPACES = [
21+
'http://www.opengis.net/kml/2.2',
22+
'http://earth.google.com/kml/2.2',
23+
'http://earth.google.com/kml/2.1',
24+
'http://earth.google.com/kml/2.0',
25+
];
26+
27+
/** @var array<int, string> */
28+
protected array $namespaces;
29+
30+
protected string $documentNamespace = '';
1231

1332
protected SimpleXMLElement $xml;
1433

34+
/**
35+
* @param array<int, string>|null $namespaces Accepted namespaces, defaults to DEFAULT_NAMESPACES.
36+
*/
37+
public function __construct(?array $namespaces = null)
38+
{
39+
$namespaces = array_values(array_filter($namespaces ?? self::DEFAULT_NAMESPACES));
40+
41+
$this->namespaces = $namespaces !== [] ? $namespaces : self::DEFAULT_NAMESPACES;
42+
}
43+
44+
/**
45+
* The namespace declared by the document that was validated last.
46+
*/
47+
public function documentNamespace(): string
48+
{
49+
return $this->documentNamespace;
50+
}
51+
1552
/**
1653
* Parse and validate raw KML content.
1754
*
@@ -46,11 +83,18 @@ public function validateDocument(SimpleXMLElement $xml): void
4683
$this->xml = $xml;
4784

4885
$namespaces = $xml->getDocNamespaces();
49-
if (! isset($namespaces['']) || $namespaces[''] !== $this->namespace) {
86+
$declared = $namespaces[''] ?? null;
87+
88+
if ($declared === null || ! in_array($declared, $this->namespaces, true)) {
5089
throw new KmlException('Invalid or missing KML namespace');
5190
}
5291

53-
$xml->registerXPathNamespace('kml', $this->namespace);
92+
/*
93+
* XPath has to run against the namespace the document actually
94+
* declares, not the one we would have preferred it to use.
95+
*/
96+
$this->documentNamespace = $declared;
97+
$xml->registerXPathNamespace('kml', $declared);
5498

5599
if (empty($xml->Document)) {
56100
throw new KmlException('Missing required element: Document');

tests/NamespaceSupportTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
use PlinCode\KmlParser\Exceptions\KmlException;
4+
use PlinCode\KmlParser\KmlParser;
5+
use PlinCode\KmlParser\Validators\KmlValidator;
6+
7+
function kmlIn(string $namespace): string
8+
{
9+
return <<<XML
10+
<?xml version="1.0" encoding="UTF-8"?>
11+
<kml xmlns="{$namespace}">
12+
<Document>
13+
<name>Legacy export</name>
14+
<Placemark>
15+
<name>Lago Blu</name>
16+
<Point>
17+
<coordinates>7.7300965,45.8635629,0</coordinates>
18+
</Point>
19+
</Placemark>
20+
</Document>
21+
</kml>
22+
XML;
23+
}
24+
25+
it('parses a document in the OGC 2.2 namespace', function () {
26+
$parser = (new KmlParser)->loadFromString(kmlIn('http://www.opengis.net/kml/2.2'));
27+
28+
expect($parser->getPlacemarks())->toHaveCount(1)
29+
->and($parser->getDocumentName())->toBe('Legacy export');
30+
});
31+
32+
it('parses documents in the legacy Google namespaces', function (string $namespace) {
33+
$parser = (new KmlParser)->loadFromString(kmlIn($namespace));
34+
35+
expect($parser->getPlacemarks())->toHaveCount(1)
36+
->and($parser->getPlacemarks()[0]['name'])->toBe('Lago Blu')
37+
->and($parser->getDocumentName())->toBe('Legacy export');
38+
})->with([
39+
'http://earth.google.com/kml/2.2',
40+
'http://earth.google.com/kml/2.1',
41+
'http://earth.google.com/kml/2.0',
42+
]);
43+
44+
it('still rejects a namespace that is not KML', function () {
45+
expect(fn () => (new KmlParser)->loadFromString(kmlIn('http://wrong.namespace')))
46+
->toThrow(KmlException::class, 'Invalid or missing KML namespace');
47+
});
48+
49+
it('accepts a namespace added through the config', function () {
50+
config()->set('kml-parser.supported_namespaces', ['http://example.test/kml']);
51+
52+
$parser = (new KmlParser)->loadFromString(kmlIn('http://example.test/kml'));
53+
54+
expect($parser->getPlacemarks())->toHaveCount(1);
55+
});
56+
57+
it('keeps accepting the configured primary namespace', function () {
58+
config()->set('kml-parser.namespace', 'http://example.test/kml');
59+
config()->set('kml-parser.supported_namespaces', []);
60+
61+
$parser = (new KmlParser)->loadFromString(kmlIn('http://example.test/kml'));
62+
63+
expect($parser->getPlacemarks())->toHaveCount(1);
64+
});
65+
66+
it('exposes the namespace the document declared', function () {
67+
$validator = new KmlValidator;
68+
$validator->validateDocument(new SimpleXMLElement(kmlIn('http://earth.google.com/kml/2.1')));
69+
70+
expect($validator->documentNamespace())->toBe('http://earth.google.com/kml/2.1');
71+
});

0 commit comments

Comments
 (0)