Completed
Push — master ( 7c3f70...4ee5e3 )
by Tim
16s queued 13s
created

Utils::xpQuery()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 5
eloc 19
c 2
b 0
f 1
nc 8
nop 2
dl 0
loc 29
rs 9.3222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\XML;
6
7
use DOMDocument;
8
use DOMElement;
9
use DOMNode;
10
use InvalidArgumentException;
11
use SimpleSAML\Assert\Assert;
12
use SimpleSAML\XML\Utils\XPath;
13
14
/**
15
 * Helper functions for the XML library.
16
 *
17
 * @package simplesamlphp/xml-common
18
 */
19
class Utils
20
{
21
    /**
22
     * Make an exact copy the specific \DOMElement.
23
     *
24
     * @param \DOMElement $element The element we should copy.
25
     * @param \DOMElement|null $parent The target parent element.
26
     * @return \DOMElement The copied element.
27
     */
28
    public static function copyElement(DOMElement $element, DOMElement $parent = null): DOMElement
29
    {
30
        if ($parent === null) {
31
            $document = DOMDocumentFactory::create();
32
        } else {
33
            $document = $parent->ownerDocument;
34
            Assert::notNull($document);
35
            /** @psalm-var \DOMDocument $document */
36
        }
37
38
        $namespaces = [];
39
        for ($e = $element; $e instanceof DOMNode; $e = $e->parentNode) {
40
            $xpCache = XPath::getXPath($e);
41
            foreach (XPath::xpQuery($e, './namespace::*', $xpCache) as $ns) {
42
                $prefix = $ns->localName;
43
                if ($prefix === 'xml' || $prefix === 'xmlns') {
44
                    continue;
45
                }
46
                $uri = $ns->nodeValue;
47
                if (!isset($namespaces[$prefix])) {
48
                    $namespaces[$prefix] = $uri;
49
                }
50
            }
51
        }
52
53
        /** @var \DOMElement $newElement */
54
        $newElement = $document->importNode($element, true);
55
        if ($parent !== null) {
56
            /* We need to append the child to the parent before we add the namespaces. */
57
            $parent->appendChild($newElement);
58
        }
59
60
        foreach ($namespaces as $prefix => $uri) {
61
            $newElement->setAttributeNS($uri, $prefix . ':__ns_workaround__', 'tmp');
62
            $newElement->removeAttributeNS($uri, '__ns_workaround__');
63
        }
64
65
        return $newElement;
66
    }
67
68
69
    /**
70
     * Extract localized strings from a set of nodes.
71
     *
72
     * @param \DOMElement $parent The element that contains the localized strings.
73
     * @param string $namespaceURI The namespace URI the localized strings should have.
74
     * @param string $localName The localName of the localized strings.
75
     * @return array Localized strings.
76
     */
77
    public static function extractLocalizedStrings(\DOMElement $parent, string $namespaceURI, string $localName): array
78
    {
79
        $ret = [];
80
        foreach ($parent->childNodes as $node) {
81
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
82
                continue;
83
            } elseif (!($node instanceof DOMElement)) {
84
                continue;
85
            }
86
87
            if ($node->hasAttribute('xml:lang')) {
88
                $language = $node->getAttribute('xml:lang');
89
            } else {
90
                $language = 'en';
91
            }
92
            $ret[$language] = trim($node->textContent);
93
        }
94
95
        return $ret;
96
    }
97
98
99
    /**
100
     * Extract strings from a set of nodes.
101
     *
102
     * @param \DOMElement $parent The element that contains the localized strings.
103
     * @param string $namespaceURI The namespace URI the string elements should have.
104
     * @param string $localName The localName of the string elements.
105
     * @return array The string values of the various nodes.
106
     */
107
    public static function extractStrings(DOMElement $parent, string $namespaceURI, string $localName): array
108
    {
109
        $ret = [];
110
        foreach ($parent->childNodes as $node) {
111
            if ($node->namespaceURI !== $namespaceURI || $node->localName !== $localName) {
112
                continue;
113
            }
114
            $ret[] = trim($node->textContent);
115
        }
116
117
        return $ret;
118
    }
119
120
121
    /**
122
     * Append string element.
123
     *
124
     * @param \DOMElement $parent The parent element we should append the new nodes to.
125
     * @param string $namespace The namespace of the created element.
126
     * @param string $name The name of the created element.
127
     * @param string $value The value of the element.
128
     * @return \DOMElement The generated element.
129
     */
130
    public static function addString(
131
        DOMElement $parent,
132
        string $namespace,
133
        string $name,
134
        string $value
135
    ): DOMElement {
136
        $doc = $parent->ownerDocument;
137
        Assert::notNull($doc);
138
        /** @psalm-var \DOMDocument $doc */
139
140
        $n = $doc->createElementNS($namespace, $name);
141
        $n->appendChild($doc->createTextNode($value));
142
        $parent->appendChild($n);
143
144
        return $n;
145
    }
146
147
148
    /**
149
     * Append string elements.
150
     *
151
     * @param \DOMElement $parent The parent element we should append the new nodes to.
152
     * @param string $namespace The namespace of the created elements
153
     * @param string $name The name of the created elements
154
     * @param bool $localized Whether the strings are localized, and should include the xml:lang attribute.
155
     * @param array $values The values we should create the elements from.
156
     */
157
    public static function addStrings(
158
        DOMElement $parent,
159
        string $namespace,
160
        string $name,
161
        bool $localized,
162
        array $values
163
    ): void {
164
        $doc = $parent->ownerDocument;
165
        Assert::notNull($doc);
166
        /** @psalm-var \DOMDocument $doc */
167
168
        foreach ($values as $index => $value) {
169
            $n = $doc->createElementNS($namespace, $name);
170
            $n->appendChild($doc->createTextNode($value));
171
            if ($localized) {
172
                $n->setAttribute('xml:lang', $index);
173
            }
174
            $parent->appendChild($n);
175
        }
176
    }
177
178
179
    /**
180
     * This function converts a SAML2 timestamp on the form
181
     * yyyy-mm-ddThh:mm:ss(\.s+)?Z to a UNIX timestamp. The sub-second
182
     * part is ignored.
183
     *
184
     * Andreas comments:
185
     *  I got this timestamp from Shibboleth 1.3 IdP: 2008-01-17T11:28:03.577Z
186
     *  Therefore I added to possibility to have microseconds to the format.
187
     * Added: (\.\\d{1,3})? to the regex.
188
     *
189
     * Note that we always require a 'Z' timezone for the dateTime to be valid.
190
     * This is not in the SAML spec but that's considered to be a bug in the
191
     * spec. See https://github.com/simplesamlphp/saml2/pull/36 for some
192
     * background.
193
     *
194
     * @param string $time The time we should convert.
195
     * @throws \Exception
196
     * @return int Converted to a unix timestamp.
197
     */
198
    public static function xsDateTimeToTimestamp(string $time): int
199
    {
200
        $matches = [];
201
202
        // We use a very strict regex to parse the timestamp.
203
        $regex = '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d{1,9})?Z$/D';
204
        if (preg_match($regex, $time, $matches) == 0) {
205
            throw new InvalidArgumentException(
206
                'Invalid SAML2 timestamp passed to xsDateTimeToTimestamp: ' . $time
207
            );
208
        }
209
210
        // Extract the different components of the time from the  matches in the regex.
211
        // intval will ignore leading zeroes in the string.
212
        $year   = intval($matches[1]);
213
        $month  = intval($matches[2]);
214
        $day    = intval($matches[3]);
215
        $hour   = intval($matches[4]);
216
        $minute = intval($matches[5]);
217
        $second = intval($matches[6]);
218
219
        // We use gmmktime because the timestamp will always be given
220
        //in UTC.
221
        $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
222
223
        return $ts;
224
    }
225
}
226