diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 84ef710d9..851a63bdd 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -304,7 +304,7 @@ class iTopExtensionsMap { // Found a module try { - $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sSearchDir.'/'.$sFile); + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sSearchDir.'/'.$sFile); } catch(ModuleFileReaderException $e){ continue; } diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 234b53461..8d2aaf1e1 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -503,7 +503,7 @@ class ModuleDiscovery self::SetModulePath($sRelDir); $sModuleFilePath = $sDirectory.'/'.$sFile; try { - $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sDirectory.'/'.$sFile); + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sDirectory.'/'.$sFile); SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo[2]); } catch(ModuleFileReaderException $e){ continue; diff --git a/setup/modulediscovery/ModuleFileParser.php b/setup/modulediscovery/ModuleFileParser.php index 41ef5d250..7432a621b 100644 --- a/setup/modulediscovery/ModuleFileParser.php +++ b/setup/modulediscovery/ModuleFileParser.php @@ -72,9 +72,7 @@ class ModuleFileParser { throw new ModuleFileReaderException("2nd parameter to SetupWebPage::AddModule not a string: " . get_class($oModuleId->value), 0, null, $sModuleFilePath); } - /** @var PhpParser\Node\Scalar\String_ $sModuleIdStringObj */ - $sModuleIdStringObj = $oModuleId->value; - $sModuleId = $sModuleIdStringObj->value; + $sModuleId = $oModuleId->value->value; $oModuleConfigInfo = $aArgs[2]; if (false === ($oModuleConfigInfo instanceof PhpParser\Node\Arg)) { @@ -92,6 +90,7 @@ class ModuleFileParser { if (! is_array($aModuleConfig)){ throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); } + return [ $sModuleFilePath, $sModuleId, @@ -102,37 +101,40 @@ class ModuleFileParser { public function FillModuleInformationFromArray(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleInformation) : void { $iIndex=0; + /** @var \PhpParser\Node\Expr\ArrayItem $oValue */ foreach ($oArray->items as $oArrayItem){ if ($oArrayItem->key instanceof PhpParser\Node\Scalar\String_) { //dictionnary $sKey = $oArrayItem->key->value; } else if ($oArrayItem->key instanceof \PhpParser\Node\Expr\ConstFetch) { + //dictionnary $sKey = $this->EvaluateConstantExpression($oArrayItem->key); if (is_null($sKey)){ continue; } - }else { + } else { + //array $sKey = $iIndex++; } $oValue = $oArrayItem->value; - if ($oValue instanceof PhpParser\Node\Expr\Array_) { $aSubConfig=[]; $this->FillModuleInformationFromArray($oValue, $aSubConfig); $aModuleInformation[$sKey]=$aSubConfig; - } - - if ($oValue instanceof PhpParser\Node\Scalar\String_||$oValue instanceof PhpParser\Node\Scalar\Int_) { - $aModuleInformation[$sKey]=$oValue->value; continue; } - if ($oValue instanceof \PhpParser\Node\Expr\ConstFetch) { - $oEvaluatedConstant = $this->EvaluateConstantExpression($oValue); - $aModuleInformation[$sKey]= $oEvaluatedConstant; + try { + $oEvaluatuedValue = $this->EvaluateExpression($oValue); + } catch(ModuleFileReaderException $e){ + //required to support legacy below dump dependency + //'dependencies' => ['itop-config-mgmt/2.0.0'||'itop-structure/3.0.0'] + continue; } + + $aModuleInformation[$sKey]=$oEvaluatuedValue; } } @@ -155,6 +157,7 @@ class ModuleFileParser { } } } + return null; } @@ -163,19 +166,13 @@ class ModuleFileParser { /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ $bCondition = $this->EvaluateExpression($oElseIfSubNode->cond); if ($bCondition) { - $aModuleConfig = $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oElseIfSubNode->stmts); - if (!is_null($aModuleConfig)) { - return $aModuleConfig; - } - break; + return $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oElseIfSubNode->stmts); } } } if (! is_null($oNode->else)) { - $aModuleConfig = $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oNode->else->stmts); - - return $aModuleConfig; + return $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oNode->else->stmts); } return null; @@ -195,59 +192,74 @@ class ModuleFileParser { return null; } - //TODO replace eval public function EvaluateConstantExpression(\PhpParser\Node\Expr\ArrayItem|\PhpParser\Node\Expr\ConstFetch $oValue) : mixed { - $bResult = false; - try{ - @eval('$bResult = '.$oValue->name.';'); - } catch (Throwable $t) { - throw new ModuleFileReaderException("Eval of ' . $oValue->name . ' caused an error: ".$t->getMessage()); - } - - return $bResult; + return $this->UnprotectedComputeBooleanExpression($oValue->name); } - private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr $oExpr) : string + public function EvaluateClassConstantExpression(\PhpParser\Node\Expr\ClassConstFetch $oValue) : mixed { - if ($oExpr instanceof \PhpParser\Node\Scalar\Int_ || $oExpr instanceof \PhpParser\Node\Scalar\Float_){ - return "" . $oExpr->value; + $sClassName = $oValue->class->name; + $sProperty = $oValue->name->name; + if (class_exists($sClassName)){ + $class = new \ReflectionClass($sClassName); + if (array_key_exists($sProperty, $class->getConstants())) { + $oReflectionConstant = $class->getReflectionConstant($sProperty); + if ($oReflectionConstant->isPublic()){ + return $class->getConstant($sProperty); + } + } } - return $this->EvaluateExpression($oExpr) ? "true" : "false"; + if ('class' === $sProperty){ + return $sClassName; + } + + return null; + } + + public function EvaluateStaticPropertyExpression(\PhpParser\Node\Expr\StaticPropertyFetch $oValue) : mixed + { + $sClassName = $oValue->class->name; + $sProperty = $oValue->name->name; + if (class_exists($sClassName)){ + $class = new \ReflectionClass($sClassName); + if (array_key_exists($sProperty, $class->getStaticProperties())) { + $oReflectionProperty = $class->getProperty($sProperty); + if ($oReflectionProperty->isPublic()){ + return $class->getStaticPropertyValue($sProperty); + } + } + } + + return null; } /** * @param string $sBooleanExpr * - * @return bool + * @return mixed * @throws ModuleFileReaderException */ - private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : bool + private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : mixed { - $bResult = false; try{ + $bResult = null; @eval('$bResult = '.$sBooleanExpr.';'); + return $bResult; } catch (Throwable $t) { throw new ModuleFileReaderException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); } - - return $bResult; } /** * @param string $sBooleanExpr - * @param bool $bSafe: when true, evaluation relies on unsafe eval() call * * @return bool * @throws ModuleFileReaderException */ - public function EvaluateBooleanExpression(string $sBooleanExpr, $bSafe=true) : bool + public function EvaluateBooleanExpression(string $sBooleanExpr) : bool { - if (! $bSafe){ - return $this->UnprotectedComputeBooleanExpression($sBooleanExpr); - } - $sPhpContent = <<ParsePhpCode($sPhpContent); $oExpr = $aNodes[0]; - return $this->EvaluateExpression($oExpr->expr); + $oRes = $this->EvaluateExpression($oExpr->expr); + + return (bool) $oRes; + } catch (Throwable $t) { throw new ModuleFileReaderException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); } } - private function EvaluateExpression(\PhpParser\Node\Expr $oCondExpression) : bool + private function GetMixedValueToString(mixed $oExpr) : string { - if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ - $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) - . " " - . $oCondExpression->getOperatorSigil() - . " " - . $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->right); - return $this->EvaluateBooleanExpression($sExpr, false); + if (false === $oExpr){ + return "false"; } - if ($oCondExpression instanceof \PhpParser\Node\Expr\BooleanNot){ - return ! $this->EvaluateExpression($oCondExpression->expr); + if (true === $oExpr){ + return "true"; } - if ($oCondExpression instanceof \PhpParser\Node\Expr\FuncCall){ - return $this->EvaluateCallFunction($oCondExpression); - } - - if ($oCondExpression instanceof \PhpParser\Node\Expr\StaticCall){ - return $this->EvaluateStaticCallFunction($oCondExpression); - } - - if ($oCondExpression instanceof \PhpParser\Node\Expr\ConstFetch){ - return $this->EvaluateConstantExpression($oCondExpression); - } - - return true; + return $oExpr; } - private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool + private function EvaluateExpression(\PhpParser\Node\Expr $oExpression) : mixed + { + if ($oExpression instanceof \PhpParser\Node\Expr\BinaryOp){ + $sExpr = sprintf("%s %s %s", + $this->GetMixedValueToString($this->EvaluateExpression($oExpression->left)), + $oExpression->getOperatorSigil(), + $this->GetMixedValueToString($this->EvaluateExpression($oExpression->right)) + ); + //return $this->UnprotectedComputeBooleanExpression($sBooleanExpr);; + return $this->UnprotectedComputeBooleanExpression($sExpr); + } + + if ($oExpression instanceof \PhpParser\Node\Expr\BooleanNot){ + return ! $this->EvaluateExpression($oExpression->expr); + } + + if ($oExpression instanceof \PhpParser\Node\Expr\FuncCall){ + return $this->EvaluateCallFunction($oExpression); + } + + if ($oExpression instanceof \PhpParser\Node\Expr\StaticCall){ + return $this->EvaluateStaticCallFunction($oExpression); + } + + if ($oExpression instanceof \PhpParser\Node\Expr\ConstFetch){ + return $this->EvaluateConstantExpression($oExpression); + } + + if ($oExpression instanceof \PhpParser\Node\Expr\ClassConstFetch) { + return $this->EvaluateClassConstantExpression($oExpression); + } + + if ($oExpression instanceof \PhpParser\Node\Expr\StaticPropertyFetch) { + return $this->EvaluateStaticPropertyExpression($oExpression); + } + + return $oExpression->value; + } + + private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : mixed { $sFunction = $oFunct->name->name; $aWhiteList = ["function_exists", "class_exists", "method_exists"]; @@ -306,17 +343,17 @@ PHP; } $oReflectionFunction = new ReflectionFunction($sFunction); - return (bool)$oReflectionFunction->invoke(...$aArgs); + return $oReflectionFunction->invoke(...$aArgs); } /** * @param \PhpParser\Node\Expr\StaticCall $oStaticCall * - * @return bool + * @return mixed * @throws \ModuleFileReaderException * @throws \ReflectionException */ - private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : bool + private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : mixed { $sClassName = $oStaticCall->class->name; $sMethodName = $oStaticCall->name->name; @@ -338,6 +375,6 @@ PHP; throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not public"); } - return (bool) $method->invokeArgs(null, $aArgs); + return $method->invokeArgs(null, $aArgs); } } \ No newline at end of file diff --git a/setup/modulediscovery/ModuleFileReader.php b/setup/modulediscovery/ModuleFileReader.php index b6c23be14..e2229f680 100644 --- a/setup/modulediscovery/ModuleFileReader.php +++ b/setup/modulediscovery/ModuleFileReader.php @@ -27,12 +27,58 @@ class ModuleFileReader { /** * Read the information from a module file (module.xxx.php) - * Use this method to load the ModuleInstallerAPI * @param string $sModuleFile * @return array * @throws ModuleFileReaderException */ - public function ReadModuleFileConfigurationUnsafe(string $sModuleFilePath) : array + public function ReadModuleFileInformation(string $sModuleFilePath) : array + { + try + { + $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode(file_get_contents($sModuleFilePath)); + } + catch (PhpParser\Error $e) { + throw new \ModuleFileReaderException($e->getMessage(), 0, $e, $sModuleFilePath); + } + + try { + foreach ($aNodes as $sKey => $oNode) { + if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { + $aModuleInfo = ModuleFileParser::GetInstance()->GetModuleInformationFromAddModuleCall($sModuleFilePath, $oNode); + if (! is_null($aModuleInfo)){ + $this->CompleteModuleInfoWithFilePath($aModuleInfo); + return $aModuleInfo; + } + } + + if ($oNode instanceof PhpParser\Node\Stmt\If_) { + $aModuleInfo = ModuleFileParser::GetInstance()->GetModuleInformationFromIf($sModuleFilePath, $oNode); + if (! is_null($aModuleInfo)){ + $this->CompleteModuleInfoWithFilePath($aModuleInfo); + return $aModuleInfo; + } + } + } + } catch(ModuleFileReaderException $e) { + // Continue... + throw $e; + } catch(Exception $e) { + // Continue... + throw new ModuleFileReaderException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e, $sModuleFilePath); + } + + throw new ModuleFileReaderException("No proper call to SetupWebPage::AddModule found in module file", 0, null, $sModuleFilePath); + } + + /** + * Read the information from a module file (module.xxx.php) + * Warning: this method is using eval() function to load the ModuleInstallerAPI classes. + * Current method is never called at design/runtime. It is acceptable to use it during setup only. + * @param string $sModuleFile + * @return array + * @throws ModuleFileReaderException + */ + public function ReadModuleFileInformationUnsafe(string $sModuleFilePath) : array { $aModuleInfo = []; // will be filled by the "eval" line below... try @@ -84,52 +130,6 @@ class ModuleFileReader { return $aModuleInfo; } - - /** - * Read the information from a module file (module.xxx.php) - * @param string $sModuleFile - * @return array - * @throws ModuleFileReaderException - */ - public function ReadModuleFileConfiguration(string $sModuleFilePath) : array - { - try - { - $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode(file_get_contents($sModuleFilePath)); - } - catch (PhpParser\Error $e) { - throw new \ModuleFileReaderException($e->getMessage(), 0, $e, $sModuleFilePath); - } - - try { - foreach ($aNodes as $sKey => $oNode) { - if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { - $aModuleInfo = ModuleFileParser::GetInstance()->GetModuleInformationFromAddModuleCall($sModuleFilePath, $oNode); - if (! is_null($aModuleInfo)){ - $this->CompleteModuleInfoWithFilePath($aModuleInfo); - return $aModuleInfo; - } - } - - if ($oNode instanceof PhpParser\Node\Stmt\If_) { - $aModuleInfo = ModuleFileParser::GetInstance()->GetModuleInformationFromIf($sModuleFilePath, $oNode); - if (! is_null($aModuleInfo)){ - $this->CompleteModuleInfoWithFilePath($aModuleInfo); - return $aModuleInfo; - } - } - } - } catch(ModuleFileReaderException $e) { - // Continue... - throw $e; - } catch(Exception $e) { - // Continue... - throw new ModuleFileReaderException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e, $sModuleFilePath); - } - - throw new ModuleFileReaderException("No proper call to SetupWebPage::AddModule found in module file", 0, null, $sModuleFilePath); - } - /** * * Internal trick: additional path is added into the module info structure to handle ModuleInstallerAPI execution during setup @@ -153,7 +153,7 @@ class ModuleFileReader { $sModuleInstallerClass = $aModuleInfo['installer']; if (!class_exists($sModuleInstallerClass)) { $sModuleFilePath = $aModuleInfo['module_file_path']; - $this->ReadModuleFileConfigurationUnsafe($sModuleFilePath); + $this->ReadModuleFileInformationUnsafe($sModuleFilePath); } if (!class_exists($sModuleInstallerClass)) diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php index 550d34232..87f15d607 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php @@ -6,9 +6,14 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use ModuleFileParser; use ModuleFileReader; use PhpParser\ParserFactory; +use SetupUtils; class ModuleFileParserTest extends ItopDataTestCase { + public static $STATIC_PROPERTY = 123; + private static $PRIVATE_STATIC_PROPERTY = 123; + private const PRIVATE_CONSTANT = 123; + protected function setUp(): void { parent::setUp(); @@ -20,17 +25,20 @@ class ModuleFileParserTest extends ItopDataTestCase return [ "true" => [ "expr" => "true", "expected" => true], "(true)" => [ "expr" => "(true)", "expected" => true], + "(false|true)" => [ "expr" => "(false|true)", "expected" => true], "(false||true)" => [ "expr" => "(false||true)", "expected" => true], "false" => [ "expr" => "false", "expected" => false], "(false)" => [ "expr" => "(false)", "expected" => false], "(false&&true)" => [ "expr" => "(false&&true)", "expected" => false], + "(false&true)" => [ "expr" => "(false&true)", "expected" => false], + "10 * 10" => [ "expr" => "10 * 10", "expected" => 100], ]; } /** * @dataProvider EvaluateBooleanExpressionProvider */ - public function testEvaluateBooleanExpression(string $sBooleanExpression, bool $expected){ + public function testEvaluateBooleanExpression(string $sBooleanExpression, $expected){ $this->assertEquals($expected, ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); } @@ -90,6 +98,67 @@ PHP; $this->assertEquals(APPROOT, $val); } + public function testEvaluateClassConstantExpression_PublicConstant() + { + $this->validateEvaluateClassConstantExpression('SetupUtils::PHP_MIN_VERSION', SetupUtils::PHP_MIN_VERSION); + } + + public function testEvaluateClassConstantExpression_PrivateConstantShouldNotBeFound() + { + $this->validateEvaluateClassConstantExpression('Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\ModuleFileParserTest::PRIVATE_CONSTANT', null); + } + + public function testEvaluateClassConstant_UnknownConstant() + { + $this->validateEvaluateClassConstantExpression('SetupUtils::UNKOWN_CONSTANT', null); + } + + public function testEvaluateClassConstant_UnknownClass() + { + $this->validateEvaluateClassConstantExpression('UnknownGaBuZoMeuClass::PHP_MIN_VERSION', null); + } + + public function testEvaluateClassConstant_UnknownClassGetClass() + { + $this->validateEvaluateClassConstantExpression('UnknownGaBuZoMeuClass::class', 'UnknownGaBuZoMeuClass'); + } + + public function validateEvaluateClassConstantExpression($sExpression, $expected) + { + $sPHP = <<ParsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleFileParser::class, "EvaluateClassConstantExpression", ModuleFileParser::GetInstance(), [$oExpr->expr]); + $this->assertEquals($expected, $val, "$sExpression"); + } + + public function testEvaluateClassConstant_PublicGetStaticProperty() + { + $this->validateEvaluateStaticPropertyExpression('Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\ModuleFileParserTest::$STATIC_PROPERTY', ModuleFileParserTest::$STATIC_PROPERTY); + } + + public function testEvaluateClassConstant_PrivateGetStaticPropertyShouldNotBeFound() + { + $this->validateEvaluateStaticPropertyExpression('Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\ModuleFileParserTest::$PRIVATE_STATIC_PROPERTY', null); + } + + public function validateEvaluateStaticPropertyExpression($sExpression, $expected) + { + $sPHP = <<ParsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleFileParser::class, "EvaluateStaticPropertyExpression", ModuleFileParser::GetInstance(), [$oExpr->expr]); + $this->assertEquals($expected, $val, "$sExpression"); + } + public static function EvaluateExpressionBooleanProvider() { $sTruePHP = << [ + "code" => str_replace("COND", '"true"', $sTruePHP), + "bool_expected" => "true", + + ], "true" => [ "code" => str_replace("COND", "true", $sTruePHP), "bool_expected" => true, @@ -108,6 +182,11 @@ PHP; "code" => str_replace("COND", "false", $sTruePHP), "bool_expected" => false, + ], + '"false"' => [ + "code" => str_replace("COND", '"false"', $sTruePHP), + "bool_expected" => "false", + ], "not ok" => [ "code" => str_replace("COND", "! false", $sTruePHP), diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php index d2af4c868..aeeffd7a0 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php @@ -15,10 +15,10 @@ class ModuleFileReaderTest extends ItopDataTestCase $this->RequireOnceItopFile('setup/modulediscovery/ModuleFileReader.php'); } - public function testReadModuleFileConfigurationLegacy() + public function testReadModuleFileInformationUnsafe() { $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; - $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); $this->assertCount(3, $aRes); $this->assertEquals($sModuleFilePath, $aRes[0]); @@ -30,34 +30,66 @@ class ModuleFileReaderTest extends ItopDataTestCase /*public function testAllReadModuleFileConfiguration() { - foreach (glob(__DIR__.'/resources/all/module.*.php') as $sModuleFilePath){ - $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + $aErrors=[]; + foreach (glob(__DIR__.'/resources/all_factory/module.*.php') as $sModuleFilePath){ + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); - $this->assertEquals($aExpected, $aRes); - - $aAutoselect = $aRes[2]['auto_select'] ?? ""; - if (strlen($aAutoselect) >0){ - var_dump($aAutoselect); + if ($aExpected !== $aRes){ + $aErrors[]=basename($sModuleFilePath); + continue; } + //$this->assertEquals($aExpected, $aRes); } + + $this->assertEquals([], $aErrors); }*/ - public function testReadModuleFileConfiguration() + public static function ReadModuleFileConfigurationFileNameProvider() { - $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; - $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileConfigurationUnsafe($sModuleFilePath); + return [ + 'nominal case : module.itop-full-itil.php' => ['module.itop-full-itil.php'], + 'constant as value of a dict entry: module.authent-ldap.php' => ['module.authent-ldap.php'], + 'int operation evaluation required: email-synchro' => ['module.combodo-email-synchro.php'], + 'module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php' => ['module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php'], + 'unknown class name to evaluation as installer: module.itop-global-requests-mgmt.php' => ['module.itop-global-requests-mgmt.php'], + ]; + } + + /** + * @dataProvider ReadModuleFileConfigurationFileNameProvider + */ + public function testReadModuleFileConfigurationVsLegacyMethod(string $sModuleBasename) + { + $sModuleFilePath = __DIR__."/resources/$sModuleBasename"; + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); $this->assertEquals($aExpected, $aRes); } - public function testReadModuleFileConfigurationWithConstants() - { - $sModuleFilePath = __DIR__.'/resources/module.authent-ldap.php'; - $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileConfigurationUnsafe($sModuleFilePath); + /** + * Covers below legacy usecase + * 'dependencies' => array( + * 'itop-config-mgmt/2.0.0'||'itop-structure/3.0.0', + * 'itop-request-mgmt/2.0.0||itop-request-mgmt-itil/2.0.0||itop-incident-mgmt-itil/2.0.0', + * ), + * + * @param string $sModuleBasename + * + * @return void + * @throws \ModuleFileReaderException + */ + public function testReadModuleFileConfiguration_BadlyWrittenDependencies(){ + //$sModuleFilePath = __DIR__."/resources/module.combodo-make-it-vip.php"; + $sModuleFilePath = __DIR__."/resources/module.itop-admin-delegation-profiles.php"; + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); + //do not check dumb conf on dependencies + $aDependencies=$aRes[2]['dependencies']; + $aDependencies= array_merge([true], $aDependencies); + $aRes[2]['dependencies']=$aDependencies; $this->assertEquals($aExpected, $aRes); } @@ -68,10 +100,9 @@ class ModuleFileReaderTest extends ItopDataTestCase $this->expectException(\ModuleFileReaderException::class); $this->expectExceptionMessage("Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting ',' or ']' or ')' on line 31"); - ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); } - /** * local tool function */ @@ -80,7 +111,7 @@ class ModuleFileReaderTest extends ItopDataTestCase $this->sTempModuleFilePath = tempnam(__DIR__, "test"); file_put_contents($this->sTempModuleFilePath, $sPHpCode); try { - return ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); + return ModuleFileReader::GetInstance()->ReadModuleFileInformation($this->sTempModuleFilePath); } finally { @unlink($this->sTempModuleFilePath); @@ -213,7 +244,7 @@ PHP; try { $this->assertFalse(class_exists($sModuleInstallerClass)); - $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($this->sTempModuleFilePath); $this->assertFalse(class_exists($sModuleInstallerClass)); $this->assertEquals($sModuleInstallerClass, ModuleFileReader::GetInstance()->GetAndCheckModuleInstallerClass($aModuleInfo[2])); diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-email-synchro.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-email-synchro.php new file mode 100644 index 000000000..c4d0675a8 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-email-synchro.php @@ -0,0 +1,123 @@ + 'Tickets synchronization via e-mail', + 'category' => 'business', + // Setup + 'dependencies' => array( + 'itop-profiles-itil/3.0.0', + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => 'EmailSynchroInstaller', + // Components + 'datamodel' => array( + 'classes/autoload.php', + 'model.combodo-email-synchro.php', + ), + 'dictionary' => array(), + 'data.struct' => array( + ), + 'data.sample' => array( + ), + // Documentation + 'doc.manual_setup' => '', // No manual installation required + 'doc.more_information' => '', // None + // Default settings + 'settings' => array( + 'notify_errors_to' => '', // mandatory to track errors not handled by the email processing module + 'notify_errors_from' => '', // mandatory as well (can be set at the same value as notify_errors_to) + 'debug' => false, // Set to true to turn on debugging + 'periodicity' => 30, // interval at which to check for incoming emails (in s) + 'retention_period' => 1, // number of hour we keep the replica + 'body_parts_order' => 'text/html,text/plain', // Order in which to read the parts of the incoming emails + 'pop3_auth_option' => 'USER', + 'imap_options' => array('imap'), + 'imap_open_options' => array(), + 'maximum_email_size' => '10M', // Maximum allowed size for incoming emails + 'big_files_dir' => '', + 'exclude_attachment_types' => array('application/exe'), // Example: 'application/exe', 'application/x-winexe', 'application/msdos-windows' + // Lines to be removed just above the 'new part' in a reply-to message... add your own patterns below + 'introductory-patterns' => array( + '/^le .+ a écrit :$/i', // Thunderbird French + '/^on .+ wrote:$/i', // Thunderbird English + '|^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2} .+:$|', // Gmail style + ), + // Some patterns which delimit the previous message in case of a Reply + // The "new" part of the message is the text before the pattern + // Add your own multi-line patterns (use \\R for a line break) + // These patterns depend on the mail client/server used... feel free to add your own discoveries to the list + 'multiline-delimiter-patterns' => array( + '/\\RFrom: .+\\RSent: .+\\R/m', // Outlook English + '/\\R_+\\R/m', // A whole line made only of underscore characters + '/\\RDe : .+\\R\\R?Envoyé : /m', // Outlook French, HTML and rich text + '/\\RDe : .+\\RDate d\'envoi : .+\\R/m', // Outlook French, plain text + '/\\R-----Message d\'origine-----\\R/m', + ), + 'use_message_id_as_uid' => false, // Do NOT change this unless you known what you are doing!! + 'images_minimum_size' => '100x20', // Images smaller that these dimensions will be ignored (signatures...) + 'images_maximum_size' => '', // Images bigger that these dimensions will be resized before uploading into iTop + 'recommended_max_allowed_packet' => 10*1024*1024, // MySQL parameter for attachments + ), + ) +); + +if (!class_exists('EmailSynchroInstaller')) +{ + + // Module installation handler + // + class EmailSynchroInstaller extends ModuleInstallerAPI + { + + /** + * Handler called after the creation/update of the database schema + * + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string Previous version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + * + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \DictExceptionMissingString + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + // For each email sources, update email replicas by setting mailbox_path to source.mailbox where mailbox_path is null + SetupLog::Info("Updating email replicas to set their mailbox path."); + + // Preparing mailboxes search + $oSearch = new DBObjectSearch('MailInboxBase'); + + // Retrieving definition of attribute to update + $sTableName = MetaModel::DBGetTable('EmailReplica'); + + $UidlAttDef = MetaModel::GetAttributeDef('EmailReplica', 'uidl'); + $sUidlColName = $UidlAttDef->Get('sql'); + + $oMailboxAttDef = MetaModel::GetAttributeDef('EmailReplica', 'mailbox_path'); + $sMailboxColName = $oMailboxAttDef->Get('sql'); + + $sFrienlynameAttCode = MetaModel::GetFriendlyNameAttributeCode('EmailReplica'); + + // Looping on inboxes to update + $oSet = new DBObjectSet($oSearch); + while ($oInbox = $oSet->Fetch()) + { + $sUpdateQuery = "UPDATE $sTableName SET $sMailboxColName = " . CMDBSource::Quote($oInbox->Get('mailbox')) . " WHERE $sUidlColName LIKE " . CMDBSource::Quote($oInbox->Get('login') . '_%') . " AND $sMailboxColName IS NULL"; + SetupLog::Info("Executing query: " . $sUpdateQuery); + $iRet = CMDBSource::Query($sUpdateQuery); // Throws an exception in case of error + SetupLog::Info("Updated $iRet rows."); + } + } + + } + +} diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-make-it-vip.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-make-it-vip.php new file mode 100644 index 000000000..3ee370f0e --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-make-it-vip.php @@ -0,0 +1,57 @@ + 'Flag important contacts in your database and highlight tickets', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-config-mgmt/2.0.0'||'itop-structure/3.0.0', + 'itop-request-mgmt/2.0.0||itop-request-mgmt-itil/2.0.0||itop-incident-mgmt-itil/2.0.0', + ), + 'mandatory' => false, + 'visible' => true, + + // Components + // + 'datamodel' => array( + 'model.combodo-make-it-vip.php', + 'main.combodo-make-it-vip.php', + ), + 'webservice' => array( + + ), + 'data.struct' => array( + // add your 'structure' definition XML files here, + ), + 'data.sample' => array( + // add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array( + // Module specific settings go here, if any + ), + ) +); diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php new file mode 100644 index 000000000..e94a8d770 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php @@ -0,0 +1,51 @@ + 'Profiles per admin fonction: Mail inboxes and messages', + 'category' => 'Datamodel', + + // Setup + // + 'dependencies' => array( + 'itop-admin-delegation-profiles/1.0.0', + 'itop-admin-delegation-profiles/1.0.0 || combodo-email-synchro/3.7.2 || itop-oauth-client/2.7.7', // Optional dependency to silence the setup to not display a warning if the other module is not present + ), + 'mandatory' => false, + 'visible' => false, + 'auto_select' => 'SetupInfo::ModuleIsSelected("itop-admin-delegation-profiles") && SetupInfo::ModuleIsSelected("combodo-email-synchro") && SetupInfo::ModuleIsSelected("itop-oauth-client")', + + // Components + // + 'datamodel' => array( + 'model.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php' + ), + 'webservice' => array( + + ), + 'data.struct' => array( + // add your 'structure' definition XML files here, + ), + 'data.sample' => array( + // add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array( + // Module specific settings go here, if any + ), + ) +); diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles.php new file mode 100644 index 000000000..fa29d1ff8 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles.php @@ -0,0 +1,52 @@ + 'Profiles per admin fonction', + 'category' => 'Datamodel', + + // Setup + // + 'dependencies' => array( + 'itop-config-mgmt/2.7.0' || 'itop-structure/3.0.0', + // itop-profiles-itil is here to ensure that the /itop_design/groups/group[@id="History"] alteration comes after those from that module. + // This allows to define the missing "History" group in iTop 2.7 / 3.0, while merging smoothly with iTop 3.1+ + 'itop-profiles-itil/2.7.0', + ), + 'mandatory' => false, + 'visible' => true, + + // Components + // + 'datamodel' => array( + 'model.itop-admin-delegation-profiles.php' + ), + 'webservice' => array( + + ), + 'data.struct' => array( + // add your 'structure' definition XML files here, + ), + 'data.sample' => array( + // add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array( + // Module specific settings go here, if any + ), + ) +); diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-global-requests-mgmt.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-global-requests-mgmt.php new file mode 100644 index 000000000..64cf608f1 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-global-requests-mgmt.php @@ -0,0 +1,114 @@ + 'iTop Global Requests Management', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-portal-base/3.2.0', + 'approval-base/2.5.1', + 'combodo-approval-extended/1.2.3', + 'itop-config-mgmt/3.2.0', + 'itop-tickets/3.2.0', + 'combodo-dispatch-userrequest/1.1.4', + 'itop-request-mgmt-itil/3.2.0||itop-request-mgmt/3.2.0', + 'itop-service-mgmt/3.2.0||itop-service-mgmt-provider/3.2.0', + 'itop-request-template/2.0.1', + 'itop-request-template-portal/1.0.0', + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => GlobalRequestInstaller::class, + + // Components + // + 'datamodel' => array( + 'vendor/autoload.php', + // Explicitly load hooks classes + 'src/Hook/GRPopupMenuExtension.php', + // Explicitly load DM classes + 'model.itop-global-requests-mgmt.php', + //Needed for symfony dependency injection + 'src/Portal/Router/GlobalRequestBrickRouter.php', + ), + 'webservice' => array(), + 'data.struct' => array(// add your 'structure' definition XML files here, + ), + 'data.sample' => array(// add your sample data XML files here, + ), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array( + 'target_state' => 'new', + 'bypass_profiles' => 'Administrator, Service Manager', + 'reuse_previous_answers' => true, + ), + ) +); + + +class GlobalRequestInstaller extends ModuleInstallerAPI +{ + /** + * Handler called before creating or upgrading the database schema + * + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string Previous version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + * + * @throws \CoreException + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + if (strlen($sPreviousVersion) > 0 && version_compare($sPreviousVersion, '1.6.0', '<')) { + $slnkGRTypeToServiceSubcategory = MetaModel::DBGetTable('lnkGRTypeToServiceSubcategory','parent_servicesubcategory_id'); + $oAttDefToUpdate = MetaModel::GetAttributeDef('lnkGRTypeToServiceSubcategory', 'parent_servicesubcategory_id'); + $aColumnsToUpdate = array_keys($oAttDefToUpdate->GetSQLColumns()); + $sColumnToUpdate = $aColumnsToUpdate[0]; // We know that a string has only one column*/ + $oAttDefLink = MetaModel::GetAttributeDef('lnkGRTypeToServiceSubcategory', 'servicesubcategory_id'); + $aColumnsLink = array_keys($oAttDefLink->GetSQLColumns()); + $sColumnLink = $aColumnsLink[0]; // We know that a string has only one column*/ + + $sTableToRead = MetaModel::DBGetTable('ServiceSubcategory', 'parent_servicesubcategory_id'); + $oAttDefToRead = MetaModel::GetAttributeDef('ServiceSubcategory', 'parent_servicesubcategory_id'); + $aColumnsToReads = array_keys($oAttDefToRead->GetSQLColumns()); + $sColumnToRead = $aColumnsToReads[0]; // We know that a string has only one column + $sTableToReadPrimaryKey = MetaModel::DBGetKey('ServiceSubcategory'); + + $sQueryUpdate = " + UPDATE `$slnkGRTypeToServiceSubcategory` + JOIN `$sTableToRead` + ON `$slnkGRTypeToServiceSubcategory`.`$sColumnLink` = `$sTableToRead`.`$sTableToReadPrimaryKey` + SET `$slnkGRTypeToServiceSubcategory`.`$sColumnToUpdate` = `$sTableToRead`.`$sColumnToRead` + WHERE `$slnkGRTypeToServiceSubcategory`.`$sColumnToUpdate` = 0 OR `$slnkGRTypeToServiceSubcategory`.`$sColumnToUpdate` IS NULL + "; + SetupLog::Info(" GlobalRequestInstaller Query: " . $sQueryUpdate); + CMDBSource::Query($sQueryUpdate); + } + } +} + +