diff --git a/src/Constants.php b/src/Constants.php
index b1155343..aba799ba 100644
--- a/src/Constants.php
+++ b/src/Constants.php
@@ -52,6 +52,11 @@ class Constants extends \SimpleSAML\SAML2\Constants
*/
public const NS_SEC_UTIL = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';
+ /**
+ * The namespace for the Metadata Exchange protocol.
+ */
+ public const NS_METADATA_EXCHANGE = 'http://schemas.xmlsoap.org/ws/2004/09/mex';
+
/**
* The schema-defined wsa fault codes
*/
diff --git a/src/XML/wst/AbstractBinaryExchangeType.php b/src/XML/wst/AbstractBinaryExchangeType.php
index 0c16de65..a49d4570 100644
--- a/src/XML/wst/AbstractBinaryExchangeType.php
+++ b/src/XML/wst/AbstractBinaryExchangeType.php
@@ -22,7 +22,7 @@ abstract class AbstractBinaryExchangeType extends AbstractWstElement
use ExtendableAttributesTrait;
use StringElementTrait;
- /** @var string|\SimpleSAML\XML\XsNamespace */
+ /** The namespace-attribute for the xs:anyAttribute element */
public const XS_ANY_ATTR_NAMESPACE = NS::OTHER;
diff --git a/src/XML/wsx/AbstractWsxElement.php b/src/XML/wsx/AbstractWsxElement.php
new file mode 100644
index 00000000..106a2185
--- /dev/null
+++ b/src/XML/wsx/AbstractWsxElement.php
@@ -0,0 +1,22 @@
+setContent($content);
+ }
+}
diff --git a/src/XML/wsx/GetMetadata.php b/src/XML/wsx/GetMetadata.php
new file mode 100644
index 00000000..b1e67872
--- /dev/null
+++ b/src/XML/wsx/GetMetadata.php
@@ -0,0 +1,127 @@
+ $namespacedAttributes
+ */
+ final public function __construct(
+ protected ?Dialect $dialect = null,
+ protected ?Identifier $identifier = null,
+ array $namespacedAttributes = []
+ ) {
+ $this->setAttributesNS($namespacedAttributes);
+ }
+
+
+ /**
+ * Get the dialect property.
+ *
+ * @return \SimpleSAML\WSSecurity\XML\wsx\Dialect|null
+ */
+ public function getDialect(): ?Dialect
+ {
+ return $this->dialect;
+ }
+
+
+ /**
+ * Get the identifier property.
+ *
+ * @return \SimpleSAML\WSSecurity\XML\wsx\Identifier|null
+ */
+ public function getIdentifier(): ?Identifier
+ {
+ return $this->identifier;
+ }
+
+
+ /**
+ * Test if an object, at the state it's in, would produce an empty XML-element
+ *
+ * @return bool
+ */
+ public function isEmptyElement(): bool
+ {
+ return empty($this->getDialect())
+ && empty($this->getIdentifier())
+ && empty($this->getAttributesNS());
+ }
+
+
+ /**
+ * Create an instance of this object from its XML representation.
+ *
+ * @param \DOMElement $xml
+ * @return static
+ *
+ * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
+ * if the qualified name of the supplied element is wrong
+ */
+ public static function fromXML(DOMElement $xml): static
+ {
+ Assert::same($xml->localName, static::getLocalName(), InvalidDOMElementException::class);
+ Assert::same($xml->namespaceURI, static::NS, InvalidDOMElementException::class);
+
+ $dialect = Dialect::getChildrenOfClass($xml);
+ Assert::maxCount($dialect, 1, TooManyElementsException::class);
+
+ $identifier = Identifier::getChildrenOfClass($xml);
+ Assert::maxCount($identifier, 1, TooManyElementsException::class);
+
+ return new static(
+ array_pop($dialect),
+ array_pop($identifier),
+ self::getAttributesNSFromXML($xml),
+ );
+ }
+
+
+ /**
+ * Add this GetMetadata to an XML element.
+ *
+ * @param \DOMElement $parent The element we should append this GetMetadata to.
+ * @return \DOMElement
+ */
+ public function toXML(DOMElement $parent = null): DOMElement
+ {
+ $e = parent::instantiateParentElement($parent);
+
+ $this->getDialect()?->toXML($e);
+ $this->getIdentifier()?->toXML($e);
+
+ foreach ($this->getAttributesNS() as $attr) {
+ $attr->toXML($e);
+ }
+
+ return $e;
+ }
+}
diff --git a/src/XML/wsx/Identifier.php b/src/XML/wsx/Identifier.php
new file mode 100644
index 00000000..f28b286d
--- /dev/null
+++ b/src/XML/wsx/Identifier.php
@@ -0,0 +1,26 @@
+setContent($content);
+ }
+}
diff --git a/src/XML/wsx/Location.php b/src/XML/wsx/Location.php
new file mode 100644
index 00000000..d3b9e67b
--- /dev/null
+++ b/src/XML/wsx/Location.php
@@ -0,0 +1,26 @@
+setContent($content);
+ }
+}
diff --git a/src/XML/wsx/Metadata.php b/src/XML/wsx/Metadata.php
new file mode 100644
index 00000000..5ed17ace
--- /dev/null
+++ b/src/XML/wsx/Metadata.php
@@ -0,0 +1,132 @@
+ $metadataSection
+ * @param array<\SimpleSAML\XML\SerializableElementInterface> $children
+ * @param array<\SimpleSAML\XML\Attribute> $namespacedAttributes
+ */
+ final public function __construct(
+ protected array $metadataSection = [],
+ array $children = [],
+ array $namespacedAttributes = []
+ ) {
+ $this->setElements($children);
+ $this->setAttributesNS($namespacedAttributes);
+ }
+
+
+ /**
+ * Get the child property.
+ *
+ * @return array<\SimpleSAML\WSSecurity\XML\wsx\MetadataSection>
+ */
+ public function getMetadataSection(): array
+ {
+ return $this->metadataSection;
+ }
+
+
+ /**
+ * Test if an object, at the state it's in, would produce an empty XML-element
+ *
+ * @return bool
+ */
+ public function isEmptyElement(): bool
+ {
+ return empty($this->getMetadataSection())
+ && empty($this->getElements())
+ && empty($this->getAttributesNS());
+ }
+
+
+ /**
+ * Create an instance of this object from its XML representation.
+ *
+ * @param \DOMElement $xml
+ * @return static
+ *
+ * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
+ * if the qualified name of the supplied element is wrong
+ */
+ public static function fromXML(DOMElement $xml): static
+ {
+ Assert::same($xml->localName, static::getLocalName(), InvalidDOMElementException::class);
+ Assert::same($xml->namespaceURI, static::NS, InvalidDOMElementException::class);
+
+ $children = [];
+ foreach ($xml->childNodes as $child) {
+ if (!($child instanceof DOMElement)) {
+ continue;
+ } elseif ($child->namespaceURI === static::NS) {
+ continue;
+ }
+
+ $children[] = new Chunk($child);
+ }
+
+ return new static(
+ MetadataSection::getChildrenOfClass($xml),
+ $children,
+ self::getAttributesNSFromXML($xml),
+ );
+ }
+
+
+ /**
+ * Add this Metadata to an XML element.
+ *
+ * @param \DOMElement $parent The element we should append this Metadata to.
+ * @return \DOMElement
+ */
+ public function toXML(DOMElement $parent = null): DOMElement
+ {
+ $e = parent::instantiateParentElement($parent);
+
+ foreach ($this->getMetadataSection() as $metadataSection) {
+ $metadataSection->toXML($e);
+ }
+
+ foreach ($this->getElements() as $elt) {
+ if (!$elt->isEmptyElement()) {
+ $elt->toXML($e);
+ }
+ }
+
+ foreach ($this->getAttributesNS() as $attr) {
+ $attr->toXML($e);
+ }
+
+ return $e;
+ }
+}
diff --git a/src/XML/wsx/MetadataReference.php b/src/XML/wsx/MetadataReference.php
new file mode 100644
index 00000000..9d143941
--- /dev/null
+++ b/src/XML/wsx/MetadataReference.php
@@ -0,0 +1,88 @@
+setElements($children);
+ }
+
+
+ /**
+ * Create an instance of this object from its XML representation.
+ *
+ * @param \DOMElement $xml
+ * @return static
+ *
+ * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
+ * if the qualified name of the supplied element is wrong
+ */
+ public static function fromXML(DOMElement $xml): static
+ {
+ Assert::same($xml->localName, static::getLocalName(), InvalidDOMElementException::class);
+ Assert::same($xml->namespaceURI, static::NS, InvalidDOMElementException::class);
+
+ $children = [];
+ foreach ($xml->childNodes as $child) {
+ if (!($child instanceof DOMElement)) {
+ continue;
+ } elseif ($child->namespaceURI === static::NS) {
+ continue;
+ }
+
+ $children[] = new Chunk($child);
+ }
+
+ return new static($children);
+ }
+
+
+ /**
+ * Add this MetadataReference to an XML element.
+ *
+ * @param \DOMElement $parent The element we should append this MetadataReference to.
+ * @return \DOMElement
+ */
+ public function toXML(DOMElement $parent = null): DOMElement
+ {
+ $e = parent::instantiateParentElement($parent);
+
+ foreach ($this->getElements() as $child) {
+ if (!$child->isEmptyElement()) {
+ $child->toXML($e);
+ }
+ }
+
+ return $e;
+ }
+}
diff --git a/src/XML/wsx/MetadataSection.php b/src/XML/wsx/MetadataSection.php
new file mode 100644
index 00000000..706414ec
--- /dev/null
+++ b/src/XML/wsx/MetadataSection.php
@@ -0,0 +1,155 @@
+ $namespacedAttributes
+ */
+ final public function __construct(
+ protected SerializableElementInterface|MetadataReference|Location $child,
+ protected string $Dialect,
+ protected ?string $Identifier = null,
+ array $namespacedAttributes = []
+ ) {
+ Assert::validURI($Dialect);
+ Assert::nullOrValidURI($Identifier);
+
+ $this->setAttributesNS($namespacedAttributes);
+ }
+
+
+ /**
+ * Get the child property.
+ *
+ * @return (\SimpleSAML\XML\SerializableElementInterface|
+ * \SimpleSAML\WSSecurity\XML\wsx\MetadataReference|
+ * \SimpleSAML\WSSecurity\XML\wsx\Location)
+ */
+ public function getChild(): SerializableElementInterface|MetadataReference|Location
+ {
+ return $this->child;
+ }
+
+
+ /**
+ * Get the Dialect property.
+ *
+ * @return string
+ */
+ public function getDialect(): string
+ {
+ return $this->Dialect;
+ }
+
+
+ /**
+ * Get the Identifier property.
+ *
+ * @return string|null
+ */
+ public function getIdentifier(): ?string
+ {
+ return $this->Identifier;
+ }
+
+
+ /**
+ * Create an instance of this object from its XML representation.
+ *
+ * @param \DOMElement $xml
+ * @return static
+ *
+ * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
+ * if the qualified name of the supplied element is wrong
+ */
+ public static function fromXML(DOMElement $xml): static
+ {
+ Assert::same($xml->localName, static::getLocalName(), InvalidDOMElementException::class);
+ Assert::same($xml->namespaceURI, static::NS, InvalidDOMElementException::class);
+
+ $children = [];
+ foreach ($xml->childNodes as $child) {
+ if (!($child instanceof DOMElement)) {
+ continue;
+ } elseif ($child->namespaceURI === static::NS) {
+ if ($child->localName === 'MetadataReference') {
+ $children[] = MetadataReference::fromXML($child);
+ } elseif ($child->localName === 'Location') {
+ $children[] = Location::fromXML($child);
+ }
+ continue;
+ }
+
+ $children[] = new Chunk($child);
+ }
+
+ Assert::minCount($children, 1, MissingElementException::class);
+ Assert::maxCount($children, 1, TooManyElementsException::class);
+
+ return new static(
+ array_pop($children),
+ self::getAttribute($xml, 'Dialect'),
+ self::getOptionalAttribute($xml, 'Identifier', null),
+ self::getAttributesNSFromXML($xml),
+ );
+ }
+
+
+ /**
+ * Add this MetadataSection to an XML element.
+ *
+ * @param \DOMElement $parent The element we should append this MetadataSection to.
+ * @return \DOMElement
+ */
+ public function toXML(DOMElement $parent = null): DOMElement
+ {
+ $e = parent::instantiateParentElement($parent);
+ $e->setAttribute('Dialect', $this->getDialect());
+
+ if ($this->getIdentifier() !== null) {
+ $e->setAttribute('Identifier', $this->getIdentifier());
+ }
+
+ $this->getChild()->toXML($e);
+
+ foreach ($this->getAttributesNS() as $attr) {
+ $attr->toXML($e);
+ }
+
+ return $e;
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/DialectTest.php b/tests/WSSecurity/XML/wsx/DialectTest.php
new file mode 100644
index 00000000..f6ed722f
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/DialectTest.php
@@ -0,0 +1,59 @@
+assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($dialect),
+ );
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/GetMetadataTest.php b/tests/WSSecurity/XML/wsx/GetMetadataTest.php
new file mode 100644
index 00000000..f804f286
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/GetMetadataTest.php
@@ -0,0 +1,67 @@
+assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($getMetadata)
+ );
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/IdentifierTest.php b/tests/WSSecurity/XML/wsx/IdentifierTest.php
new file mode 100644
index 00000000..f1a487d1
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/IdentifierTest.php
@@ -0,0 +1,59 @@
+assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($identifier),
+ );
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/LocationTest.php b/tests/WSSecurity/XML/wsx/LocationTest.php
new file mode 100644
index 00000000..7fb867e6
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/LocationTest.php
@@ -0,0 +1,59 @@
+assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($location),
+ );
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/MetadataReferenceTest.php b/tests/WSSecurity/XML/wsx/MetadataReferenceTest.php
new file mode 100644
index 00000000..47e2089e
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/MetadataReferenceTest.php
@@ -0,0 +1,89 @@
+SomeChunk'
+ )->documentElement;
+ }
+
+
+ // test marshalling
+
+
+ /**
+ * Test creating an MetadataReference object from scratch.
+ */
+ public function testMarshalling(): void
+ {
+ $attr1 = new Attribute('urn:x-simplesamlphp:namespace', 'ssp', 'test1', 'value1');
+ $attr2 = new Attribute('urn:x-simplesamlphp:namespace', 'ssp', 'test2', 'value2');
+
+ $chunk = new Chunk(self::$customContent);
+
+ $endpointReference = new EndpointReference(
+ new Address('https://login.microsoftonline.com/login.srf', [$attr2]),
+ [],
+ [],
+ [$chunk],
+ [$attr1],
+ );
+
+ $metadataReference = new MetadataReference([$endpointReference]);
+
+ $this->assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($metadataReference)
+ );
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/MetadataSectionTest.php b/tests/WSSecurity/XML/wsx/MetadataSectionTest.php
new file mode 100644
index 00000000..415812ae
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/MetadataSectionTest.php
@@ -0,0 +1,67 @@
+assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($metadataSection)
+ );
+ }
+}
diff --git a/tests/WSSecurity/XML/wsx/MetadataTest.php b/tests/WSSecurity/XML/wsx/MetadataTest.php
new file mode 100644
index 00000000..07513947
--- /dev/null
+++ b/tests/WSSecurity/XML/wsx/MetadataTest.php
@@ -0,0 +1,86 @@
+Some',
+ );
+
+ $metadataSection = new MetadataSection(
+ new Location('urn:x-simplesamlphp:namespace'),
+ 'urn:x-simplesamlphp:namespace',
+ 'urn:x-simplesamlphp:namespace',
+ [$attr2],
+ );
+
+ $metadata = new Metadata([$metadataSection], [new Chunk($child->documentElement)], [$attr1]);
+
+ $this->assertEquals(
+ self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
+ strval($metadata)
+ );
+ }
+
+
+ /**
+ */
+ public function testMarshallingEmpty(): void
+ {
+ $metadata = new Metadata();
+
+ $this->assertTrue($metadata->isEmptyElement());
+ }
+}
diff --git a/tests/resources/xml/wsx_Dialect.xml b/tests/resources/xml/wsx_Dialect.xml
new file mode 100644
index 00000000..3ffcdf11
--- /dev/null
+++ b/tests/resources/xml/wsx_Dialect.xml
@@ -0,0 +1 @@
+urn:x-simplesamlphp:namespace
diff --git a/tests/resources/xml/wsx_GetMetadata.xml b/tests/resources/xml/wsx_GetMetadata.xml
new file mode 100644
index 00000000..f28a0f78
--- /dev/null
+++ b/tests/resources/xml/wsx_GetMetadata.xml
@@ -0,0 +1,4 @@
+
+ urn:x-simplesamlphp:namespace
+ urn:x-simplesamlphp:namespace
+
diff --git a/tests/resources/xml/wsx_Identifier.xml b/tests/resources/xml/wsx_Identifier.xml
new file mode 100644
index 00000000..39651b91
--- /dev/null
+++ b/tests/resources/xml/wsx_Identifier.xml
@@ -0,0 +1 @@
+urn:x-simplesamlphp:namespace
diff --git a/tests/resources/xml/wsx_Location.xml b/tests/resources/xml/wsx_Location.xml
new file mode 100644
index 00000000..b4a0d7e9
--- /dev/null
+++ b/tests/resources/xml/wsx_Location.xml
@@ -0,0 +1 @@
+urn:x-simplesamlphp:namespace
diff --git a/tests/resources/xml/wsx_Metadata.xml b/tests/resources/xml/wsx_Metadata.xml
new file mode 100644
index 00000000..749d26bc
--- /dev/null
+++ b/tests/resources/xml/wsx_Metadata.xml
@@ -0,0 +1,6 @@
+
+
+ urn:x-simplesamlphp:namespace
+
+ Some
+
diff --git a/tests/resources/xml/wsx_MetadataReference.xml b/tests/resources/xml/wsx_MetadataReference.xml
new file mode 100644
index 00000000..25972d9c
--- /dev/null
+++ b/tests/resources/xml/wsx_MetadataReference.xml
@@ -0,0 +1,6 @@
+
+
+ https://login.microsoftonline.com/login.srf
+ SomeChunk
+
+
diff --git a/tests/resources/xml/wsx_MetadataSection.xml b/tests/resources/xml/wsx_MetadataSection.xml
new file mode 100644
index 00000000..79982e62
--- /dev/null
+++ b/tests/resources/xml/wsx_MetadataSection.xml
@@ -0,0 +1,3 @@
+
+ urn:x-simplesamlphp:namespace
+