Passed
Push — master ( 02bc56...f9cdf8 )
by Tim
03:30 queued 01:30
created

getAttributeExclusions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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