/application/helpers/expressions/em_core_helper.php
PHP | 3552 lines | 2981 code | 131 blank | 440 comment | 464 complexity | 848677265598830f395523e3e4dea5ab MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, BSD-3-Clause, GPL-3.0, LGPL-3.0
Large files files are truncated, but you can click here to view the full file
- <?php
- /**
- * LimeSurvey
- * Copyright (C) 2007-2011 The LimeSurvey Project Team / Carsten Schmitz
- * All rights reserved.
- * License: GNU/GPL License v2 or later, see LICENSE.php
- * LimeSurvey is free software. This version may have been modified pursuant
- * to the GNU General Public License, and as distributed it includes or
- * is derivative of works licensed under the GNU General Public License or
- * other free or open source software licenses.
- * See COPYRIGHT.php for copyright notices and details.
- *
- * $Id$
- */
- /**
- * Description of ExpressionManager
- * (1) Does safe evaluation of PHP expressions. Only registered Functions, and known Variables are allowed.
- * (a) Functions include any math, string processing, conditional, formatting, etc. functions
- * (2) This class replaces LimeSurvey's <= 1.91+ process of resolving strings that contain LimeReplacementFields
- * (a) String is split by expressions (by curly braces, but safely supporting strings and escaped curly braces)
- * (b) Expressions (things surrounded by curly braces) are evaluated - thereby doing LimeReplacementField substitution and/or more complex calculations
- * (c) Non-expressions are left intact
- * (d) The array of stringParts are re-joined to create the desired final string.
- * (3) The core of Expression Manager is a Recursive Descent Parser (RDP), based off of one build via JavaCC by TMSWhite in 1999.
- * (a) Functions that start with RDP_ should not be touched unless you really understand compiler design.
- *
- * @author Thomas M. White (TMSWhite)
- */
- class ExpressionManager {
- // These are the allowable suffixes for variables - each represents an attribute of a variable.
- static $RDP_regex_var_attr = 'code|gid|grelevance|gseq|jsName|mandatory|NAOK|qid|qseq|question|readWrite|relevanceStatus|relevance|rowdivid|sgqa|shown|type|valueNAOK|value';
- // These three variables are effectively static once constructed
- private $RDP_ExpressionRegex;
- private $RDP_TokenType;
- private $RDP_TokenizerRegex;
- private $RDP_CategorizeTokensRegex;
- private $RDP_ValidFunctions; // names and # params of valid functions
- // Thes variables are used while processing the equation
- private $RDP_expr; // the source expression
- private $RDP_tokens; // the list of generated tokens
- private $RDP_count; // total number of $RDP_tokens
- private $RDP_pos; // position within the $token array while processing equation
- private $RDP_errs; // array of syntax errors
- private $RDP_onlyparse;
- private $RDP_stack; // stack of intermediate results
- private $RDP_result; // final result of evaluating the expression;
- private $RDP_evalStatus; // true if $RDP_result is a valid result, and there are no serious errors
- private $varsUsed; // list of variables referenced in the equation
- // These variables are only used by sProcessStringContainingExpressions
- private $allVarsUsed; // full list of variables used within the string, even if contains multiple expressions
- private $prettyPrintSource; // HTML formatted output of running sProcessStringContainingExpressions
- private $substitutionNum; // Keeps track of number of substitions performed XXX
- private $substitutionInfo; // array of JavaScripts to managing dynamic substitution
- private $jsExpression; // caches computation of JavaScript equivalent for an Expression
- private $questionSeq; // sequence order of question - so can detect if try to use variable before it is set
- private $groupSeq; // sequence order of groups - so can detect if try to use variable before it is set
- private $surveyMode='group';
- // The following are only needed to enable click on variable names within pretty print and open new window to edit them
- private $sid=NULL; // the survey ID
- private $hyperlinkSyntaxHighlighting=true; // TODO - change this back to false
- private $sgqaNaming=false;
- function __construct()
- {
- // List of token-matching regular expressions
- // Note, this is effectively a Lexer using Regular Expressions. Don't change this unless you understand compiler design.
- $RDP_regex_dq_string = '(?<!\\\\)".*?(?<!\\\\)"';
- $RDP_regex_sq_string = '(?<!\\\\)\'.*?(?<!\\\\)\'';
- $RDP_regex_whitespace = '\s+';
- $RDP_regex_lparen = '\(';
- $RDP_regex_rparen = '\)';
- $RDP_regex_comma = ',';
- $RDP_regex_not = '!';
- $RDP_regex_inc_dec = '\+\+|--';
- $RDP_regex_binary = '[+*/-]';
- $RDP_regex_compare = '<=|<|>=|>|==|!=|\ble\b|\blt\b|\bge\b|\bgt\b|\beq\b|\bne\b';
- $RDP_regex_assign = '='; // '=|\+=|-=|\*=|/=';
- $RDP_regex_sgqa = '(?:INSERTANS:)?[0-9]+X[0-9]+X[0-9]+[A-Z0-9_]*\#?[01]?(?:\.(?:' . ExpressionManager::$RDP_regex_var_attr . '))?';
- $RDP_regex_word = '(?:TOKEN:)?(?:[A-Z][A-Z0-9_]*)?(?:\.(?:[A-Z][A-Z0-9_]*))*(?:\.(?:' . ExpressionManager::$RDP_regex_var_attr . '))?';
- $RDP_regex_number = '[0-9]+\.?[0-9]*|\.[0-9]+';
- $RDP_regex_andor = '\band\b|\bor\b|&&|\|\|';
- $RDP_regex_lcb = '{';
- $RDP_regex_rcb = '}';
- $RDP_regex_sq = '\'';
- $RDP_regex_dq= '"';
- $RDP_regex_bs = '\\\\';
- $RDP_StringSplitRegex = array(
- $RDP_regex_lcb,
- $RDP_regex_rcb,
- $RDP_regex_sq,
- $RDP_regex_dq,
- $RDP_regex_bs,
- );
- // RDP_ExpressionRegex is the regular expression that splits apart strings that contain curly braces in order to find expressions
- $this->RDP_ExpressionRegex = '#(' . implode('|',$RDP_StringSplitRegex) . ')#i';
- // asTokenRegex and RDP_TokenType must be kept in sync (same number and order)
- $RDP_TokenRegex = array(
- $RDP_regex_dq_string,
- $RDP_regex_sq_string,
- $RDP_regex_whitespace,
- $RDP_regex_lparen,
- $RDP_regex_rparen,
- $RDP_regex_comma,
- $RDP_regex_andor,
- $RDP_regex_compare,
- $RDP_regex_sgqa,
- $RDP_regex_word,
- $RDP_regex_number,
- $RDP_regex_not,
- $RDP_regex_inc_dec,
- $RDP_regex_assign,
- $RDP_regex_binary,
- );
- $this->RDP_TokenType = array(
- 'DQ_STRING',
- 'SQ_STRING',
- 'SPACE',
- 'LP',
- 'RP',
- 'COMMA',
- 'AND_OR',
- 'COMPARE',
- 'SGQA',
- 'WORD',
- 'NUMBER',
- 'NOT',
- 'OTHER',
- 'ASSIGN',
- 'BINARYOP',
- );
- // $RDP_TokenizerRegex - a single regex used to split and equation into tokens
- $this->RDP_TokenizerRegex = '#(' . implode('|',$RDP_TokenRegex) . ')#i';
- // $RDP_CategorizeTokensRegex - an array of patterns so can categorize the type of token found - would be nice if could get this from preg_split
- // Adding ability to capture 'OTHER' type, which indicates an error - unsupported syntax element
- $this->RDP_CategorizeTokensRegex = preg_replace("#^(.*)$#","#^$1$#i",$RDP_TokenRegex);
- $this->RDP_CategorizeTokensRegex[] = '/.+/';
- $this->RDP_TokenType[] = 'OTHER';
- // Each allowed function is a mapping from local name to external name + number of arguments
- // Functions can have a list of serveral allowable #s of arguments.
- // If the value is -1, the function must have a least one argument but can have an unlimited number of them
- // -2 means that at least one argument is required. -3 means at least two arguments are required, etc.
- $this->RDP_ValidFunctions = array(
- 'abs' => array('abs', 'Math.abs', $this->gT('Absolute value'), 'number abs(number)', 'http://www.php.net/manual/en/function.checkdate.php', 1),
- 'acos' => array('acos', 'Math.acos', $this->gT('Arc cosine'), 'number acos(number)', 'http://www.php.net/manual/en/function.acos.php', 1),
- 'addslashes' => array('addslashes', $this->gT('addslashes'), 'Quote string with slashes', 'string addslashes(string)', 'http://www.php.net/manual/en/function.addslashes.php', 1),
- 'asin' => array('asin', 'Math.asin', $this->gT('Arc sine'), 'number asin(number)', 'http://www.php.net/manual/en/function.asin.php', 1),
- 'atan' => array('atan', 'Math.atan', $this->gT('Arc tangent'), 'number atan(number)', 'http://www.php.net/manual/en/function.atan.php', 1),
- 'atan2' => array('atan2', 'Math.atan2', $this->gT('Arc tangent of two variables'), 'number atan2(number, number)', 'http://www.php.net/manual/en/function.atan2.php', 2),
- 'ceil' => array('ceil', 'Math.ceil', $this->gT('Round fractions up'), 'number ceil(number)', 'http://www.php.net/manual/en/function.ceil.php', 1),
- 'checkdate' => array('checkdate', 'checkdate', $this->gT('Returns true(1) if it is a valid date in gregorian calendar'), 'bool checkdate(month,day,year)', 'http://www.php.net/manual/en/function.checkdate.php', 3),
- 'cos' => array('cos', 'Math.cos', $this->gT('Cosine'), 'number cos(number)', 'http://www.php.net/manual/en/function.cos.php', 1),
- 'count' => array('exprmgr_count', 'LEMcount', $this->gT('Count the number of answered questions in the list'), 'number count(arg1, arg2, ... argN)', '', -1),
- 'countif' => array('exprmgr_countif', 'LEMcountif', $this->gT('Count the number of answered questions in the list equal the first argument'), 'number countif(matches, arg1, arg2, ... argN)', '', -2),
- 'countifop' => array('exprmgr_countifop', 'LEMcountifop', $this->gT('Count the number of answered questions in the list which pass the critiera (arg op value)'), 'number countifop(op, value, arg1, arg2, ... argN)', '', -3),
- 'date' => array('date', 'date', $this->gT('Format a local date/time'), 'string date(format [, timestamp=time()])', 'http://www.php.net/manual/en/function.date.php', 1,2),
- 'exp' => array('exp', 'Math.exp', $this->gT('Calculates the exponent of e'), 'number exp(number)', 'http://www.php.net/manual/en/function.exp.php', 1),
- 'fixnum' => array('exprmgr_fixnum', 'LEMfixnum', $this->gT('Display numbers with comma as decimal separator, if needed'), 'string fixnum(number)', '', 1),
- 'floor' => array('floor', 'Math.floor', $this->gT('Round fractions down'), 'number floor(number)', 'http://www.php.net/manual/en/function.floor.php', 1),
- 'gmdate' => array('gmdate', 'gmdate', $this->gT('Format a GMT date/time'), 'string gmdate(format [, timestamp=time()])', 'http://www.php.net/manual/en/function.gmdate.php', 1,2),
- 'html_entity_decode' => array('html_entity_decode', 'html_entity_decode', $this->gT('Convert all HTML entities to their applicable characters (always uses ENT_QUOTES and UTF-8)'), 'string html_entity_decode(string)', 'http://www.php.net/manual/en/function.html-entity-decode.php', 1),
- 'htmlentities' => array('htmlentities', 'htmlentities', $this->gT('Convert all applicable characters to HTML entities (always uses ENT_QUOTES and UTF-8)'), 'string htmlentities(string)', 'http://www.php.net/manual/en/function.htmlentities.php', 1),
- 'htmlspecialchars' => array('expr_mgr_htmlspecialchars', 'htmlspecialchars', $this->gT('Convert special characters to HTML entities (always uses ENT_QUOTES and UTF-8)'), 'string htmlspecialchars(string)', 'http://www.php.net/manual/en/function.htmlspecialchars.php', 1),
- 'htmlspecialchars_decode' => array('expr_mgr_htmlspecialchars_decode', 'htmlspecialchars_decode', $this->gT('Convert special HTML entities back to characters (always uses ENT_QUOTES and UTF-8)'), 'string htmlspecialchars_decode(string)', 'http://www.php.net/manual/en/function.htmlspecialchars-decode.php', 1),
- 'idate' => array('idate', 'idate', $this->gT('Format a local time/date as integer'), 'string idate(string [, timestamp=time()])', 'http://www.php.net/manual/en/function.idate.php', 1,2),
- 'if' => array('exprmgr_if', 'LEMif', $this->gT('Conditional processing'), 'if(test,result_if_true,result_if_false)', '', 3),
- 'implode' => array('exprmgr_implode', 'LEMimplode', $this->gT('Join array elements with a string'), 'string implode(glue,arg1,arg2,...,argN)', 'http://www.php.net/manual/en/function.implode.php', -2),
- 'intval' => array('intval', 'LEMintval', $this->gT('Get the integer value of a variable'), 'int intval(number [, base=10])', 'http://www.php.net/manual/en/function.intval.php', 1,2),
- 'is_empty' => array('exprmgr_empty', 'LEMempty', $this->gT('Determine whether a variable is considered to be empty'), 'bool is_empty(var)', 'http://www.php.net/manual/en/function.empty.php', 1),
- 'is_float' => array('is_float', 'LEMis_float', $this->gT('Finds whether the type of a variable is float'), 'bool is_float(var)', 'http://www.php.net/manual/en/function.is-float.php', 1),
- 'is_int' => array('is_int', 'LEMis_int', $this->gT('Find whether the type of a variable is integer'), 'bool is_int(var)', 'http://www.php.net/manual/en/function.is-int.php', 1),
- 'is_nan' => array('is_nan', 'isNaN', $this->gT('Finds whether a value is not a number'), 'bool is_nan(var)', 'http://www.php.net/manual/en/function.is-nan.php', 1),
- 'is_null' => array('is_null', 'LEMis_null', $this->gT('Finds whether a variable is NULL'), 'bool is_null(var)', 'http://www.php.net/manual/en/function.is-null.php', 1),
- 'is_numeric' => array('is_numeric', 'LEMis_numeric', $this->gT('Finds whether a variable is a number or a numeric string'), 'bool is_numeric(var)', 'http://www.php.net/manual/en/function.is-numeric.php', 1),
- 'is_string' => array('is_string', 'LEMis_string', $this->gT('Find whether the type of a variable is string'), 'bool is_string(var)', 'http://www.php.net/manual/en/function.is-string.php', 1),
- 'join' => array('exprmgr_join', 'LEMjoin', $this->gT('Join strings, return joined string.This function is an alias of implode("",argN)'), 'string join(arg1,arg2,...,argN)', '', -1),
- 'list' => array('exprmgr_list', 'LEMlist', $this->gT('Return comma-separated list of values'), 'string list(arg1, arg2, ... argN)', '', -2),
- 'log' => array('exprmgr_log', 'LEMlog', $this->gT('The logarithm of number to base, if given, or the natural logarithm. '), 'number log(number,base=e)', 'http://www.php.net/manual/en/function.log.php', -2),
- 'ltrim' => array('ltrim', 'ltrim', $this->gT('Strip whitespace (or other characters) from the beginning of a string'), 'string ltrim(string [, charlist])', 'http://www.php.net/manual/en/function.ltrim.php', 1,2),
- 'max' => array('max', 'Math.max', $this->gT('Find highest value'), 'number max(arg1, arg2, ... argN)', 'http://www.php.net/manual/en/function.max.php', -2),
- 'min' => array('min', 'Math.min', $this->gT('Find lowest value'), 'number min(arg1, arg2, ... argN)', 'http://www.php.net/manual/en/function.min.php', -2),
- 'mktime' => array('mktime', 'mktime', $this->gT('Get UNIX timestamp for a date (each of the 6 arguments are optional)'), 'number mktime([hour [, minute [, second [, month [, day [, year ]]]]]])', 'http://www.php.net/manual/en/function.mktime.php', 0,1,2,3,4,5,6),
- 'nl2br' => array('nl2br', 'nl2br', $this->gT('Inserts HTML line breaks before all newlines in a string'), 'string nl2br(string)', 'http://www.php.net/manual/en/function.nl2br.php', 1,1),
- 'number_format' => array('number_format', 'number_format', $this->gT('Format a number with grouped thousands'), 'string number_format(number)', 'http://www.php.net/manual/en/function.number-format.php', 1),
- 'pi' => array('pi', 'LEMpi', $this->gT('Get value of pi'), 'number pi()', '', 0),
- 'pow' => array('pow', 'Math.pow', $this->gT('Exponential expression'), 'number pow(base, exp)', 'http://www.php.net/manual/en/function.pow.php', 2),
- 'quoted_printable_decode' => array('quoted_printable_decode', 'quoted_printable_decode', $this->gT('Convert a quoted-printable string to an 8 bit string'), 'string quoted_printable_decode(string)', 'http://www.php.net/manual/en/function.quoted-printable-decode.php', 1),
- 'quoted_printable_encode' => array('quoted_printable_encode', 'quoted_printable_encode', $this->gT('Convert a 8 bit string to a quoted-printable string'), 'string quoted_printable_encode(string)', 'http://www.php.net/manual/en/function.quoted-printable-encode.php', 1),
- 'quotemeta' => array('quotemeta', 'quotemeta', $this->gT('Quote meta characters'), 'string quotemeta(string)', 'http://www.php.net/manual/en/function.quotemeta.php', 1),
- 'rand' => array('rand', 'rand', $this->gT('Generate a random integer'), 'int rand() OR int rand(min, max)', 'http://www.php.net/manual/en/function.rand.php', 0,2),
- 'regexMatch' => array('exprmgr_regexMatch', 'LEMregexMatch', $this->gT('Compare a string to a regular expression pattern'), 'bool regexMatch(pattern,input)', '', 2),
- 'round' => array('round', 'round', $this->gT('Rounds a number to an optional precision'), 'number round(val [, precision])', 'http://www.php.net/manual/en/function.round.php', 1,2),
- 'rtrim' => array('rtrim', 'rtrim', $this->gT('Strip whitespace (or other characters) from the end of a string'), 'string rtrim(string [, charlist])', 'http://www.php.net/manual/en/function.rtrim.php', 1,2),
- 'sin' => array('sin', 'Math.sin', $this->gT('Sine'), 'number sin(arg)', 'http://www.php.net/manual/en/function.sin.php', 1),
- 'sprintf' => array('sprintf', 'sprintf', $this->gT('Return a formatted string'), 'string sprintf(format, arg1, arg2, ... argN)', 'http://www.php.net/manual/en/function.sprintf.php', -2),
- 'sqrt' => array('sqrt', 'Math.sqrt', $this->gT('Square root'), 'number sqrt(arg)', 'http://www.php.net/manual/en/function.sqrt.php', 1),
- 'stddev' => array('exprmgr_stddev', 'LEMstddev', $this->gT('Calculate the Sample Standard Deviation for the list of numbers'), 'number stddev(arg1, arg2, ... argN)', '', -2),
- 'str_pad' => array('str_pad', 'str_pad', $this->gT('Pad a string to a certain length with another string'), 'string str_pad(input, pad_length [, pad_string])', 'http://www.php.net/manual/en/function.str-pad.php', 2,3),
- 'str_repeat' => array('str_repeat', 'str_repeat', $this->gT('Repeat a string'), 'string str_repeat(input, multiplier)', 'http://www.php.net/manual/en/function.str-repeat.php', 2),
- 'str_replace' => array('str_replace', 'LEMstr_replace', $this->gT('Replace all occurrences of the search string with the replacement string'), 'string str_replace(search, replace, subject)', 'http://www.php.net/manual/en/function.str-replace.php', 3),
- 'strcasecmp' => array('strcasecmp', 'strcasecmp', $this->gT('Binary safe case-insensitive string comparison'), 'int strcasecmp(str1, str2)', 'http://www.php.net/manual/en/function.strcasecmp.php', 2),
- 'strcmp' => array('strcmp', 'strcmp', $this->gT('Binary safe string comparison'), 'int strcmp(str1, str2)', 'http://www.php.net/manual/en/function.strcmp.php', 2),
- 'strip_tags' => array('strip_tags', 'strip_tags', $this->gT('Strip HTML and PHP tags from a string'), 'string strip_tags(str, allowable_tags)', 'http://www.php.net/manual/en/function.strip-tags.php', 1,2),
- 'stripos' => array('stripos', 'stripos', $this->gT('Find position of first occurrence of a case-insensitive string'), 'int stripos(haystack, needle [, offset=0])', 'http://www.php.net/manual/en/function.stripos.php', 2,3),
- 'stripslashes' => array('stripslashes', 'stripslashes', $this->gT('Un-quotes a quoted string'), 'string stripslashes(string)', 'http://www.php.net/manual/en/function.stripslashes.php', 1),
- 'stristr' => array('stristr', 'stristr', $this->gT('Case-insensitive strstr'), 'string stristr(haystack, needle [, before_needle=false])', 'http://www.php.net/manual/en/function.stristr.php', 2,3),
- 'strlen' => array('strlen', 'LEMstrlen', $this->gT('Get string length'), 'int strlen(string)', 'http://www.php.net/manual/en/function.strlen.php', 1),
- 'strpos' => array('strpos', 'LEMstrpos', $this->gT('Find position of first occurrence of a string'), 'int strpos(haystack, needle [ offset=0])', 'http://www.php.net/manual/en/function.strpos.php', 2,3),
- 'strrev' => array('strrev', 'strrev', $this->gT('Reverse a string'), 'string strrev(string)', 'http://www.php.net/manual/en/function.strrev.php', 1),
- 'strstr' => array('strstr', 'strstr', $this->gT('Find first occurrence of a string'), 'string strstr(haystack, needle)', 'http://www.php.net/manual/en/function.strstr.php', 2),
- 'strtolower' => array('strtolower', 'LEMstrtolower', $this->gT('Make a string lowercase'), 'string strtolower(string)', 'http://www.php.net/manual/en/function.strtolower.php', 1),
- 'strtoupper' => array('strtoupper', 'LEMstrtoupper', $this->gT('Make a string uppercase'), 'string strtoupper(string)', 'http://www.php.net/manual/en/function.strtoupper.php', 1),
- 'substr' => array('substr', 'substr', $this->gT('Return part of a string'), 'string substr(string, start [, length])', 'http://www.php.net/manual/en/function.substr.php', 2,3),
- 'sum' => array('array_sum', 'LEMsum', $this->gT('Calculate the sum of values in an array'), 'number sum(arg1, arg2, ... argN)', '', -2),
- 'sumifop' => array('exprmgr_sumifop', 'LEMsumifop', $this->gT('Sum the values of answered questions in the list which pass the critiera (arg op value)'), 'number sumifop(op, value, arg1, arg2, ... argN)', '', -3),
- 'tan' => array('tan', 'Math.tan', $this->gT('Tangent'), 'number tan(arg)', 'http://www.php.net/manual/en/function.tan.php', 1),
- 'time' => array('time', 'time', $this->gT('Return current UNIX timestamp'), 'number time()', 'http://www.php.net/manual/en/function.time.php', 0),
- 'trim' => array('trim', 'trim', $this->gT('Strip whitespace (or other characters) from the beginning and end of a string'), 'string trim(string [, charlist])', 'http://www.php.net/manual/en/function.trim.php', 1,2),
- 'ucwords' => array('ucwords', 'ucwords', $this->gT('Uppercase the first character of each word in a string'), 'string ucwords(string)', 'http://www.php.net/manual/en/function.ucwords.php', 1),
- 'unique' => array('exprmgr_unique', 'LEMunique', $this->gT('Returns true if all non-empty responses are unique'), 'boolean unique(arg1, ..., argN)', '', -1),
- );
- }
- /**
- * Add an error to the error log
- *
- * @param <type> $errMsg
- * @param <type> $token
- */
- private function RDP_AddError($errMsg, $token)
- {
- $this->RDP_errs[] = array($errMsg, $token);
- }
- /**
- * RDP_EvaluateBinary() computes binary expressions, such as (a or b), (c * d), popping the top two entries off the
- * stack and pushing the result back onto the stack.
- *
- * @param array $token
- * @return boolean - false if there is any error, else true
- */
- private function RDP_EvaluateBinary(array $token)
- {
- if (count($this->RDP_stack) < 2)
- {
- $this->RDP_AddError($this->gT("Unable to evaluate binary operator - fewer than 2 entries on stack"), $token);
- return false;
- }
- $arg2 = $this->RDP_StackPop();
- $arg1 = $this->RDP_StackPop();
- if (is_null($arg1) or is_null($arg2))
- {
- $this->RDP_AddError($this->gT("Invalid value(s) on the stack"), $token);
- return false;
- }
- // TODO: try to determine datatype?
- $bNumericArg1 = is_numeric($arg1[0]) || $arg1[0] == '';
- $bNumericArg2 = is_numeric($arg2[0]) || $arg2[0] == '';
- $bStringArg1 = !$bNumericArg1 || $arg1[0] == '';
- $bStringArg2 = !$bNumericArg2 || $arg2[0] == '';
- $bBothNumeric = ($bNumericArg1 && $bNumericArg2);
- $bBothString = ($bStringArg1 && $bStringArg2);
- $bMismatchType = (!$bBothNumeric && !$bBothString);
- switch(strtolower($token[0]))
- {
- case 'or':
- case '||':
- $result = array(($arg1[0] or $arg2[0]),$token[1],'NUMBER');
- break;
- case 'and':
- case '&&':
- $result = array(($arg1[0] and $arg2[0]),$token[1],'NUMBER');
- break;
- case '==':
- case 'eq':
- $result = array(($arg1[0] == $arg2[0]),$token[1],'NUMBER');
- break;
- case '!=':
- case 'ne':
- $result = array(($arg1[0] != $arg2[0]),$token[1],'NUMBER');
- break;
- case '<':
- case 'lt':
- if ($bMismatchType) {
- $result = array(false,$token[1],'NUMBER');
- }
- else {
- $result = array(($arg1[0] < $arg2[0]),$token[1],'NUMBER');
- }
- break;
- case '<=';
- case 'le':
- if ($bMismatchType) {
- $result = array(false,$token[1],'NUMBER');
- }
- else {
- // Need this explicit comparison in order to be in agreement with JavaScript
- if (($arg1[0] == '0' && $arg2[0] == '') || ($arg1[0] == '' && $arg2[0] == '0')) {
- $result = array(true,$token[1],'NUMBER');
- }
- else {
- $result = array(($arg1[0] <= $arg2[0]),$token[1],'NUMBER');
- }
- }
- break;
- case '>':
- case 'gt':
- if ($bMismatchType) {
- $result = array(false,$token[1],'NUMBER');
- }
- else {
- // Need this explicit comparison in order to be in agreement with JavaScript
- if (($arg1[0] == '0' && $arg2[0] == '') || ($arg1[0] == '' && $arg2[0] == '0')) {
- $result = array(false,$token[1],'NUMBER');
- }
- else {
- $result = array(($arg1[0] > $arg2[0]),$token[1],'NUMBER');
- }
- }
- break;
- case '>=';
- case 'ge':
- if ($bMismatchType) {
- $result = array(false,$token[1],'NUMBER');
- }
- else {
- $result = array(($arg1[0] >= $arg2[0]),$token[1],'NUMBER');
- }
- break;
- case '+':
- if ($bBothNumeric) {
- $result = array(($arg1[0] + $arg2[0]),$token[1],'NUMBER');
- }
- else {
- $result = array($arg1[0] . $arg2[0],$token[1],'STRING');
- }
- break;
- case '-':
- if ($bBothNumeric) {
- $result = array(($arg1[0] - $arg2[0]),$token[1],'NUMBER');
- }
- else {
- $result = array(NAN,$token[1],'NUMBER');
- }
- break;
- case '*':
- if ($bBothNumeric) {
- $result = array(($arg1[0] * $arg2[0]),$token[1],'NUMBER');
- }
- else {
- $result = array(NAN,$token[1],'NUMBER');
- }
- break;
- case '/';
- if ($bBothNumeric) {
- if ($arg2[0] == 0) {
- $result = array(NAN,$token[1],'NUMBER');
- }
- else {
- $result = array(($arg1[0] / $arg2[0]),$token[1],'NUMBER');
- }
- }
- else {
- $result = array(NAN,$token[1],'NUMBER');
- }
- break;
- }
- $this->RDP_StackPush($result);
- return true;
- }
- /**
- * Processes operations like +a, -b, !c
- * @param array $token
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateUnary(array $token)
- {
- if (count($this->RDP_stack) < 1)
- {
- $this->RDP_AddError($this->gT("Unable to evaluate unary operator - no entries on stack"), $token);
- return false;
- }
- $arg1 = $this->RDP_StackPop();
- if (is_null($arg1))
- {
- $this->RDP_AddError($this->gT("Invalid value(s) on the stack"), $token);
- return false;
- }
- // TODO: try to determine datatype?
- switch($token[0])
- {
- case '+':
- $result = array((+$arg1[0]),$token[1],'NUMBER');
- break;
- case '-':
- $result = array((-$arg1[0]),$token[1],'NUMBER');
- break;
- case '!';
- $result = array((!$arg1[0]),$token[1],'NUMBER');
- break;
- }
- $this->RDP_StackPush($result);
- return true;
- }
- /**
- * Main entry function
- * @param <type> $expr
- * @param <type> $onlyparse - if true, then validate the syntax without computing an answer
- * @return boolean - true if success, false if any error occurred
- */
- public function RDP_Evaluate($expr, $onlyparse=false)
- {
- $this->RDP_expr = $expr;
- $this->RDP_tokens = $this->RDP_Tokenize($expr);
- $this->RDP_count = count($this->RDP_tokens);
- $this->RDP_pos = -1; // starting position within array (first act will be to increment it)
- $this->RDP_errs = array();
- $this->RDP_onlyparse = $onlyparse;
- $this->RDP_stack = array();
- $this->RDP_evalStatus = false;
- $this->RDP_result = NULL;
- $this->varsUsed = array();
- $this->jsExpression = NULL;
- if ($this->HasSyntaxErrors()) {
- return false;
- }
- elseif ($this->RDP_EvaluateExpressions())
- {
- if ($this->RDP_pos < $this->RDP_count)
- {
- $this->RDP_AddError($this->gT("Extra tokens found"), $this->RDP_tokens[$this->RDP_pos]);
- return false;
- }
- $this->RDP_result = $this->RDP_StackPop();
- if (is_null($this->RDP_result))
- {
- return false;
- }
- if (count($this->RDP_stack) == 0)
- {
- $this->RDP_evalStatus = true;
- return true;
- }
- else
- {
- $this-RDP_AddError($this->gT("Unbalanced equation - values left on stack"),NULL);
- return false;
- }
- }
- else
- {
- $this->RDP_AddError($this->gT("Not a valid expression"),NULL);
- return false;
- }
- }
- /**
- * Process "a op b" where op in (+,-,concatenate)
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateAdditiveExpression()
- {
- if (!$this->RDP_EvaluateMultiplicativeExpression())
- {
- return false;
- }
- while (($this->RDP_pos + 1) < $this->RDP_count)
- {
- $token = $this->RDP_tokens[++$this->RDP_pos];
- if ($token[2] == 'BINARYOP')
- {
- switch ($token[0])
- {
- case '+':
- case '-';
- if ($this->RDP_EvaluateMultiplicativeExpression())
- {
- if (!$this->RDP_EvaluateBinary($token))
- {
- return false;
- }
- // else continue;
- }
- else
- {
- return false;
- }
- break;
- default:
- --$this->RDP_pos;
- return true;
- }
- }
- else
- {
- --$this->RDP_pos;
- return true;
- }
- }
- return true;
- }
- /**
- * Process a Constant (number of string), retrieve the value of a known variable, or process a function, returning result on the stack.
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateConstantVarOrFunction()
- {
- if ($this->RDP_pos + 1 >= $this->RDP_count)
- {
- $this->RDP_AddError($this->gT("Poorly terminated expression - expected a constant or variable"), NULL);
- return false;
- }
- $token = $this->RDP_tokens[++$this->RDP_pos];
- switch ($token[2])
- {
- case 'NUMBER':
- case 'DQ_STRING':
- case 'SQ_STRING':
- $this->RDP_StackPush($token);
- return true;
- break;
- case 'WORD':
- case 'SGQA':
- if (($this->RDP_pos + 1) < $this->RDP_count and $this->RDP_tokens[($this->RDP_pos + 1)][2] == 'LP')
- {
- return $this->RDP_EvaluateFunction();
- }
- else
- {
- if ($this->RDP_isValidVariable($token[0]))
- {
- $this->varsUsed[] = $token[0]; // add this variable to list of those used in this equation
- if (preg_match("/\.(gid|grelevance|gseq|jsName|mandatory|qid|qseq|question|readWrite|relevance|rowdivid|sgqa|type)$/",$token[0]))
- {
- $relStatus=1; // static, so always relevant
- }
- else
- {
- $relStatus = $this->GetVarAttribute($token[0],'relevanceStatus',1);
- }
- if ($relStatus==1)
- {
- $result = array($this->GetVarAttribute($token[0],NULL,''),$token[1],'NUMBER');
- }
- else
- {
- $result = array(NULL,$token[1],'NUMBER'); // was 0 instead of NULL
- }
- $this->RDP_StackPush($result);
- // TODO - currently, will try to process value anyway, but want to show a potential error. Should it be a definitive error (e.g. prevent this behavior)?
- $groupSeq = $this->GetVarAttribute($token[0],'gseq',-1);
- if (($groupSeq != -1 && $this->groupSeq != -1) && ($groupSeq > $this->groupSeq))
- {
- $this->RDP_AddError($this->gT("Variable not declared until a later page"),$token);
- return false;
- }
- return true;
- }
- else
- {
- $this->RDP_AddError($this->gT("Undefined variable"), $token);
- return false;
- }
- }
- break;
- case 'COMMA':
- --$this->RDP_pos;
- $this->RDP_AddError($this->gT("Should never get to this line?"),$token);
- return false;
- default:
- return false;
- break;
- }
- }
- /**
- * Process "a == b", "a eq b", "a != b", "a ne b"
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateEqualityExpression()
- {
- if (!$this->RDP_EvaluateRelationExpression())
- {
- return false;
- }
- while (($this->RDP_pos + 1) < $this->RDP_count)
- {
- $token = $this->RDP_tokens[++$this->RDP_pos];
- switch (strtolower($token[0]))
- {
- case '==':
- case 'eq':
- case '!=':
- case 'ne':
- if ($this->RDP_EvaluateRelationExpression())
- {
- if (!$this->RDP_EvaluateBinary($token))
- {
- return false;
- }
- // else continue;
- }
- else
- {
- return false;
- }
- break;
- default:
- --$this->RDP_pos;
- return true;
- }
- }
- return true;
- }
- /**
- * Process a single expression (e.g. without commas)
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateExpression()
- {
- if ($this->RDP_pos + 2 < $this->RDP_count)
- {
- $token1 = $this->RDP_tokens[++$this->RDP_pos];
- $token2 = $this->RDP_tokens[++$this->RDP_pos];
- if ($token2[2] == 'ASSIGN')
- {
- if ($this->RDP_isValidVariable($token1[0]))
- {
- $this->varsUsed[] = $token1[0]; // add this variable to list of those used in this equation
- if ($this->RDP_isWritableVariable($token1[0]))
- {
- $evalStatus = $this->RDP_EvaluateLogicalOrExpression();
- if ($evalStatus)
- {
- $result = $this->RDP_StackPop();
- if (!is_null($result))
- {
- $newResult = $token2;
- $newResult[2] = 'NUMBER';
- $newResult[0] = $this->RDP_SetVariableValue($token2[0], $token1[0], $result[0]);
- $this->RDP_StackPush($newResult);
- }
- else
- {
- $evalStatus = false;
- }
- }
- return $evalStatus;
- }
- else
- {
- $this->RDP_AddError($this->gT('The value of this variable can not be changed'), $token1);
- return false;
- }
- }
- else
- {
- $this->RDP_AddError($this->gT('Only variables can be assigned values'), $token1);
- return false;
- }
- }
- else
- {
- // not an assignment expression, so try something else
- $this->RDP_pos -= 2;
- return $this->RDP_EvaluateLogicalOrExpression();
- }
- }
- else
- {
- return $this->RDP_EvaluateLogicalOrExpression();
- }
- }
- /**
- * Process "expression [, expression]*
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateExpressions()
- {
- $evalStatus = $this->RDP_EvaluateExpression();
- if (!$evalStatus)
- {
- return false;
- }
- while (++$this->RDP_pos < $this->RDP_count) {
- $token = $this->RDP_tokens[$this->RDP_pos];
- if ($token[2] == 'RP')
- {
- return true; // presumbably the end of an expression
- }
- elseif ($token[2] == 'COMMA')
- {
- if ($this->RDP_EvaluateExpression())
- {
- $secondResult = $this->RDP_StackPop();
- $firstResult = $this->RDP_StackPop();
- if (is_null($firstResult))
- {
- return false;
- }
- $this->RDP_StackPush($secondResult);
- $evalStatus = true;
- }
- else
- {
- return false; // an error must have occurred
- }
- }
- else
- {
- $this->RDP_AddError($this->gT("Expected expressions separated by commas"),$token);
- $evalStatus = false;
- break;
- }
- }
- while (++$this->RDP_pos < $this->RDP_count)
- {
- $token = $this->RDP_tokens[$this->RDP_pos];
- $this->RDP_AddError($this->gT("Extra token found after expressions"),$token);
- $evalStatus = false;
- }
- return $evalStatus;
- }
- /**
- * Process a function call
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateFunction()
- {
- $funcNameToken = $this->RDP_tokens[$this->RDP_pos]; // note that don't need to increment position for functions
- $funcName = $funcNameToken[0];
- if (!$this->RDP_isValidFunction($funcName))
- {
- $this->RDP_AddError($this->gT("Undefined function"), $funcNameToken);
- return false;
- }
- $token2 = $this->RDP_tokens[++$this->RDP_pos];
- if ($token2[2] != 'LP')
- {
- $this->RDP_AddError($this->gT("Expected left parentheses after function name"), $funcNameToken);
- }
- $params = array(); // will just store array of values, not tokens
- while ($this->RDP_pos + 1 < $this->RDP_count)
- {
- $token3 = $this->RDP_tokens[$this->RDP_pos + 1];
- if (count($params) > 0)
- {
- // should have COMMA or RP
- if ($token3[2] == 'COMMA')
- {
- ++$this->RDP_pos; // consume the token so can process next clause
- if ($this->RDP_EvaluateExpression())
- {
- $value = $this->RDP_StackPop();
- if (is_null($value))
- {
- return false;
- }
- $params[] = $value[0];
- continue;
- }
- else
- {
- $this->RDP_AddError($this->gT("Extra comma found in function"), $token3);
- return false;
- }
- }
- }
- if ($token3[2] == 'RP')
- {
- ++$this->RDP_pos; // consume the token so can process next clause
- return $this->RDP_RunFunction($funcNameToken,$params);
- }
- else
- {
- if ($this->RDP_EvaluateExpression())
- {
- $value = $this->RDP_StackPop();
- if (is_null($value))
- {
- return false;
- }
- $params[] = $value[0];
- continue;
- }
- else
- {
- return false;
- }
- }
- }
- }
- /**
- * Process "a && b" or "a and b"
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateLogicalAndExpression()
- {
- if (!$this->RDP_EvaluateEqualityExpression())
- {
- return false;
- }
- while (($this->RDP_pos + 1) < $this->RDP_count)
- {
- $token = $this->RDP_tokens[++$this->RDP_pos];
- switch (strtolower($token[0]))
- {
- case '&&':
- case 'and':
- if ($this->RDP_EvaluateEqualityExpression())
- {
- if (!$this->RDP_EvaluateBinary($token))
- {
- return false;
- }
- // else continue
- }
- else
- {
- return false; // an error must have occurred
- }
- break;
- default:
- --$this->RDP_pos;
- return true;
- }
- }
- return true;
- }
- /**
- * Process "a || b" or "a or b"
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateLogicalOrExpression()
- {
- if (!$this->RDP_EvaluateLogicalAndExpression())
- {
- return false;
- }
- while (($this->RDP_pos + 1) < $this->RDP_count)
- {
- $token = $this->RDP_tokens[++$this->RDP_pos];
- switch (strtolower($token[0]))
- {
- case '||':
- case 'or':
- if ($this->RDP_EvaluateLogicalAndExpression())
- {
- if (!$this->RDP_EvaluateBinary($token))
- {
- return false;
- }
- // else continue
- }
- else
- {
- // an error must have occurred
- return false;
- }
- break;
- default:
- // no more expressions being ORed together, so continue parsing
- --$this->RDP_pos;
- return true;
- }
- }
- // no more tokens to parse
- return true;
- }
- /**
- * Process "a op b" where op in (*,/)
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateMultiplicativeExpression()
- {
- if (!$this->RDP_EvaluateUnaryExpression())
- {
- return false;
- }
- while (($this->RDP_pos + 1) < $this->RDP_count)
- {
- $token = $this->RDP_tokens[++$this->RDP_pos];
- if ($token[2] == 'BINARYOP')
- {
- switch ($token[0])
- {
- case '*':
- case '/';
- if ($this->RDP_EvaluateUnaryExpression())
- {
- if (!$this->RDP_EvaluateBinary($token))
- {
- return false;
- }
- // else continue
- }
- else
- {
- // an error must have occurred
- return false;
- }
- break;
- break;
- default:
- --$this->RDP_pos;
- return true;
- }
- }
- else
- {
- --$this->RDP_pos;
- return true;
- }
- }
- return true;
- }
- /**
- * Process expressions including functions and parenthesized blocks
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluatePrimaryExpression()
- {
- if (($this->RDP_pos + 1) >= $this->RDP_count) {
- $this->RDP_AddError($this->gT("Poorly terminated expression - expected a constant or variable"), NULL);
- return false;
- }
- $token = $this->RDP_tokens[++$this->RDP_pos];
- if ($token[2] == 'LP')
- {
- if (!$this->RDP_EvaluateExpressions())
- {
- return false;
- }
- $token = $this->RDP_tokens[$this->RDP_pos];
- if ($token[2] == 'RP')
- {
- return true;
- }
- else
- {
- $this->RDP_AddError($this->gT("Expected right parentheses"), $token);
- return false;
- }
- }
- else
- {
- --$this->RDP_pos;
- return $this->RDP_EvaluateConstantVarOrFunction();
- }
- }
- /**
- * Process "a op b" where op in (lt, gt, le, ge, <, >, <=, >=)
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateRelationExpression()
- {
- if (!$this->RDP_EvaluateAdditiveExpression())
- {
- return false;
- }
- while (($this->RDP_pos + 1) < $this->RDP_count)
- {
- $token = $this->RDP_tokens[++$this->RDP_pos];
- switch (strtolower($token[0]))
- {
- case '<':
- case 'lt':
- case '<=';
- case 'le':
- case '>':
- case 'gt':
- case '>=';
- case 'ge':
- if ($this->RDP_EvaluateAdditiveExpression())
- {
- if (!$this->RDP_EvaluateBinary($token))
- {
- return false;
- }
- // else continue
- }
- else
- {
- // an error must have occurred
- return false;
- }
- break;
- default:
- --$this->RDP_pos;
- return true;
- }
- }
- return true;
- }
- /**
- * Process "op a" where op in (+,-,!)
- * @return boolean - true if success, false if any error occurred
- */
- private function RDP_EvaluateUnaryExpression()
- {
- if (($this->RDP_pos + 1) >= $this->RDP_count) {
- $this->RDP_AddError($this->gT("Poorly terminated expression - expected a constant or variable"), NULL);
- return false;
- }
- $token = $this->RDP_tokens[++$this->RDP_pos];
- if ($token[2] == 'NOT' || $token[2] == 'BINARYOP')
- {
- switch ($token[0])
- {
- case '+':
- case '-':
- case '!':
- if (!$this->RDP_EvaluatePrimaryExpression())
- {
- return false;
- }
- return $this->RDP_EvaluateUnary($token);
- break;
- default:
- --$this->RDP_pos;
- return $this->RDP_EvaluatePrimaryExpression();
- }
- }
- else
- {
- --$this->RDP_pos;
- return $this->RDP_EvaluatePrimar…
Large files files are truncated, but you can click here to view the full file