ExtendableAttributesTrait   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 100
dl 0
loc 249
rs 9.44
c 0
b 0
f 0
wmc 37

7 Methods

Rating   Name   Duplication   Size   Complexity  
A hasAttributeNS() 0 8 4
A getAttributeNS() 0 8 4
A getAttributesNS() 0 3 1
C getAttributesNSFromXML() 0 65 15
A getAttributeNamespace() 0 10 1
B setAttributesNS() 0 80 10
A getAttributeExclusions() 0 7 2
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\Constants\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 string|string[]|null $namespace
94
     * @return array<int, \SimpleSAML\XML\Attribute>
95
     */
96
    protected static function getAttributesNSFromXML(
97
        DOMElement $xml,
98
        string|array|null $namespace = null,
99
    ): array {
100
        $namespace = $namespace ?? self::XS_ANY_ATTR_NAMESPACE;
101
        $exclusionList = self::getAttributeExclusions();
102
        $attributes = [];
103
104
        // Validate namespace value
105
        if (!is_array($namespace)) {
106
            // Must be one of the predefined values
107
            Assert::oneOf($namespace, NS::$PREDEFINED);
108
109
            foreach ($xml->attributes as $a) {
110
                if (in_array([$a->namespaceURI, $a->localName], $exclusionList, true)) {
111
                    continue;
112
                } elseif ($namespace === NS::OTHER && in_array($a->namespaceURI, [self::NS, null], true)) {
113
                    continue;
114
                } elseif ($namespace === NS::TARGETNAMESPACE && $a->namespaceURI !== self::NS) {
115
                    continue;
116
                } elseif ($namespace === NS::LOCAL && $a->namespaceURI !== null) {
117
                    continue;
118
                }
119
120
                $attributes[] = new Attribute(
121
                    $a->namespaceURI,
122
                    $a->prefix,
123
                    $a->localName,
124
                    StringValue::fromString($a->nodeValue),
125
                );
126
            }
127
        } else {
128
            // Array must be non-empty and cannot contain ##any or ##other
129
            Assert::notEmpty($namespace);
130
            Assert::allStringNotEmpty($namespace);
131
            Assert::allNotSame($namespace, NS::ANY);
132
            Assert::allNotSame($namespace, NS::OTHER);
133
134
            // Replace the ##targetedNamespace with the actual namespace
135
            if (($key = array_search(NS::TARGETNAMESPACE, $namespace)) !== false) {
136
                $namespace[$key] = self::NS;
137
            }
138
139
            // Replace the ##local with null
140
            if (($key = array_search(NS::LOCAL, $namespace)) !== false) {
141
                $namespace[$key] = null;
142
            }
143
144
            foreach ($xml->attributes as $a) {
145
                if (in_array([$a->namespaceURI, $a->localName], $exclusionList, true)) {
146
                    continue;
147
                } elseif (!in_array($a->namespaceURI, $namespace, true)) {
148
                    continue;
149
                }
150
151
                $attributes[] = new Attribute(
152
                    $a->namespaceURI,
153
                    $a->prefix,
154
                    $a->localName,
155
                    StringValue::fromString($a->nodeValue),
156
                );
157
            }
158
        }
159
160
        return $attributes;
161
    }
162
163
164
    /**
165
     * @param array<int, \SimpleSAML\XML\Attribute> $attributes
166
     * @throws \SimpleSAML\Assert\AssertionFailedException if $attributes contains anything other than Attribute objects
167
     */
168
    protected function setAttributesNS(array $attributes): void
169
    {
170
        Assert::maxCount($attributes, C::UNBOUNDED_LIMIT);
171
        Assert::allIsInstanceOf(
172
            $attributes,
173
            Attribute::class,
174
            'Arbitrary XML attributes can only be an instance of Attribute.',
175
        );
176
        $namespace = $this->getAttributeNamespace();
177
178
        // Validate namespace value
179
        if (!is_array($namespace)) {
180
            // Must be one of the predefined values
181
            Assert::oneOf($namespace, NS::$PREDEFINED);
182
        } else {
183
            // Array must be non-empty and cannot contain ##any or ##other
184
            Assert::notEmpty($namespace);
185
            Assert::allNotSame($namespace, NS::ANY);
186
            Assert::allNotSame($namespace, NS::OTHER);
187
        }
188
189
        // Get namespaces for all attributes
190
        $actual_namespaces = array_map(
191
            /**
192
             * @param \SimpleSAML\XML\Attribute $elt
193
             * @return string|null
194
             */
195
            function (Attribute $attr) {
196
                return $attr->getNamespaceURI();
197
            },
198
            $attributes,
199
        );
200
201
        if ($namespace === NS::LOCAL) {
202
            // If ##local then all namespaces must be null
203
            Assert::allNull($actual_namespaces);
204
        } elseif (is_array($namespace)) {
205
            // Make a local copy of the property that we can edit
206
            $allowed_namespaces = $namespace;
207
208
            // Replace the ##targetedNamespace with the actual namespace
209
            if (($key = array_search(NS::TARGETNAMESPACE, $allowed_namespaces)) !== false) {
210
                $allowed_namespaces[$key] = self::NS;
211
            }
212
213
            // Replace the ##local with null
214
            if (($key = array_search(NS::LOCAL, $allowed_namespaces)) !== false) {
215
                $allowed_namespaces[$key] = null;
216
            }
217
218
            $diff = array_diff($actual_namespaces, $allowed_namespaces);
219
            Assert::isEmpty(
220
                $diff,
221
                sprintf(
222
                    'Attributes from namespaces [ %s ] are not allowed inside a %s element.',
223
                    rtrim(implode(', ', $diff)),
224
                    self::NS,
225
                ),
226
            );
227
        } else {
228
            if ($namespace === NS::OTHER) {
229
                // All attributes must be namespaced, ergo non-null
230
                Assert::allNotNull($actual_namespaces);
231
232
                // Must be any namespace other than the parent element
233
                Assert::allNotSame($actual_namespaces, self::NS);
234
            } elseif ($namespace === NS::TARGETNAMESPACE) {
235
                // Must be the same namespace as the one of the parent element
236
                Assert::allSame($actual_namespaces, self::NS);
237
            }
238
        }
239
240
        $exclusionList = self::getAttributeExclusions();
241
        foreach ($attributes as $i => $attr) {
242
            if (in_array([$attr->getNamespaceURI(), $attr->getAttrName()], $exclusionList, true)) {
243
                unset($attributes[$i]);
244
            }
245
        }
246
247
        $this->namespacedAttributes = $attributes;
248
    }
249
250
251
    /**
252
     * @return string[]|string
253
     */
254
    public function getAttributeNamespace(): array|string
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{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...
271
     */
272
    public static function getAttributeExclusions(): array
273
    {
274
        if (defined('self::XS_ANY_ATTR_EXCLUSIONS')) {
275
            return self::XS_ANY_ATTR_EXCLUSIONS;
276
        }
277
278
        return [];
279
    }
280
}
281