diff --git a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php index 952678714..79de55005 100644 --- a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php @@ -3,7 +3,14 @@ namespace PhpParser; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use PhpParser\Node\Scalar; +use Exception; use function array_merge; @@ -30,7 +37,13 @@ class ConstExprEvaluator { /** @var callable|null */ private $fallbackEvaluator; - /** + /** @var array $functions_whitelist */ + private $functions_whitelist; + + /** @var array staticcalls_whitelist */ + private $staticcalls_whitelist; + + /** * Create a constant expression evaluator. * * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See @@ -44,8 +57,21 @@ class ConstExprEvaluator { "Expression of type {$expr->getType()} cannot be evaluated" ); }; + + $this->functions_whitelist=[]; + $this->staticcalls_whitelist=[]; } + public function setFunctionsWhitelist(array $functions_whitelist): void + { + $this->functions_whitelist = $functions_whitelist; + } + + public function setStaticcallsWhitelist(array $staticcalls_whitelist): void + { + $this->staticcalls_whitelist = $staticcalls_whitelist; + } + /** * Silently evaluates a constant expression into a PHP value. * @@ -145,6 +171,41 @@ class ConstExprEvaluator { 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\Variable) { + return $this->evaluateVariable($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); } @@ -225,13 +286,253 @@ class ConstExprEvaluator { /** @return mixed */ private function evaluateConstFetch(Expr\ConstFetch $expr) { - $name = $expr->name->toLowerString(); - switch ($name) { - case 'null': return null; - case 'false': return false; - case 'true': return true; - } + 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 mixed */ + 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 { + if ($expr->name instanceof Name){ + $function = $expr->name->name; + } else { + $function = $this->evaluate($expr->name); + } + + if (! in_array($function, $this->functions_whitelist)){ + 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 (! is_null($name) && isset($name)) { + global $$name; + return $$name; + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateStaticCall(Expr\StaticCall $expr) + { + try { + $classname = $expr->class->name; + if ($expr->name instanceof Identifier){ + $methodname = $expr->name->name; + } else { + $methodname = $this->evaluate($expr->name); + } + + $static_call_description = "$classname::$methodname"; + if (! in_array($static_call_description, $this->staticcalls_whitelist)){ + 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($classname); + $method = $class->getMethod($methodname); + if ($method->isPublic()){ + return $method->invokeArgs(null, $args); + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluatePropertyFetch(Expr\NullsafePropertyFetch|Expr\PropertyFetch $expr) + { + try { + $var = $this->evaluateVariable($expr->var); + if (is_null($var)) { + return null; + } + + 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) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateMethodCall(Expr\MethodCall|Expr\NullsafeMethodCall $expr) + { + try { + $var = $this->evaluateVariable($expr->var); + if (is_null($var)) { + return null; + } + + $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) {} + + return ($this->fallbackEvaluator)($expr); + } + } diff --git a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php index 9c6ed56b8..ed1d07fd7 100644 --- a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php @@ -12,23 +12,10 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase { public static function EvaluateExpressionProvider() { return [ - 'ConstFetch: false' => [ 'sExpression' => 'false'], - 'ConstFetch: (false)' => [ 'sExpression' => 'false'], - 'ConstFetch: true' => [ 'sExpression' => 'true'], - 'ConstFetch: (true)' => [ 'sExpression' => 'true'], - 'ClassConstFetch: public existing constant' => [ 'sExpression' => 'SetupUtils::PHP_MIN_VERSION'], - 'ClassConstFetch: unknown constant' => [ 'sExpression' => 'SetupUtils::UNKNOWN_CONSTANT'], - 'ClassConstFetch: unknown class:constant' => [ 'sExpression' => 'GabuZomeuUnknownClass::UNKNOWN_CONSTANT'], - 'ClassConstFetch: unknown class:class' => [ 'sExpression' => 'GabuZomeuUnknownClass::class'], - 'ClassConstFetch: private existing constant' => [ - 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::PRIVATE_CONSTANT', - 'forced_expected' => null, - ], - 'StaticProperty: public existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$STATIC_PROPERTY'], - 'StaticProperty: private existing constant' => [ - 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$PRIVATE_STATIC_PROPERTY', - 'forced_expected' => null, - ], + 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], + 'Array: ["a"]' => ['sExpression' => '["a"]'], + 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], + 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], 'BinaryOperator: false|true' => [ 'sExpression' => 'false|true'], 'BinaryOperator: false||true' => [ 'sExpression' => 'false||true'], 'BinaryOperator: false&&true' => [ 'sExpression' => 'false&&true'], @@ -41,36 +28,8 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase { 'BinaryOperator: 1 <= 1' => [ 'sExpression' => '1 <= 1'], 'BinaryOperator: PHP_VERSION_ID == PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID == PHP_VERSION_ID'], 'BinaryOperator: PHP_VERSION_ID != PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID != PHP_VERSION_ID'], - 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], - 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], - 'UnaryMinus: -1' => ['sExpression' => '-1'], - 'UnaryPlus: +1' => ['sExpression' => '+1'], - 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], - 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], - //'Variable: $_SERVER' => ['sExpression' => '$_SERVER'], - 'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar'], - 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], - 'Array: ["a"]' => ['sExpression' => '["a"]'], - 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], - 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'], - 'NullsafePropertyFetch: $oNullVar?->b' => ['sExpression' => '$oNullVar?->b'], - 'NullsafePropertyFetch: $oEvaluationFakeClass?->bIsOk' => ['sExpression' => '$oEvaluationFakeClass?->bIsOk'], - 'PropertyFetch: $oEvaluationFakeClass->bIsOk' => ['sExpression' => '$oEvaluationFakeClass->bIsOk'], - 'NullsafeMethodCall: $oEvaluationFakeClass?->GetName()' => ['sExpression' => '$oEvaluationFakeClass?->GetName()'], - 'NullsafeMethodCall: $oEvaluationFakeClass?->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass?->GetLongName("aa")'], - 'MethodCall: $oEvaluationFakeClass->GetName()' => ['sExpression' => '$oEvaluationFakeClass->GetName()'], - 'MethodCall: $oEvaluationFakeClass->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass->GetLongName("aa")'], - 'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1'], - 'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1'], - 'Isset: isset($a)' => ['sExpression' => 'isset($a)'], - 'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)'], - 'Isset: isset($_SERVER)' => ['sExpression' => 'isset($_SERVER)'], - 'Isset: isset($_SERVER, $a)' => ['sExpression' => 'isset($_SERVER, $a)'], 'BitwiseNot: ~3' => ['sExpression' => '~3'], - 'Mod: 3%2' => ['sExpression' => '3%2'], 'BitwiseXor: 3^2' => ['sExpression' => '3^2'], - 'Ternary: (true) ? 1 : 2' => ['sExpression' => '(true) ? 1 : 2'], - 'Ternary: (false) ? 1 : 2' => ['sExpression' => '(false) ? 1 : 2'], 'Cast: (array)3' => ['sExpression' => '(array)3'], 'Cast: (bool)1' => ['sExpression' => '(bool)1'], 'Cast: (bool)0' => ['sExpression' => '(bool)0'], @@ -78,24 +37,96 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase { 'Cast: (float)3' => ['sExpression' => '(float)3'], 'Cast: (int)3' => ['sExpression' => '(int)3'], 'Cast: (object)3' => ['sExpression' => '(object)3'], - 'Cast: (string)$oEvaluationFakeClass' => ['sExpression' => '(string)$oEvaluationFakeClass'], + 'Cast: (string) $oEvaluationFakeClass' => ['sExpression' => '(string) $oEvaluationFakeClass', "toString"], + 'ClassConstFetch: public existing constant' => [ 'sExpression' => 'SetupUtils::PHP_MIN_VERSION'], + 'ClassConstFetch: unknown constant' => [ 'sExpression' => 'SetupUtils::UNKNOWN_CONSTANT'], + 'ClassConstFetch: unknown class:constant' => [ 'sExpression' => 'GabuZomeuUnknownClass::UNKNOWN_CONSTANT'], + 'ClassConstFetch: unknown class:class' => [ 'sExpression' => 'GabuZomeuUnknownClass::class'], + 'ClassConstFetch: private existing constant' => [ + 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::PRIVATE_CONSTANT', + 'forced_expected' => null, + ], + 'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1', 1], + 'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1', 1], + 'Coalesce: $oGlobalNonNullVar ?? 1' => ['sExpression' => '$oGlobalNonNullVar ?? 1', "a"], + 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], + 'ConstFetch: false' => [ 'sExpression' => 'false'], + 'ConstFetch: (false)' => [ 'sExpression' => 'false'], + 'ConstFetch: true' => [ 'sExpression' => 'true'], + 'ConstFetch: (true)' => [ 'sExpression' => 'true'], + 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], + 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], + 'Isset: isset($oNonNullVar)' => ['sExpression' => 'isset($oNonNullVar)', false], + 'Isset: isset($oGlobalNonNullVar)' => ['sExpression' => 'isset($oGlobalNonNullVar)', true], + 'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)', false], + 'Isset: isset($_SERVER)' => ['sExpression' => 'isset($_SERVER)', true], + 'Isset: isset($_SERVER, $a)' => ['sExpression' => 'isset($_SERVER, $a)', false], + 'Isset: isset($oGlobalNonNullVar, $_SERVER)' => ['sExpression' => 'isset($oGlobalNonNullVar, $_SERVER)', true], + 'MethodCall: $oEvaluationFakeClass->GetName()' => ['sExpression' => '$oEvaluationFakeClass->GetName()', "gabuzomeu"], + 'MethodCall: $oEvaluationFakeClass->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass->GetLongName("aa")', "gabuzomeu_aa"], + 'Mod: 3%2' => ['sExpression' => '3%2'], + 'NullsafeMethodCall: $oNullVar?->GetName()' => ['sExpression' => '$oNullVar?->GetName()', null], + 'NullsafeMethodCall: $oNullVar?->GetLongName("aa")' => ['sExpression' => '$oNullVar?->GetLongName("aa")', null], + 'NullsafeMethodCall: $oEvaluationFakeClass?->GetName()' => ['sExpression' => '$oEvaluationFakeClass?->GetName()', "gabuzomeu"], + 'NullsafeMethodCall: $oEvaluationFakeClass?->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass?->GetLongName("aa")', "gabuzomeu_aa"], + 'NullsafePropertyFetch: $oNullVar?->b' => ['sExpression' => '$oNullVar?->b', null], + 'NullsafePropertyFetch: $oEvaluationFakeClass?->iIsOk' => ['sExpression' => '$oEvaluationFakeClass?->iIsOk', "IsOkValue"], + 'PropertyFetch: $oEvaluationFakeClass->iIsOk' => ['sExpression' => '$oEvaluationFakeClass->iIsOk', "IsOkValue"], + 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'], + 'StaticProperty: public existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Sources\PhpParser\Evaluation\PhpExpressionEvaluatorTest::$STATIC_PROPERTY'], + 'StaticProperty: private existing constant' => [ + 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$PRIVATE_STATIC_PROPERTY', + 'forced_expected' => null, + ], + 'Ternary: (true) ? 1 : 2' => ['sExpression' => '(true) ? 1 : 2'], + 'Ternary: (false) ? 1 : 2' => ['sExpression' => '(false) ? 1 : 2'], + 'UnaryMinus: -1' => ['sExpression' => '-1'], + 'UnaryPlus: +1' => ['sExpression' => '+1'], + 'Variable: $_SERVER' => ['sExpression' => '$_SERVER', ['toto' => 'titi']], + 'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar', null], + 'Variable: $oGlobalNonNullVar' => ['sExpression' => '$oGlobalNonNullVar', "a"], ]; } /** * @dataProvider EvaluateExpressionProvider */ - public function testEvaluateExpression($sExpression, $forced_expected="NOTPROVIDED") + public function testEvaluateExpressionWithItopAlgo($sExpression, $forced_expected="NOTPROVIDED") { - $oNullVar=null; + $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::ITOP_ALGO); + } + + /** + * @dataProvider EvaluateExpressionProvider + */ + public function testEvaluateExpressionWithLibAndItopFallback($sExpression, $forced_expected="NOTPROVIDED") + { + $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::LIB_AND_FALLBACK); + } + + /** + * @dataProvider EvaluateExpressionProvider + */ + public function testEvaluateExpressionWithLibOnly($sExpression, $forced_expected="NOTPROVIDED") + { + $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::LIB_ONLY); + } + + public function evaluateExpressionWithMode($sExpression, $forced_expected, $iMode) + { + global $oGlobalNonNullVar; + $oGlobalNonNullVar="a"; $oNonNullVar="a"; + + $oNullVar=null; $_SERVER=[ 'toto' => 'titi', ]; + global $oEvaluationFakeClass; $oEvaluationFakeClass = new EvaluationFakeClass(); - $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression); + $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression, $iMode); if ($forced_expected === "NOTPROVIDED"){ $this->assertEquals($this->UnprotectedComputeExpression($sExpression), $res, $sExpression); } else { @@ -159,7 +190,7 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase { } class EvaluationFakeClass { - public bool $bIsOk=true; + public string $iIsOk="IsOkValue"; public function GetName() { @@ -173,6 +204,6 @@ class EvaluationFakeClass { public function __toString(): string { - return "a"; + return "toString"; } } \ No newline at end of file