ExtendableElementTrait   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 88
dl 0
loc 202
rs 9.68
c 0
b 0
f 0
wmc 34

5 Methods

Rating   Name   Duplication   Size   Complexity  
B setElements() 0 74 11
A getElementNamespace() 0 10 1
A getElementExclusions() 0 7 2
A getElements() 0 3 1
D getChildElementsFromXML() 0 62 19
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\Chunk;
11
use SimpleSAML\XML\Constants as C;
12
use SimpleSAML\XML\Registry\ElementRegistry;
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 is_array;
21
use function rtrim;
22
use function sprintf;
23
24
/**
25
 * Trait grouping common functionality for elements implementing the xs:any element.
26
 *
27
 * @package simplesamlphp/xml-common
28
 */
29
trait ExtendableElementTrait
30
{
31
    /** @var \SimpleSAML\XML\SerializableElementInterface[] */
32
    protected array $elements = [];
33
34
35
    /**
36
     * Parse an XML document and get the child elements from the specified namespace(s).
37
     * The namespace defaults to the XS_ANY_ELT_NAMESPACE constant on the element.
38
     * NOTE: In case the namespace is ##any, this method will also return local non-namespaced elements!
39
     *
40
     * @param \DOMElement $xml
41
     * @param string|string[]|null $namespace
42
     * @return list<\SimpleSAML\XML\SerializableElementInterface> $elements
43
     */
44
    protected static function getChildElementsFromXML(
45
        DOMElement $xml,
46
        string|array|null $namespace = null,
47
    ): array {
48
        $namespace = $namespace ?? self::XS_ANY_ELT_NAMESPACE;
49
        $exclusionList = self::getElementExclusions();
50
        $registry = ElementRegistry::getInstance();
51
        $elements = [];
52
53
        // Validate namespace value
54
        if (!is_array($namespace)) {
55
            // Must be one of the predefined values
56
            Assert::oneOf($namespace, NS::$PREDEFINED);
57
58
            foreach ($xml->childNodes as $elt) {
59
                if (!($elt instanceof DOMElement)) {
60
                    continue;
61
                } elseif (in_array([$elt->namespaceURI, $elt->localName], $exclusionList, true)) {
62
                    continue;
63
                } elseif ($namespace === NS::OTHER && in_array($elt->namespaceURI, [self::NS, null], true)) {
0 ignored issues
show
Bug introduced by
The constant SimpleSAML\XML\ExtendableElementTrait::NS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
64
                    continue;
65
                } elseif ($namespace === NS::TARGETNAMESPACE && $elt->namespaceURI !== self::NS) {
66
                    continue;
67
                } elseif ($namespace === NS::LOCAL && $elt->namespaceURI !== null) {
68
                    continue;
69
                }
70
71
                $handler = $registry->getElementHandler($elt->namespaceURI, $elt->localName);
72
                $elements[] = ($handler === null) ? Chunk::fromXML($elt) : $handler::fromXML($elt);
73
            }
74
        } else {
75
            // Array must be non-empty and cannot contain ##any or ##other
76
            Assert::notEmpty($namespace);
77
            Assert::allStringNotEmpty($namespace);
78
            Assert::allNotSame($namespace, NS::ANY);
79
            Assert::allNotSame($namespace, NS::OTHER);
80
81
            // Replace the ##targetedNamespace with the actual namespace
82
            if (($key = array_search(NS::TARGETNAMESPACE, $namespace)) !== false) {
83
                $namespace[$key] = self::NS;
84
            }
85
86
            // Replace the ##local with null
87
            if (($key = array_search(NS::LOCAL, $namespace)) !== false) {
88
                $namespace[$key] = null;
89
            }
90
91
            foreach ($xml->childNodes as $elt) {
92
                if (!($elt instanceof DOMElement)) {
93
                    continue;
94
                } elseif (in_array([$elt->namespaceURI, $elt->localName], $exclusionList, true)) {
95
                    continue;
96
                } elseif (!in_array($elt->namespaceURI, $namespace, true)) {
97
                    continue;
98
                }
99
100
                $handler = $registry->getElementHandler($elt->namespaceURI, $elt->localName);
101
                $elements[] = ($handler === null) ? Chunk::fromXML($elt) : $handler::fromXML($elt);
102
            }
103
        }
104
105
        return $elements;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $elements returns the type array|array<mixed,SimpleSAML\XML\Chunk|mixed> which is incompatible with the documented return type SimpleSAML\XML\list.
Loading history...
106
    }
107
108
109
    /**
110
     * Set an array with all elements present.
111
     *
112
     * @param \SimpleSAML\XML\SerializableElementInterface[] $elements
113
     * @return void
114
     */
115
    protected function setElements(array $elements): void
116
    {
117
        Assert::maxCount($elements, C::UNBOUNDED_LIMIT);
118
        Assert::allIsInstanceOf($elements, SerializableElementInterface::class);
119
120
        $namespace = $this->getElementNamespace();
121
        // Validate namespace value
122
        if (!is_array($namespace)) {
123
            // Must be one of the predefined values
124
            Assert::oneOf($namespace, NS::$PREDEFINED);
125
        } else {
126
            // Array must be non-empty and cannot contain ##any or ##other
127
            Assert::notEmpty($namespace);
128
            Assert::allNotSame($namespace, NS::ANY);
129
            Assert::allNotSame($namespace, NS::OTHER);
130
        }
131
132
        // Get namespaces for all elements
133
        /** @var array<\SimpleSAML\XML\AbstractElement|\SimpleSAML\XML\Chunk> $elements */
134
        $actual_namespaces = array_map(
135
            /**
136
             * @return string|null
137
             */
138
            function (AbstractElement|Chunk $elt): ?string {
139
                return ($elt instanceof Chunk) ? $elt->getNamespaceURI() : $elt::getNamespaceURI();
140
            },
141
            $elements,
142
        );
143
144
        if ($namespace === NS::LOCAL) {
145
            // If ##local then all namespaces must be null
146
            Assert::allNull($actual_namespaces);
147
        } elseif (is_array($namespace)) {
148
            // Make a local copy of the property that we can edit
149
            $allowed_namespaces = $namespace;
150
151
            // Replace the ##targetedNamespace with the actual namespace
152
            if (($key = array_search(NS::TARGETNAMESPACE, $allowed_namespaces)) !== false) {
153
                $allowed_namespaces[$key] = self::NS;
154
            }
155
156
            // Replace the ##local with null
157
            if (($key = array_search(NS::LOCAL, $allowed_namespaces)) !== false) {
158
                $allowed_namespaces[$key] = null;
159
            }
160
161
            $diff = array_diff($actual_namespaces, $allowed_namespaces);
162
            Assert::isEmpty(
163
                $diff,
164
                sprintf(
165
                    'Elements from namespaces [ %s ] are not allowed inside a %s element.',
166
                    rtrim(implode(', ', $diff)),
167
                    self::NS,
168
                ),
169
            );
170
        } elseif ($namespace === NS::OTHER) {
171
            // Must be any namespace other than the parent element, excluding elements with no namespace
172
            Assert::notInArray(null, $actual_namespaces);
173
            Assert::allNotSame($actual_namespaces, self::NS);
174
        } elseif ($namespace === NS::TARGETNAMESPACE) {
175
            // Must be the same namespace as the one of the parent element
176
            Assert::allSame($actual_namespaces, self::NS);
177
        } else {
178
            // XS_ANY_NS_ANY
179
        }
180
181
        $exclusionList = self::getElementExclusions();
182
        foreach ($elements as $i => $elt) {
183
            if (in_array([$elt->getNamespaceURI(), $elt->getLocalName()], $exclusionList, true)) {
184
                unset($elements[$i]);
185
            }
186
        }
187
188
        $this->elements = $elements;
189
    }
190
191
192
    /**
193
     * Get an array with all elements present.
194
     *
195
     * @return \SimpleSAML\XML\SerializableElementInterface[]
196
     */
197
    public function getElements(): array
198
    {
199
        return $this->elements;
200
    }
201
202
203
    /**
204
     * @return string|string[]
205
     */
206
    public function getElementNamespace(): array|string
207
    {
208
        Assert::true(
209
            defined('self::XS_ANY_ELT_NAMESPACE'),
210
            self::getClassName(self::class)
211
            . '::XS_ANY_ELT_NAMESPACE constant must be defined and set to the namespace for the xs:any element.',
212
            RuntimeException::class,
213
        );
214
215
        return self::XS_ANY_ELT_NAMESPACE;
216
    }
217
218
219
    /**
220
     * Get the exclusions list for getChildElementsFromXML.
221
     *
222
     * @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...
223
     */
224
    public static function getElementExclusions(): array
225
    {
226
        if (defined('self::XS_ANY_ELT_EXCLUSIONS')) {
227
            return self::XS_ANY_ELT_EXCLUSIONS;
228
        }
229
230
        return [];
231
    }
232
}
233