Skip to content

Commit 901084c

Browse files
committed
Refactor QName type
1 parent a763d89 commit 901084c

File tree

5 files changed

+134
-43
lines changed

5 files changed

+134
-43
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/Base64BinaryValue.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use SimpleSAML\XML\Assert\Assert;
88
use SimpleSAML\XML\Exception\SchemaViolationException;
99

10-
use preg_replace;
10+
use function preg_replace;
1111

1212
/**
1313
* @package simplesaml/xml-common

src/Type/QNameValue.php

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

55
namespace SimpleSAML\XML\Type;
66

7+
use DOMElement;
78
use SimpleSAML\XML\Assert\Assert;
89
use SimpleSAML\XML\Exception\SchemaViolationException;
10+
use SimpleSAML\XML\Type\{AnyURIValue, NCNameValue};
911

10-
use function explode;
12+
use function preg_match;
1113

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

42103

@@ -47,12 +108,7 @@ protected function validateValue(string $value): void
47108
*/
48109
public function getNamespacePrefix(): ?NCNameValue
49110
{
50-
$qname = explode(':', $this->getValue(), 2);
51-
if (count($qname) === 2) {
52-
return NCNameValue::fromString($qname[0]);
53-
}
54-
55-
return null;
111+
return $this->namespacePrefix;
56112
}
57113

58114

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

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

src/TypedTextContentTrait.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use DOMElement;
88
use SimpleSAML\XML\Assert\Assert;
99
use SimpleSAML\XML\Exception\{InvalidDOMElementException, InvalidValueTypeException};
10-
use SimpleSAML\XML\Type\{ValueTypeInterface, StringValue};
10+
use SimpleSAML\XML\Type\{QNameValue, StringValue, ValueTypeInterface};
1111

1212
use function defined;
1313
use function strval;
@@ -40,9 +40,14 @@ public static function fromXML(DOMElement $xml): static
4040
Assert::same($xml->namespaceURI, static::NS, InvalidDOMElementException::class);
4141

4242
$type = self::getTextContentType();
43-
$text = $type::fromString($xml->textContent);
43+
if ($type === QNameValue::class) {
44+
$qName = QNameValue::fromDocument($xml->textContent, $xml);
45+
$text = $qName->getRawValue();
46+
} else {
47+
$text = $xml->textContent;
48+
}
4449

45-
return new static($text);
50+
return new static($type::fromString($text));
4651
}
4752

4853

tests/Type/QNameValueTest.php

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@
66

77
use PHPUnit\Framework\Attributes\{CoversClass, DataProvider, DataProviderExternal, DependsOnClass};
88
use PHPUnit\Framework\TestCase;
9-
use SimpleSAML\Test\XML\Assert\QNameTest;
109
use SimpleSAML\XML\Exception\SchemaViolationException;
1110
use SimpleSAML\XML\Type\QNameValue;
1211

13-
use function strval;
14-
1512
/**
1613
* Class \SimpleSAML\Test\XML\Type\QNameValueTest
1714
*
@@ -26,8 +23,6 @@ final class QNameValueTest extends TestCase
2623
*/
2724
#[DataProvider('provideInvalidQName')]
2825
#[DataProvider('provideValidQName')]
29-
#[DataProviderExternal(QNameTest::class, 'provideValidQName')]
30-
#[DependsOnClass(QNameTest::class)]
3126
public function testQName(bool $shouldPass, string $qname): void
3227
{
3328
try {
@@ -39,29 +34,22 @@ public function testQName(bool $shouldPass, string $qname): void
3934
}
4035

4136

42-
/**
43-
*/
44-
#[DependsOnClass(QNameTest::class)]
45-
public function testHelpers(): void
46-
{
47-
$qn = QNameValue::fromString('some:Test');
48-
$this->assertEquals(strval($qn->getNamespacePrefix()), 'some');
49-
$this->assertEquals(strval($qn->getLocalName()), 'Test');
50-
51-
$qn = QNameValue::fromString('Test');
52-
$this->assertNull($qn->getNamespacePrefix());
53-
$this->assertEquals(strval($qn->getLocalName()), 'Test');
54-
}
55-
56-
5737
/**
5838
* @return array<string, array{0: true, 1: string}>
5939
*/
6040
public static function provideValidQName(): array
6141
{
6242
return [
63-
'prefixed newline' => [true, "\nsome:Test"],
64-
'trailing newline' => [true, "some:Test\n"],
43+
'valid' => [true, '{urn:x-simplesamlphp:namespace}ssp:Chunk'],
44+
'valid without namespace' => [true, '{urn:x-simplesamlphp:namespace}Chunk'],
45+
// both parts can contain a dash
46+
'1st part containing dash' => [true, '{urn:x-simplesamlphp:namespace}s-sp:Chunk'],
47+
'2nd part containing dash' => [true, '{urn:x-simplesamlphp:namespace}ssp:Ch-unk'],
48+
'both parts containing dash' => [true, '{urn:x-simplesamlphp:namespace}s-sp:Ch-unk'],
49+
// A single NCName is also a valid QName
50+
'no colon' => [true, 'Test'],
51+
'prefixed newline' => [true, "\nTest"],
52+
'trailing newline' => [true, "Test\n"],
6553
];
6654
}
6755

@@ -72,8 +60,8 @@ public static function provideValidQName(): array
7260
public static function provideInvalidQName(): array
7361
{
7462
return [
75-
'start 2nd part with dash' => [false, 'some:-Test'],
76-
'start both parts with dash' => [false, '-some:-Test'],
63+
'empty namespace' => [false, '{}Test'],
64+
'start 2nd part with dash' => [false, '-Test'],
7765
'start with colon' => [false, ':test'],
7866
'multiple colons' => [false, 'test:test:test'],
7967
'start with digit' => [false, '1Test'],

0 commit comments

Comments
 (0)