PluginProbe ʕ •ᴥ•ʔ
JetFormBuilder — Dynamic Blocks Form Builder / trunk
JetFormBuilder — Dynamic Blocks Form Builder vtrunk
3.6.4.1 3.6.4 3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.1.0 2.1.1 2.1.10 2.1.11 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 3.0.0 3.0.0.1 3.0.0.2 3.0.0.3 3.0.1 3.0.1.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.0.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.3.1 3.3.4 3.3.4.1 3.3.4.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.5.1 3.4.5.2 3.4.6 3.4.7 3.4.7.1 3.5.0 3.5.1 3.5.1.1 3.5.1.2 3.5.2 3.5.2.1 3.5.3 3.5.4 3.5.5 3.5.6 3.5.6.1 3.5.6.2 3.5.6.3 3.6.0
jetformbuilder / modules / gateways / trusted-price-resolver-expression-parser.php
jetformbuilder / modules / gateways Last commit date
actions-abstract 2 years ago assets 23 hours ago db-models 2 years ago export 2 years ago legacy 2 years ago meta-boxes 2 years ago pages 2 years ago paypal 23 hours ago query-views 2 years ago rest-api 8 months ago scenarios-abstract 2 years ago tab-handlers 2 years ago table-views 23 hours ago base-gateway-action.php 1 year ago base-gateway.php 3 months ago base-scenario-gateway.php 2 years ago gateways-editor-data.php 23 hours ago legacy-base-gateway.php 23 hours ago migrate-legacy-data.php 2 years ago module.php 23 hours ago scenario-item.php 2 years ago secure-price-notice.php 23 hours ago trusted-price-resolver-expression-parser.php 23 hours ago trusted-price-resolver.php 23 hours ago
trusted-price-resolver-expression-parser.php
580 lines
1 <?php
2
3 namespace JFB_Modules\Gateways;
4
5 use Jet_Form_Builder\Exceptions\Gateway_Exception;
6
7 // If this file is called directly, abort.
8 if ( ! defined( 'WPINC' ) ) {
9 die;
10 }
11
12 class Trusted_Price_Resolver_Expression_Parser {
13
14 /**
15 * @var string
16 */
17 private $expression;
18
19 /**
20 * @var int
21 */
22 private $length;
23
24 /**
25 * @var int
26 */
27 private $position = 0;
28
29 public function __construct( string $expression ) {
30 $this->expression = preg_replace( '/\s+/', '', $expression );
31 $this->length = strlen( $this->expression );
32 }
33
34 /**
35 * @throws Gateway_Exception
36 */
37 public function parse(): float {
38 if ( '' === $this->expression ) {
39 throw new Gateway_Exception( 'Calculated price expression is empty.' );
40 }
41
42 $tree = $this->parse_conditional();
43
44 if ( $this->position < $this->length ) {
45 throw new Gateway_Exception( 'Calculated price expression contains invalid syntax.' );
46 }
47
48 $result = $this->evaluate_node( $tree );
49
50 if ( ! is_finite( $result ) ) {
51 throw new Gateway_Exception( 'Calculated price expression produced a non-finite value.' );
52 }
53
54 return $result;
55 }
56
57 /**
58 * @throws Gateway_Exception
59 */
60 private function parse_conditional(): array {
61 $condition = $this->parse_logical_or();
62
63 if ( ! $this->match( '?' ) ) {
64 return $condition;
65 }
66
67 $consequent = $this->parse_conditional();
68
69 if ( ! $this->match( ':' ) ) {
70 throw new Gateway_Exception( 'Calculated price expression contains an invalid conditional.' );
71 }
72
73 return array( 'conditional', $condition, $consequent, $this->parse_conditional() );
74 }
75
76 /**
77 * @throws Gateway_Exception
78 */
79 private function parse_logical_or(): array {
80 $node = $this->parse_logical_and();
81
82 while ( $this->match( '||' ) ) {
83 $node = array( 'binary', '||', $node, $this->parse_logical_and() );
84 }
85
86 return $node;
87 }
88
89 /**
90 * @throws Gateway_Exception
91 */
92 private function parse_logical_and(): array {
93 $node = $this->parse_bitwise_xor();
94
95 while ( $this->match( '&&' ) ) {
96 $node = array( 'binary', '&&', $node, $this->parse_bitwise_xor() );
97 }
98
99 return $node;
100 }
101
102 /**
103 * Match JavaScript operator precedence: equality binds before bitwise XOR,
104 * while logical AND binds after it.
105 *
106 * @throws Gateway_Exception
107 */
108 private function parse_bitwise_xor(): array {
109 $node = $this->parse_equality();
110
111 while ( $this->match( '^' ) ) {
112 $node = array( 'binary', '^', $node, $this->parse_equality() );
113 }
114
115 return $node;
116 }
117
118 /**
119 * @throws Gateway_Exception
120 */
121 private function parse_equality(): array {
122 $node = $this->parse_comparison();
123
124 while ( true ) {
125 $operator = $this->match_any( array( '===', '!==', '==', '!=' ) );
126
127 if ( false === $operator ) {
128 return $node;
129 }
130
131 $node = array( 'binary', $operator, $node, $this->parse_comparison() );
132 }
133 }
134
135 /**
136 * @throws Gateway_Exception
137 */
138 private function parse_comparison(): array {
139 $node = $this->parse_expression();
140
141 while ( true ) {
142 $operator = $this->match_any( array( '<=', '>=', '<', '>' ) );
143
144 if ( false === $operator ) {
145 return $node;
146 }
147
148 $node = array( 'binary', $operator, $node, $this->parse_expression() );
149 }
150 }
151
152 /**
153 * @throws Gateway_Exception
154 */
155 private function parse_expression(): array {
156 $node = $this->parse_term();
157
158 while ( true ) {
159 $operator = $this->match_any( array( '+', '-' ) );
160
161 if ( false === $operator ) {
162 return $node;
163 }
164
165 $node = array( 'binary', $operator, $node, $this->parse_term() );
166 }
167 }
168
169 /**
170 * @throws Gateway_Exception
171 */
172 private function parse_term(): array {
173 $node = $this->parse_power();
174
175 while ( true ) {
176 if ( '*' === $this->peek() && '*' === $this->peek( 1 ) ) {
177 return $node;
178 }
179
180 $operator = $this->match_any( array( '*', '/', '%' ) );
181
182 if ( false === $operator ) {
183 return $node;
184 }
185
186 $node = array( 'binary', $operator, $node, $this->parse_power() );
187 }
188 }
189
190 /**
191 * @throws Gateway_Exception
192 */
193 private function parse_power(): array {
194 $node = $this->parse_factor();
195
196 if ( $this->match( '**' ) ) {
197 $node = array( 'binary', '**', $node, $this->parse_power() );
198 }
199
200 return $node;
201 }
202
203 /**
204 * @throws Gateway_Exception
205 */
206 private function parse_factor(): array {
207 $operator = $this->peek();
208
209 if ( '+' === $operator || '-' === $operator || '!' === $operator ) {
210 ++$this->position;
211
212 return array( 'unary', $operator, $this->parse_factor() );
213 }
214
215 if ( '(' === $operator ) {
216 ++$this->position;
217 $node = $this->parse_conditional();
218
219 if ( ')' !== $this->peek() ) {
220 throw new Gateway_Exception( 'Calculated price expression contains unbalanced parentheses.' );
221 }
222
223 ++$this->position;
224
225 return $node;
226 }
227
228 if ( false !== $operator && preg_match( '/[a-zA-Z_]/', $operator ) ) {
229 return $this->parse_identifier();
230 }
231
232 return $this->parse_number();
233 }
234
235 /**
236 * @throws Gateway_Exception
237 */
238 private function parse_number(): array {
239 $remaining = substr( $this->expression, $this->position );
240
241 if ( ! preg_match( '/^(?:(?:[0-9]+(?:\.[0-9]*)?)|(?:\.[0-9]+))(?:[eE][+-]?[0-9]+)?/', $remaining, $matches ) ) {
242 throw new Gateway_Exception( 'Calculated price expression contains an invalid token.' );
243 }
244
245 $number = $matches[0];
246 $this->position += strlen( $number );
247
248 if ( ! is_numeric( $number ) ) {
249 throw new Gateway_Exception( 'Calculated price expression contains an invalid number.' );
250 }
251
252 return array( 'number', (float) $number );
253 }
254
255 /**
256 * @throws Gateway_Exception
257 */
258 private function parse_identifier(): array {
259 $remaining = substr( $this->expression, $this->position );
260
261 if ( ! preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?/', $remaining, $matches ) ) {
262 throw new Gateway_Exception( 'Calculated price expression contains an invalid identifier.' );
263 }
264
265 $name = $matches[0];
266 $this->position += strlen( $name );
267
268 if ( ! $this->match( '(' ) ) {
269 return array( 'constant', $name );
270 }
271
272 $arguments = array();
273
274 if ( ! $this->match( ')' ) ) {
275 do {
276 $arguments[] = $this->parse_conditional();
277 } while ( $this->match( ',' ) );
278
279 if ( ! $this->match( ')' ) ) {
280 throw new Gateway_Exception( 'Calculated price expression contains an invalid function call.' );
281 }
282 }
283
284 return array( 'call', $name, $arguments );
285 }
286
287 /**
288 * @throws Gateway_Exception
289 */
290 private function evaluate_node( array $node ): float {
291 switch ( $node[0] ) {
292 case 'number':
293 return $node[1];
294
295 case 'constant':
296 return $this->evaluate_constant( $node[1] );
297
298 case 'call':
299 return $this->evaluate_math_call( $node[1], $node[2] );
300
301 case 'unary':
302 $value = $this->evaluate_node( $node[2] );
303
304 if ( '!' === $node[1] ) {
305 return $this->is_truthy( $value ) ? 0.0 : 1.0;
306 }
307
308 return '-' === $node[1] ? -1 * $value : $value;
309
310 case 'conditional':
311 return $this->is_truthy( $this->evaluate_node( $node[1] ) )
312 ? $this->evaluate_node( $node[2] )
313 : $this->evaluate_node( $node[3] );
314
315 case 'binary':
316 return $this->evaluate_binary( $node[1], $node[2], $node[3] );
317 }
318
319 throw new Gateway_Exception( 'Calculated price expression contains an invalid node.' );
320 }
321
322 /**
323 * @throws Gateway_Exception
324 */
325 private function evaluate_binary( string $operator, array $left_node, array $right_node ): float {
326 $left = $this->evaluate_node( $left_node );
327
328 if ( '&&' === $operator ) {
329 return $this->is_truthy( $left ) ? $this->evaluate_node( $right_node ) : $left;
330 }
331
332 if ( '||' === $operator ) {
333 return $this->is_truthy( $left ) ? $left : $this->evaluate_node( $right_node );
334 }
335
336 $right = $this->evaluate_node( $right_node );
337
338 switch ( $operator ) {
339 case '+':
340 return $left + $right;
341
342 case '-':
343 return $left - $right;
344
345 case '*':
346 return $left * $right;
347
348 case '/':
349 if ( 0.0 === $right ) {
350 throw new Gateway_Exception( 'Calculated price expression attempted division by zero.' );
351 }
352
353 return $left / $right;
354
355 case '%':
356 if ( 0.0 === $right ) {
357 throw new Gateway_Exception( 'Calculated price expression attempted modulo by zero.' );
358 }
359
360 return fmod( $left, $right );
361
362 case '**':
363 return $left ** $right;
364
365 case '^':
366 return (float) ( $this->to_int32( $left ) ^ $this->to_int32( $right ) );
367
368 case '<':
369 return $left < $right ? 1.0 : 0.0;
370
371 case '<=':
372 return $left <= $right ? 1.0 : 0.0;
373
374 case '>':
375 return $left > $right ? 1.0 : 0.0;
376
377 case '>=':
378 return $left >= $right ? 1.0 : 0.0;
379
380 case '==':
381 case '===':
382 return $left === $right ? 1.0 : 0.0;
383
384 case '!=':
385 case '!==':
386 return $left !== $right ? 1.0 : 0.0;
387 }
388
389 throw new Gateway_Exception( 'Calculated price expression contains an unsupported operator.' );
390 }
391
392 /**
393 * Reproduce JavaScript ToInt32 conversion used by bitwise operators.
394 */
395 private function to_int32( float $value ): int {
396 if ( ! is_finite( $value ) || 0.0 === $value ) {
397 return 0;
398 }
399
400 $integer = $value < 0 ? ceil( $value ) : floor( $value );
401 $uint32 = fmod( $integer, 4294967296.0 );
402
403 if ( $uint32 < 0 ) {
404 $uint32 += 4294967296.0;
405 }
406
407 if ( $uint32 >= 2147483648.0 ) {
408 $uint32 -= 4294967296.0;
409 }
410
411 return (int) $uint32;
412 }
413
414 /**
415 * @throws Gateway_Exception
416 */
417 private function evaluate_constant( string $name ): float {
418 $constants = array(
419 'true' => 1.0,
420 'false' => 0.0,
421 'Math.E' => M_E,
422 'Math.LN2' => M_LN2,
423 'Math.LN10' => M_LN10,
424 'Math.LOG2E' => M_LOG2E,
425 'Math.LOG10E' => M_LOG10E,
426 'Math.PI' => M_PI,
427 'Math.SQRT1_2' => M_SQRT1_2,
428 'Math.SQRT2' => M_SQRT2,
429 );
430
431 if ( ! array_key_exists( $name, $constants ) ) {
432 throw new Gateway_Exception( 'Calculated price expression contains an unsupported identifier.' );
433 }
434
435 return (float) $constants[ $name ];
436 }
437
438 /**
439 * @throws Gateway_Exception
440 */
441 private function evaluate_math_call( string $name, array $argument_nodes ): float {
442 $arguments = array_map(
443 function ( $node ) {
444 return $this->evaluate_node( $node );
445 },
446 $argument_nodes
447 );
448
449 switch ( $name ) {
450 case 'Math.abs':
451 $this->assert_argument_count( $name, $arguments, 1 );
452 $result = abs( $arguments[0] );
453 break;
454
455 case 'Math.ceil':
456 $this->assert_argument_count( $name, $arguments, 1 );
457 $result = ceil( $arguments[0] );
458 break;
459
460 case 'Math.floor':
461 $this->assert_argument_count( $name, $arguments, 1 );
462 $result = floor( $arguments[0] );
463 break;
464
465 case 'Math.round':
466 $this->assert_argument_count( $name, $arguments, 1 );
467 $result = $this->js_math_round( $arguments[0] );
468 break;
469
470 case 'Math.trunc':
471 $this->assert_argument_count( $name, $arguments, 1 );
472 $result = $arguments[0] < 0 ? ceil( $arguments[0] ) : floor( $arguments[0] );
473 break;
474
475 case 'Math.sign':
476 $this->assert_argument_count( $name, $arguments, 1 );
477 $result = $arguments[0] <=> 0;
478 break;
479
480 case 'Math.sqrt':
481 $this->assert_argument_count( $name, $arguments, 1 );
482 $result = sqrt( $arguments[0] );
483 break;
484
485 case 'Math.pow':
486 $this->assert_argument_count( $name, $arguments, 2 );
487 $result = $arguments[0] ** $arguments[1];
488 break;
489
490 case 'Math.min':
491 $this->assert_argument_count( $name, $arguments, 1, PHP_INT_MAX );
492 $result = min( $arguments );
493 break;
494
495 case 'Math.max':
496 $this->assert_argument_count( $name, $arguments, 1, PHP_INT_MAX );
497 $result = max( $arguments );
498 break;
499
500 default:
501 throw new Gateway_Exception( 'Calculated price expression contains an unsupported function.' );
502 }
503
504 if ( ! is_finite( (float) $result ) ) {
505 throw new Gateway_Exception( 'Calculated price expression produced a non-finite value.' );
506 }
507
508 return (float) $result;
509 }
510
511 /**
512 * Reproduce JavaScript Math.round: round half toward +Infinity.
513 *
514 * Calling floor( $value + 0.5 ) diverges for values just below .5 (e.g.
515 * 0.49999999999999994), where forming $value + 0.5 rounds up to 1.0 and
516 * over-rounds. Comparing the fractional part against 0.5 avoids that.
517 */
518 private function js_math_round( float $value ): float {
519 $floor = floor( $value );
520 $fraction = $value - $floor;
521
522 return $fraction >= 0.5 ? $floor + 1.0 : $floor;
523 }
524
525 /**
526 * @throws Gateway_Exception
527 */
528 private function assert_argument_count(
529 string $name,
530 array $arguments,
531 int $minimum,
532 ?int $maximum = null
533 ): void {
534 $maximum = null === $maximum ? $minimum : $maximum;
535 $count = count( $arguments );
536
537 if ( $count < $minimum || $count > $maximum ) {
538 throw new Gateway_Exception(
539 sprintf(
540 'The "%1$s" function has an invalid number of arguments.',
541 esc_html( $name )
542 )
543 );
544 }
545 }
546
547 private function is_truthy( float $value ): bool {
548 return 0.0 !== $value && ! is_nan( $value );
549 }
550
551 private function match( string $token ): bool {
552 if ( substr( $this->expression, $this->position, strlen( $token ) ) !== $token ) {
553 return false;
554 }
555
556 $this->position += strlen( $token );
557
558 return true;
559 }
560
561 /**
562 * @return false|string
563 */
564 private function match_any( array $tokens ) {
565 foreach ( $tokens as $token ) {
566 if ( $this->match( $token ) ) {
567 return $token;
568 }
569 }
570
571 return false;
572 }
573
574 private function peek( int $offset = 0 ) {
575 $position = $this->position + $offset;
576
577 return $position < $this->length ? $this->expression[ $position ] : false;
578 }
579 }
580