diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 5d4cbf00e..c1e4be895 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -79,6 +79,17 @@ class ModuleDiscoveryService { return $aModuleInfo; } + /** + * @param string $sPhpContent + * + * @return \PhpParser\Node\Stmt[]|null + */ + public function parsePhpCode(string $sPhpContent): ?array + { + $oParser = (new ParserFactory())->createForNewestSupportedVersion(); + return $oParser->parse($sPhpContent); + } + /** * Read the information from a module file (module.xxx.php) * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles @@ -90,8 +101,7 @@ class ModuleDiscoveryService { { try { - $oParser = (new ParserFactory())->createForNewestSupportedVersion(); - $aNodes = $oParser->parse(file_get_contents($sModuleFilePath)); + $aNodes = $this->parsePhpCode(file_get_contents($sModuleFilePath)); } catch (PhpParser\Error $e) { throw new \ModuleDiscoveryServiceException($e->getMessage(), 0, $e, $sModuleFilePath); @@ -124,24 +134,6 @@ class ModuleDiscoveryService { throw new ModuleDiscoveryServiceException("No proper call to SetupWebPage::AddModule found in module file", 0, null, $sModuleFilePath); } - /** - * @param string $sBooleanExpr - * - * @return bool - * @throws ModuleDiscoveryServiceException - */ - public function ComputeBooleanExpression(string $sBooleanExpr) : bool - { - $bResult = false; - try{ - @eval('$bResult = '.$sBooleanExpr.';'); - } catch (Throwable $t) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); - } - - return $bResult; - } - private function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleConfig) : void { $iIndex=0; @@ -242,7 +234,7 @@ class ModuleDiscoveryService { return [ $sModuleFilePath, $sModuleId, - $aModuleConfig + $aModuleConfig, ]; } @@ -268,20 +260,27 @@ class ModuleDiscoveryService { return null; } - foreach ($oNode->elseifs as $oElseIfSubNode) { - /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode*/ - $bCondition = $this->EvaluateBooleanExpression($oElseIfSubNode->cond); - if($bCondition){ - $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->stmts); - if (!is_null($aModuleConfig)) { - return $aModuleConfig; + if (! is_null($oNode->elseifs)) { + foreach ($oNode->elseifs as $oElseIfSubNode) { + /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ + $bCondition = $this->EvaluateBooleanExpression($oElseIfSubNode->cond); + if ($bCondition) { + $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oElseIfSubNode->stmts); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + break; } - break; } } - $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); - return $aModuleConfig; + if (! is_null($oNode->else)) { + $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); + + return $aModuleConfig; + } + + return null; } @@ -311,11 +310,96 @@ class ModuleDiscoveryService { return $bResult; } + private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr $oExpr) : string + { + if ($oExpr instanceof \PhpParser\Node\Scalar\Int_ || $oExpr instanceof \PhpParser\Node\Scalar\Float_){ + return "" . $oExpr->value; + } + + return $this->EvaluateBooleanExpression($oExpr) ? "true" : "false"; + } + + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + public function ComputeBooleanExpression(string $sBooleanExpr) : bool + { + $bResult = false; + try{ + @eval('$bResult = '.$sBooleanExpr.';'); + } catch (Throwable $t) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); + } + + return $bResult; + } + + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + public function ComputeBooleanExpression3(string $sBooleanExpr) : bool + { + $sPhpContent = <<parsePhpCode($sPhpContent); + $oExpr = $aNodes[0]; + return $this->EvaluateBooleanExpression($oExpr->expr); + } + private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression) : bool { //var_dump($oCondExpression); + + if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ + $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) + . " " + . $oCondExpression->getOperatorSigil() + . " " + . $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->right); + return $this->ComputeBooleanExpression($sExpr); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\BooleanNot){ + return ! $this->EvaluateBooleanExpression($oCondExpression->expr); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\FuncCall){ + return $this->CallFunction($oCondExpression); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\ConstFetch){ + return $this->EvaluateConstantExpression($oCondExpression); + } + return true; } + + private function CallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool + { + $sFunction = $oFunct->name->name; + $aWhiteList = ["function_exists"]; + if (! in_array($sFunction, $aWhiteList)){ + throw new ModuleDiscoveryServiceException("FuncCall $sFunction not supported"); + //return false; + } + + $aArgs=[]; + foreach ($oFunct->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $oReflectionFunction = new ReflectionFunction($sFunction); + return (bool)$oReflectionFunction->invoke(...$aArgs); + } } class ModuleDiscoveryServiceException extends Exception diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php index 52d5be98e..da760f094 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -4,9 +4,11 @@ namespace Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use ModuleDiscoveryService; +use PhpParser\ParserFactory; class ModuleDiscoveryServiceTest extends ItopDataTestCase { + private string $sTempModuleFilePath; protected function setUp(): void { parent::setUp(); @@ -26,6 +28,16 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase $this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null); } + /*public function testAllReadModuleFileConfiguration() + { + foreach (glob(__DIR__.'/resources/all/module.*.php') as $sModuleFilePath){ + $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + + $this->assertEquals($aExpected, $aRes); + } + }*/ + public function testReadModuleFileConfiguration() { $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; @@ -79,4 +91,251 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error: Undefined constant "a"'); $this->assertTrue(ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression("(a || true)")); } + + public function testEvaluateConstantExpression() + { + $sPHP = <<parsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateConstantExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->expr]); + $this->assertEquals(APPROOT, $val); + } + + public function CallReadModuleFileConfiguration($sPHpCode) + { + $this->sTempModuleFilePath = tempnam(__DIR__, "test"); + file_put_contents($this->sTempModuleFilePath, $sPHpCode); + try { + return $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "ReadModuleFileConfiguration", ModuleDiscoveryService::GetInstance(), [$this->sTempModuleFilePath]); + } + finally { + @unlink($this->sTempModuleFilePath); + } + + } + + public function testReadModuleFileConfigurationCheckBasicStatementWithoutIf() + { + $sPHP = << "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "noif", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_IfConditionVerified() + { + $sPHP = << "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "if", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_IfNoConditionVerifiedAndNoElse() + { + $sPHP = << "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "outsideif", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_ElseApplied() + { + $sPHP = << "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "else", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_FirstElseIfApplied() + { + $sPHP = << "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "elseif1", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApplied() + { + $sPHP = << "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "elseif2", ["c" => "d"]], $val); + } + + public static function EvaluateExpressionBooleanProvider() { + $sTruePHP = << [ + "code" => str_replace("COND", "true", $sTruePHP), + "bool_expected" => true + + ], + "false" => [ + "code" => str_replace("COND", "false", $sTruePHP), + "bool_expected" => false + + ], + "not ok" => [ + "code" => str_replace("COND", "! false", $sTruePHP), + "bool_expected" => true + + ], + "not ko" => [ + "code" => str_replace("COND", "! (true)", $sTruePHP), + "bool_expected" => false + + ], + "AND ko" => [ + "code" => str_replace("COND", "true && false", $sTruePHP), + "bool_expected" => false + + ], + "AND ok1" => [ + "code" => str_replace("COND", "true && true", $sTruePHP), + "bool_expected" => true + + ], + "AND ko2" => [ + "code" => str_replace("COND", "true && true && false", $sTruePHP), + "bool_expected" => false + + ], + "OR ko" => [ + "code" => str_replace("COND", "false || false", $sTruePHP), + "bool_expected" => false + + ], + "OR ok" => [ + "code" => str_replace("COND", "false ||true", $sTruePHP), + "bool_expected" => true + + ], + "OR ok2" => [ + "code" => str_replace("COND", "false ||false||true", $sTruePHP), + "bool_expected" => true + + ], + "function_exists('ldap_connect')" => [ + "code" => str_replace("COND", "function_exists('ldap_connect')", $sTruePHP), + "bool_expected" => function_exists('ldap_connect') + + ], + "function_exists('gabuzomeushouldnotexist')" => [ + "code" => str_replace("COND", "function_exists('gabuzomeushouldnotexist')", $sTruePHP), + "bool_expected" => function_exists('gabuzomeushouldnotexist') + + ], + "1 > 2" => [ + "code" => str_replace("COND", "1 > 2", $sTruePHP), + "bool_expected" => false + + ], + "1 == 1" => [ + "code" => str_replace("COND", "1 == 1", $sTruePHP), + "bool_expected" => true + + ], + "1 < 2" => [ + "code" => str_replace("COND", "1 < 2", $sTruePHP), + "bool_expected" => true + ], + "PHP_VERSION_ID == PHP_VERSION_ID" => [ + "code" => str_replace("COND", "PHP_VERSION_ID == PHP_VERSION_ID", $sTruePHP), + "bool_expected" => true + ], + "PHP_VERSION_ID != PHP_VERSION_ID" => [ + "code" => str_replace("COND", "PHP_VERSION_ID != PHP_VERSION_ID", $sTruePHP), + "bool_expected" => false + ], + ]; + } + + /** + * @dataProvider EvaluateExpressionBooleanProvider + */ + public function testEvaluateExpressionBoolean($sPHP, $bExpected) + { + $aNodes = ModuleDiscoveryService::GetInstance()->parsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateBooleanExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->cond]); + $this->assertEquals($bExpected, $val); + } } \ No newline at end of file