diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 1aafd90d0..0025c9add 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -2857,7 +2857,7 @@ class Config } } - ModuleDiscoveryService::GetInstance()->CallInstallerBeforeWritingConfigMethod($this, $aModuleInfo); + RunTimeEnvironment::CallInstallerHandler($aModuleInfo, "BeforeWritingConfig", [$this]); } } $this->SetAddOns($aAddOns); diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index afc5ba6a3..84ef710d9 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -304,8 +304,8 @@ class iTopExtensionsMap { // Found a module try { - $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sSearchDir.'/'.$sFile); - } catch(ModuleDiscoveryServiceException $e){ + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sSearchDir.'/'.$sFile); + } catch(ModuleFileReaderException $e){ continue; } // If we are not already inside a formal extension, then the module itself is considered diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 899099bf0..234b53461 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -19,7 +19,7 @@ * */ -require_once(APPROOT.'setup/modulediscovery/ModuleDiscoveryService.php'); +require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php'); class MissingDependencyException extends CoreException { @@ -108,7 +108,7 @@ class ModuleDiscovery public static function AddModule($sFilePath, $sId, $aArgs) { if (is_null($aArgs)||! is_array($aArgs)){ - throw new ModuleDiscoveryServiceException("Error parsing module file args", 0, null, $sFilePath); + throw new ModuleFileReaderException("Error parsing module file args", 0, null, $sFilePath); } if (!array_key_exists('itop_version', $aArgs)) { @@ -391,8 +391,8 @@ class ModuleDiscovery { $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); try{ - $bResult = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($sBooleanExpr); - } catch(ModuleDiscoveryServiceException $e){ + $bResult = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpr); + } catch(ModuleFileReaderException $e){ //logged already echo "Failed to parse the boolean Expression = '$sBooleanExpr'
"; } @@ -503,9 +503,9 @@ class ModuleDiscovery self::SetModulePath($sRelDir); $sModuleFilePath = $sDirectory.'/'.$sFile; try { - $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sDirectory.'/'.$sFile); + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sDirectory.'/'.$sFile); SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo[2]); - } catch(ModuleDiscoveryServiceException $e){ + } catch(ModuleFileReaderException $e){ continue; } } diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php deleted file mode 100644 index e2668e0d9..000000000 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ /dev/null @@ -1,227 +0,0 @@ -'], '', $sModuleFileContents); - $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFilePath)."'", $sModuleFileContents); - preg_match_all('/class ([A-Za-z0-9_]+) extends ([A-Za-z0-9_]+)/', $sModuleFileContents, $aMatches); - //print_r($aMatches); - $idx = 0; - foreach($aMatches[1] as $sClassName) - { - if (class_exists($sClassName)) - { - // rename any class declaration inside the code to prevent a "duplicate class" declaration - // and change its parent class as well so that nobody will find it and try to execute it - // Note: don't use the same naming scheme as ModuleDiscovery otherwise you 'll have the duplicate class error again !! - $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_Ext_'.(ModuleDiscoveryService::$iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); - } - $idx++; - } - // Replace the main function call by an assignment to a variable, as an array... - $sModuleFileContents = str_replace(['SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'], '$aModuleInfo = array', $sModuleFileContents); - eval($sModuleFileContents); // Assigns $aModuleInfo - - if (count($aModuleInfo) === 0) - { - throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath did not return the expected information..."); - } - - $this->CompleteConfigWithModuleFilePath($aModuleInfo); - } - catch(ModuleDiscoveryServiceException $e) - { - // Continue... - throw $e; - } - catch(ParseError $e) - { - // Continue... - throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused a parse error: ".$e->getMessage()." at line ".$e->getLine()); - } - catch(Exception $e) - { - // Continue... - throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e); - } - return $aModuleInfo; - } - - - /** - * Read the information from a module file (module.xxx.php) - * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles - * @param string $sModuleFile - * @return array - * @throws ModuleDiscoveryServiceException - */ - public function ReadModuleFileConfiguration(string $sModuleFilePath) : array - { - try - { - $aNodes = ModuleDiscoveryEvaluationService::GetInstance()->ParsePhpCode(file_get_contents($sModuleFilePath)); - } - catch (PhpParser\Error $e) { - throw new \ModuleDiscoveryServiceException($e->getMessage(), 0, $e, $sModuleFilePath); - } - - try { - foreach ($aNodes as $sKey => $oNode) { - if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { - $aModuleConfig = ModuleDiscoveryEvaluationService::GetInstance()->BrowseAddModuleCallAndReturnModuleConfiguration($sModuleFilePath, $oNode); - if (! is_null($aModuleConfig)){ - $this->CompleteConfigWithModuleFilePath($aModuleConfig); - return $aModuleConfig; - } - } - - if ($oNode instanceof PhpParser\Node\Stmt\If_) { - $aModuleConfig = ModuleDiscoveryEvaluationService::GetInstance()->BrowseIfStructure($sModuleFilePath, $oNode); - if (! is_null($aModuleConfig)){ - $this->CompleteConfigWithModuleFilePath($aModuleConfig); - return $aModuleConfig; - } - } - } - } catch(ModuleDiscoveryServiceException $e) { - // Continue... - throw $e; - } catch(Exception $e) { - // Continue... - throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e, $sModuleFilePath); - } - - throw new ModuleDiscoveryServiceException("No proper call to SetupWebPage::AddModule found in module file", 0, null, $sModuleFilePath); - } - - /** - * N°4789 - Parse datamodel module.xxx.php files instead of interpreting them - * additional path added to handle ModuleInstallerAPI declaration during setup only - * @param array &$aModuleInfo - * - * @return void - */ - private function CompleteConfigWithModuleFilePath(array &$aModuleInfo) - { - if (count($aModuleInfo)==3) { - $aModuleInfo[2]['module_file_path'] = $aModuleInfo[0]; - } - } - - /** - * - * @param \Config $oConfig - * @param array $aModuleConfig - * - * @return void - * @throws \ModuleDiscoveryServiceException - */ - public function CallInstallerBeforeWritingConfigMethod(Config $oConfig, array $aModuleConfig) - { - $sModuleInstallerClass = $this->DeclareModuleInstallerAPI($aModuleConfig); - if (is_null($sModuleInstallerClass)){ - return; - } - - $aCallSpec = [$sModuleInstallerClass, 'BeforeWritingConfig']; - call_user_func_array($aCallSpec, [$oConfig]); - } - - /** - * Call the given handler method for all selected modules having an installation handler - * - * @param Config $oConfig - * @param array $aModuleConfig - * @param array $aModule - * @param string $sHandlerName - * - * @throws CoreException - */ - public function CallInstallerHandler(Config $oConfig, array $aModuleConfig, array $aModule, $sHandlerName) - { - $sModuleInstallerClass = $this->DeclareModuleInstallerAPI($aModuleConfig); - if (is_null($sModuleInstallerClass)){ - return; - } - - SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})"); - $aCallSpec = [$sModuleInstallerClass, $sHandlerName]; - if (is_callable($aCallSpec)) - { - try { - call_user_func_array($aCallSpec, [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']]); - } catch (Exception $e) { - $sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler"; - $aExceptionContextData = [ - 'ModulelId' => $sModuleId, - 'ModuleInstallerClass' => $sModuleInstallerClass, - 'ModuleInstallerHandler' => $sHandlerName, - 'ExceptionClass' => get_class($e), - 'ExceptionMessage' => $e->getMessage(), - ]; - throw new CoreException($sErrorMessage, $aExceptionContextData, '', $e); - } - } - } - - private function DeclareModuleInstallerAPI($aModuleConfig) : ?string - { - if (! isset($aModuleConfig['installer'])){ - return null; - } - - $sModuleInstallerClass = $aModuleConfig['installer']; - if (!class_exists($sModuleInstallerClass)) { - $sModuleFilePath = $aModuleConfig['module_file_path']; - $this->ReadModuleFileConfigurationLegacy($sModuleFilePath); - } - - if (!class_exists($sModuleInstallerClass)) - { - throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleConfig['label']); - } - if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) - { - throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleConfig['label']); - } - - return $sModuleInstallerClass; - } -} diff --git a/setup/modulediscovery/ModuleDiscoveryEvaluationService.php b/setup/modulediscovery/ModuleFileParser.php similarity index 69% rename from setup/modulediscovery/ModuleDiscoveryEvaluationService.php rename to setup/modulediscovery/ModuleFileParser.php index 1d5cb357d..41ef5d250 100644 --- a/setup/modulediscovery/ModuleDiscoveryEvaluationService.php +++ b/setup/modulediscovery/ModuleFileParser.php @@ -3,13 +3,13 @@ use PhpParser\ParserFactory; use PhpParser\Node\Expr\Assign; -class ModuleDiscoveryEvaluationService { - private static ModuleDiscoveryEvaluationService $oInstance; +class ModuleFileParser { + private static ModuleFileParser $oInstance; protected function __construct() { } - final public static function GetInstance(): ModuleDiscoveryEvaluationService { + final public static function GetInstance(): ModuleFileParser { if (!isset(static::$oInstance)) { static::$oInstance = new static(); } @@ -17,7 +17,7 @@ class ModuleDiscoveryEvaluationService { return static::$oInstance; } - final public static function SetInstance(?ModuleDiscoveryEvaluationService $oInstance): void { + final public static function SetInstance(?ModuleFileParser $oInstance): void { static::$oInstance = $oInstance; } @@ -37,9 +37,9 @@ class ModuleDiscoveryEvaluationService { * @param \PhpParser\Node\Expr\Assign $oAssignation * * @return array|null - * @throws \ModuleDiscoveryServiceException + * @throws \ModuleFileReaderException */ - public function BrowseAddModuleCallAndReturnModuleConfiguration(string $sModuleFilePath, \PhpParser\Node\Stmt\Expression $oExpression) : ?array + public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \PhpParser\Node\Stmt\Expression $oExpression) : ?array { /** @var Assign $oAssignation */ $oAssignation = $oExpression->expr; @@ -59,17 +59,17 @@ class ModuleDiscoveryEvaluationService { $aArgs = $oAssignation?->args; if (count($aArgs) != 3) { - throw new ModuleDiscoveryServiceException("Not enough parameters when calling SetupWebPage::AddModule", 0, null, $sModuleFilePath); + throw new ModuleFileReaderException("Not enough parameters when calling SetupWebPage::AddModule", 0, null, $sModuleFilePath); } $oModuleId = $aArgs[1]; if (false === ($oModuleId instanceof PhpParser\Node\Arg)) { - throw new ModuleDiscoveryServiceException("2nd parameter to SetupWebPage::AddModule call issue: " . get_class($oModuleId), 0, null, $sModuleFilePath); + throw new ModuleFileReaderException("2nd parameter to SetupWebPage::AddModule call issue: " . get_class($oModuleId), 0, null, $sModuleFilePath); } /** @var PhpParser\Node\Arg $oModuleId */ if (false === ($oModuleId->value instanceof PhpParser\Node\Scalar\String_)) { - throw new ModuleDiscoveryServiceException("2nd parameter to SetupWebPage::AddModule not a string: " . get_class($oModuleId->value), 0, null, $sModuleFilePath); + throw new ModuleFileReaderException("2nd parameter to SetupWebPage::AddModule not a string: " . get_class($oModuleId->value), 0, null, $sModuleFilePath); } /** @var PhpParser\Node\Scalar\String_ $sModuleIdStringObj */ @@ -78,19 +78,19 @@ class ModuleDiscoveryEvaluationService { $oModuleConfigInfo = $aArgs[2]; if (false === ($oModuleConfigInfo instanceof PhpParser\Node\Arg)) { - throw new ModuleDiscoveryServiceException("3rd parameter to SetupWebPage::AddModule call issue: " . get_class($oModuleConfigInfo), 0, null, $sModuleFilePath); + throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule call issue: " . get_class($oModuleConfigInfo), 0, null, $sModuleFilePath); } /** @var PhpParser\Node\Arg $oModuleConfigInfo */ if (false === ($oModuleConfigInfo->value instanceof PhpParser\Node\Expr\Array_)) { - throw new ModuleDiscoveryServiceException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); + throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); } $aModuleConfig=[]; - $this->BrowseArrayStructure($oModuleConfigInfo->value, $aModuleConfig); + $this->FillModuleInformationFromArray($oModuleConfigInfo->value, $aModuleConfig); if (! is_array($aModuleConfig)){ - throw new ModuleDiscoveryServiceException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); + throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); } return [ $sModuleFilePath, @@ -99,7 +99,7 @@ class ModuleDiscoveryEvaluationService { ]; } - public function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleConfig) : void + public function FillModuleInformationFromArray(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleInformation) : void { $iIndex=0; /** @var \PhpParser\Node\Expr\ArrayItem $oValue */ @@ -120,18 +120,18 @@ class ModuleDiscoveryEvaluationService { if ($oValue instanceof PhpParser\Node\Expr\Array_) { $aSubConfig=[]; - $this->BrowseArrayStructure($oValue, $aSubConfig); - $aModuleConfig[$sKey]=$aSubConfig; + $this->FillModuleInformationFromArray($oValue, $aSubConfig); + $aModuleInformation[$sKey]=$aSubConfig; } if ($oValue instanceof PhpParser\Node\Scalar\String_||$oValue instanceof PhpParser\Node\Scalar\Int_) { - $aModuleConfig[$sKey]=$oValue->value; + $aModuleInformation[$sKey]=$oValue->value; continue; } if ($oValue instanceof \PhpParser\Node\Expr\ConstFetch) { $oEvaluatedConstant = $this->EvaluateConstantExpression($oValue); - $aModuleConfig[$sKey]= $oEvaluatedConstant; + $aModuleInformation[$sKey]= $oEvaluatedConstant; } } } @@ -141,15 +141,15 @@ class ModuleDiscoveryEvaluationService { * @param \PhpParser\Node\Stmt\If_ $oNode * * @return array|null - * @throws \ModuleDiscoveryServiceException + * @throws \ModuleFileReaderException */ - public function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt\If_ $oNode) : ?array + public function GetModuleInformationFromIf(string $sModuleFilePath, \PhpParser\Node\Stmt\If_ $oNode) : ?array { $bCondition = $this->EvaluateExpression($oNode->cond); if ($bCondition) { foreach ($oNode->stmts as $oSubNode) { if ($oSubNode instanceof \PhpParser\Node\Stmt\Expression) { - $aModuleConfig = $this->BrowseAddModuleCallAndReturnModuleConfiguration($sModuleFilePath, $oSubNode); + $aModuleConfig = $this->GetModuleInformationFromAddModuleCall($sModuleFilePath, $oSubNode); if (!is_null($aModuleConfig)) { return $aModuleConfig; } @@ -163,7 +163,7 @@ class ModuleDiscoveryEvaluationService { /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ $bCondition = $this->EvaluateExpression($oElseIfSubNode->cond); if ($bCondition) { - $aModuleConfig = $this->BrowseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oElseIfSubNode->stmts); + $aModuleConfig = $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oElseIfSubNode->stmts); if (!is_null($aModuleConfig)) { return $aModuleConfig; } @@ -173,7 +173,7 @@ class ModuleDiscoveryEvaluationService { } if (! is_null($oNode->else)) { - $aModuleConfig = $this->BrowseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); + $aModuleConfig = $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oNode->else->stmts); return $aModuleConfig; } @@ -181,11 +181,11 @@ class ModuleDiscoveryEvaluationService { return null; } - public function BrowseStatementsAndReturnModuleConfiguration(string $sModuleFilePath, array $aStmts) : ?array + public function GetModuleConfigurationFromStatement(string $sModuleFilePath, array $aStmts) : ?array { foreach ($aStmts as $oSubNode) { if ($oSubNode instanceof \PhpParser\Node\Stmt\Expression) { - $aModuleConfig = $this->BrowseAddModuleCallAndReturnModuleConfiguration($sModuleFilePath, $oSubNode); + $aModuleConfig = $this->GetModuleInformationFromAddModuleCall($sModuleFilePath, $oSubNode); if (!is_null($aModuleConfig)) { return $aModuleConfig; } @@ -195,13 +195,14 @@ class ModuleDiscoveryEvaluationService { 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 ModuleDiscoveryServiceException("Eval of ' . $oValue->name . ' caused an error: ".$t->getMessage()); + throw new ModuleFileReaderException("Eval of ' . $oValue->name . ' caused an error: ".$t->getMessage()); } return $bResult; @@ -220,7 +221,7 @@ class ModuleDiscoveryEvaluationService { * @param string $sBooleanExpr * * @return bool - * @throws ModuleDiscoveryServiceException + * @throws ModuleFileReaderException */ private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : bool { @@ -228,7 +229,7 @@ class ModuleDiscoveryEvaluationService { try{ @eval('$bResult = '.$sBooleanExpr.';'); } catch (Throwable $t) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); + throw new ModuleFileReaderException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); } return $bResult; @@ -239,7 +240,7 @@ class ModuleDiscoveryEvaluationService { * @param bool $bSafe: when true, evaluation relies on unsafe eval() call * * @return bool - * @throws ModuleDiscoveryServiceException + * @throws ModuleFileReaderException */ public function EvaluateBooleanExpression(string $sBooleanExpr, $bSafe=true) : bool { @@ -256,7 +257,7 @@ PHP; $oExpr = $aNodes[0]; return $this->EvaluateExpression($oExpr->expr); } catch (Throwable $t) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); + throw new ModuleFileReaderException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); } } @@ -293,10 +294,9 @@ PHP; private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool { $sFunction = $oFunct->name->name; - $aWhiteList = ["function_exists"]; + $aWhiteList = ["function_exists", "class_exists", "method_exists"]; if (! in_array($sFunction, $aWhiteList)){ - throw new ModuleDiscoveryServiceException("FuncCall $sFunction not supported"); - //return false; + throw new ModuleFileReaderException("FuncCall $sFunction not supported"); } $aArgs=[]; @@ -313,7 +313,7 @@ PHP; * @param \PhpParser\Node\Expr\StaticCall $oStaticCall * * @return bool - * @throws \ModuleDiscoveryServiceException + * @throws \ModuleFileReaderException * @throws \ReflectionException */ private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : bool @@ -323,7 +323,7 @@ PHP; $aWhiteList = ["SetupInfo::ModuleIsSelected"]; $sStaticCallDescription = "$sClassName::$sMethodName"; if (! in_array($sStaticCallDescription, $aWhiteList)){ - throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not supported"); + throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not supported"); } $aArgs=[]; @@ -335,7 +335,7 @@ PHP; $class = new \ReflectionClass($sClassName); $method = $class->getMethod($sMethodName); if (! $method->isPublic()){ - throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not public"); + throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not public"); } return (bool) $method->invokeArgs(null, $aArgs); diff --git a/setup/modulediscovery/ModuleFileReader.php b/setup/modulediscovery/ModuleFileReader.php new file mode 100644 index 000000000..b6c23be14 --- /dev/null +++ b/setup/modulediscovery/ModuleFileReader.php @@ -0,0 +1,170 @@ +'], '', $sModuleFileContents); + $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFilePath)."'", $sModuleFileContents); + preg_match_all('/class ([A-Za-z0-9_]+) extends ([A-Za-z0-9_]+)/', $sModuleFileContents, $aMatches); + //print_r($aMatches); + $idx = 0; + foreach($aMatches[1] as $sClassName) + { + if (class_exists($sClassName)) + { + // rename any class declaration inside the code to prevent a "duplicate class" declaration + // and change its parent class as well so that nobody will find it and try to execute it + // Note: don't use the same naming scheme as ModuleDiscovery otherwise you 'll have the duplicate class error again !! + $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_Ext_'.(ModuleFileReader::$iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); + } + $idx++; + } + // Replace the main function call by an assignment to a variable, as an array... + $sModuleFileContents = str_replace(['SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'], '$aModuleInfo = array', $sModuleFileContents); + eval($sModuleFileContents); // Assigns $aModuleInfo + + if (count($aModuleInfo) === 0) + { + throw new ModuleFileReaderException("Eval of $sModuleFilePath did not return the expected information..."); + } + + $this->CompleteModuleInfoWithFilePath($aModuleInfo); + } + catch(ModuleFileReaderException $e) + { + // Continue... + throw $e; + } + catch(ParseError $e) + { + // Continue... + throw new ModuleFileReaderException("Eval of $sModuleFilePath caused a parse error: ".$e->getMessage()." at line ".$e->getLine()); + } + catch(Exception $e) + { + // Continue... + throw new ModuleFileReaderException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e); + } + 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 + * @param array &$aModuleInfo + * + * @return void + */ + private function CompleteModuleInfoWithFilePath(array &$aModuleInfo) + { + if (count($aModuleInfo)==3) { + $aModuleInfo[2]['module_file_path'] = $aModuleInfo[0]; + } + } + + public function GetAndCheckModuleInstallerClass($aModuleInfo) : ?string + { + if (! isset($aModuleInfo['installer'])){ + return null; + } + + $sModuleInstallerClass = $aModuleInfo['installer']; + if (!class_exists($sModuleInstallerClass)) { + $sModuleFilePath = $aModuleInfo['module_file_path']; + $this->ReadModuleFileConfigurationUnsafe($sModuleFilePath); + } + + if (!class_exists($sModuleInstallerClass)) + { + throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); + } + if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) + { + throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); + } + + return $sModuleInstallerClass; + } +} diff --git a/setup/modulediscovery/ModuleDiscoveryServiceException.php b/setup/modulediscovery/ModuleFileReaderException.php similarity index 83% rename from setup/modulediscovery/ModuleDiscoveryServiceException.php rename to setup/modulediscovery/ModuleFileReaderException.php index 99ab3bb47..00f074819 100644 --- a/setup/modulediscovery/ModuleDiscoveryServiceException.php +++ b/setup/modulediscovery/ModuleFileReaderException.php @@ -1,9 +1,9 @@ EvaluateBooleanExpression($oModule->GetAutoSelect()); + $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($oModule->GetAutoSelect()); if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module $bModuleAdded = true; } - } catch(ModuleDiscoveryServiceException $e){ + } catch(ModuleFileReaderException $e){ //do nothing. logged already } } @@ -1089,7 +1089,44 @@ class RunTimeEnvironment { if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) { - ModuleDiscoveryService::GetInstance()->CallInstallerHandler(MetaModel::GetConfig(), $aAvailableModules[$sModuleId], $aModule, $sHandlerName); + $aArgs = [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']]; + RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs); + } + } + } + + /** + * Call the given handler method for all selected modules having an installation handler + * + * @param array $aModuleInfo + * @param string $sHandlerName + * @param array $aArgs + * + * @throws CoreException + */ + public static function CallInstallerHandler(array $aModuleInfo, $sHandlerName, array $aArgs) + { + $sModuleInstallerClass = ModuleFileReader::GetInstance()->GetAndCheckModuleInstallerClass($aModuleInfo); + if (is_null($sModuleInstallerClass)){ + return; + } + + SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName", null, $aArgs); + $aCallSpec = [$sModuleInstallerClass, $sHandlerName]; + if (is_callable($aCallSpec)) + { + try { + call_user_func_array($aCallSpec, $aArgs); + } catch (Exception $e) { + $sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler"; + $aExceptionContextData = [ + 'ModulelId' => $sModuleId, + 'ModuleInstallerClass' => $sModuleInstallerClass, + 'ModuleInstallerHandler' => $sHandlerName, + 'ExceptionClass' => get_class($e), + 'ExceptionMessage' => $e->getMessage(), + ]; + throw new CoreException($sErrorMessage, $aExceptionContextData, '', $e); } } } diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index fdb6788e7..45c2ffaee 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -270,14 +270,14 @@ class InstallationFileService { { try { SetupInfo::SetSelectedModules($this->aSelectedModules); - $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); + $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { // Modules in data/production-modules/ are considered as mandatory and always installed $this->aSelectedModules[$sModuleId] = true; } } - catch (ModuleDiscoveryServiceException $e) { + catch (ModuleFileReaderException $e) { //logged already } } diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index f1209d0ce..0defe7247 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1787,9 +1787,9 @@ EOF // Check the module selection try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($aInfo['auto_select']); + $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($aInfo['auto_select']); } - catch (ModuleDiscoveryServiceException $e) { + catch (ModuleFileReaderException $e) { //logged already $bSelected = false; } @@ -1865,7 +1865,7 @@ EOF try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); + $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { $aModules[$sModuleId] = true; // store the Id of the selected module @@ -1873,7 +1873,7 @@ EOF $bModuleAdded = true; } } - catch(ModuleDiscoveryServiceException $e) + catch(ModuleFileReaderException $e) { //logged already $sDisplayChoices .= '
  • Warning: auto_select failed with exception ('.$e->getMessage().') for module "'.$sModuleId.'"
  • '; diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php similarity index 80% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php index 18317fe66..550d34232 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php @@ -3,17 +3,16 @@ namespace Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; -use ModuleDiscoveryEvaluationService; -use ModuleDiscoveryService; +use ModuleFileParser; +use ModuleFileReader; use PhpParser\ParserFactory; -class ModuleDiscoveryEvaluationServiceTest extends ItopDataTestCase +class ModuleFileParserTest extends ItopDataTestCase { - private string $sTempModuleFilePath; protected function setUp(): void { parent::setUp(); - $this->RequireOnceItopFile('setup/modulediscovery/ModuleDiscoveryService.php'); + $this->RequireOnceItopFile('setup/modulediscovery/ModuleFileReader.php'); } public static function EvaluateBooleanExpressionProvider() @@ -32,13 +31,13 @@ class ModuleDiscoveryEvaluationServiceTest extends ItopDataTestCase * @dataProvider EvaluateBooleanExpressionProvider */ public function testEvaluateBooleanExpression(string $sBooleanExpression, bool $expected){ - $this->assertEquals($expected, ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); + $this->assertEquals($expected, ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); } public function testEvaluateBooleanExpression_BrokenBooleanExpression(){ - $this->expectException(\ModuleDiscoveryServiceException::class); + $this->expectException(\ModuleFileReaderException::class); $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); - $this->assertTrue(ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression("(a || true)")); + $this->assertTrue(ModuleFileParser::GetInstance()->EvaluateBooleanExpression("(a || true)")); } @@ -75,7 +74,7 @@ class ModuleDiscoveryEvaluationServiceTest extends ItopDataTestCase */ public function testEvaluateBooleanExpression_Autoselect(string $sBooleanExpression, bool $expected){ \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); - $this->assertEquals($expected, ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); + $this->assertEquals($expected, ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); } public function testEvaluateConstantExpression() @@ -84,10 +83,10 @@ class ModuleDiscoveryEvaluationServiceTest extends ItopDataTestCase ParsePhpCode($sPHP); + $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode($sPHP); /** @var \PhpParser\Node\Expr $oExpr */ $oExpr = $aNodes[0]; - $val = $this->InvokeNonPublicMethod(ModuleDiscoveryEvaluationService::class, "EvaluateConstantExpression", ModuleDiscoveryEvaluationService::GetInstance(), [$oExpr->expr]); + $val = $this->InvokeNonPublicMethod(ModuleFileParser::class, "EvaluateConstantExpression", ModuleFileParser::GetInstance(), [$oExpr->expr]); $this->assertEquals(APPROOT, $val); } @@ -190,10 +189,10 @@ PHP; */ public function testEvaluateExpression($sPHP, $bExpected) { - $aNodes = ModuleDiscoveryEvaluationService::GetInstance()->ParsePhpCode($sPHP); + $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode($sPHP); /** @var \PhpParser\Node\Expr $oExpr */ $oExpr = $aNodes[0]; - $val = $this->InvokeNonPublicMethod(ModuleDiscoveryEvaluationService::class, "EvaluateExpression", ModuleDiscoveryEvaluationService::GetInstance(), [$oExpr->cond]); + $val = $this->InvokeNonPublicMethod(ModuleFileParser::class, "EvaluateExpression", ModuleFileParser::GetInstance(), [$oExpr->cond]); $this->assertEquals($bExpected, $val); } } \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php similarity index 81% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php index e4105e171..d2af4c868 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php @@ -3,22 +3,22 @@ namespace Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; -use ModuleDiscoveryService; +use ModuleFileReader; use PhpParser\ParserFactory; -class ModuleDiscoveryServiceTest extends ItopDataTestCase +class ModuleFileReaderTest extends ItopDataTestCase { private string $sTempModuleFilePath; protected function setUp(): void { parent::setUp(); - $this->RequireOnceItopFile('setup/modulediscovery/ModuleDiscoveryService.php'); + $this->RequireOnceItopFile('setup/modulediscovery/ModuleFileReader.php'); } public function testReadModuleFileConfigurationLegacy() { $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; - $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); $this->assertCount(3, $aRes); $this->assertEquals($sModuleFilePath, $aRes[0]); @@ -31,8 +31,8 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase /*public function testAllReadModuleFileConfiguration() { foreach (glob(__DIR__.'/resources/all/module.*.php') as $sModuleFilePath){ - $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); $this->assertEquals($aExpected, $aRes); @@ -46,8 +46,8 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase public function testReadModuleFileConfiguration() { $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; - $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileConfigurationUnsafe($sModuleFilePath); $this->assertEquals($aExpected, $aRes); } @@ -55,8 +55,8 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase public function testReadModuleFileConfigurationWithConstants() { $sModuleFilePath = __DIR__.'/resources/module.authent-ldap.php'; - $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileConfigurationUnsafe($sModuleFilePath); $this->assertEquals($aExpected, $aRes); } @@ -65,10 +65,10 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase { $sModuleFilePath = __DIR__.'/resources/module.__MODULE__.php'; - $this->expectException(\ModuleDiscoveryServiceException::class); + $this->expectException(\ModuleFileReaderException::class); $this->expectExceptionMessage("Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting ',' or ']' or ')' on line 31"); - ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); } @@ -80,7 +80,7 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase $this->sTempModuleFilePath = tempnam(__DIR__, "test"); file_put_contents($this->sTempModuleFilePath, $sPHpCode); try { - return ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); + return ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); } finally { @unlink($this->sTempModuleFilePath); @@ -202,7 +202,7 @@ PHP; $this->assertEquals([$this->sTempModuleFilePath, "elseif2", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } - public function testCallDeclaredInstaller() + public function testGetAndCheckModuleInstallerClass() { $sModuleInstallerClass = "TicketsInstaller" . uniqid(); $sPHpCode = file_get_contents(__DIR__.'/resources/module.itop-tickets.php'); @@ -213,10 +213,10 @@ PHP; try { $this->assertFalse(class_exists($sModuleInstallerClass)); - $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); $this->assertFalse(class_exists($sModuleInstallerClass)); - ModuleDiscoveryService::GetInstance()->CallInstallerBeforeWritingConfigMethod(\MetaModel::GetConfig(), $aModuleInfo[2]); + $this->assertEquals($sModuleInstallerClass, ModuleFileReader::GetInstance()->GetAndCheckModuleInstallerClass($aModuleInfo[2])); } finally { @unlink($this->sTempModuleFilePath);