PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / eval-math / eval-math.php

eval-math.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/eval-math/eval-math.php

370 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes\EvalMath;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 /**
10 * Class EvalMath. Supports basic math only (removed eval function).
11 *
12 * Based on EvalMath by Miles Kaufmann Copyright (C) 2005 Miles Kaufmann http://www.twmagic.com/.
13 */
14 class EvalMath {
15
16 /**
17 * Last error.
18 *
19 * @var string
20 */
21 public static $last_error = null;
22
23 /**
24 * Variables (and constants).
25 *
26 * @var array
27 */
28 public static $v = array(
29 'e' => 2.71,
30 'pi' => 3.14,
31 );
32
33 /**
34 * User-defined functions.
35 *
36 * @var array
37 */
38 public static $f = array();
39
40 /**
41 * Constants.
42 *
43 * @var array
44 */
45 public static $vb = array( 'e', 'pi' );
46
47 /**
48 * Built-in functions.
49 *
50 * @var array
51 */
52 public static $fb = array();
53
54 /**
55 * Evaluate maths string.
56 *
57 * @param string $expr
58 * @return mixed
59 */
60 public static function evaluate( $expr ) {
61 self::$last_error = null;
62 $expr = trim( $expr );
63 if ( substr( $expr, -1, 1 ) == ';' ) {
64 $expr = substr( $expr, 0, strlen( $expr ) - 1 ); // strip semicolons at the end
65 }
66 // ===============
67 // is it a variable assignment?
68 if ( preg_match( '/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches ) ) {
69 if ( in_array( $matches[1], self::$vb ) ) { // make sure we're not assigning to a constant
70 return self::trigger( "cannot assign to constant '$matches[1]'" );
71 }
72 if ( ( $tmp = self::pfx( self::nfx( $matches[2] ) ) ) === false ) {
73 return false; // get the result and make sure it's good
74 }
75 self::$v[ $matches[1] ] = $tmp; // if so, stick it in the variable array
76 return self::$v[ $matches[1] ]; // and return the resulting value
77 // ===============
78 // is it a function assignment?
79 } elseif ( preg_match( '/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches ) ) {
80 $fnn = $matches[1]; // get the function name
81 if ( in_array( $matches[1], self::$fb ) ) { // make sure it isn't built in
82 return self::trigger( "cannot redefine built-in function '$matches[1]()'" );
83 }
84 $args = explode( ',', preg_replace( '/\s+/', '', $matches[2] ) ); // get the arguments
85 if ( ( $stack = self::nfx( $matches[3] ) ) === false ) {
86 return false; // see if it can be converted to postfix
87 }
88 $stack_size = count( $stack );
89 for ( $i = 0; $i < $stack_size; $i++ ) { // freeze the state of the non-argument variables
90 $token = $stack[ $i ];
91 if ( preg_match( '/^[a-z]\w*$/', $token ) and ! in_array( $token, $args ) ) {
92 if ( array_key_exists( $token, self::$v ) ) {
93 $stack[ $i ] = self::$v[ $token ];
94 } else {
95 return self::trigger( "undefined variable '$token' in function definition" );
96 }
97 }
98 }
99 self::$f[ $fnn ] = array(
100 'args' => $args,
101 'func' => $stack,
102 );
103 return true;
104 // ===============
105 } else {
106 return self::pfx( self::nfx( $expr ) ); // straight up evaluation, woo
107 }
108 }
109
110 /**
111 * Convert infix to postfix notation.
112 *
113 * @param string $expr
114 *
115 * @return array|string
116 */
117 private static function nfx( $expr ) {
118 $index = 0;
119 $stack = new EvalMathStack();
120 $output = array(); // postfix form of expression, to be passed to pfx()
121 $expr = trim( $expr );
122
123 $ops = array( '+', '-', '*', '/', '^', '_' );
124 $ops_r = array(
125 '+' => 0,
126 '-' => 0,
127 '*' => 0,
128 '/' => 0,
129 '^' => 1,
130 ); // right-associative operator?
131 $ops_p = array(
132 '+' => 0,
133 '-' => 0,
134 '*' => 1,
135 '/' => 1,
136 '_' => 1,
137 '^' => 2,
138 ); // operator precedence
139
140 $expecting_op = false; // we use this in syntax-checking the expression
141 // and determining when a - is a negation
142 if ( preg_match( '/[^\w\s+*^\/()\.,-]/', $expr, $matches ) ) { // make sure the characters are all good
143 return self::trigger( "illegal character '{$matches[0]}'" );
144 }
145
146 while ( 1 ) { // 1 Infinite Loop ;)
147 $op = substr( $expr, $index, 1 ); // get the first character at the current index
148 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
149 $ex = preg_match( '/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr( $expr, $index ), $match );
150 // ===============
151 if ( '-' === $op and ! $expecting_op ) { // is it a negation instead of a minus?
152 $stack->push( '_' ); // put a negation on the stack
153 $index++;
154 } elseif ( '_' === $op ) { // we have to explicitly deny this, because it's legal on the stack
155 return self::trigger( "illegal character '_'" ); // but not in the input expression
156 // ===============
157 } elseif ( ( in_array( $op, $ops ) or $ex ) and $expecting_op ) { // are we putting an operator on the stack?
158 if ( $ex ) { // are we expecting an operator but have a number/variable/function/opening parenthesis?
159 $op = '*';
160 $index--; // it's an implicit multiplication
161 }
162 // heart of the algorithm:
163 while ( $stack->count > 0 and ( $o2 = $stack->last() ) and in_array( $o2, $ops ) and ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) {
164 $output[] = $stack->pop(); // pop stuff off the stack into the output
165 }
166 // many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
167 $stack->push( $op ); // finally put OUR operator onto the stack
168 $index++;
169 $expecting_op = false;
170 // ===============
171 } elseif ( ')' === $op && $expecting_op ) { // ready to close a parenthesis?
172 while ( ( $o2 = $stack->pop() ) != '(' ) { // pop off the stack back to the last (
173 if ( is_null( $o2 ) ) {
174 return self::trigger( "unexpected ')'" );
175 } else {
176 $output[] = $o2;
177 }
178 }
179 if ( preg_match( '/^([A-Za-z]\w*)\($/', $stack->last( 2 ), $matches ) ) { // did we just close a function?
180 $fnn = $matches[1]; // get the function name
181 $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
182 $output[] = $stack->pop(); // pop the function and push onto the output
183 if ( in_array( $fnn, self::$fb ) ) { // check the argument count
184 if ( $arg_count > 1 ) {
185 return self::trigger( "too many arguments ($arg_count given, 1 expected)" );
186 }
187 } elseif ( array_key_exists( $fnn, self::$f ) ) {
188 if ( count( self::$f[ $fnn ]['args'] ) != $arg_count ) {
189 return self::trigger( "wrong number of arguments ($arg_count given, " . count( self::$f[ $fnn ]['args'] ) . ' expected)' );
190 }
191 } else { // did we somehow push a non-function on the stack? this should never happen
192 return self::trigger( 'internal error' );
193 }
194 }
195 $index++;
196 // ===============
197 } elseif ( ',' === $op and $expecting_op ) { // did we just finish a function argument?
198 while ( ( $o2 = $stack->pop() ) != '(' ) {
199 if ( is_null( $o2 ) ) {
200 return self::trigger( "unexpected ','" ); // oops, never had a (
201 } else {
202 $output[] = $o2; // pop the argument expression stuff and push onto the output
203 }
204 }
205 // make sure there was a function
206 if ( ! preg_match( '/^([A-Za-z]\w*)\($/', $stack->last( 2 ), $matches ) ) {
207 return self::trigger( "unexpected ','" );
208 }
209 $stack->push( $stack->pop() + 1 ); // increment the argument count
210 $stack->push( '(' ); // put the ( back on, we'll need to pop back to it again
211 $index++;
212 $expecting_op = false;
213 // ===============
214 } elseif ( '(' === $op and ! $expecting_op ) {
215 $stack->push( '(' ); // that was easy
216 $index++;
217 // ===============
218 } elseif ( $ex and ! $expecting_op ) { // do we now have a function/variable/number?
219 $expecting_op = true;
220 $val = $match[1];
221 if ( preg_match( '/^([A-Za-z]\w*)\($/', $val, $matches ) ) { // may be func, or variable w/ implicit multiplication against parentheses...
222 if ( in_array( $matches[1], self::$fb ) or array_key_exists( $matches[1], self::$f ) ) { // it's a func
223 $stack->push( $val );
224 $stack->push( 1 );
225 $stack->push( '(' );
226 $expecting_op = false;
227 } else { // it's a var w/ implicit multiplication
228 $val = $matches[1];
229 $output[] = $val;
230 }
231 } else { // it's a plain old var or num
232 $output[] = $val;
233 }
234 $index += strlen( $val );
235 // ===============
236 } elseif ( ')' === $op ) { // miscellaneous error checking
237 return self::trigger( "unexpected ')'" );
238 } elseif ( in_array( $op, $ops ) and ! $expecting_op ) {
239 return self::trigger( "unexpected operator '$op'" );
240 } else { // I don't even want to know what you did to get here
241 return self::trigger( 'an unexpected error occurred' );
242 }
243 if ( strlen( $expr ) == $index ) {
244 if ( in_array( $op, $ops ) ) { // did we end with an operator? bad.
245 return self::trigger( "operator '$op' lacks operand" );
246 } else {
247 break;
248 }
249 }
250 while ( substr( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace
251 $index++; // into implicit multiplication if no operator is there)
252 }
253 }
254 while ( ! is_null( $op = $stack->pop() ) ) { // pop everything off the stack and push onto output
255 if ( '(' === $op ) {
256 return self::trigger( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced
257 }
258 $output[] = $op;
259 }
260 return $output;
261 }
262
263 /**
264 * Evaluate postfix notation.
265 *
266 * @param mixed $tokens
267 * @param array $vars
268 *
269 * @return mixed
270 */
271 private static function pfx( $tokens, $vars = array() ) {
272 if ( false == $tokens ) {
273 return false;
274 }
275 $stack = new EvalMathStack();
276
277 foreach ( $tokens as $token ) { // nice and easy
278 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
279 if ( in_array( $token, array( '+', '-', '*', '/', '^' ) ) ) {
280 if ( is_null( $op2 = $stack->pop() ) ) {
281 return self::trigger( 'internal error' );
282 }
283 if ( is_null( $op1 = $stack->pop() ) ) {
284 return self::trigger( 'internal error' );
285 }
286 switch ( $token ) {
287 case '+':
288 $stack->push( $op1 + $op2 );
289 break;
290 case '-':
291 $stack->push( $op1 - $op2 );
292 break;
293 case '*':
294 $stack->push( $op1 * $op2 );
295 break;
296 case '/':
297 if ( 0 == $op2 ) {
298 return self::trigger( 'division by zero' );
299 }
300 $stack->push( $op1 / $op2 );
301 break;
302 case '^':
303 $stack->push( pow( $op1, $op2 ) );
304 break;
305 }
306 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
307 } elseif ( '_' === $token ) {
308 $stack->push( -1 * $stack->pop() );
309 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
310 } elseif ( ! preg_match( '/^([a-z]\w*)\($/', $token, $matches ) ) {
311 if ( is_numeric( $token ) ) {
312 $stack->push( $token );
313 } elseif ( array_key_exists( $token, self::$v ) ) {
314 $stack->push( self::$v[ $token ] );
315 } elseif ( array_key_exists( $token, $vars ) ) {
316 $stack->push( $vars[ $token ] );
317 } else {
318 return self::trigger( "undefined variable '$token'" );
319 }
320 }
321 }
322 // when we're out of tokens, the stack should have a single element, the final result
323 if ( 1 != $stack->count ) {
324 return self::trigger( 'internal error' );
325 }
326 return $stack->pop();
327 }
328
329 /**
330 * Trigger an error, but nicely, if need be.
331 *
332 * @param string $msg
333 *
334 * @return bool
335 */
336 private static function trigger( string $msg ): bool {
337 self::$last_error = $msg;
338 if ( ! defined( 'DOING_AJAX' ) && ( defined('WP_DEBUG') && WP_DEBUG ) ) {
339 echo "\nError found in:";
340 self::debugPrintCallingFunction();
341 trigger_error( $msg, E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error, WordPress.Security.EscapeOutput.OutputNotEscaped -- debug output.
342 }
343 return false;
344 }
345
346 /**
347 * Prints the file name, function name, and
348 * line number which called your function
349 * (not this function, then one that called
350 * it to begin with)
351 */
352 private static function debugPrintCallingFunction() {
353 $file = 'n/a';
354 $func = 'n/a';
355 $line = 'n/a';
356 $debugTrace = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- debug output.
357
358 if ( isset( $debugTrace[1] ) ) {
359 $file = $debugTrace[1]['file'] ?: 'n/a';
360 $line = $debugTrace[1]['line'] ?: 'n/a';
361 }
362
363 if ( isset( $debugTrace[2] ) ) {
364 $func = $debugTrace[2]['function'] ?: 'n/a';
365 }
366
367 echo "\n$file, $func, $line\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
368 }
369 }
370