ExtendableAttributesTrait::getAttributesNS()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XML;
6
7
use DOMElement;
8
use RuntimeException;
9
use SimpleSAML\Assert\Assert;
10
use SimpleSAML\XML\Attribute;
11
use SimpleSAML\XML\Constants as C;
12
use SimpleSAML\XML\Type\StringValue;
13
use SimpleSAML\XML\XsNamespace as NS;
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 \SimpleSAML\XML\XsNamespace|array|null $namespace
94
     *
95
     * @return array<int, \SimpleSAML\XML\Attribute> $attributes
96
     */
97
    protected static function getAttributesNSFromXML(DOMElement $xml, NS|array|null $namespace = null): array
98
    {
99
        $namespace = $namespace ?? self::XS_ANY_ATTR_NAMESPACE;
100
        $exclusionList = self::getAttributeExclusions();
101
        $attributes = [];
102
103
        // Validate namespace value
104
        if (!is_array($namespace)) {
105
            // Must be one of the predefined values
106
            Assert::oneOf($namespace, NS::cases());
107
108
            foreach ($xml->attributes as $a) {
109
                if (in_array([$a->namespaceURI, $a->localName], $exclusionList, true)) {
110
                    continue;
111
                } elseif ($namespace === NS::OTHER && in_array($a->namespaceURI, [self::NS, null], true)) {
112
                    continue;
113
                } elseif ($namespace === NS::TARGET && $a->namespaceURI !== self::NS) {
114
                    continue;
115
                } elseif ($namespace === NS::LOCAL && $a->namespaceURI !== null) {
116
                    continue;
117
                }
118
119
                $attributes[] = new Attribute(
120
                    $a->namespaceURI,
121
                    $a->prefix,
122
                    $a->localName,
123
                    StringValue::fromString($a->nodeValue),
124
                );
125
            }
126
        } else {
127
            // Array must be non-empty and cannot contain ##any or ##other
128
            Assert::notEmpty($namespace);
129
            Assert::allStringNotEmpty($namespace);
130
            Assert::allNotSame($namespace, NS::ANY);
131
            Assert::allNotSame($namespace, NS::OTHER);
132
133
            // Replace the ##targetedNamespace with the actual namespace
134
            if (($key = array_search(NS::TARGET, $namespace)) !== false) {
135
                $namespace[$key] = self::NS;
136
            }
137
138
            // Replace the ##local with null
139
            if (($key = array_search(NS::LOCAL, $namespace)) !== false) {
140
                $namespace[$key] = null;
141
            }
142
143
            foreach ($xml->attributes as $a) {
144
                if (in_array([$a->namespaceURI, $a->localName], $exclusionList, true)) {
145
                    continue;
146
                } elseif (!in_array($a->namespaceURI, $namespace, true)) {
147
                    continue;
148
                }
149
150
                $attributes[] = new Attribute(
151
                    $a->namespaceURI,
152
                    $a->prefix,
153
                    $a->localName,
154
                    StringValue::fromString($a->nodeValue),
155
                );
156
            }
157
        }
158
159
        return $attributes;
160
    }
161
162
163
    /**
164
     * @param array<int, \SimpleSAML\XML\Attribute> $attributes
165
     * @throws \SimpleSAML\Assert\AssertionFailedException if $attributes contains anything other than Attribute objects
166
     */
167
    protected function setAttributesNS(array $attributes): void
168
    {
169
        Assert::maxCount($attributes, C::UNBOUNDED_LIMIT);
170
        Assert::allIsInstanceOf(
171
            $attributes,
172
            Attribute::class,
173
            'Arbitrary XML attributes can only be an instance of Attribute.',
174
        );
175
        $namespace = $this->getAttributeNamespace();
176
177
        // Validate namespace value
178
        if (!is_array($namespace)) {
179
            // Must be one of the predefined values
180
            Assert::oneOf($namespace, NS::cases());
181
        } else {
182
            // Array must be non-empty and cannot contain ##any or ##other
183
            Assert::notEmpty($namespace);
184
            Assert::allNotSame($namespace, NS::ANY);
185
            Assert::allNotSame($namespace, NS::OTHER);
186
        }
187
188
        // Get namespaces for all attributes
189
        $actual_namespaces = array_map(
190
            /**
191
             * @param \SimpleSAML\XML\Attribute $elt
192
             * @return string|null
193
             */
194
            function (Attribute $attr) {
195
                return $attr->getNamespaceURI();
196
            },
197
            $attributes,
198
        );
199
200
        if ($namespace === NS::LOCAL) {
201
            // If ##local then all namespaces must be null
202
            Assert::allNull($actual_namespaces);
203
        } elseif (is_array($namespace)) {
204
            // Make a local copy of the property that we can edit
205
            $allowed_namespaces = $namespace;
206
207
            // Replace the ##targetedNamespace with the actual namespace
208
            if (($key = array_search(NS::TARGET, $allowed_namespaces)) !== false) {
209
                $allowed_namespaces[$key] = self::NS;
210
            }
211
212
            // Replace the ##local with null
213
            if (($key = array_search(NS::LOCAL, $allowed_namespaces)) !== false) {
214
                $allowed_namespaces[$key] = null;
215
            }
216
217
            $diff = array_diff($actual_namespaces, $allowed_namespaces);
218
            Assert::isEmpty(
219
                $diff,
220
                sprintf(
221
                    'Attributes from namespaces [ %s ] are not allowed inside a %s element.',
222
                    rtrim(implode(', ', $diff)),
223
                    self::NS,
224
                ),
225
            );
226
        } else {
227
            if ($namespace === NS::OTHER) {
228
                // All attributes must be namespaced, ergo non-null
229
                Assert::allNotNull($actual_namespaces);
230
231
                // Must be any namespace other than the parent element
232
                Assert::allNotSame($actual_namespaces, self::NS);
233
            } elseif ($namespace === NS::TARGET) {
234
                // Must be the same namespace as the one of the parent element
235
                Assert::allSame($actual_namespaces, self::NS);
236
            }
237
        }
238
239
        $exclusionList = self::getAttributeExclusions();
240
        foreach ($attributes as $i => $attr) {
241
            if (in_array([$attr->getNamespaceURI(), $attr->getAttrName()], $exclusionList, true)) {
242
                unset($attributes[$i]);
243
            }
244
        }
245
246
        $this->namespacedAttributes = $attributes;
247
    }
248
249
250
251
    /**
252
     * @return array|\SimpleSAML\XML\XsNamespace
253
     */
254
    public function getAttributeNamespace(): array|NS
255
    {
256
        Assert::true(
257
            defined('self::XS_ANY_ATTR_NAMESPACE'),
258
            self::getClassName(self::class)
259
            . '::XS_ANY_ATTR_NAMESPACE constant must be defined and set to the namespace for the xs:anyAttribute.',
260
            RuntimeException::class,
261
        );
262
263
        return self::XS_ANY_ATTR_NAMESPACE;
264
    }
265
266
267
    /**
268
     * Get the exclusions list for getAttributeNSFromXML.
269
     *
270
     * @return array<string, string>
271
     */
272
    public static function getAttributeExclusions(): array
273
    {
274
        if (defined('self::XS_ANY_ATTR_EXCLUSIONS')) {
275
            return self::XS_ANY_ATTR_EXCLUSIONS;
1 ignored issue
show
Bug introduced by
The constant SimpleSAML\XML\Extendabl...:XS_ANY_ATTR_EXCLUSIONS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
276
        }
277
278
        return [];
279
    }
280
}
281