Issues (33)

src/XML/ExtendableElementTrait.php (3 issues)

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