Skip to content

Commit f9a52ce

Browse files
committed
Refactor QName type
1 parent a763d89 commit f9a52ce

File tree

3 files changed

+128
-22
lines changed

3 files changed

+128
-22
lines changed

src/AbstractElement.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use SimpleSAML\XML\Assert\Assert;
1010
use SimpleSAML\XML\Exception\{MissingAttributeException, SchemaViolationException};
1111
use SimpleSAML\XML\SerializableElementTrait;
12-
use SimpleSAML\XML\Type\{StringValue, ValueTypeInterface};
12+
use SimpleSAML\XML\Type\{QNameValue, StringValue, ValueTypeInterface};
1313

1414
use function array_slice;
1515
use function defined;
@@ -79,7 +79,7 @@ public static function getAttribute(
7979
);
8080

8181
$value = $xml->getAttribute($name);
82-
return $type::fromString($value);
82+
return ($type === QNameValue::class) ? QNameValue::fromDocument($value, $xml) : $type::fromString($value);
8383
}
8484

8585

src/Type/QNameValue.php

Lines changed: 112 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,37 @@
44

55
namespace SimpleSAML\XML\Type;
66

7+
use DOMElement;
8+
use PREG_UNMATCHED_AS_NULL;
79
use SimpleSAML\XML\Assert\Assert;
810
use SimpleSAML\XML\Exception\SchemaViolationException;
11+
use SimpleSAML\XML\Type\{AnyURIValue, NCNameValue};
912

10-
use function explode;
13+
use function preg_match;
1114

1215
/**
1316
* @package simplesaml/xml-common
1417
*/
1518
class QNameValue extends AbstractValueType
1619
{
20+
protected ?AnyURIValue $namespaceURI;
21+
protected ?NCNameValue $namespacePrefix;
22+
protected NCNameValue $localName;
23+
24+
private static string $qname_regex = '/^
25+
(?:
26+
\{ # Match a literal {
27+
(\S+) # Match one or more non-whitespace character
28+
\} # Match a literal }
29+
(?:
30+
([\w_][\w.-]*) # Match a-z or underscore followed by any word-character, dot or dash
31+
: # Match a literal :
32+
)?
33+
)? # Namespace and prefix are optional
34+
([\w_][\w.-]*) # Match a-z or underscore followed by any word-character, dot or dash
35+
$/Dimx';
36+
37+
1738
/**
1839
* Sanitize the value.
1940
*
@@ -35,8 +56,49 @@ protected function sanitizeValue(string $value): string
3556
*/
3657
protected function validateValue(string $value): void
3758
{
38-
// Note: value must already be sanitized before validating
39-
Assert::validQName($this->sanitizeValue($value), SchemaViolationException::class);
59+
$qName = $this->sanitizeValue($value);
60+
61+
/**
62+
* Split our custom format of {<namespaceURI>}<prefix>:<localName> into individual parts
63+
*/
64+
$result = preg_match(
65+
self::$qname_regex,
66+
$qName,
67+
$matches,
68+
PREG_UNMATCHED_AS_NULL,
69+
);
70+
71+
if ($result && count($matches) === 4) {
72+
list($qName, $namespaceURI, $namespacePrefix, $localName) = $matches;
73+
74+
$this->namespaceURI = ($namespaceURI !== null) ? AnyURIValue::fromString($namespaceURI) : null;
75+
$this->namespacePrefix = ($namespacePrefix !== null) ? NCNameValue::fromString($namespacePrefix) : null;
76+
$this->localName = NCNameValue::fromString($localName);
77+
} else {
78+
throw new SchemaViolationException(sprintf('\'%s\' is not a valid xs:QName.', $qName));
79+
}
80+
}
81+
82+
83+
/**
84+
* Get the value.
85+
*
86+
* @return string
87+
*/
88+
public function getValue(): string
89+
{
90+
return $this->getNamespacePrefix() . ':' . $this->getLocalName();
91+
}
92+
93+
94+
/**
95+
* Get the namespaceURI for this qualified name.
96+
*
97+
* @return \SimpleSAML\SAML11\Type\AnyURIValue|null
98+
*/
99+
public function getNamespaceURI(): ?AnyURIValue
100+
{
101+
return $this->namespaceURI;
40102
}
41103

42104

@@ -47,12 +109,7 @@ protected function validateValue(string $value): void
47109
*/
48110
public function getNamespacePrefix(): ?NCNameValue
49111
{
50-
$qname = explode(':', $this->getValue(), 2);
51-
if (count($qname) === 2) {
52-
return NCNameValue::fromString($qname[0]);
53-
}
54-
55-
return null;
112+
return $this->namespacePrefix;
56113
}
57114

58115

@@ -63,11 +120,53 @@ public function getNamespacePrefix(): ?NCNameValue
63120
*/
64121
public function getLocalName(): NCNameValue
65122
{
66-
$qname = explode(':', $this->getValue(), 2);
67-
if (count($qname) === 2) {
68-
return NCNameValue::fromString($qname[1]);
123+
return $this->localName;
124+
}
125+
126+
127+
/**
128+
* @param \SimpleSAML\XML\Type\NCNameValue $localName
129+
* @param \SimpleSAML\XML\Type\AnyURIValue|null $namespaceURI
130+
* @param \SimpleSAML\XML\Type\NCNameValue|null $namespacePrefix
131+
* @return static
132+
*/
133+
public static function fromParts(
134+
NCNameValue $localName,
135+
?AnyURIValue $namespaceURI,
136+
?NCNameValue $namespacePrefix,
137+
): static {
138+
if ($namespaceURI === null) {
139+
// If we don't have a namespace, we can't have a prefix either
140+
Assert::null($namespacePrefix, SchemaViolationException::class);
141+
return new static($localName);
69142
}
70143

71-
return NCNameValue::fromString($qname[0]);
144+
return new static(
145+
'{' . $namespaceURI->getValue() . '}'
146+
. ($namespacePrefix ? ($namespacePrefix->getValue() . ':') : '')
147+
. $localName,
148+
);
149+
}
150+
151+
152+
/**
153+
* @param string $qname
154+
*/
155+
public static function fromDocument(
156+
string $qName,
157+
DOMElement $element,
158+
) {
159+
$namespacePrefix = null;
160+
if (str_contains($qName, ':')) {
161+
list($namespacePrefix, $localName) = explode(':', $qName, 2);
162+
} else {
163+
// No prefix
164+
$localName = $qName;
165+
}
166+
167+
// Will return the default namespace (if any) when prefix is NULL
168+
$namespaceURI = $element->lookupNamespaceUri($namespacePrefix);
169+
170+
return new static('{' . $namespaceURI . '}' . $namespacePrefix . ':' . $localName);
72171
}
73172
}

tests/Type/QNameValueTest.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ final class QNameValueTest extends TestCase
2626
*/
2727
#[DataProvider('provideInvalidQName')]
2828
#[DataProvider('provideValidQName')]
29-
#[DataProviderExternal(QNameTest::class, 'provideValidQName')]
30-
#[DependsOnClass(QNameTest::class)]
29+
// #[DependsOnClass(QNameTest::class)]
3130
public function testQName(bool $shouldPass, string $qname): void
3231
{
3332
try {
@@ -40,7 +39,6 @@ public function testQName(bool $shouldPass, string $qname): void
4039

4140

4241
/**
43-
*/
4442
#[DependsOnClass(QNameTest::class)]
4543
public function testHelpers(): void
4644
{
@@ -52,6 +50,7 @@ public function testHelpers(): void
5250
$this->assertNull($qn->getNamespacePrefix());
5351
$this->assertEquals(strval($qn->getLocalName()), 'Test');
5452
}
53+
*/
5554

5655

5756
/**
@@ -60,8 +59,16 @@ public function testHelpers(): void
6059
public static function provideValidQName(): array
6160
{
6261
return [
63-
'prefixed newline' => [true, "\nsome:Test"],
64-
'trailing newline' => [true, "some:Test\n"],
62+
'valid' => [true, '{urn:x-simplesamlphp:namespace}ssp:Chunk'],
63+
'valid without namespace' => [true, '{urn:x-simplesamlphp:namespace}Chunk'],
64+
// both parts can contain a dash
65+
'1st part containing dash' => [true, '{urn:x-simplesamlphp:namespace}s-sp:Chunk'],
66+
'2nd part containing dash' => [true, '{urn:x-simplesamlphp:namespace}ssp:Ch-unk'],
67+
'both parts containing dash' => [true, '{urn:x-simplesamlphp:namespace}s-sp:Ch-unk'],
68+
// A single NCName is also a valid QName
69+
'no colon' => [true, 'Test'],
70+
'prefixed newline' => [true, "\nTest"],
71+
'trailing newline' => [true, "Test\n"],
6572
];
6673
}
6774

@@ -72,8 +79,8 @@ public static function provideValidQName(): array
7279
public static function provideInvalidQName(): array
7380
{
7481
return [
75-
'start 2nd part with dash' => [false, 'some:-Test'],
76-
'start both parts with dash' => [false, '-some:-Test'],
82+
'empty namespace' => [false, '{}Test'],
83+
'start 2nd part with dash' => [false, '-Test'],
7784
'start with colon' => [false, ':test'],
7885
'multiple colons' => [false, 'test:test:test'],
7986
'start with digit' => [false, '1Test'],

0 commit comments

Comments
 (0)