1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Copyright (c) 2017–2018 Ryan Parman <http://ryanparman.com>. |
4
|
|
|
* Copyright (c) 2017–2018 Contributors. |
5
|
|
|
* |
6
|
|
|
* http://opensource.org/licenses/Apache2.0 |
7
|
|
|
*/ |
8
|
|
|
declare(strict_types=1); |
9
|
|
|
|
10
|
|
|
namespace SimplePie\Middleware\Xml; |
11
|
|
|
|
12
|
|
|
use SimplePie\Middleware\AbstractMiddleware; |
13
|
|
|
use SimplePie\Type\Node; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* The base XML middleware class that all other XML middleware classes extend from. It handles low-level functionality |
17
|
|
|
* that is shared across all XML middleware classes. |
18
|
|
|
*/ |
19
|
|
|
abstract class AbstractXmlMiddleware extends AbstractMiddleware |
20
|
|
|
{ |
21
|
|
|
/** |
22
|
|
|
* The status of case-sensitivity. |
23
|
|
|
* |
24
|
|
|
* @var bool |
25
|
|
|
*/ |
26
|
|
|
protected $caseSensitive = true; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* By default, SimplePie NG is case-sensitive (as per the specification). If an invalid feed is parsed that does not |
30
|
|
|
* follow the specification with regard to casing of XML elements, this method allows you to trade some performance |
31
|
|
|
* in favor of case-insensitive parsing. |
32
|
|
|
* |
33
|
|
|
* @param bool $makeInsensitive Whether or not the handling should be made case-insensitive. A value of `true` |
34
|
|
|
* means that the handling should be case-insensitive. A value of `false` means that |
35
|
|
|
* the handling should be case-sensitive. The default value is `true`. |
36
|
|
|
* |
37
|
|
|
* @return self |
38
|
|
|
*/ |
39
|
|
|
public function setCaseInsensitive(bool $makeInsensitive = true): self |
40
|
|
|
{ |
41
|
|
|
$this->caseSensitive = !$makeInsensitive; |
42
|
|
|
|
43
|
|
|
return $this; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Replace all instances of `%s` with the `$namespaceAlias` parameter. |
48
|
|
|
* |
49
|
|
|
* This is similar to `sprintf()`, but the `$namespaceAlias` is applied to _all_ instances of `%s`. |
50
|
|
|
* |
51
|
|
|
* @param string $query An XPath query where `%s` is used in-place of the XML namespace alias. |
52
|
|
|
* @param string $namespaceAlias The XML namespace alias to apply. |
53
|
|
|
* |
54
|
|
|
* @return string |
55
|
|
|
*/ |
56
|
|
|
public function applyNsToQuery(string $query, string $namespaceAlias): string |
57
|
|
|
{ |
58
|
|
|
return \str_replace('%s', $namespaceAlias, $query); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Produce an XPath 1.0 expression which is used to query XML document nodes. |
63
|
|
|
* |
64
|
|
|
* ```php |
65
|
|
|
* ['feed', 'entry', 5, 'id', '@xml:lang'] |
66
|
|
|
* ``` |
67
|
|
|
* |
68
|
|
|
* ```xpath |
69
|
|
|
* /feed/entry[5]/id/@xml:lang (simplified) |
70
|
|
|
* ``` |
71
|
|
|
* |
72
|
|
|
* @param string $namespaceAlias The XML namespace alias to apply. |
73
|
|
|
* @param array $path An ordered array of nested elements, starting from the top-level XML node. |
74
|
|
|
* If an integer is added, then it is assumed that the element before it should be |
75
|
|
|
* handled as an array and the integer is its index. Expression is generated |
76
|
|
|
* left-to-right. |
77
|
|
|
* |
78
|
|
|
* @return string An XPath 1.0 expression. |
79
|
|
|
*/ |
80
|
|
|
public function generateQuery(string $namespaceAlias, array $path): string |
81
|
|
|
{ |
82
|
|
|
$query = ''; |
83
|
|
|
|
84
|
|
|
while (\count($path)) { |
85
|
|
|
$p = \array_shift($path); |
86
|
|
|
$next = $path[0] ?? null; |
87
|
|
|
|
88
|
|
|
// Reduce to only the upper/lower of the active letters |
89
|
|
|
// ≈30-35% faster than the full alphabet |
90
|
|
|
$pLet = \count_chars($p, 3); |
91
|
|
|
$pLow = \mb_strtolower($pLet); |
92
|
|
|
$pUp = \mb_strtoupper($pLet); |
93
|
|
|
$pAttr = ('@' === \mb_substr($p, 0, 1)); |
94
|
|
|
|
95
|
|
|
if (\is_int($next)) { |
96
|
|
|
if ($this->caseSensitive || $pAttr) { |
97
|
|
|
// case; next |
98
|
|
|
$query .= \sprintf( |
99
|
|
|
'/%s%s[position() = %d]', |
100
|
|
|
(!$pAttr |
101
|
|
|
? $namespaceAlias . ':' |
102
|
|
|
: ''), |
103
|
|
|
$p, |
104
|
|
|
\array_shift($path) + 1 |
105
|
|
|
); |
106
|
|
|
} else { |
107
|
|
|
// icase; next |
108
|
|
|
$query .= \sprintf( |
109
|
|
|
'/%s*[translate(name(), \'%s\', \'%s\') = \'%s\'][position() = %d]', |
110
|
|
|
(!$pAttr |
111
|
|
|
? $namespaceAlias . ':' |
112
|
|
|
: ''), |
113
|
|
|
$pUp, |
114
|
|
|
$pLow, |
115
|
|
|
$p, |
116
|
|
|
\array_shift($path) + 1 |
117
|
|
|
); |
118
|
|
|
} |
119
|
|
|
} else { |
120
|
|
|
if ($this->caseSensitive || $pAttr) { |
121
|
|
|
// case; no-next |
122
|
|
|
$query .= \sprintf( |
123
|
|
|
'/%s%s', |
124
|
|
|
(!$pAttr |
125
|
|
|
? $namespaceAlias . ':' |
126
|
|
|
: ''), |
127
|
|
|
$p |
128
|
|
|
); |
129
|
|
|
} else { |
130
|
|
|
// icase; no-next |
131
|
|
|
$query .= \sprintf( |
132
|
|
|
'/%s*[translate(name(), \'%s\', \'%s\') = \'%s\']', |
133
|
|
|
(!$pAttr |
134
|
|
|
? $namespaceAlias . ':' |
135
|
|
|
: ''), |
136
|
|
|
$pUp, |
137
|
|
|
$pLow, |
138
|
|
|
$p |
139
|
|
|
); |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
return $query; |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|