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\RendererGenerators\PHP; |
9
|
|
|
|
10
|
|
|
use RuntimeException; |
11
|
|
|
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; |
12
|
|
|
|
13
|
|
|
class Quick |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Generate the Quick renderer's source |
17
|
|
|
* |
18
|
|
|
* @param array $compiledTemplates Array of tagName => compiled template |
19
|
|
|
* @return string |
20
|
|
|
*/ |
21
|
|
|
public static function getSource(array $compiledTemplates) |
22
|
|
|
{ |
23
|
|
|
$map = ['dynamic' => [], 'php' => [], 'static' => []]; |
24
|
|
|
$tagNames = []; |
25
|
|
|
$unsupported = []; |
26
|
|
|
|
27
|
|
|
// Ignore system tags |
28
|
|
|
unset($compiledTemplates['br']); |
29
|
|
|
unset($compiledTemplates['e']); |
30
|
|
|
unset($compiledTemplates['i']); |
31
|
|
|
unset($compiledTemplates['p']); |
32
|
|
|
unset($compiledTemplates['s']); |
33
|
|
|
|
34
|
|
|
foreach ($compiledTemplates as $tagName => $php) |
35
|
|
|
{ |
36
|
|
|
if (preg_match('(^(?:br|[ieps])$)', $tagName)) |
37
|
|
|
{ |
38
|
|
|
continue; |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
$rendering = self::getRenderingStrategy($php); |
42
|
|
|
if ($rendering === false) |
43
|
|
|
{ |
44
|
|
|
$unsupported[] = $tagName; |
45
|
|
|
continue; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
foreach ($rendering as $i => list($strategy, $replacement)) |
49
|
|
|
{ |
50
|
|
|
$match = (($i) ? '/' : '') . $tagName; |
51
|
|
|
$map[$strategy][$match] = $replacement; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
// Record the names of tags whose template does not contain a passthrough |
55
|
|
|
if (!isset($rendering[1])) |
56
|
|
|
{ |
57
|
|
|
$tagNames[] = $tagName; |
58
|
|
|
} |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
$php = []; |
62
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
63
|
|
|
$php[] = ' public $enableQuickRenderer=true;'; |
64
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
65
|
|
|
$php[] = ' protected $static=' . self::export($map['static']) . ';'; |
66
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
67
|
|
|
$php[] = ' protected $dynamic=' . self::export($map['dynamic']) . ';'; |
68
|
|
|
|
69
|
|
|
$quickSource = ''; |
70
|
|
|
if (!empty($map['php'])) |
71
|
|
|
{ |
72
|
|
|
list($quickBranches, $quickSource) = self::generateBranchTable('$qb', $map['php']); |
73
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
74
|
|
|
$php[] = ' protected $quickBranches=' . self::export($quickBranches) . ';'; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
|
78
|
|
|
// Build a regexp that matches all the tags |
79
|
|
|
$regexp = '(<(?:(?!/)('; |
80
|
|
|
$regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)'; |
81
|
|
|
$regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s'; |
82
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
83
|
|
|
$php[] = ' protected $quickRegexp=' . var_export($regexp, true) . ';'; |
84
|
|
|
|
85
|
|
|
// Build a regexp that matches tags that cannot be rendered with the Quick renderer |
86
|
|
|
if (!empty($unsupported)) |
87
|
|
|
{ |
88
|
|
|
$regexp = '(<(?:[!?]|' . RegexpBuilder::fromList($unsupported, ['useLookahead' => true]) . '[ />]))'; |
89
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
90
|
|
|
$php[] = ' protected $quickRenderingTest=' . var_export($regexp, true) . ';'; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
$php[] = ' /** {@inheritdoc} */'; |
94
|
|
|
$php[] = ' protected function renderQuickTemplate($qb, $xml)'; |
95
|
|
|
$php[] = ' {'; |
96
|
|
|
$php[] = ' $attributes=$this->matchAttributes($xml);'; |
97
|
|
|
$php[] = " \$html='';" . $quickSource; |
98
|
|
|
$php[] = ''; |
99
|
|
|
$php[] = ' return $html;'; |
100
|
|
|
$php[] = ' }'; |
101
|
|
|
|
102
|
|
|
return implode("\n", $php); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Export an array as PHP |
107
|
|
|
* |
108
|
|
|
* @param array $arr |
109
|
|
|
* @return string |
110
|
|
|
*/ |
111
|
|
|
protected static function export(array $arr) |
112
|
|
|
{ |
113
|
|
|
$exportKeys = (array_keys($arr) !== range(0, count($arr) - 1)); |
114
|
|
|
ksort($arr); |
115
|
|
|
|
116
|
|
|
$entries = []; |
117
|
|
|
foreach ($arr as $k => $v) |
118
|
|
|
{ |
119
|
|
|
$entries[] = (($exportKeys) ? var_export($k, true) . '=>' : '') |
120
|
|
|
. ((is_array($v)) ? self::export($v) : var_export($v, true)); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
return '[' . implode(',', $entries) . ']'; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Compute the rendering strategy for a compiled template |
128
|
|
|
* |
129
|
|
|
* @param string $php Template compiled for the PHP renderer |
130
|
|
|
* @return array|bool An array containing the type of replacement ('static', 'dynamic' or |
131
|
|
|
* 'php') and the replacement, or FALSE |
132
|
|
|
*/ |
133
|
|
|
public static function getRenderingStrategy($php) |
134
|
|
|
{ |
135
|
|
|
$chunks = explode('$this->at($node);', $php); |
136
|
|
|
$renderings = []; |
137
|
|
|
|
138
|
|
|
// If there is zero or one passthrough, we try string replacements first |
139
|
|
|
if (count($chunks) <= 2) |
140
|
|
|
{ |
141
|
|
|
foreach ($chunks as $k => $chunk) |
142
|
|
|
{ |
143
|
|
|
// Try a static replacement first |
144
|
|
|
$rendering = self::getStaticRendering($chunk); |
145
|
|
|
if ($rendering !== false) |
146
|
|
|
{ |
147
|
|
|
$renderings[$k] = ['static', $rendering]; |
148
|
|
|
continue; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
// If this is the first chunk, we can try a dynamic replacement. This wouldn't work |
152
|
|
|
// for the second chunk because we wouldn't have access to the attribute values |
153
|
|
|
if ($k === 0) |
154
|
|
|
{ |
155
|
|
|
$rendering = self::getDynamicRendering($chunk); |
156
|
|
|
if ($rendering !== false) |
157
|
|
|
{ |
158
|
|
|
$renderings[$k] = ['dynamic', $rendering]; |
159
|
|
|
continue; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
$renderings[$k] = false; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
// If we can completely render a template with string replacements, we return now |
167
|
|
|
if (!in_array(false, $renderings, true)) |
168
|
|
|
{ |
169
|
|
|
return $renderings; |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
// Test whether this template can be rendered with the Quick rendering |
174
|
|
|
$phpRenderings = self::getQuickRendering($php); |
175
|
|
|
if ($phpRenderings === false) |
176
|
|
|
{ |
177
|
|
|
return false; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
// Keep string rendering where possible, use PHP rendering wherever else |
181
|
|
|
foreach ($phpRenderings as $i => $phpRendering) |
182
|
|
|
{ |
183
|
|
|
if (!isset($renderings[$i]) || $renderings[$i] === false || strpos($phpRendering, '$this->attributes[]') !== false) |
184
|
|
|
{ |
185
|
|
|
$renderings[$i] = ['php', $phpRendering]; |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
return $renderings; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Generate the code for rendering a compiled template with the Quick renderer |
194
|
|
|
* |
195
|
|
|
* Parse and record every code path that contains a passthrough. Parse every if-else structure. |
196
|
|
|
* When the whole structure is parsed, there are 3 possible situations: |
197
|
|
|
* - no code path contains a passthrough, in which case we discard the data |
198
|
|
|
* - all the code paths including the mandatory "else" branch contain a passthrough, in which |
199
|
|
|
* case we keep the data |
200
|
|
|
* - only some code paths contain a passthrough, in which case we throw an exception |
201
|
|
|
* |
202
|
|
|
* @param string $php Template compiled for the PHP renderer |
203
|
|
|
* @return array|bool An array containing one or two strings of PHP, or FALSE |
204
|
|
|
*/ |
205
|
|
|
protected static function getQuickRendering($php) |
206
|
|
|
{ |
207
|
|
|
// xsl:apply-templates elements with a select expression are not supported |
208
|
|
|
if (preg_match('(\\$this->at\\((?!\\$node\\);))', $php)) |
209
|
|
|
{ |
210
|
|
|
return false; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
// Tokenize the PHP and add an empty token as terminator |
214
|
|
|
$tokens = token_get_all('<?php ' . $php); |
215
|
|
|
$tokens[] = [0, '']; |
216
|
|
|
|
217
|
|
|
// Remove the first token, which is a T_OPEN_TAG |
218
|
|
|
array_shift($tokens); |
219
|
|
|
$cnt = count($tokens); |
220
|
|
|
|
221
|
|
|
// Prepare the main branch |
222
|
|
|
$branch = [ |
223
|
|
|
// We purposefully use a value that can never match |
224
|
|
|
'braces' => -1, |
225
|
|
|
'branches' => [], |
226
|
|
|
'head' => '', |
227
|
|
|
'passthrough' => 0, |
228
|
|
|
'statement' => '', |
229
|
|
|
'tail' => '' |
230
|
|
|
]; |
231
|
|
|
|
232
|
|
|
$braces = 0; |
233
|
|
|
$i = 0; |
234
|
|
|
do |
235
|
|
|
{ |
236
|
|
|
// Test whether we've reached a passthrough |
237
|
|
|
if ($tokens[$i ][0] === T_VARIABLE |
238
|
|
|
&& $tokens[$i ][1] === '$this' |
239
|
|
|
&& $tokens[$i + 1][0] === T_OBJECT_OPERATOR |
240
|
|
|
&& $tokens[$i + 2][0] === T_STRING |
241
|
|
|
&& $tokens[$i + 2][1] === 'at' |
242
|
|
|
&& $tokens[$i + 3] === '(' |
243
|
|
|
&& $tokens[$i + 4][0] === T_VARIABLE |
244
|
|
|
&& $tokens[$i + 4][1] === '$node' |
245
|
|
|
&& $tokens[$i + 5] === ')' |
246
|
|
|
&& $tokens[$i + 6] === ';') |
247
|
|
|
{ |
248
|
|
|
if (++$branch['passthrough'] > 1) |
249
|
|
|
{ |
250
|
|
|
// Multiple passthroughs are not supported |
251
|
|
|
return false; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
// Skip to the semi-colon |
255
|
|
|
$i += 6; |
256
|
|
|
|
257
|
|
|
continue; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
$key = ($branch['passthrough']) ? 'tail' : 'head'; |
261
|
|
|
$branch[$key] .= (is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; |
262
|
|
|
|
263
|
|
|
if ($tokens[$i] === '{') |
264
|
|
|
{ |
265
|
|
|
++$braces; |
266
|
|
|
continue; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
if ($tokens[$i] === '}') |
270
|
|
|
{ |
271
|
|
|
--$braces; |
272
|
|
|
|
273
|
|
|
if ($branch['braces'] === $braces) |
274
|
|
|
{ |
275
|
|
|
// Remove the last brace from the branch's content |
276
|
|
|
$branch[$key] = substr($branch[$key], 0, -1); |
277
|
|
|
|
278
|
|
|
// Jump back to the parent branch |
279
|
|
|
$branch =& $branch['parent']; |
280
|
|
|
|
281
|
|
|
// Copy the current index to look ahead |
282
|
|
|
$j = $i; |
283
|
|
|
|
284
|
|
|
// Skip whitespace |
285
|
|
|
while ($tokens[++$j][0] === T_WHITESPACE); |
286
|
|
|
|
287
|
|
|
// Test whether this is the last brace of an if-else structure by looking for |
288
|
|
|
// an additional elseif/else case |
289
|
|
|
if ($tokens[$j][0] !== T_ELSEIF |
290
|
|
|
&& $tokens[$j][0] !== T_ELSE) |
291
|
|
|
{ |
292
|
|
|
$passthroughs = self::getBranchesPassthrough($branch['branches']); |
293
|
|
|
|
294
|
|
|
if ($passthroughs === [0]) |
295
|
|
|
{ |
296
|
|
|
// No branch was passthrough, move their PHP source back to this branch |
297
|
|
|
// then discard the data |
298
|
|
|
foreach ($branch['branches'] as $child) |
299
|
|
|
{ |
300
|
|
|
$branch['head'] .= $child['statement'] . '{' . $child['head'] . '}'; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
$branch['branches'] = []; |
304
|
|
|
continue; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
if ($passthroughs === [1]) |
308
|
|
|
{ |
309
|
|
|
// All branches were passthrough, so their parent is passthrough |
310
|
|
|
++$branch['passthrough']; |
311
|
|
|
|
312
|
|
|
continue; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
// Mixed branches (with/out passthrough) are not supported |
316
|
|
|
return false; |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
continue; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
// We don't have to record child branches if we know that current branch is passthrough. |
324
|
|
|
// If a child branch contains a passthrough, it will be treated as a multiple |
325
|
|
|
// passthrough and we will abort |
326
|
|
|
if ($branch['passthrough']) |
327
|
|
|
{ |
328
|
|
|
continue; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
if ($tokens[$i][0] === T_IF |
332
|
|
|
|| $tokens[$i][0] === T_ELSEIF |
333
|
|
|
|| $tokens[$i][0] === T_ELSE) |
334
|
|
|
{ |
335
|
|
|
// Remove the statement from the branch's content |
336
|
|
|
$branch[$key] = substr($branch[$key], 0, -strlen($tokens[$i][1])); |
337
|
|
|
|
338
|
|
|
// Create a new branch |
339
|
|
|
$branch['branches'][] = [ |
340
|
|
|
'braces' => $braces, |
341
|
|
|
'branches' => [], |
342
|
|
|
'head' => '', |
343
|
|
|
'parent' => &$branch, |
344
|
|
|
'passthrough' => 0, |
345
|
|
|
'statement' => '', |
346
|
|
|
'tail' => '' |
347
|
|
|
]; |
348
|
|
|
|
349
|
|
|
// Jump to the new branch |
350
|
|
|
$branch =& $branch['branches'][count($branch['branches']) - 1]; |
351
|
|
|
|
352
|
|
|
// Record the PHP statement |
353
|
|
|
do |
354
|
|
|
{ |
355
|
|
|
$branch['statement'] .= (is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; |
356
|
|
|
} |
357
|
|
|
while ($tokens[++$i] !== '{'); |
358
|
|
|
|
359
|
|
|
// Account for the brace in the statement |
360
|
|
|
++$braces; |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
while (++$i < $cnt); |
364
|
|
|
|
365
|
|
|
list($head, $tail) = self::buildPHP($branch['branches']); |
366
|
|
|
$head = $branch['head'] . $head; |
367
|
|
|
$tail .= $branch['tail']; |
368
|
|
|
|
369
|
|
|
// Convert the PHP renderer source to the format used in the Quick renderer |
370
|
|
|
self::convertPHP($head, $tail, (bool) $branch['passthrough']); |
371
|
|
|
|
372
|
|
|
// Test whether any method call was left unconverted. If so, we cannot render this template |
373
|
|
|
if (preg_match('((?<!-|\\$this)->(?!params\\[))', $head . $tail)) |
374
|
|
|
{ |
375
|
|
|
return false; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
return ($branch['passthrough']) ? [$head, $tail] : [$head]; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Convert the two sides of a compiled template to quick rendering |
383
|
|
|
* |
384
|
|
|
* @param string &$head |
385
|
|
|
* @param string &$tail |
386
|
|
|
* @param bool $passthrough |
387
|
|
|
* @return void |
388
|
|
|
*/ |
389
|
|
|
protected static function convertPHP(&$head, &$tail, $passthrough) |
390
|
|
|
{ |
391
|
|
|
// Test whether the attributes must be saved when rendering the head because they're needed |
392
|
|
|
// when rendering the tail |
393
|
|
|
$saveAttributes = (bool) preg_match('(\\$node->(?:get|has)Attribute)', $tail); |
394
|
|
|
|
395
|
|
|
// Collect the names of all the attributes so that we can initialize them with a null value |
396
|
|
|
// to avoid undefined variable notices. We exclude attributes that seem to be in an if block |
397
|
|
|
// that tests its existence beforehand. This last part is not an accurate process as it |
398
|
|
|
// would be much more expensive to do it accurately but where it fails the only consequence |
399
|
|
|
// is we needlessly add the attribute to the list. There is no difference in functionality |
400
|
|
|
preg_match_all( |
401
|
|
|
"(\\\$node->getAttribute\\('([^']+)'\\))", |
402
|
|
|
preg_replace_callback( |
403
|
|
|
'(if\\(\\$node->hasAttribute\\(([^\\)]+)[^}]+)', |
404
|
|
|
function ($m) |
405
|
|
|
{ |
406
|
|
|
return str_replace('$node->getAttribute(' . $m[1] . ')', '', $m[0]); |
407
|
|
|
}, |
408
|
|
|
$head . $tail |
409
|
|
|
), |
410
|
|
|
$matches |
411
|
|
|
); |
412
|
|
|
$attrNames = array_unique($matches[1]); |
413
|
|
|
|
414
|
|
|
// Replace the source in $head and $tail |
415
|
|
|
self::replacePHP($head); |
416
|
|
|
self::replacePHP($tail); |
417
|
|
|
|
418
|
|
|
if (!$passthrough && strpos($head, '$node->textContent') !== false) |
419
|
|
|
{ |
420
|
|
|
$head = '$textContent=$this->getQuickTextContent($xml);' . str_replace('$node->textContent', '$textContent', $head); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
if (!empty($attrNames)) |
424
|
|
|
{ |
425
|
|
|
ksort($attrNames); |
426
|
|
|
$head = "\$attributes+=['" . implode("'=>null,'", $attrNames) . "'=>null];" . $head; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
if ($saveAttributes) |
430
|
|
|
{ |
431
|
|
|
$head .= '$this->attributes[]=$attributes;'; |
432
|
|
|
$tail = '$attributes=array_pop($this->attributes);' . $tail; |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Replace the PHP code used in a compiled template to be used by the Quick renderer |
438
|
|
|
* |
439
|
|
|
* @param string &$php |
440
|
|
|
* @return void |
441
|
|
|
*/ |
442
|
|
|
protected static function replacePHP(&$php) |
443
|
|
|
{ |
444
|
|
|
if ($php === '') |
445
|
|
|
{ |
446
|
|
|
return; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
if (strpos($php, "\$this->out.='';") !== false) |
450
|
|
|
{ |
451
|
|
|
die($php); |
|
|
|
|
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
$php = str_replace('$this->out', '$html', $php); |
455
|
|
|
|
456
|
|
|
// Expression that matches a $node->getAttribute() call and captures its string argument |
457
|
|
|
$getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)"; |
458
|
|
|
|
459
|
|
|
// An attribute value escaped as ENT_NOQUOTES. We only need to unescape quotes |
460
|
|
|
$php = preg_replace( |
461
|
|
|
'(htmlspecialchars\\(' . $getAttribute . ',' . ENT_NOQUOTES . '\\))', |
462
|
|
|
"str_replace('"','\"',\$attributes[\$1])", |
463
|
|
|
$php |
464
|
|
|
); |
465
|
|
|
|
466
|
|
|
// An attribute value escaped as ENT_COMPAT can be used as-is |
467
|
|
|
$php = preg_replace( |
468
|
|
|
'(htmlspecialchars\\(' . $getAttribute . ',' . ENT_COMPAT . '\\))', |
469
|
|
|
'$attributes[$1]', |
470
|
|
|
$php |
471
|
|
|
); |
472
|
|
|
|
473
|
|
|
// Character replacement can be performed directly on the escaped value provided that it is |
474
|
|
|
// then escaped as ENT_COMPAT and that replacements do not interfere with the escaping of |
475
|
|
|
// the characters &<>" or their representation &<>" |
476
|
|
|
$php = preg_replace( |
477
|
|
|
'(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . ENT_COMPAT . '\\))', |
478
|
|
|
'strtr($attributes[$1],$2,$3)', |
479
|
|
|
$php |
480
|
|
|
); |
481
|
|
|
|
482
|
|
|
// A comparison between two attributes. No need to unescape |
483
|
|
|
$php = preg_replace( |
484
|
|
|
'(' . $getAttribute . '(!?=+)' . $getAttribute . ')', |
485
|
|
|
'$attributes[$1]$2$attributes[$3]', |
486
|
|
|
$php |
487
|
|
|
); |
488
|
|
|
|
489
|
|
|
// A comparison between an attribute and a literal string. Rather than unescape the |
490
|
|
|
// attribute value, we escape the literal. This applies to comparisons using XPath's |
491
|
|
|
// contains() as well (translated to PHP's strpos()) |
492
|
|
|
$php = preg_replace_callback( |
493
|
|
|
'(' . $getAttribute . "===('.*?(?<!\\\\)(?:\\\\\\\\)*'))s", |
494
|
|
|
function ($m) |
495
|
|
|
{ |
496
|
|
|
return '$attributes[' . $m[1] . ']===' . htmlspecialchars($m[2], ENT_COMPAT); |
497
|
|
|
}, |
498
|
|
|
$php |
499
|
|
|
); |
500
|
|
|
$php = preg_replace_callback( |
501
|
|
|
"(('.*?(?<!\\\\)(?:\\\\\\\\)*')===" . $getAttribute . ')s', |
502
|
|
|
function ($m) |
503
|
|
|
{ |
504
|
|
|
return htmlspecialchars($m[1], ENT_COMPAT) . '===$attributes[' . $m[2] . ']'; |
505
|
|
|
}, |
506
|
|
|
$php |
507
|
|
|
); |
508
|
|
|
$php = preg_replace_callback( |
509
|
|
|
'(strpos\\(' . $getAttribute . ",('.*?(?<!\\\\)(?:\\\\\\\\)*')\\)([!=]==(?:0|false)))s", |
510
|
|
|
function ($m) |
511
|
|
|
{ |
512
|
|
|
return 'strpos($attributes[' . $m[1] . "]," . htmlspecialchars($m[2], ENT_COMPAT) . ')' . $m[3]; |
513
|
|
|
}, |
514
|
|
|
$php |
515
|
|
|
); |
516
|
|
|
$php = preg_replace_callback( |
517
|
|
|
"(strpos\\(('.*?(?<!\\\\)(?:\\\\\\\\)*')," . $getAttribute . '\\)([!=]==(?:0|false)))s', |
518
|
|
|
function ($m) |
519
|
|
|
{ |
520
|
|
|
return 'strpos(' . htmlspecialchars($m[1], ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3]; |
521
|
|
|
}, |
522
|
|
|
$php |
523
|
|
|
); |
524
|
|
|
|
525
|
|
|
// An attribute value used in an arithmetic comparison or operation does not need to be |
526
|
|
|
// unescaped. The same applies to empty() and isset() |
527
|
|
|
$php = preg_replace( |
528
|
|
|
'(' . $getAttribute . '(?=(?:==|[-+*])\\d+))', |
529
|
|
|
'$attributes[$1]', |
530
|
|
|
$php |
531
|
|
|
); |
532
|
|
|
$php = preg_replace( |
533
|
|
|
'((?<!\\w)(\\d+(?:==|[-+*]))' . $getAttribute . ')', |
534
|
|
|
'$1$attributes[$2]', |
535
|
|
|
$php |
536
|
|
|
); |
537
|
|
|
$php = preg_replace( |
538
|
|
|
"(empty\\(\\\$node->getAttribute\\(('[^']+')\\)\\))", |
539
|
|
|
'empty($attributes[$1])', |
540
|
|
|
$php |
541
|
|
|
); |
542
|
|
|
$php = preg_replace( |
543
|
|
|
"(\\\$node->hasAttribute\\(('[^']+')\\))", |
544
|
|
|
'isset($attributes[$1])', |
545
|
|
|
$php |
546
|
|
|
); |
547
|
|
|
$php = str_replace( |
548
|
|
|
'($node->attributes->length)', |
549
|
|
|
'($this->hasNonNullValues($attributes))', |
550
|
|
|
$php |
551
|
|
|
); |
552
|
|
|
|
553
|
|
|
// In all other situations, unescape the attribute value before use |
554
|
|
|
$php = preg_replace( |
555
|
|
|
"(\\\$node->getAttribute\\(('[^']+')\\))", |
556
|
|
|
'htmlspecialchars_decode($attributes[$1])', |
557
|
|
|
$php |
558
|
|
|
); |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
/** |
562
|
|
|
* Build the source for the two sides of a templates based on the structure extracted from its |
563
|
|
|
* original source |
564
|
|
|
* |
565
|
|
|
* @param array $branches |
566
|
|
|
* @return string[] |
567
|
|
|
*/ |
568
|
|
|
protected static function buildPHP(array $branches) |
569
|
|
|
{ |
570
|
|
|
$return = ['', '']; |
571
|
|
|
foreach ($branches as $branch) |
572
|
|
|
{ |
573
|
|
|
$return[0] .= $branch['statement'] . '{' . $branch['head']; |
574
|
|
|
$return[1] .= $branch['statement'] . '{'; |
575
|
|
|
|
576
|
|
|
if ($branch['branches']) |
577
|
|
|
{ |
578
|
|
|
list($head, $tail) = self::buildPHP($branch['branches']); |
579
|
|
|
|
580
|
|
|
$return[0] .= $head; |
581
|
|
|
$return[1] .= $tail; |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
$return[0] .= '}'; |
585
|
|
|
$return[1] .= $branch['tail'] . '}'; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
return $return; |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
/** |
592
|
|
|
* Get the unique values for the "passthrough" key of given branches |
593
|
|
|
* |
594
|
|
|
* @param array $branches |
595
|
|
|
* @return integer[] |
596
|
|
|
*/ |
597
|
|
|
protected static function getBranchesPassthrough(array $branches) |
598
|
|
|
{ |
599
|
|
|
$values = []; |
600
|
|
|
foreach ($branches as $branch) |
601
|
|
|
{ |
602
|
|
|
$values[] = $branch['passthrough']; |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
// If the last branch isn't an "else", we act as if there was an additional branch with no |
606
|
|
|
// passthrough |
607
|
|
|
if ($branch['statement'] !== 'else') |
608
|
|
|
{ |
609
|
|
|
$values[] = 0; |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
return array_unique($values); |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
/** |
616
|
|
|
* Get a string suitable as a preg_replace() replacement for given PHP code |
617
|
|
|
* |
618
|
|
|
* @param string $php Original code |
619
|
|
|
* @return array|bool Array of [regexp, replacement] if possible, or FALSE otherwise |
620
|
|
|
*/ |
621
|
|
|
protected static function getDynamicRendering($php) |
622
|
|
|
{ |
623
|
|
|
$rendering = ''; |
624
|
|
|
|
625
|
|
|
$literal = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')"; |
626
|
|
|
$attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))"; |
627
|
|
|
$value = "(?<value>$literal|$attribute)"; |
628
|
|
|
$output = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)"; |
629
|
|
|
|
630
|
|
|
$copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})"; |
631
|
|
|
|
632
|
|
|
$regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)'; |
633
|
|
|
|
634
|
|
|
if (!preg_match($regexp, $php, $m)) |
635
|
|
|
{ |
636
|
|
|
return false; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
// Attributes that are copied in the replacement |
640
|
|
|
$copiedAttributes = []; |
641
|
|
|
|
642
|
|
|
// Attributes whose value is used in the replacement |
643
|
|
|
$usedAttributes = []; |
644
|
|
|
|
645
|
|
|
$regexp = '(' . $output . '|' . $copyOfAttribute . ')A'; |
646
|
|
|
$offset = 0; |
647
|
|
|
while (preg_match($regexp, $php, $m, 0, $offset)) |
648
|
|
|
{ |
649
|
|
|
// Test whether it's normal output or a copy of attribute |
650
|
|
|
if ($m['output']) |
651
|
|
|
{ |
652
|
|
|
// 12 === strlen('$this->out.=') |
653
|
|
|
$offset += 12; |
654
|
|
|
|
655
|
|
|
while (preg_match('(' . $value . ')A', $php, $m, 0, $offset)) |
656
|
|
|
{ |
657
|
|
|
// Test whether it's a literal or an attribute value |
658
|
|
|
if ($m['literal']) |
659
|
|
|
{ |
660
|
|
|
// Unescape the literal |
661
|
|
|
$str = stripslashes(substr($m[0], 1, -1)); |
662
|
|
|
|
663
|
|
|
// Escape special characters |
664
|
|
|
$rendering .= preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str); |
665
|
|
|
} |
666
|
|
|
else |
667
|
|
|
{ |
668
|
|
|
$attrName = end($m); |
669
|
|
|
|
670
|
|
|
// Generate a unique ID for this attribute name, we'll use it as a |
671
|
|
|
// placeholder until we have the full list of captures and we can replace it |
672
|
|
|
// with the capture number |
673
|
|
|
if (!isset($usedAttributes[$attrName])) |
674
|
|
|
{ |
675
|
|
|
$usedAttributes[$attrName] = uniqid($attrName, true); |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
$rendering .= $usedAttributes[$attrName]; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
// Skip the match plus the next . or ; |
682
|
|
|
$offset += 1 + strlen($m[0]); |
683
|
|
|
} |
684
|
|
|
} |
685
|
|
|
else |
686
|
|
|
{ |
687
|
|
|
$attrName = end($m); |
688
|
|
|
|
689
|
|
|
if (!isset($copiedAttributes[$attrName])) |
690
|
|
|
{ |
691
|
|
|
$copiedAttributes[$attrName] = uniqid($attrName, true); |
692
|
|
|
} |
693
|
|
|
|
694
|
|
|
$rendering .= $copiedAttributes[$attrName]; |
695
|
|
|
$offset += strlen($m[0]); |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
// Gather the names of the attributes used in the replacement either by copy or by value |
700
|
|
|
$attrNames = array_keys($copiedAttributes + $usedAttributes); |
701
|
|
|
|
702
|
|
|
// Sort them alphabetically |
703
|
|
|
sort($attrNames); |
704
|
|
|
|
705
|
|
|
// Keep a copy of the attribute names to be used in the fillter subpattern |
706
|
|
|
$remainingAttributes = array_combine($attrNames, $attrNames); |
707
|
|
|
|
708
|
|
|
// Prepare the final regexp |
709
|
|
|
$regexp = '(^[^ ]+'; |
710
|
|
|
$index = 0; |
711
|
|
|
foreach ($attrNames as $attrName) |
712
|
|
|
{ |
713
|
|
|
// Add a subpattern that matches (and skips) any attribute definition that is not one of |
714
|
|
|
// the remaining attributes we're trying to match |
715
|
|
|
$regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*'; |
716
|
|
|
unset($remainingAttributes[$attrName]); |
717
|
|
|
|
718
|
|
|
$regexp .= '('; |
719
|
|
|
|
720
|
|
|
if (isset($copiedAttributes[$attrName])) |
721
|
|
|
{ |
722
|
|
|
self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index); |
723
|
|
|
} |
724
|
|
|
else |
725
|
|
|
{ |
726
|
|
|
$regexp .= '?>'; |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
$regexp .= ' ' . $attrName . '="'; |
730
|
|
|
|
731
|
|
|
if (isset($usedAttributes[$attrName])) |
732
|
|
|
{ |
733
|
|
|
$regexp .= '('; |
734
|
|
|
|
735
|
|
|
self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index); |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
$regexp .= '[^"]*'; |
739
|
|
|
|
740
|
|
|
if (isset($usedAttributes[$attrName])) |
741
|
|
|
{ |
742
|
|
|
$regexp .= ')'; |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
$regexp .= '")?'; |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
$regexp .= '.*)s'; |
749
|
|
|
|
750
|
|
|
return [$regexp, $rendering]; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* Get a string suitable as a str_replace() replacement for given PHP code |
755
|
|
|
* |
756
|
|
|
* @param string $php Original code |
757
|
|
|
* @return bool|string Static replacement if possible, or FALSE otherwise |
758
|
|
|
*/ |
759
|
|
|
protected static function getStaticRendering($php) |
760
|
|
|
{ |
761
|
|
|
if ($php === '') |
762
|
|
|
{ |
763
|
|
|
return ''; |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
$regexp = "(^\\\$this->out\.='((?>[^'\\\\]+|\\\\['\\\\])*)';\$)"; |
767
|
|
|
|
768
|
|
|
if (!preg_match($regexp, $php, $m)) |
769
|
|
|
{ |
770
|
|
|
return false; |
771
|
|
|
} |
772
|
|
|
|
773
|
|
|
return stripslashes($m[1]); |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
/** |
777
|
|
|
* Replace all instances of a uniqid with a PCRE replacement in a string |
778
|
|
|
* |
779
|
|
|
* @param string &$str PCRE replacement |
780
|
|
|
* @param string $uniqid Unique ID |
781
|
|
|
* @param integer $index Capture index |
782
|
|
|
* @return void |
783
|
|
|
*/ |
784
|
|
|
protected static function replacePlaceholder(&$str, $uniqid, $index) |
785
|
|
|
{ |
786
|
|
|
$str = preg_replace_callback( |
787
|
|
|
'(' . preg_quote($uniqid) . '(.))', |
788
|
|
|
function ($m) use ($index) |
789
|
|
|
{ |
790
|
|
|
// Replace with $1 where unambiguous and ${1} otherwise |
791
|
|
|
if (is_numeric($m[1])) |
792
|
|
|
{ |
793
|
|
|
return '${' . $index . '}' . $m[1]; |
794
|
|
|
} |
795
|
|
|
else |
796
|
|
|
{ |
797
|
|
|
return '$' . $index . $m[1]; |
798
|
|
|
} |
799
|
|
|
}, |
800
|
|
|
$str |
801
|
|
|
); |
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
/** |
805
|
|
|
* Generate a series of conditionals |
806
|
|
|
* |
807
|
|
|
* @param string $expr Expression tested for equality |
808
|
|
|
* @param array $statements List of PHP statements |
809
|
|
|
* @return string |
810
|
|
|
*/ |
811
|
|
|
public static function generateConditionals($expr, array $statements) |
812
|
|
|
{ |
813
|
|
|
$keys = array_keys($statements); |
814
|
|
|
$cnt = count($statements); |
815
|
|
|
$min = (int) $keys[0]; |
816
|
|
|
$max = (int) $keys[$cnt - 1]; |
817
|
|
|
|
818
|
|
|
if ($cnt <= 4) |
819
|
|
|
{ |
820
|
|
|
if ($cnt === 1) |
821
|
|
|
{ |
822
|
|
|
return end($statements); |
823
|
|
|
} |
824
|
|
|
|
825
|
|
|
$php = ''; |
826
|
|
|
$k = $min; |
827
|
|
|
do |
828
|
|
|
{ |
829
|
|
|
$php .= 'if(' . $expr . '===' . $k . '){' . $statements[$k] . '}else'; |
830
|
|
|
} |
831
|
|
|
while (++$k < $max); |
832
|
|
|
|
833
|
|
|
$php .= '{' . $statements[$max] . '}'; |
834
|
|
|
|
835
|
|
|
return $php; |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
$cutoff = ceil($cnt / 2); |
839
|
|
|
$chunks = array_chunk($statements, $cutoff, true); |
840
|
|
|
|
841
|
|
|
return 'if(' . $expr . '<' . key($chunks[1]) . '){' . self::generateConditionals($expr, array_slice($statements, 0, $cutoff, true)) . '}else' . self::generateConditionals($expr, array_slice($statements, $cutoff, null, true)); |
842
|
|
|
} |
843
|
|
|
|
844
|
|
|
/** |
845
|
|
|
* Generate a branch table (with its source) for an array of PHP statements |
846
|
|
|
* |
847
|
|
|
* @param string $expr PHP expression used to determine the branch |
848
|
|
|
* @param array $statements Map of [value => statement] |
849
|
|
|
* @return array Two elements: first is the branch table, second is the source |
850
|
|
|
*/ |
851
|
|
|
public static function generateBranchTable($expr, array $statements) |
852
|
|
|
{ |
853
|
|
|
// Map of [statement => id] |
854
|
|
|
$branchTable = []; |
855
|
|
|
|
856
|
|
|
// Map of [value => id] |
857
|
|
|
$branchIds = []; |
858
|
|
|
|
859
|
|
|
// Sort the PHP statements by the value used to identify their branch |
860
|
|
|
ksort($statements); |
861
|
|
|
|
862
|
|
|
foreach ($statements as $value => $statement) |
863
|
|
|
{ |
864
|
|
|
if (!isset($branchIds[$statement])) |
865
|
|
|
{ |
866
|
|
|
$branchIds[$statement] = count($branchIds); |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
$branchTable[$value] = $branchIds[$statement]; |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
return [$branchTable, self::generateConditionals($expr, array_keys($branchIds))]; |
873
|
|
|
} |
874
|
|
|
} |
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exit
expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.