1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @package s9e\TextFormatter |
5
|
|
|
* @copyright Copyright (c) 2010-2017 The s9e Authors |
6
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php The MIT License |
7
|
|
|
*/ |
8
|
|
|
namespace s9e\TextFormatter\Configurator\Helpers; |
9
|
|
|
|
10
|
|
|
use InvalidArgumentException; |
11
|
|
|
use RuntimeException; |
12
|
|
|
|
13
|
|
|
abstract class XPathHelper |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Export a literal as an XPath expression |
17
|
|
|
* |
18
|
|
|
* @param mixed $value Literal, e.g. "foo" |
19
|
|
|
* @return string XPath expression, e.g. "'foo'" |
20
|
|
|
*/ |
21
|
|
|
public static function export($value) |
22
|
|
|
{ |
23
|
|
|
if (!is_scalar($value)) |
24
|
|
|
{ |
25
|
|
|
throw new InvalidArgumentException(__METHOD__ . '() cannot export non-scalar values'); |
26
|
|
|
} |
27
|
|
|
if (is_int($value)) |
28
|
|
|
{ |
29
|
|
|
return (string) $value; |
30
|
|
|
} |
31
|
|
|
if (is_float($value)) |
32
|
|
|
{ |
33
|
|
|
// Avoid locale issues by using sprintf() |
34
|
|
|
return preg_replace('(\\.?0+$)', '', sprintf('%F', $value)); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
return self::exportString($value); |
|
|
|
|
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Export a string as an XPath expression |
42
|
|
|
* |
43
|
|
|
* @param string $str Literal, e.g. "foo" |
44
|
|
|
* @return string XPath expression, e.g. "'foo'" |
45
|
|
|
*/ |
46
|
|
|
protected static function exportString($str) |
47
|
|
|
{ |
48
|
|
|
// foo becomes 'foo' |
49
|
|
|
if (strpos($str, "'") === false) |
50
|
|
|
{ |
51
|
|
|
return "'" . $str . "'"; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
// d'oh becomes "d'oh" |
55
|
|
|
if (strpos($str, '"') === false) |
56
|
|
|
{ |
57
|
|
|
return '"' . $str . '"'; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
// This string contains both ' and ". XPath 1.0 doesn't have a mechanism to escape quotes, |
61
|
|
|
// so we have to get creative and use concat() to join chunks in single quotes and chunks |
62
|
|
|
// in double quotes |
63
|
|
|
$toks = []; |
64
|
|
|
$c = '"'; |
65
|
|
|
$pos = 0; |
66
|
|
|
while ($pos < strlen($str)) |
67
|
|
|
{ |
68
|
|
|
$spn = strcspn($str, $c, $pos); |
69
|
|
|
if ($spn) |
70
|
|
|
{ |
71
|
|
|
$toks[] = $c . substr($str, $pos, $spn) . $c; |
72
|
|
|
$pos += $spn; |
73
|
|
|
} |
74
|
|
|
$c = ($c === '"') ? "'" : '"'; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
return 'concat(' . implode(',', $toks) . ')'; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Return the list of variables used in a given XPath expression |
82
|
|
|
* |
83
|
|
|
* @param string $expr XPath expression |
84
|
|
|
* @return array Alphabetically sorted list of unique variable names |
85
|
|
|
*/ |
86
|
|
|
public static function getVariables($expr) |
87
|
|
|
{ |
88
|
|
|
// First, remove strings' contents to prevent false-positives |
89
|
|
|
$expr = preg_replace('/(["\']).*?\\1/s', '$1$1', $expr); |
90
|
|
|
|
91
|
|
|
// Capture all the variable names |
92
|
|
|
preg_match_all('/\\$(\\w+)/', $expr, $matches); |
93
|
|
|
|
94
|
|
|
// Dedupe and sort names |
95
|
|
|
$varNames = array_unique($matches[1]); |
96
|
|
|
sort($varNames); |
97
|
|
|
|
98
|
|
|
return $varNames; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Determine whether given XPath expression definitely evaluates to a number |
103
|
|
|
* |
104
|
|
|
* @param string $expr XPath expression |
105
|
|
|
* @return bool Whether given XPath expression definitely evaluates to a number |
106
|
|
|
*/ |
107
|
|
|
public static function isExpressionNumeric($expr) |
108
|
|
|
{ |
109
|
|
|
// Trim the expression and remove parentheses that are not part of a function call. PCRE |
110
|
|
|
// does not support lookbehind assertions of variable length so we have to flip the string. |
111
|
|
|
// We exclude the XPath operator "div" (flipped into "vid") to avoid false positives |
112
|
|
|
$expr = strrev(preg_replace('(\\((?!\\s*(?!vid(?!\\w))\\w))', ' ', strrev($expr))); |
113
|
|
|
$expr = str_replace(')', ' ', $expr); |
114
|
|
|
if (preg_match('(^\\s*([$@][-\\w]++|-?\\.\\d++|-?\\d++(?:\\.\\d++)?)(?>\\s*(?>[-+*]|div)\\s*(?1))++\\s*$)', $expr)) |
115
|
|
|
{ |
116
|
|
|
return true; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
return false; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Remove extraneous space in a given XPath expression |
124
|
|
|
* |
125
|
|
|
* @param string $expr Original XPath expression |
126
|
|
|
* @return string Minified XPath expression |
127
|
|
|
*/ |
128
|
|
|
public static function minify($expr) |
129
|
|
|
{ |
130
|
|
|
$old = $expr; |
131
|
|
|
$strings = []; |
132
|
|
|
|
133
|
|
|
// Trim the surrounding whitespace then temporarily remove literal strings |
134
|
|
|
$expr = preg_replace_callback( |
135
|
|
|
'/"[^"]*"|\'[^\']*\'/', |
136
|
|
|
function ($m) use (&$strings) |
137
|
|
|
{ |
138
|
|
|
$uniqid = '(' . sha1(uniqid()) . ')'; |
139
|
|
|
$strings[$uniqid] = $m[0]; |
140
|
|
|
|
141
|
|
|
return $uniqid; |
142
|
|
|
}, |
143
|
|
|
trim($expr) |
144
|
|
|
); |
145
|
|
|
|
146
|
|
|
if (preg_match('/[\'"]/', $expr)) |
147
|
|
|
{ |
148
|
|
|
throw new RuntimeException("Cannot parse XPath expression '" . $old . "'"); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
// Normalize whitespace to a single space |
152
|
|
|
$expr = preg_replace('/\\s+/', ' ', $expr); |
153
|
|
|
|
154
|
|
|
// Remove the space between a non-word character and a word character |
155
|
|
|
$expr = preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr); |
156
|
|
|
$expr = preg_replace('/([^-a-z_0-9]) ([-a-z_0-9])/i', '$1$2', $expr); |
157
|
|
|
|
158
|
|
|
// Remove the space between two non-word characters as long as they're not two - |
159
|
|
|
$expr = preg_replace('/(?!- -)([^-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr); |
160
|
|
|
|
161
|
|
|
// Remove the space between a - and a word character, as long as there's a space before - |
162
|
|
|
$expr = preg_replace('/ - ([a-z_0-9])/i', ' -$1', $expr); |
163
|
|
|
|
164
|
|
|
// Remove the spaces between a number and the div operator and the next token |
165
|
|
|
$expr = preg_replace('/((?:^|[ \\(])\\d+) div ?/', '$1div', $expr); |
166
|
|
|
|
167
|
|
|
// Remove the space between the div operator the next token |
168
|
|
|
$expr = preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr); |
169
|
|
|
|
170
|
|
|
// Restore the literals |
171
|
|
|
$expr = strtr($expr, $strings); |
172
|
|
|
|
173
|
|
|
return $expr; |
174
|
|
|
} |
175
|
|
|
} |
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.