forked from simplesamlphp/xml-common
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathXPath.php
More file actions
135 lines (113 loc) · 4.15 KB
/
Copy pathXPath.php
File metadata and controls
135 lines (113 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
declare(strict_types=1);
namespace SimpleSAML\XPath;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMXPath;
use SimpleSAML\XML\Assert\Assert;
use SimpleSAML\XML\Constants as C_XML;
use SimpleSAML\XMLSchema\Constants as C_XS;
/**
* XPath helper functions for the XML library.
*
* @package simplesamlphp/xml-common
*/
class XPath
{
/**
* Get an instance of DOMXPath associated with a DOMNode
*
* - Reuses a cached DOMXPath per document.
* - Registers core XML-related namespaces: 'xml' and 'xs'.
* - Enriches the XPath with all prefixed xmlns declarations found on the
* current node and its ancestors (up to the document element), so
* custom prefixes declared anywhere up the tree can be used in queries.
*
* @param \DOMNode $node The associated node
* @return \DOMXPath
*/
public static function getXPath(DOMNode $node): DOMXPath
{
static $xpCache = null;
if ($node instanceof DOMDocument) {
$doc = $node;
} else {
$doc = $node->ownerDocument;
Assert::notNull($doc);
}
if ($xpCache === null || !$xpCache->document->isSameNode($doc)) {
$xpCache = new DOMXPath($doc);
}
$xpCache->registerNamespace('xml', C_XML::NS_XML);
$xpCache->registerNamespace('xs', C_XS::NS_XS);
// Enrich with ancestor-declared prefixes for this document context.
self::registerAncestorNamespaces($xpCache, $node);
return $xpCache;
}
/**
* Walk from the given node up to the document element, registering all prefixed xmlns declarations.
*
* Safety:
* - Only attributes in the XMLNS namespace (http://www.w3.org/2000/xmlns/).
* - Skip default xmlns (localName === 'xmlns') because XPath requires prefixes.
* - Skip empty URIs.
* - Do not override core 'xml' and 'xs' prefixes (already bound).
* - Nearest binding wins during this pass (prefixes are added once).
*
* @param \DOMXPath $xp
* @param \DOMNode $node
*/
private static function registerAncestorNamespaces(DOMXPath $xp, DOMNode $node): void
{
$xmlnsNs = 'http://www.w3.org/2000/xmlns/';
// Avoid re-binding while walking upwards.
$registered = [
'xml' => true,
'xs' => true,
];
// Start from the nearest element (or documentElement if a DOMDocument is passed).
$current = $node instanceof DOMDocument
? $node->documentElement
: ($node instanceof DOMElement ? $node : $node->parentNode);
while ($current instanceof DOMElement) {
if ($current->hasAttributes()) {
foreach ($current->attributes as $attr) {
if ($attr->namespaceURI !== $xmlnsNs) {
continue;
}
$prefix = $attr->localName; // e.g., 'slate' for xmlns:slate, 'xmlns' for default
$uri = (string) $attr->nodeValue;
if (
$prefix === null || $prefix === '' ||
$prefix === 'xmlns' || $uri === '' ||
isset($registered[$prefix])
) {
continue;
}
$xp->registerNamespace($prefix, $uri);
$registered[$prefix] = true;
}
}
$current = $current->parentNode instanceof DOMElement ? $current->parentNode : null;
}
}
/**
* Do an XPath query on an XML node.
*
* @param \DOMNode $node The XML node.
* @param string $query The query.
* @param \DOMXPath $xpCache The DOMXPath object
* @return array<\DOMNode> Array with matching DOM nodes.
*/
public static function xpQuery(DOMNode $node, string $query, DOMXPath $xpCache): array
{
$ret = [];
$results = $xpCache->query($query, $node);
Assert::notFalse($results, 'Malformed XPath query or invalid contextNode provided.');
for ($i = 0; $i < $results->length; $i++) {
$ret[$i] = $results->item($i);
}
return $ret;
}
}