ExtendableAttributesTrait::getAttributeNamespace()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
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;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XML\Constants was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use SimpleSAML\XMLSchema\Exception\InvalidDOMAttributeException;
13
use SimpleSAML\XMLSchema\Exception\SchemaViolationException;
14
use SimpleSAML\XMLSchema\Type\StringValue;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSchema\Type\StringValue was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use SimpleSAML\XMLSchema\XML\Constants\NS;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSchema\XML\Constants\NS was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
17
use function array_diff;
18
use function array_map;
19
use function array_search;
20
use function defined;
21
use function implode;
22
use function in_array;
23
use function is_array;
24
use function rtrim;
25
use function sprintf;
26
27
/**
28
 * Trait for elements that can have arbitrary namespaced attributes.
29
 *
30
 * @package simplesamlphp/xml-common
31
 */
32
trait ExtendableAttributesTrait
33
{
34
    /**
35
     * Extra (namespace qualified) attributes.
36
     *
37
     * @var array<int, \SimpleSAML\XML\Attribute>
38
     */
39
    protected array $namespacedAttributes = [];
40
41
42
    /**
43
     * Check if a namespace-qualified attribute exists.
44
     *
45
     * @param string|null $namespaceURI The namespace URI.
46
     * @param string $localName The local name.
47
     * @return bool true if the attribute exists, false if not.
48
     */
49
    public function hasAttributeNS(?string $namespaceURI, string $localName): bool
50
    {
51
        foreach ($this->getAttributesNS() as $attr) {
52
            if ($attr->getNamespaceURI() === $namespaceURI && $attr->getAttrName() === $localName) {
53
                return true;
54
            }
55
        }
56
        return false;
57
    }
58
59
60
    /**
61
     * Get a namespace-qualified attribute.
62
     *
63
     * @param string|null $namespaceURI The namespace URI.
64
     * @param string $localName The local name.
65
     * @return \SimpleSAML\XML\Attribute|null The value of the attribute, or null if the attribute does not exist.
66
     */
67
    public function getAttributeNS(?string $namespaceURI, string $localName): ?Attribute
68
    {
69
        foreach ($this->getAttributesNS() as $attr) {
70
            if ($attr->getNamespaceURI() === $namespaceURI && $attr->getAttrName() === $localName) {
71
                return $attr;
72
            }
73
        }
74
        return null;
75
    }
76
77
78
    /**
79
     * Get the namespaced attributes in this element.
80
     *
81
     * @return array<int, \SimpleSAML\XML\Attribute>
82
     */
83
    public function getAttributesNS(): array
84
    {
85
        return $this->namespacedAttributes;
86
    }
87
88
89
    /**
90
     * Parse an XML document and get the namespaced attributes from the specified namespace(s).
91
     * The namespace defaults to the XS_ANY_ATTR_NAMESPACE constant on the element.
92
     * NOTE: In case the namespace is ##any, this method will also return local non-namespaced attributes!
93
     *
94
     * @param \DOMElement $xml
95
     * @param string|string[]|null $namespace
96
     * @return list<\SimpleSAML\XML\Attribute>
97
     */
98
    protected static function getAttributesNSFromXML(
99
        DOMElement $xml,
100
        string|array|null $namespace = null,
101
    ): array {
102
        $namespace = $namespace ?? static::XS_ANY_ATTR_NAMESPACE;
103
        $exclusionList = self::getAttributeExclusions();
104
        $attributes = [];
105
106
        // Validate namespace value
107
        if (!is_array($namespace)) {
108
            // Must be one of the predefined values
109
            Assert::oneOf($namespace, NS::$PREDEFINED);
110
111
            foreach ($xml->attributes as $a) {
112
                if (
113
                    $exclusionList
0 ignored issues
show
Bug Best Practice introduced by
The expression $exclusionList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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