Skip to content

Commit

Permalink
Refactor QName type
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed Jan 25, 2025
1 parent a763d89 commit 9891e57
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 44 deletions.
4 changes: 2 additions & 2 deletions src/AbstractElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use SimpleSAML\XML\Assert\Assert;
use SimpleSAML\XML\Exception\{MissingAttributeException, SchemaViolationException};
use SimpleSAML\XML\SerializableElementTrait;
use SimpleSAML\XML\Type\{StringValue, ValueTypeInterface};
use SimpleSAML\XML\Type\{QNameValue, StringValue, ValueTypeInterface};

use function array_slice;
use function defined;
Expand Down Expand Up @@ -79,7 +79,7 @@ public static function getAttribute(
);

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


Expand Down
2 changes: 1 addition & 1 deletion src/Type/Base64BinaryValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use SimpleSAML\XML\Assert\Assert;
use SimpleSAML\XML\Exception\SchemaViolationException;

use preg_replace;
use function preg_replace;

/**
* @package simplesaml/xml-common
Expand Down
2 changes: 1 addition & 1 deletion src/Type/DateTimeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
class DateTimeValue extends AbstractValueType
{
public const string DATETIME_FORMAT = 'Y-m-d\\TH:i:sP';
public const DATETIME_FORMAT = 'Y-m-d\\TH:i:sP';


/**
Expand Down
124 changes: 111 additions & 13 deletions src/Type/QNameValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,36 @@

namespace SimpleSAML\XML\Type;

use DOMElement;
use SimpleSAML\XML\Assert\Assert;
use SimpleSAML\XML\Exception\SchemaViolationException;
use SimpleSAML\XML\Type\{AnyURIValue, NCNameValue};

use function explode;
use function preg_match;

/**
* @package simplesaml/xml-common
*/
class QNameValue extends AbstractValueType
{
protected ?AnyURIValue $namespaceURI;
protected ?NCNameValue $namespacePrefix;
protected NCNameValue $localName;

private static string $qname_regex = '/^
(?:
\{ # Match a literal {
(\S+) # Match one or more non-whitespace character
\} # Match a literal }
(?:
([\w_][\w.-]*) # Match a-z or underscore followed by any word-character, dot or dash
: # Match a literal :
)?
)? # Namespace and prefix are optional
([\w_][\w.-]*) # Match a-z or underscore followed by any word-character, dot or dash
$/Dimx';


/**
* Sanitize the value.
*
Expand All @@ -35,8 +55,49 @@ protected function sanitizeValue(string $value): string
*/
protected function validateValue(string $value): void
{
// Note: value must already be sanitized before validating
Assert::validQName($this->sanitizeValue($value), SchemaViolationException::class);
$qName = $this->sanitizeValue($value);

/**
* Split our custom format of {<namespaceURI>}<prefix>:<localName> into individual parts
*/
$result = preg_match(
self::$qname_regex,
$qName,
$matches,
PREG_UNMATCHED_AS_NULL,
);

if ($result && count($matches) === 4) {
list($qName, $namespaceURI, $namespacePrefix, $localName) = $matches;

$this->namespaceURI = ($namespaceURI !== null) ? AnyURIValue::fromString($namespaceURI) : null;
$this->namespacePrefix = ($namespacePrefix !== null) ? NCNameValue::fromString($namespacePrefix) : null;
$this->localName = NCNameValue::fromString($localName);
} else {
throw new SchemaViolationException(sprintf('\'%s\' is not a valid xs:QName.', $qName));
}
}


/**
* Get the value.
*
* @return string
*/
public function getValue(): string
{
return $this->getNamespacePrefix() . ':' . $this->getLocalName();
}


/**
* Get the namespaceURI for this qualified name.
*
* @return \SimpleSAML\XML\Type\AnyURIValue|null
*/
public function getNamespaceURI(): ?AnyURIValue
{
return $this->namespaceURI;
}


Expand All @@ -47,12 +108,7 @@ protected function validateValue(string $value): void
*/
public function getNamespacePrefix(): ?NCNameValue
{
$qname = explode(':', $this->getValue(), 2);
if (count($qname) === 2) {
return NCNameValue::fromString($qname[0]);
}

return null;
return $this->namespacePrefix;
}


Expand All @@ -63,11 +119,53 @@ public function getNamespacePrefix(): ?NCNameValue
*/
public function getLocalName(): NCNameValue
{
$qname = explode(':', $this->getValue(), 2);
if (count($qname) === 2) {
return NCNameValue::fromString($qname[1]);
return $this->localName;
}


/**
* @param \SimpleSAML\XML\Type\NCNameValue $localName
* @param \SimpleSAML\XML\Type\AnyURIValue|null $namespaceURI
* @param \SimpleSAML\XML\Type\NCNameValue|null $namespacePrefix
* @return static
*/
public static function fromParts(
NCNameValue $localName,
?AnyURIValue $namespaceURI,
?NCNameValue $namespacePrefix,
): static {
if ($namespaceURI === null) {
// If we don't have a namespace, we can't have a prefix either
Assert::null($namespacePrefix->getValue(), SchemaViolationException::class);
return new static($localName->getValue());
}

return NCNameValue::fromString($qname[0]);
return new static(
'{' . $namespaceURI->getValue() . '}'
. ($namespacePrefix ? ($namespacePrefix->getValue() . ':') : '')
. $localName,
);
}


/**
* @param string $qName
*/
public static function fromDocument(
string $qName,
DOMElement $element,
): static {
$namespacePrefix = null;
if (str_contains($qName, ':')) {
list($namespacePrefix, $localName) = explode(':', $qName, 2);
} else {
// No prefix
$localName = $qName;
}

// Will return the default namespace (if any) when prefix is NULL
$namespaceURI = $element->lookupNamespaceUri($namespacePrefix);

return new static('{' . $namespaceURI . '}' . $namespacePrefix . ':' . $localName);
}
}
11 changes: 8 additions & 3 deletions src/TypedTextContentTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use DOMElement;
use SimpleSAML\XML\Assert\Assert;
use SimpleSAML\XML\Exception\{InvalidDOMElementException, InvalidValueTypeException};
use SimpleSAML\XML\Type\{ValueTypeInterface, StringValue};
use SimpleSAML\XML\Type\{QNameValue, StringValue, ValueTypeInterface};

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

$type = self::getTextContentType();
$text = $type::fromString($xml->textContent);
if ($type === QNameValue::class) {
$qName = QNameValue::fromDocument($xml->textContent, $xml);
$text = $qName->getRawValue();
} else {
$text = $xml->textContent;
}

return new static($text);
return new static($type::fromString($text));
}


Expand Down
36 changes: 12 additions & 24 deletions tests/Type/QNameValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@

use PHPUnit\Framework\Attributes\{CoversClass, DataProvider, DataProviderExternal, DependsOnClass};
use PHPUnit\Framework\TestCase;
use SimpleSAML\Test\XML\Assert\QNameTest;
use SimpleSAML\XML\Exception\SchemaViolationException;
use SimpleSAML\XML\Type\QNameValue;

use function strval;

/**
* Class \SimpleSAML\Test\XML\Type\QNameValueTest
*
Expand All @@ -26,8 +23,6 @@ final class QNameValueTest extends TestCase
*/
#[DataProvider('provideInvalidQName')]
#[DataProvider('provideValidQName')]
#[DataProviderExternal(QNameTest::class, 'provideValidQName')]
#[DependsOnClass(QNameTest::class)]
public function testQName(bool $shouldPass, string $qname): void
{
try {
Expand All @@ -39,29 +34,22 @@ public function testQName(bool $shouldPass, string $qname): void
}


/**
*/
#[DependsOnClass(QNameTest::class)]
public function testHelpers(): void
{
$qn = QNameValue::fromString('some:Test');
$this->assertEquals(strval($qn->getNamespacePrefix()), 'some');
$this->assertEquals(strval($qn->getLocalName()), 'Test');

$qn = QNameValue::fromString('Test');
$this->assertNull($qn->getNamespacePrefix());
$this->assertEquals(strval($qn->getLocalName()), 'Test');
}


/**
* @return array<string, array{0: true, 1: string}>
*/
public static function provideValidQName(): array
{
return [
'prefixed newline' => [true, "\nsome:Test"],
'trailing newline' => [true, "some:Test\n"],
'valid' => [true, '{urn:x-simplesamlphp:namespace}ssp:Chunk'],
'valid without namespace' => [true, '{urn:x-simplesamlphp:namespace}Chunk'],
// both parts can contain a dash
'1st part containing dash' => [true, '{urn:x-simplesamlphp:namespace}s-sp:Chunk'],
'2nd part containing dash' => [true, '{urn:x-simplesamlphp:namespace}ssp:Ch-unk'],
'both parts containing dash' => [true, '{urn:x-simplesamlphp:namespace}s-sp:Ch-unk'],
// A single NCName is also a valid QName
'no colon' => [true, 'Test'],
'prefixed newline' => [true, "\nTest"],
'trailing newline' => [true, "Test\n"],
];
}

Expand All @@ -72,8 +60,8 @@ public static function provideValidQName(): array
public static function provideInvalidQName(): array
{
return [
'start 2nd part with dash' => [false, 'some:-Test'],
'start both parts with dash' => [false, '-some:-Test'],
'empty namespace' => [false, '{}Test'],
'start 2nd part with dash' => [false, '-Test'],
'start with colon' => [false, ':test'],
'multiple colons' => [false, 'test:test:test'],
'start with digit' => [false, '1Test'],
Expand Down

0 comments on commit 9891e57

Please sign in to comment.