PluginProbe ʕ •ᴥ•ʔ
JetFormBuilder — Dynamic Blocks Form Builder / 3.6.4.2
JetFormBuilder — Dynamic Blocks Form Builder v3.6.4.2
3.6.5 3.6.4.2 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 1 week ago db-models 2 years ago export 2 years ago legacy 2 years ago meta-boxes 2 years ago pages 2 years ago paypal 2 weeks ago query-views 2 years ago rest-api 9 months ago scenarios-abstract 2 years ago tab-handlers 2 years ago table-views 2 weeks 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 2 weeks ago legacy-base-gateway.php 2 weeks ago migrate-legacy-data.php 2 years ago module.php 2 weeks ago scenario-item.php 2 years ago secure-price-notice.php 1 week ago trusted-price-resolver-expression-parser.php 1 week ago trusted-price-resolver.php 2 weeks ago
trusted-price-resolver-expression-parser.php
668 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 $result = $this->evaluate_node( $this->parse_tree() );
39
40 if ( ! is_finite( $result ) ) {
41 throw new Gateway_Exception( 'Calculated price expression produced a non-finite value.' );
42 }
43
44 return $result;
45 }
46
47 /**
48 * Validate the expression grammar without evaluating its runtime result.
49 *
50 * @throws Gateway_Exception
51 */
52 public function validate(): void {
53 $this->validate_node( $this->parse_tree() );
54 }
55
56 /**
57 * @throws Gateway_Exception
58 */
59 private function parse_tree(): array {
60 if ( '' === $this->expression ) {
61 throw new Gateway_Exception( 'Calculated price expression is empty.' );
62 }
63
64 $this->position = 0;
65
66 $tree = $this->parse_conditional();
67
68 if ( $this->position < $this->length ) {
69 throw new Gateway_Exception( 'Calculated price expression contains invalid syntax.' );
70 }
71
72 return $tree;
73 }
74
75 /**
76 * @throws Gateway_Exception
77 */
78 private function parse_conditional(): array {
79 $condition = $this->parse_logical_or();
80
81 if ( ! $this->match( '?' ) ) {
82 return $condition;
83 }
84
85 $consequent = $this->parse_conditional();
86
87 if ( ! $this->match( ':' ) ) {
88 throw new Gateway_Exception( 'Calculated price expression contains an invalid conditional.' );
89 }
90
91 return array( 'conditional', $condition, $consequent, $this->parse_conditional() );
92 }
93
94 /**
95 * @throws Gateway_Exception
96 */
97 private function parse_logical_or(): array {
98 $node = $this->parse_logical_and();
99
100 while ( $this->match( '||' ) ) {
101 $node = array( 'binary', '||', $node, $this->parse_logical_and() );
102 }
103
104 return $node;
105 }
106
107 /**
108 * @throws Gateway_Exception
109 */
110 private function parse_logical_and(): array {
111 $node = $this->parse_bitwise_xor();
112
113 while ( $this->match( '&&' ) ) {
114 $node = array( 'binary', '&&', $node, $this->parse_bitwise_xor() );
115 }
116
117 return $node;
118 }
119
120 /**
121 * Match JavaScript operator precedence: equality binds before bitwise XOR,
122 * while logical AND binds after it.
123 *
124 * @throws Gateway_Exception
125 */
126 private function parse_bitwise_xor(): array {
127 $node = $this->parse_equality();
128
129 while ( $this->match( '^' ) ) {
130 $node = array( 'binary', '^', $node, $this->parse_equality() );
131 }
132
133 return $node;
134 }
135
136 /**
137 * @throws Gateway_Exception
138 */
139 private function parse_equality(): array {
140 $node = $this->parse_comparison();
141
142 while ( true ) {
143 $operator = $this->match_any( array( '===', '!==', '==', '!=' ) );
144
145 if ( false === $operator ) {
146 return $node;
147 }
148
149 $node = array( 'binary', $operator, $node, $this->parse_comparison() );
150 }
151 }
152
153 /**
154 * @throws Gateway_Exception
155 */
156 private function parse_comparison(): array {
157 $node = $this->parse_expression();
158
159 while ( true ) {
160 $operator = $this->match_any( array( '<=', '>=', '<', '>' ) );
161
162 if ( false === $operator ) {
163 return $node;
164 }
165
166 $node = array( 'binary', $operator, $node, $this->parse_expression() );
167 }
168 }
169
170 /**
171 * @throws Gateway_Exception
172 */
173 private function parse_expression(): array {
174 $node = $this->parse_term();
175
176 while ( true ) {
177 $operator = $this->match_any( array( '+', '-' ) );
178
179 if ( false === $operator ) {
180 return $node;
181 }
182
183 $node = array( 'binary', $operator, $node, $this->parse_term() );
184 }
185 }
186
187 /**
188 * @throws Gateway_Exception
189 */
190 private function parse_term(): array {
191 $node = $this->parse_power();
192
193 while ( true ) {
194 if ( '*' === $this->peek() && '*' === $this->peek( 1 ) ) {
195 return $node;
196 }
197
198 $operator = $this->match_any( array( '*', '/', '%' ) );
199
200 if ( false === $operator ) {
201 return $node;
202 }
203
204 $node = array( 'binary', $operator, $node, $this->parse_power() );
205 }
206 }
207
208 /**
209 * @throws Gateway_Exception
210 */
211 private function parse_power(): array {
212 $node = $this->parse_factor();
213
214 if ( $this->match( '**' ) ) {
215 $node = array( 'binary', '**', $node, $this->parse_power() );
216 }
217
218 return $node;
219 }
220
221 /**
222 * @throws Gateway_Exception
223 */
224 private function parse_factor(): array {
225 $operator = $this->peek();
226
227 if ( '+' === $operator || '-' === $operator || '!' === $operator ) {
228 ++$this->position;
229
230 return array( 'unary', $operator, $this->parse_factor() );
231 }
232
233 if ( '(' === $operator ) {
234 ++$this->position;
235 $node = $this->parse_conditional();
236
237 if ( ')' !== $this->peek() ) {
238 throw new Gateway_Exception( 'Calculated price expression contains unbalanced parentheses.' );
239 }
240
241 ++$this->position;
242
243 return $node;
244 }
245
246 if ( false !== $operator && preg_match( '/[a-zA-Z_]/', $operator ) ) {
247 return $this->parse_identifier();
248 }
249
250 return $this->parse_number();
251 }
252
253 /**
254 * @throws Gateway_Exception
255 */
256 private function parse_number(): array {
257 $remaining = substr( $this->expression, $this->position );
258
259 if ( ! preg_match( '/^(?:(?:[0-9]+(?:\.[0-9]*)?)|(?:\.[0-9]+))(?:[eE][+-]?[0-9]+)?/', $remaining, $matches ) ) {
260 throw new Gateway_Exception( 'Calculated price expression contains an invalid token.' );
261 }
262
263 $number = $matches[0];
264 $this->position += strlen( $number );
265
266 if ( ! is_numeric( $number ) ) {
267 throw new Gateway_Exception( 'Calculated price expression contains an invalid number.' );
268 }
269
270 return array( 'number', (float) $number );
271 }
272
273 /**
274 * @throws Gateway_Exception
275 */
276 private function parse_identifier(): array {
277 $remaining = substr( $this->expression, $this->position );
278
279 if ( ! preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?/', $remaining, $matches ) ) {
280 throw new Gateway_Exception( 'Calculated price expression contains an invalid identifier.' );
281 }
282
283 $name = $matches[0];
284 $this->position += strlen( $name );
285
286 if ( ! $this->match( '(' ) ) {
287 return array( 'constant', $name );
288 }
289
290 $arguments = array();
291
292 if ( ! $this->match( ')' ) ) {
293 do {
294 $arguments[] = $this->parse_conditional();
295 } while ( $this->match( ',' ) );
296
297 if ( ! $this->match( ')' ) ) {
298 throw new Gateway_Exception( 'Calculated price expression contains an invalid function call.' );
299 }
300 }
301
302 return array( 'call', $name, $arguments );
303 }
304
305 /**
306 * @throws Gateway_Exception
307 */
308 private function evaluate_node( array $node ): float {
309 switch ( $node[0] ) {
310 case 'number':
311 return $node[1];
312
313 case 'constant':
314 return $this->evaluate_constant( $node[1] );
315
316 case 'call':
317 return $this->evaluate_math_call( $node[1], $node[2] );
318
319 case 'unary':
320 $value = $this->evaluate_node( $node[2] );
321
322 if ( '!' === $node[1] ) {
323 return $this->is_truthy( $value ) ? 0.0 : 1.0;
324 }
325
326 return '-' === $node[1] ? -1 * $value : $value;
327
328 case 'conditional':
329 return $this->is_truthy( $this->evaluate_node( $node[1] ) )
330 ? $this->evaluate_node( $node[2] )
331 : $this->evaluate_node( $node[3] );
332
333 case 'binary':
334 return $this->evaluate_binary( $node[1], $node[2], $node[3] );
335 }
336
337 throw new Gateway_Exception( 'Calculated price expression contains an invalid node.' );
338 }
339
340 /**
341 * @throws Gateway_Exception
342 */
343 private function validate_node( array $node ): void {
344 switch ( $node[0] ) {
345 case 'number':
346 return;
347
348 case 'constant':
349 $this->assert_supported_constant( $node[1] );
350 return;
351
352 case 'call':
353 $this->assert_supported_math_call( $node[1], $node[2] );
354
355 foreach ( $node[2] as $argument ) {
356 $this->validate_node( $argument );
357 }
358 return;
359
360 case 'unary':
361 $this->validate_node( $node[2] );
362 return;
363
364 case 'conditional':
365 $this->validate_node( $node[1] );
366 $this->validate_node( $node[2] );
367 $this->validate_node( $node[3] );
368 return;
369
370 case 'binary':
371 $this->validate_node( $node[2] );
372 $this->validate_node( $node[3] );
373 return;
374 }
375
376 throw new Gateway_Exception( 'Calculated price expression contains an invalid node.' );
377 }
378
379 /**
380 * @throws Gateway_Exception
381 */
382 private function evaluate_binary( string $operator, array $left_node, array $right_node ): float {
383 $left = $this->evaluate_node( $left_node );
384
385 if ( '&&' === $operator ) {
386 return $this->is_truthy( $left ) ? $this->evaluate_node( $right_node ) : $left;
387 }
388
389 if ( '||' === $operator ) {
390 return $this->is_truthy( $left ) ? $left : $this->evaluate_node( $right_node );
391 }
392
393 $right = $this->evaluate_node( $right_node );
394
395 switch ( $operator ) {
396 case '+':
397 return $left + $right;
398
399 case '-':
400 return $left - $right;
401
402 case '*':
403 return $left * $right;
404
405 case '/':
406 if ( 0.0 === $right ) {
407 throw new Gateway_Exception( 'Calculated price expression attempted division by zero.' );
408 }
409
410 return $left / $right;
411
412 case '%':
413 if ( 0.0 === $right ) {
414 throw new Gateway_Exception( 'Calculated price expression attempted modulo by zero.' );
415 }
416
417 return fmod( $left, $right );
418
419 case '**':
420 return $left ** $right;
421
422 case '^':
423 return (float) ( $this->to_int32( $left ) ^ $this->to_int32( $right ) );
424
425 case '<':
426 return $left < $right ? 1.0 : 0.0;
427
428 case '<=':
429 return $left <= $right ? 1.0 : 0.0;
430
431 case '>':
432 return $left > $right ? 1.0 : 0.0;
433
434 case '>=':
435 return $left >= $right ? 1.0 : 0.0;
436
437 case '==':
438 case '===':
439 return $left === $right ? 1.0 : 0.0;
440
441 case '!=':
442 case '!==':
443 return $left !== $right ? 1.0 : 0.0;
444 }
445
446 throw new Gateway_Exception( 'Calculated price expression contains an unsupported operator.' );
447 }
448
449 /**
450 * Reproduce JavaScript ToInt32 conversion used by bitwise operators.
451 */
452 private function to_int32( float $value ): int {
453 if ( ! is_finite( $value ) || 0.0 === $value ) {
454 return 0;
455 }
456
457 $integer = $value < 0 ? ceil( $value ) : floor( $value );
458 $uint32 = fmod( $integer, 4294967296.0 );
459
460 if ( $uint32 < 0 ) {
461 $uint32 += 4294967296.0;
462 }
463
464 if ( $uint32 >= 2147483648.0 ) {
465 $uint32 -= 4294967296.0;
466 }
467
468 return (int) $uint32;
469 }
470
471 /**
472 * @throws Gateway_Exception
473 */
474 private function evaluate_constant( string $name ): float {
475 $constants = $this->get_supported_constants();
476
477 $this->assert_supported_constant( $name );
478
479 return (float) $constants[ $name ];
480 }
481
482 private function get_supported_constants(): array {
483 return array(
484 'true' => 1.0,
485 'false' => 0.0,
486 'Math.E' => M_E,
487 'Math.LN2' => M_LN2,
488 'Math.LN10' => M_LN10,
489 'Math.LOG2E' => M_LOG2E,
490 'Math.LOG10E' => M_LOG10E,
491 'Math.PI' => M_PI,
492 'Math.SQRT1_2' => M_SQRT1_2,
493 'Math.SQRT2' => M_SQRT2,
494 );
495 }
496
497 /**
498 * @throws Gateway_Exception
499 */
500 private function assert_supported_constant( string $name ): void {
501 if ( ! array_key_exists( $name, $this->get_supported_constants() ) ) {
502 throw new Gateway_Exception( 'Calculated price expression contains an unsupported identifier.' );
503 }
504 }
505
506 /**
507 * @throws Gateway_Exception
508 */
509 private function evaluate_math_call( string $name, array $argument_nodes ): float {
510 $this->assert_supported_math_call( $name, $argument_nodes );
511
512 $arguments = array_map(
513 function ( $node ) {
514 return $this->evaluate_node( $node );
515 },
516 $argument_nodes
517 );
518
519 switch ( $name ) {
520 case 'Math.abs':
521 $result = abs( $arguments[0] );
522 break;
523
524 case 'Math.ceil':
525 $result = ceil( $arguments[0] );
526 break;
527
528 case 'Math.floor':
529 $result = floor( $arguments[0] );
530 break;
531
532 case 'Math.round':
533 $result = $this->js_math_round( $arguments[0] );
534 break;
535
536 case 'Math.trunc':
537 $result = $arguments[0] < 0 ? ceil( $arguments[0] ) : floor( $arguments[0] );
538 break;
539
540 case 'Math.sign':
541 $result = $arguments[0] <=> 0;
542 break;
543
544 case 'Math.sqrt':
545 $result = sqrt( $arguments[0] );
546 break;
547
548 case 'Math.pow':
549 $result = $arguments[0] ** $arguments[1];
550 break;
551
552 case 'Math.min':
553 $result = min( $arguments );
554 break;
555
556 case 'Math.max':
557 $result = max( $arguments );
558 break;
559
560 default:
561 throw new Gateway_Exception( 'Calculated price expression contains an unsupported function.' );
562 }
563
564 if ( ! is_finite( (float) $result ) ) {
565 throw new Gateway_Exception( 'Calculated price expression produced a non-finite value.' );
566 }
567
568 return (float) $result;
569 }
570
571 /**
572 * @throws Gateway_Exception
573 */
574 private function assert_supported_math_call( string $name, array $arguments ): void {
575 switch ( $name ) {
576 case 'Math.abs':
577 case 'Math.ceil':
578 case 'Math.floor':
579 case 'Math.round':
580 case 'Math.trunc':
581 case 'Math.sign':
582 case 'Math.sqrt':
583 $this->assert_argument_count( $name, $arguments, 1 );
584 return;
585
586 case 'Math.pow':
587 $this->assert_argument_count( $name, $arguments, 2 );
588 return;
589
590 case 'Math.min':
591 case 'Math.max':
592 $this->assert_argument_count( $name, $arguments, 1, PHP_INT_MAX );
593 return;
594 }
595
596 throw new Gateway_Exception( 'Calculated price expression contains an unsupported function.' );
597 }
598
599 /**
600 * Reproduce JavaScript Math.round: round half toward +Infinity.
601 *
602 * Calling floor( $value + 0.5 ) diverges for values just below .5 (e.g.
603 * 0.49999999999999994), where forming $value + 0.5 rounds up to 1.0 and
604 * over-rounds. Comparing the fractional part against 0.5 avoids that.
605 */
606 private function js_math_round( float $value ): float {
607 $floor = floor( $value );
608 $fraction = $value - $floor;
609
610 return $fraction >= 0.5 ? $floor + 1.0 : $floor;
611 }
612
613 /**
614 * @throws Gateway_Exception
615 */
616 private function assert_argument_count(
617 string $name,
618 array $arguments,
619 int $minimum,
620 ?int $maximum = null
621 ): void {
622 $maximum = null === $maximum ? $minimum : $maximum;
623 $count = count( $arguments );
624
625 if ( $count < $minimum || $count > $maximum ) {
626 throw new Gateway_Exception(
627 sprintf(
628 'The "%1$s" function has an invalid number of arguments.',
629 esc_html( $name )
630 )
631 );
632 }
633 }
634
635 private function is_truthy( float $value ): bool {
636 return 0.0 !== $value && ! is_nan( $value );
637 }
638
639 private function match( string $token ): bool {
640 if ( substr( $this->expression, $this->position, strlen( $token ) ) !== $token ) {
641 return false;
642 }
643
644 $this->position += strlen( $token );
645
646 return true;
647 }
648
649 /**
650 * @return false|string
651 */
652 private function match_any( array $tokens ) {
653 foreach ( $tokens as $token ) {
654 if ( $this->match( $token ) ) {
655 return $token;
656 }
657 }
658
659 return false;
660 }
661
662 private function peek( int $offset = 0 ) {
663 $position = $this->position + $offset;
664
665 return $position < $this->length ? $this->expression[ $position ] : false;
666 }
667 }
668