Issues (33)

src/XML/ExtendableAttributesTrait.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XML;
6
7
use DOMElement;
8
use RuntimeException;
9
use SimpleSAML\XML\Assert\Assert;
10
use SimpleSAML\XML\Attribute;
11
use SimpleSAML\XML\Constants as C;
12
use SimpleSAML\XMLSchema\Type\StringValue;
13
use SimpleSAML\XMLSchema\XML\Enumeration\NamespaceEnum;
14
15
use function array_diff;
16
use function array_map;
17
use function array_search;
18
use function defined;
19
use function implode;
20
use function in_array;
21
use function is_array;
22
use function rtrim;
23
use function sprintf;
24
25
/**
26
 * Trait for elements that can have arbitrary namespaced attributes.
27
 *
28
 * @package simplesamlphp/xml-common
29
 */
30
trait ExtendableAttributesTrait
31
{
32
    /**
33
     * Extra (namespace qualified) attributes.
34
     *
35
     * @var array<int, \SimpleSAML\XML\Attribute>
36
     */
37
    protected array $namespacedAttributes = [];
38
39
40
    /**
41
     * Check if a namespace-qualified attribute exists.
42
     *
43
     * @param string|null $namespaceURI The namespace URI.
44
     * @param string $localName The local name.
45
     * @return bool true if the attribute exists, false if not.
46
     */
47
    public function hasAttributeNS(?string $namespaceURI, string $localName): bool
48
    {
49
        foreach ($this->getAttributesNS() as $attr) {
50
            if ($attr->getNamespaceURI() === $namespaceURI && $attr->getAttrName() === $localName) {
51
                return true;
52
            }
53
        }
54
        return false;
55
    }
56
57
58
    /**
59
     * Get a namespace-qualified attribute.
60
     *
61
     * @param string|null $namespaceURI The namespace URI.
62
     * @param string $localName The local name.
63
     * @return \SimpleSAML\XML\Attribute|null The value of the attribute, or null if the attribute does not exist.
64
     */
65
    public function getAttributeNS(?string $namespaceURI, string $localName): ?Attribute
66
    {
67
        foreach ($this->getAttributesNS() as $attr) {
68
            if ($attr->getNamespaceURI() === $namespaceURI && $attr->getAttrName() === $localName) {
69
                return $attr;
70
            }
71
        }
72
        return null;
73
    }
74
75
76
    /**
77
     * Get the namespaced attributes in this element.
78
     *
79
     * @return array<int, \SimpleSAML\XML\Attribute>
80
     */
81
    public function getAttributesNS(): array
82
    {
83
        return $this->namespacedAttributes;
84
    }
85
86
87
    /**
88
     * Parse an XML document and get the namespaced attributes from the specified namespace(s).
89
     * The namespace defaults to the XS_ANY_ATTR_NAMESPACE constant on the element.
90
     * NOTE: In case the namespace is ##any, this method will also return local non-namespaced attributes!
91
     *
92
     * @param \DOMElement $xml
93
     * @param (
94
     *   \SimpleSAML\XMLSchema\XML\Enumeration\NamespaceEnum|
95
     *   array<\SimpleSAML\XMLSchema\XML\Enumeration\NamespaceEnum|string>|
96
     *   null
97
     * ) $namespace
98
     *
99
     * @return array<int, \SimpleSAML\XML\Attribute> $attributes
100
     */
101
    protected static function getAttributesNSFromXML(
102
        DOMElement $xml,
103
        NamespaceEnum|array|null $namespace = null,
104
    ): array {
105
        $namespace = $namespace ?? self::XS_ANY_ATTR_NAMESPACE;
106
        $exclusionList = self::getAttributeExclusions();
107
        $attributes = [];
108
109
        // Validate namespace value
110
        if (!is_array($namespace)) {
111
            // Must be one of the predefined values
112
            Assert::oneOf($namespace, NamespaceEnum::cases());
113
114
            foreach ($xml->attributes as $a) {
115
                if (in_array([$a->namespaceURI, $a->localName], $exclusionList, true)) {
116
                    continue;
117
                } elseif ($namespace === NamespaceEnum::Other && in_array($a->namespaceURI, [self::NS, null], true)) {
118
                    continue;
119
                } elseif ($namespace === NamespaceEnum::TargetNamespace && $a->namespaceURI !== self::NS) {
120
                    continue;
121
                } elseif ($namespace === NamespaceEnum::Local && $a->namespaceURI !== null) {
122
                    continue;
123
                }
124
125
                $attributes[] = new Attribute(
126
                    $a->namespaceURI,
127
                    $a->prefix,
128
                    $a->localName,
129
                    StringValue::fromString($a->nodeValue),
130
                );
131
            }
132
        } else {
133
            // Array must be non-empty and cannot contain ##any or ##other
134
            Assert::notEmpty($namespace);
135
            Assert::allStringNotEmpty($namespace);
136
            Assert::allNotSame($namespace, NamespaceEnum::Any);
137
            Assert::allNotSame($namespace, NamespaceEnum::Other);
138
139
            // Replace the ##targetedNamespace with the actual namespace
140
            if (($key = array_search(NamespaceEnum::TargetNamespace, $namespace)) !== false) {
141
                $namespace[$key] = self::NS;
142
            }
143
144
            // Replace the ##local with null
145
            if (($key = array_search(NamespaceEnum::Local, $namespace)) !== false) {
146
                $namespace[$key] = null;
147
            }
148
149
            foreach ($xml->attributes as $a) {
150
                if (in_array([$a->namespaceURI, $a->localName], $exclusionList, true)) {
151
                    continue;
152
                } elseif (!in_array($a->namespaceURI, $namespace, true)) {
153
                    continue;
154
                }
155
156
                $attributes[] = new Attribute(
157
                    $a->namespaceURI,
158
                    $a->prefix,
159
                    $a->localName,
160
                    StringValue::fromString($a->nodeValue),
161
                );
162
            }
163
        }
164
165
        return $attributes;
166
    }
167
168
169
    /**
170
     * @param array<int, \SimpleSAML\XML\Attribute> $attributes
171
     * @throws \SimpleSAML\Assert\AssertionFailedException if $attributes contains anything other than Attribute objects
172
     */
173
    protected function setAttributesNS(array $attributes): void
174
    {
175
        Assert::maxCount($attributes, C::UNBOUNDED_LIMIT);
176
        Assert::allIsInstanceOf(
177
            $attributes,
178
            Attribute::class,
179
            'Arbitrary XML attributes can only be an instance of Attribute.',
180
        );
181
        $namespace = $this->getAttributeNamespace();
182
183
        // Validate namespace value
184
        if (!is_array($namespace)) {
185
            // Must be one of the predefined values
186
            Assert::oneOf($namespace, NamespaceEnum::cases());
187
        } else {
188
            // Array must be non-empty and cannot contain ##any or ##other
189
            Assert::notEmpty($namespace);
190
            Assert::allNotSame($namespace, NamespaceEnum::Any);
191
            Assert::allNotSame($namespace, NamespaceEnum::Other);
192
        }
193
194
        // Get namespaces for all attributes
195
        $actual_namespaces = array_map(
196
            /**
197
             * @param \SimpleSAML\XML\Attribute $elt
198
             * @return string|null
199
             */
200
            function (Attribute $attr) {
201
                return $attr->getNamespaceURI();
202
            },
203
            $attributes,
204
        );
205
206
        if ($namespace === NamespaceEnum::Local) {
207
            // If ##local then all namespaces must be null
208
            Assert::allNull($actual_namespaces);
209
        } elseif (is_array($namespace)) {
210
            // Make a local copy of the property that we can edit
211
            $allowed_namespaces = $namespace;
212
213
            // Replace the ##targetedNamespace with the actual namespace
214
            if (($key = array_search(NamespaceEnum::TargetNamespace, $allowed_namespaces)) !== false) {
215
                $allowed_namespaces[$key] = self::NS;
216
            }
217
218
            // Replace the ##local with null
219
            if (($key = array_search(NamespaceEnum::Local, $allowed_namespaces)) !== false) {
220
                $allowed_namespaces[$key] = null;
221
            }
222
223
            $diff = array_diff($actual_namespaces, $allowed_namespaces);
224
            Assert::isEmpty(
225
                $diff,
226
                sprintf(
227
                    'Attributes from namespaces [ %s ] are not allowed inside a %s element.',
228
                    rtrim(implode(', ', $diff)),
229
                    self::NS,
230
                ),
231
            );
232
        } else {
233
            if ($namespace === NamespaceEnum::Other) {
234
                // All attributes must be namespaced, ergo non-null
235
                Assert::allNotNull($actual_namespaces);
236
237
                // Must be any namespace other than the parent element
238
                Assert::allNotSame($actual_namespaces, self::NS);
239
            } elseif ($namespace === NamespaceEnum::TargetNamespace) {
240
                // Must be the same namespace as the one of the parent element
241
                Assert::allSame($actual_namespaces, self::NS);
242
            }
243
        }
244
245
        $exclusionList = self::getAttributeExclusions();
246
        foreach ($attributes as $i => $attr) {
247
            if (in_array([$attr->getNamespaceURI(), $attr->getAttrName()], $exclusionList, true)) {
248
                unset($attributes[$i]);
249
            }
250
        }
251
252
        $this->namespacedAttributes = $attributes;
253
    }
254
255
256
257
    /**
258
     * @return (
259
     *   array<\SimpleSAML\XMLSchema\XML\Enumeration\NamespaceEnum|string>|
260
     *   \SimpleSAML\XMLSchema\XML\Enumeration\NamespaceEnum
261
     * )[]
262
     */
263
    public function getAttributeNamespace(): array|NamespaceEnum
264
    {
265
        Assert::true(
266
            defined('self::XS_ANY_ATTR_NAMESPACE'),
267
            self::getClassName(self::class)
268
            . '::XS_ANY_ATTR_NAMESPACE constant must be defined and set to the namespace for the xs:anyAttribute.',
269
            RuntimeException::class,
270
        );
271
272
        return self::XS_ANY_ATTR_NAMESPACE;
273
    }
274
275
276
    /**
277
     * Get the exclusions list for getAttributeNSFromXML.
278
     *
279
     * @return array{array{string|null, string}}|array{}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{array{string|null, string}}|array{} at position 2 could not be parsed: Expected ':' at position 2, but found 'array'.
Loading history...
280
     */
281
    public static function getAttributeExclusions(): array
282
    {
283
        if (defined('self::XS_ANY_ATTR_EXCLUSIONS')) {
284
            return self::XS_ANY_ATTR_EXCLUSIONS;
285
        }
286
287
        return [];
288
    }
289
}
290