mirror of
https://github.com/Combodo/iTop.git
synced 2026-03-01 23:24:12 +01:00
* N°4789 - Parse datamodel module.xxx.php files instead of interpreting them - refactoring all in a dedicated service first * N°4789 - fix broken setup + tests * N°4789 - replace legacy eval by module file parsing * N°4789 - handle constants and if conditional structures * N°4789 - compute boolean expressions * N°4789 - make autoselect and dependencies work as well * cleanup * N°4789 - fix BeforeWritingConfig calls during setup * N°4789 - refactor and split in ModuleDiscoveryEvaluationService + handle ModuleInstallerAPI methods calls during setup * N°4789 - PR review changes with Romain * PR review + code cleanup + added usecases and test cover * temp evaluation work * replace eval by iTop custom evaluation classes * move PhpParser/Evaluation classes in a specific namespave + composer dumpautoload * fix broken setup * fix broken setup * complete Evaluators list + autoload * cleanup useless testing resources * cleanup + replace last eval call in VariableEvaluator * fix few Evaluators code * enhance nikic evaluators + test with/without nikic lib * Evaluator fixes/enhancements + tests * bump to nikic fork temporarly * bump nikic-parser fork + use only nikic fork evaluation + cleanup itop redondant evaluators * review with Romain: use distinct whitelists in setup time/runtime + move ModuleFileParser internal logic into ModuleFileReader * PhpExpressionEvaluator used via constructor and not as a service * dumpautoload again after rebase
556 lines
17 KiB
PHP
556 lines
17 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
namespace PhpParser;
|
|
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Identifier;
|
|
use PhpParser\Node\Name;
|
|
use PhpParser\Node\Scalar;
|
|
use Exception;
|
|
|
|
use function array_merge;
|
|
|
|
/**
|
|
* Evaluates constant expressions.
|
|
*
|
|
* This evaluator is able to evaluate all constant expressions (as defined by PHP), which can be
|
|
* evaluated without further context. If a subexpression is not of this type, a user-provided
|
|
* fallback evaluator is invoked. To support all constant expressions that are also supported by
|
|
* PHP (and not already handled by this class), the fallback evaluator must be able to handle the
|
|
* following node types:
|
|
*
|
|
* * All Scalar\MagicConst\* nodes.
|
|
* * Expr\ConstFetch nodes. Only null/false/true are already handled by this class.
|
|
* * Expr\ClassConstFetch nodes.
|
|
*
|
|
* The fallback evaluator should throw ConstExprEvaluationException for nodes it cannot evaluate.
|
|
*
|
|
* The evaluation is dependent on runtime configuration in two respects: Firstly, floating
|
|
* point to string conversions are affected by the precision ini setting. Secondly, they are also
|
|
* affected by the LC_NUMERIC locale.
|
|
*/
|
|
class ConstExprEvaluator {
|
|
/** @var callable|null */
|
|
private $fallbackEvaluator;
|
|
|
|
/** @var array $functionsWhiteList */
|
|
private $functionsWhiteList;
|
|
|
|
/** @var array $staticCallsWhitelist */
|
|
private $staticCallsWhitelist;
|
|
|
|
/**
|
|
* Create a constant expression evaluator.
|
|
*
|
|
* The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
|
|
* class doc comment for more information.
|
|
*
|
|
* @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated
|
|
*/
|
|
public function __construct(?callable $fallbackEvaluator = null) {
|
|
$this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) {
|
|
throw new ConstExprEvaluationException(
|
|
"Expression of type {$expr->getType()} cannot be evaluated"
|
|
);
|
|
};
|
|
|
|
$this->functionsWhiteList = [];
|
|
$this->staticCallsWhitelist = [];
|
|
}
|
|
|
|
public function setFunctionsWhitelist(array $functionsWhiteList): void {
|
|
$this->functionsWhiteList = $functionsWhiteList;
|
|
}
|
|
|
|
public function setStaticCallsWhitelist(array $staticCallsWhitelist): void {
|
|
$this->staticCallsWhitelist = $staticCallsWhitelist;
|
|
}
|
|
|
|
/**
|
|
* Silently evaluates a constant expression into a PHP value.
|
|
*
|
|
* Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException.
|
|
* The original source of the exception is available through getPrevious().
|
|
*
|
|
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
|
|
* constructor will be invoked. By default, if no fallback is provided, an exception of type
|
|
* ConstExprEvaluationException is thrown.
|
|
*
|
|
* See class doc comment for caveats and limitations.
|
|
*
|
|
* @param Expr $expr Constant expression to evaluate
|
|
* @return mixed Result of evaluation
|
|
*
|
|
* @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred
|
|
*/
|
|
public function evaluateSilently(Expr $expr) {
|
|
set_error_handler(function ($num, $str, $file, $line) {
|
|
throw new \ErrorException($str, 0, $num, $file, $line);
|
|
});
|
|
|
|
try {
|
|
return $this->evaluate($expr);
|
|
} catch (\Throwable $e) {
|
|
if (!$e instanceof ConstExprEvaluationException) {
|
|
$e = new ConstExprEvaluationException(
|
|
"An error occurred during constant expression evaluation", 0, $e);
|
|
}
|
|
throw $e;
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Directly evaluates a constant expression into a PHP value.
|
|
*
|
|
* May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these
|
|
* into a ConstExprEvaluationException.
|
|
*
|
|
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
|
|
* constructor will be invoked. By default, if no fallback is provided, an exception of type
|
|
* ConstExprEvaluationException is thrown.
|
|
*
|
|
* See class doc comment for caveats and limitations.
|
|
*
|
|
* @param Expr $expr Constant expression to evaluate
|
|
* @return mixed Result of evaluation
|
|
*
|
|
* @throws ConstExprEvaluationException if the expression cannot be evaluated
|
|
*/
|
|
public function evaluateDirectly(Expr $expr) {
|
|
return $this->evaluate($expr);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluate(Expr $expr) {
|
|
if ($expr instanceof Scalar\Int_
|
|
|| $expr instanceof Scalar\Float_
|
|
|| $expr instanceof Scalar\String_
|
|
) {
|
|
return $expr->value;
|
|
}
|
|
|
|
if ($expr instanceof Expr\Array_) {
|
|
return $this->evaluateArray($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\Variable) {
|
|
return $this->evaluateVariable($expr);
|
|
}
|
|
|
|
// Unary operators
|
|
if ($expr instanceof Expr\UnaryPlus) {
|
|
return +$this->evaluate($expr->expr);
|
|
}
|
|
if ($expr instanceof Expr\UnaryMinus) {
|
|
return -$this->evaluate($expr->expr);
|
|
}
|
|
if ($expr instanceof Expr\BooleanNot) {
|
|
return !$this->evaluate($expr->expr);
|
|
}
|
|
if ($expr instanceof Expr\BitwiseNot) {
|
|
return ~$this->evaluate($expr->expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\BinaryOp) {
|
|
return $this->evaluateBinaryOp($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\Ternary) {
|
|
return $this->evaluateTernary($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) {
|
|
return $this->evaluate($expr->var)[$this->evaluate($expr->dim)];
|
|
}
|
|
|
|
if ($expr instanceof Expr\ConstFetch) {
|
|
return $this->evaluateConstFetch($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\Isset_) {
|
|
return $this->evaluateIsset($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\ClassConstFetch) {
|
|
return $this->evaluateClassConstFetch($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\Cast) {
|
|
return $this->evaluateCast($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\StaticPropertyFetch) {
|
|
return $this->evaluateStaticPropertyFetch($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\FuncCall) {
|
|
return $this->evaluateFuncCall($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\StaticCall) {
|
|
return $this->evaluateStaticCall($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) {
|
|
return $this->evaluatePropertyFetch($expr);
|
|
}
|
|
|
|
if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) {
|
|
return $this->evaluateMethodCall($expr);
|
|
}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
private function evaluateArray(Expr\Array_ $expr): array {
|
|
$array = [];
|
|
foreach ($expr->items as $item) {
|
|
if (null !== $item->key) {
|
|
$array[$this->evaluate($item->key)] = $this->evaluate($item->value);
|
|
} elseif ($item->unpack) {
|
|
$array = array_merge($array, $this->evaluate($item->value));
|
|
} else {
|
|
$array[] = $this->evaluate($item->value);
|
|
}
|
|
}
|
|
return $array;
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateTernary(Expr\Ternary $expr) {
|
|
if (null === $expr->if) {
|
|
return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else);
|
|
}
|
|
|
|
return $this->evaluate($expr->cond)
|
|
? $this->evaluate($expr->if)
|
|
: $this->evaluate($expr->else);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateBinaryOp(Expr\BinaryOp $expr) {
|
|
if ($expr instanceof Expr\BinaryOp\Coalesce) {
|
|
try {
|
|
$var = $this->evaluate($expr->left);
|
|
} catch(\Throwable $t) {
|
|
//left expression cannot be evaluated (! isset for exeample)
|
|
return $this->evaluate($expr->right);
|
|
}
|
|
|
|
return $var ?? $this->evaluate($expr->right);
|
|
}
|
|
|
|
// The evaluate() calls are repeated in each branch, because some of the operators are
|
|
// short-circuiting and evaluating the RHS in advance may be illegal in that case
|
|
$l = $expr->left;
|
|
$r = $expr->right;
|
|
switch ($expr->getOperatorSigil()) {
|
|
case '&': return $this->evaluate($l) & $this->evaluate($r);
|
|
case '|': return $this->evaluate($l) | $this->evaluate($r);
|
|
case '^': return $this->evaluate($l) ^ $this->evaluate($r);
|
|
case '&&': return $this->evaluate($l) && $this->evaluate($r);
|
|
case '||': return $this->evaluate($l) || $this->evaluate($r);
|
|
case '??': return $this->evaluate($l) ?? $this->evaluate($r);
|
|
case '.': return $this->evaluate($l) . $this->evaluate($r);
|
|
case '/': return $this->evaluate($l) / $this->evaluate($r);
|
|
case '==': return $this->evaluate($l) == $this->evaluate($r);
|
|
case '>': return $this->evaluate($l) > $this->evaluate($r);
|
|
case '>=': return $this->evaluate($l) >= $this->evaluate($r);
|
|
case '===': return $this->evaluate($l) === $this->evaluate($r);
|
|
case 'and': return $this->evaluate($l) and $this->evaluate($r);
|
|
case 'or': return $this->evaluate($l) or $this->evaluate($r);
|
|
case 'xor': return $this->evaluate($l) xor $this->evaluate($r);
|
|
case '-': return $this->evaluate($l) - $this->evaluate($r);
|
|
case '%': return $this->evaluate($l) % $this->evaluate($r);
|
|
case '*': return $this->evaluate($l) * $this->evaluate($r);
|
|
case '!=': return $this->evaluate($l) != $this->evaluate($r);
|
|
case '!==': return $this->evaluate($l) !== $this->evaluate($r);
|
|
case '+': return $this->evaluate($l) + $this->evaluate($r);
|
|
case '**': return $this->evaluate($l) ** $this->evaluate($r);
|
|
case '<<': return $this->evaluate($l) << $this->evaluate($r);
|
|
case '>>': return $this->evaluate($l) >> $this->evaluate($r);
|
|
case '<': return $this->evaluate($l) < $this->evaluate($r);
|
|
case '<=': return $this->evaluate($l) <= $this->evaluate($r);
|
|
case '<=>': return $this->evaluate($l) <=> $this->evaluate($r);
|
|
case '|>':
|
|
$lval = $this->evaluate($l);
|
|
return $this->evaluate($r)($lval);
|
|
}
|
|
|
|
throw new \Exception('Should not happen');
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateConstFetch(Expr\ConstFetch $expr) {
|
|
try {
|
|
$name = $expr->name;
|
|
if(! is_string($name)) {
|
|
//PHP_VERSION_ID usecase
|
|
$name = $name->name;
|
|
}
|
|
|
|
if (defined($name)) {
|
|
return constant($name);
|
|
}
|
|
} catch(\Throwable $t) {}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/** @return bool */
|
|
private function evaluateIsset(Expr\Isset_ $expr) {
|
|
try {
|
|
foreach ($expr->vars as $var) {
|
|
$var = $this->evaluate($var);
|
|
if (! isset($var)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch(\Throwable $t) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) {
|
|
try {
|
|
$classname = $expr->class->name;
|
|
$property = $expr->name->name;
|
|
|
|
if ('class' === $property) {
|
|
return $classname;
|
|
}
|
|
|
|
if (class_exists($classname)) {
|
|
$class = new \ReflectionClass($classname);
|
|
if (array_key_exists($property, $class->getConstants())) {
|
|
$oReflectionConstant = $class->getReflectionConstant($property);
|
|
if ($oReflectionConstant->isPublic()) {
|
|
return $class->getConstant($property);
|
|
}
|
|
}
|
|
}
|
|
} catch(\Throwable $t) {}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateCast(Expr\Cast $expr) {
|
|
try {
|
|
$subexpr = $this->evaluate($expr->expr);
|
|
$type = get_class($expr);
|
|
switch ($type) {
|
|
case Expr\Cast\Array_::class:
|
|
return (array) $subexpr;
|
|
|
|
case Expr\Cast\Bool_::class:
|
|
return (bool) $subexpr;
|
|
|
|
case Expr\Cast\Double::class:
|
|
switch ($expr->getAttribute("kind")) {
|
|
case Expr\Cast\Double::KIND_DOUBLE:
|
|
return (double) $subexpr;
|
|
|
|
case Expr\Cast\Double::KIND_FLOAT:
|
|
case Expr\Cast\Double::KIND_REAL:
|
|
return (float) $subexpr;
|
|
}
|
|
|
|
break;
|
|
|
|
case Expr\Cast\Int_::class:
|
|
return (int) $subexpr;
|
|
|
|
case Expr\Cast\Object_::class:
|
|
return (object) $subexpr;
|
|
|
|
case Expr\Cast\String_::class:
|
|
return (string) $subexpr;
|
|
}
|
|
} catch(\Throwable $t) {}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) {
|
|
try {
|
|
$classname = $expr->class->name;
|
|
if ($expr->name instanceof Identifier) {
|
|
$property = $expr->name->name;
|
|
} else {
|
|
$property = $this->evaluate($expr->name);
|
|
}
|
|
|
|
if (class_exists($classname)) {
|
|
$class = new \ReflectionClass($classname);
|
|
if (array_key_exists($property, $class->getStaticProperties())) {
|
|
$oReflectionProperty = $class->getProperty($property);
|
|
if ($oReflectionProperty->isPublic()) {
|
|
return $class->getStaticPropertyValue($property);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (\Throwable $t) {}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateFuncCall(Expr\FuncCall $expr) {
|
|
try {
|
|
$name = $expr->name;
|
|
if ($name instanceof Name) {
|
|
$function = $name->name;
|
|
} else {
|
|
$function = $this->evaluate($name);
|
|
}
|
|
|
|
if (! in_array($function, $this->functionsWhiteList)) {
|
|
throw new Exception("FuncCall $function not supported");
|
|
}
|
|
|
|
$args=[];
|
|
foreach ($expr->args as $arg) {
|
|
/** @var \PhpParser\Node\Arg $arg */
|
|
$args[]=$arg->value->value;
|
|
}
|
|
|
|
$reflection_function = new \ReflectionFunction($function);
|
|
return $reflection_function->invoke(...$args);
|
|
}
|
|
catch (\Throwable $t) {}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateVariable(Expr\Variable $expr) {
|
|
try {
|
|
$name = $expr->name;
|
|
if (array_key_exists($name, get_defined_vars())) {
|
|
return $$name;
|
|
}
|
|
|
|
if (array_key_exists($name, $GLOBALS)) {
|
|
global $$name;
|
|
return $$name;
|
|
}
|
|
} catch (\Throwable $t) {
|
|
}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/** @return mixed */
|
|
private function evaluateStaticCall(Expr\StaticCall $expr) {
|
|
try {
|
|
$class = $expr->class->name;
|
|
if ($expr->name instanceof Identifier) {
|
|
$method = $expr->name->name;
|
|
} else {
|
|
$method = $this->evaluate($expr->name);
|
|
}
|
|
|
|
$static_call_description = "$class::$method";
|
|
if (! in_array($static_call_description, $this->staticCallsWhitelist)) {
|
|
throw new Exception("StaticCall $static_call_description not supported");
|
|
}
|
|
|
|
$args=[];
|
|
foreach ($expr->args as $arg) {
|
|
/** @var \PhpParser\Node\Arg $arg */
|
|
$args[]=$arg->value->value;
|
|
}
|
|
|
|
$class = new \ReflectionClass($class);
|
|
$method = $class->getMethod($method);
|
|
if ($method->isPublic()) {
|
|
return $method->invokeArgs(null, $args);
|
|
}
|
|
} catch (\Throwable $t) {}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/**
|
|
* @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr
|
|
*
|
|
* @return mixed
|
|
*/
|
|
private function evaluatePropertyFetch($expr) {
|
|
try {
|
|
$var = $this->evaluate($expr->var);
|
|
} catch (\Throwable $t) {
|
|
$var = null;
|
|
}
|
|
|
|
if (! is_null($var)) {
|
|
try {
|
|
if ($expr->name instanceof Identifier) {
|
|
$name = $expr->name->name;
|
|
} else {
|
|
$name = $this->evaluate($expr->name);
|
|
}
|
|
|
|
$reflectionClass = new \ReflectionClass(get_class($var));
|
|
$property = $reflectionClass->getProperty($name);
|
|
if ($property->isPublic()) {
|
|
return $property->getValue($var);
|
|
}
|
|
}
|
|
catch (\Throwable $t) {}
|
|
} else if ($expr instanceof Expr\NullsafePropertyFetch) {
|
|
return null;
|
|
}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
|
|
/**
|
|
* @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr
|
|
*
|
|
* @return mixed
|
|
*/
|
|
private function evaluateMethodCall($expr) {
|
|
try {
|
|
$var = $this->evaluate($expr->var);
|
|
} catch (\Throwable $t) {
|
|
$var = null;
|
|
}
|
|
|
|
if (! is_null($var)) {
|
|
try {
|
|
$args = [];
|
|
foreach ($expr->args as $arg) {
|
|
/** @var \PhpParser\Node\Arg $arg */
|
|
$args[] = $arg->value->value;
|
|
}
|
|
|
|
if ($expr->name instanceof Identifier) {
|
|
$name = $expr->name->name;
|
|
} else {
|
|
$name = $this->evaluate($expr->name);
|
|
}
|
|
|
|
$reflectionClass = new \ReflectionClass(get_class($var));
|
|
$method = $reflectionClass->getMethod($name);
|
|
if ($method->isPublic()) {
|
|
return $method->invokeArgs($var, $args);
|
|
}
|
|
}
|
|
catch (\Throwable $t) {}
|
|
} else if ($expr instanceof Expr\NullsafeMethodCall) {
|
|
return null;
|
|
}
|
|
|
|
return ($this->fallbackEvaluator)($expr);
|
|
}
|
|
}
|