Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,25 @@ return [
|
*/
'namespace' => 'http://www.opengis.net/kml/2.2',


/*
|--------------------------------------------------------------------------
| Accepted KML Namespaces
|--------------------------------------------------------------------------
|
| A document is rejected unless it declares one of these as its default
| namespace. 2.2 is the OGC standard; the earth.google.com variants come
| from Google Earth and older exporters and are still common in the wild.
| XPath always runs against whichever one the document actually declares.
|
*/
'supported_namespaces' => [
'http://www.opengis.net/kml/2.2',
'http://earth.google.com/kml/2.2',
'http://earth.google.com/kml/2.1',
'http://earth.google.com/kml/2.0',
],

/*
|--------------------------------------------------------------------------
| Temporary Directory
Expand Down
18 changes: 18 additions & 0 deletions config/kml-parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@
*/
'namespace' => 'http://www.opengis.net/kml/2.2',

/*
|--------------------------------------------------------------------------
| Accepted KML Namespaces
|--------------------------------------------------------------------------
|
| A document is rejected unless it declares one of these as its default
| namespace. 2.2 is the OGC standard; the earth.google.com variants come
| from Google Earth and older exporters and are still common in the wild.
| XPath always runs against whichever one the document actually declares.
|
*/
'supported_namespaces' => [
'http://www.opengis.net/kml/2.2',
'http://earth.google.com/kml/2.2',
'http://earth.google.com/kml/2.1',
'http://earth.google.com/kml/2.0',
],

/*
|--------------------------------------------------------------------------
| Temporary Directory
Expand Down
17 changes: 15 additions & 2 deletions src/KmlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,20 @@ class KmlParser
public function __construct()
{
$this->namespace = config('kml-parser.namespace', $this->namespace);
$this->validator = new KmlValidator;
$this->validator = new KmlValidator($this->supportedNamespaces());
}

/**
* Namespaces a document is allowed to declare: the configured primary one
* plus every variant listed in the config.
*
* @return array<int, string>
*/
protected function supportedNamespaces(): array
{
$supported = config('kml-parser.supported_namespaces', KmlValidator::DEFAULT_NAMESPACES);

return array_values(array_unique(array_merge([$this->namespace], (array) $supported)));
}

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

$this->xml = $xml;
$this->xml->registerXPathNamespace('kml', $this->namespace);
$this->xml->registerXPathNamespace('kml', $this->validator->documentNamespace());

return $this;
}
Expand Down
50 changes: 47 additions & 3 deletions src/Validators/KmlValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,47 @@

class KmlValidator
{
protected string $namespace = 'http://www.opengis.net/kml/2.2';
/**
* The namespaces a KML document is allowed to declare.
*
* 2.2 is the OGC standard. The earth.google.com variants predate the OGC
* taking the format over, and exports carrying them are still in wide
* circulation, so rejecting them outright rejects valid files.
*
* @var array<int, string>
*/
public const DEFAULT_NAMESPACES = [
'http://www.opengis.net/kml/2.2',
'http://earth.google.com/kml/2.2',
'http://earth.google.com/kml/2.1',
'http://earth.google.com/kml/2.0',
];

/** @var array<int, string> */
protected array $namespaces;

protected string $documentNamespace = '';

protected SimpleXMLElement $xml;

/**
* @param array<int, string>|null $namespaces Accepted namespaces, defaults to DEFAULT_NAMESPACES.
*/
public function __construct(?array $namespaces = null)
{
$namespaces = array_values(array_filter($namespaces ?? self::DEFAULT_NAMESPACES));

$this->namespaces = $namespaces !== [] ? $namespaces : self::DEFAULT_NAMESPACES;
}

/**
* The namespace declared by the document that was validated last.
*/
public function documentNamespace(): string
{
return $this->documentNamespace;
}

/**
* Parse and validate raw KML content.
*
Expand Down Expand Up @@ -46,11 +83,18 @@ public function validateDocument(SimpleXMLElement $xml): void
$this->xml = $xml;

$namespaces = $xml->getDocNamespaces();
if (! isset($namespaces['']) || $namespaces[''] !== $this->namespace) {
$declared = $namespaces[''] ?? null;

if ($declared === null || ! in_array($declared, $this->namespaces, true)) {
throw new KmlException('Invalid or missing KML namespace');
}

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

if (empty($xml->Document)) {
throw new KmlException('Missing required element: Document');
Expand Down
71 changes: 71 additions & 0 deletions tests/NamespaceSupportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

use PlinCode\KmlParser\Exceptions\KmlException;
use PlinCode\KmlParser\KmlParser;
use PlinCode\KmlParser\Validators\KmlValidator;

function kmlIn(string $namespace): string
{
return <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="{$namespace}">
<Document>
<name>Legacy export</name>
<Placemark>
<name>Lago Blu</name>
<Point>
<coordinates>7.7300965,45.8635629,0</coordinates>
</Point>
</Placemark>
</Document>
</kml>
XML;
}

it('parses a document in the OGC 2.2 namespace', function () {
$parser = (new KmlParser)->loadFromString(kmlIn('http://www.opengis.net/kml/2.2'));

expect($parser->getPlacemarks())->toHaveCount(1)
->and($parser->getDocumentName())->toBe('Legacy export');
});

it('parses documents in the legacy Google namespaces', function (string $namespace) {
$parser = (new KmlParser)->loadFromString(kmlIn($namespace));

expect($parser->getPlacemarks())->toHaveCount(1)
->and($parser->getPlacemarks()[0]['name'])->toBe('Lago Blu')
->and($parser->getDocumentName())->toBe('Legacy export');
})->with([
'http://earth.google.com/kml/2.2',
'http://earth.google.com/kml/2.1',
'http://earth.google.com/kml/2.0',
]);

it('still rejects a namespace that is not KML', function () {
expect(fn () => (new KmlParser)->loadFromString(kmlIn('http://wrong.namespace')))
->toThrow(KmlException::class, 'Invalid or missing KML namespace');
});

it('accepts a namespace added through the config', function () {
config()->set('kml-parser.supported_namespaces', ['http://example.test/kml']);

$parser = (new KmlParser)->loadFromString(kmlIn('http://example.test/kml'));

expect($parser->getPlacemarks())->toHaveCount(1);
});

it('keeps accepting the configured primary namespace', function () {
config()->set('kml-parser.namespace', 'http://example.test/kml');
config()->set('kml-parser.supported_namespaces', []);

$parser = (new KmlParser)->loadFromString(kmlIn('http://example.test/kml'));

expect($parser->getPlacemarks())->toHaveCount(1);
});

it('exposes the namespace the document declared', function () {
$validator = new KmlValidator;
$validator->validateDocument(new SimpleXMLElement(kmlIn('http://earth.google.com/kml/2.1')));

expect($validator->documentNamespace())->toBe('http://earth.google.com/kml/2.1');
});