diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index f77d75c824..64b39c07d3 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -107,6 +107,9 @@ 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); + } if (!array_key_exists('itop_version', $aArgs)) { // Assume 1.0.2 diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 7af6130f98..5d4cbf00e0 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -88,85 +88,40 @@ class ModuleDiscoveryService { */ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array { - $aModuleInfo = []; // will be filled by the "eval" line below... try { $oParser = (new ParserFactory())->createForNewestSupportedVersion(); $aNodes = $oParser->parse(file_get_contents($sModuleFilePath)); } - catch (\Error $e) { - $sMessage = Dict::Format('config-parse-error', $e->getMessage(), $e->getLine()); - $this->oException = new \ModuleDiscoveryServiceException($sMessage, 0, $e); + catch (PhpParser\Error $e) { + throw new \ModuleDiscoveryServiceException($e->getMessage(), 0, $e, $sModuleFilePath); } try { foreach ($aNodes as $sKey => $oNode) { - if (false === ($oNode instanceof \PhpParser\Node\Stmt\Expression)) { - continue; + if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { + $aModuleConfig = $this->ParseCallToAddModuleAndReturnModuleConfiguration($sModuleFilePath, $oNode); + if (! is_null($aModuleConfig)){ + return $aModuleConfig; + } } - /** @var Assign $oAssignation */ - $oAssignation = $oNode->expr; - - if (false === ($oAssignation instanceof PhpParser\Node\Expr\StaticCall)) { - continue; + if ($oNode instanceof PhpParser\Node\Stmt\If_) { + $aModuleConfig = $this->BrowseIfStructure($sModuleFilePath, $oNode); + if (! is_null($aModuleConfig)){ + return $aModuleConfig; + } } - - /** @var PhpParser\Node\Expr\StaticCall $oAssignation */ - - if ("SetupWebPage" !== $oAssignation?->class?->name) { - continue; - } - - if ("AddModule" !== $oAssignation?->name?->name) { - continue; - } - - $aArgs = $oAssignation?->args; - if (count($aArgs) != 3) { - throw new ModuleDiscoveryServiceException("Not enough parameters when calling SetupWebPage::AddModule"); - } - - $oModuleId = $aArgs[1]; - if (false === ($oModuleId instanceof PhpParser\Node\Arg)) { - throw new ModuleDiscoveryServiceException("2nd parameter to SetupWebPage::AddModule call issue: " . get_class($oModuleId)); - } - - /** @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)); - } - - /** @var PhpParser\Node\Scalar\String_ $sModuleIdStringObj */ - $sModuleIdStringObj = $oModuleId->value; - $sModuleId = $sModuleIdStringObj->value; - - $oModuleConfigInfo = $aArgs[2]; - if (false === ($oModuleConfigInfo instanceof PhpParser\Node\Arg)) { - throw new ModuleDiscoveryServiceException("3rd parameter to SetupWebPage::AddModule call issue: " . get_class($oModuleConfigInfo)); - } - - /** @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)); - } - - $aModuleConfig=[]; - $this->BrowseArrayStructure($oModuleConfigInfo->value, $aModuleConfig); - - return [ - $sModuleFilePath, - $sModuleId, - $aModuleConfig - ]; } } catch(ModuleDiscoveryServiceException $e) { // Continue... throw $e; } catch(Exception $e) { // Continue... - throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e); + 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); } /** @@ -195,7 +150,12 @@ class ModuleDiscoveryService { if ($oArrayItem->key instanceof PhpParser\Node\Scalar\String_) { //dictionnary $sKey = $oArrayItem->key->value; - } else { + } else if ($oArrayItem->key instanceof \PhpParser\Node\Expr\ConstFetch) { + $sKey = $this->EvaluateConstantExpression($oArrayItem->key); + if (is_null($sKey)){ + continue; + } + }else { $sKey = $iIndex++; } @@ -207,16 +167,155 @@ class ModuleDiscoveryService { $aModuleConfig[$sKey]=$aSubConfig; } - if ($oValue instanceof PhpParser\Node\Scalar\String_) { + if ($oValue instanceof PhpParser\Node\Scalar\String_||$oValue instanceof PhpParser\Node\Scalar\Int_) { $aModuleConfig[$sKey]=$oValue->value; continue; } if ($oValue instanceof \PhpParser\Node\Expr\ConstFetch) { - $aModuleConfig[$sKey]= filter_var($oValue->name->name, FILTER_VALIDATE_BOOLEAN); + $oEvaluatedConstant = $this->EvaluateConstantExpression($oValue); + $aModuleConfig[$sKey]= $oEvaluatedConstant; } } } + + /** + * @param string $sModuleFilePath + * @param \PhpParser\Node\Expr\Assign $oAssignation + * + * @return array|null + * @throws \ModuleDiscoveryServiceException + */ + private function ParseCallToAddModuleAndReturnModuleConfiguration(string $sModuleFilePath, \PhpParser\Node\Stmt\Expression $oExpression) : ?array + { + /** @var Assign $oAssignation */ + $oAssignation = $oExpression->expr; + if (false === ($oAssignation instanceof PhpParser\Node\Expr\StaticCall)) { + return null; + } + + /** @var PhpParser\Node\Expr\StaticCall $oAssignation */ + + if ("SetupWebPage" !== $oAssignation?->class?->name) { + return null; + } + + if ("AddModule" !== $oAssignation?->name?->name) { + return null; + } + + $aArgs = $oAssignation?->args; + if (count($aArgs) != 3) { + throw new ModuleDiscoveryServiceException("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); + } + + /** @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); + } + + /** @var PhpParser\Node\Scalar\String_ $sModuleIdStringObj */ + $sModuleIdStringObj = $oModuleId->value; + $sModuleId = $sModuleIdStringObj->value; + + $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); + } + + /** @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); + } + + $aModuleConfig=[]; + $this->BrowseArrayStructure($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); + } + return [ + $sModuleFilePath, + $sModuleId, + $aModuleConfig + ]; + } + + /** + * @param string $sModuleFilePath + * @param \PhpParser\Node\Stmt\If_ $oNode + * + * @return array|null + * @throws \ModuleDiscoveryServiceException + */ + private function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt\If_ $oNode) : ?array + { + $bCondition = $this->EvaluateBooleanExpression($oNode->cond); + if ($bCondition) { + foreach ($oNode->stmts as $oSubNode) { + if ($oSubNode instanceof \PhpParser\Node\Stmt\Expression) { + $aModuleConfig = $this->ParseCallToAddModuleAndReturnModuleConfiguration($sModuleFilePath, $oSubNode); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + } + } + 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; + } + break; + } + } + + $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); + return $aModuleConfig; + } + + + private function ParseStatementsAndReturnModuleConfiguration(string $sModuleFilePath, array $aStmts) : ?array + { + foreach ($aStmts as $oSubNode) { + if ($oSubNode instanceof \PhpParser\Node\Stmt\Expression) { + $aModuleConfig = $this->ParseCallToAddModuleAndReturnModuleConfiguration($sModuleFilePath, $oSubNode); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + } + } + + return null; + } + + private 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()); + } + + return $bResult; + } + + private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression) : bool + { + //var_dump($oCondExpression); + return true; + } } class ModuleDiscoveryServiceException extends Exception @@ -228,11 +327,15 @@ class ModuleDiscoveryServiceException extends Exception * @param int $iHttpCode * @param Exception|null $oPrevious */ - public function __construct($sMessage, $iHttpCode = 0, Exception $oPrevious = null) + public function __construct($sMessage, $iHttpCode = 0, Exception $oPrevious = null, $sModuleFile=null) { $e = new \Exception(""); - SetupLog::Warning($sMessage, null, ['previous' => $oPrevious?->getMessage(), 'stack' => $e->getTraceAsString()]); + $aContext = ['previous' => $oPrevious?->getMessage(), 'stack' => $e->getTraceAsString()]; + if (! is_null($sModuleFile)){ + $aContext['module_file'] = $sModuleFile; + } + SetupLog::Warning($sMessage, null, $aContext); parent::__construct($sMessage, $iHttpCode, $oPrevious); } } \ 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/ModuleDiscoveryServiceTest.php index ee6ecbecf8..52d5be98ea 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -35,6 +35,26 @@ class ModuleDiscoveryServiceTest extends ItopDataTestCase $this->assertEquals($aExpected, $aRes); } + public function testReadModuleFileConfigurationWithConstants() + { + $sModuleFilePath = __DIR__.'/resources/module.authent-ldap.php'; + $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + + $this->assertEquals($aExpected, $aRes); + } + + public function testReadModuleFileConfigurationParsingIssue() + { + $sModuleFilePath = __DIR__.'/resources/module.__MODULE__.php'; + + $this->expectException(\ModuleDiscoveryServiceException::class); + $this->expectExceptionMessage("Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting ',' or ']' or ')' on line 31"); + + ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + } + + public static function ComputeBooleanExpressionProvider() { return [ diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.__MODULE__.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.__MODULE__.php new file mode 100644 index 0000000000..ae77342eb0 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.__MODULE__.php @@ -0,0 +1,56 @@ + '__module_label__', + 'category' => '__module_category__', + + // Setup + // + 'dependencies' => [ + __module_dependencies__ + ], + 'mandatory' => __module_mandatory__, + 'visible' => __module_visible__, + __module_setup_handler_class__ + + // Components + // + 'datamodel' => [ + 'vendor/autoload.php', + __module_data_model__, // Contains the PHP code generated by the "compilation" of datamodel.__module_name__.xml + ], + 'webservice' => [], + 'data.struct' => [ + // add your 'structure' definition XML files here, + ], + 'data.sample' => [ + // 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' => [ + // Module specific settings go here, if any + ], + ] +); + +__module_setup_handler__ diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.authent-ldap.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.authent-ldap.php new file mode 100644 index 0000000000..d1f7cf7151 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.authent-ldap.php @@ -0,0 +1,100 @@ + 'User authentication based on LDAP', + 'category' => 'authentication', + + // Setup + // + 'dependencies' => array( + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => 'AuthentLDAPInstaller', + + // Components + // + 'datamodel' => array( + ), + 'data.struct' => array( + //'data.struct.authent-ldap.xml', + ), + 'data.sample' => array( + //'data.sample.authent-ldap.xml', + ), + + // Documentation + // + 'doc.manual_setup' => '', + 'doc.more_information' => '', + + // Default settings + // + 'settings' => array( + 'uri' => 'ldap://localhost', // URI with host or IP address of your LDAP server + 'default_user' => '', // User and password used for initial "Anonymous" bind to LDAP + 'default_pwd' => '', // Leave both blank, if anonymous (read-only) bind is allowed + 'base_dn' => 'dc=yourcompany,dc=com', // Base DN for User queries, adjust it to your LDAP schema + 'user_query' => '(&(uid=%1$s)(inetuserstatus=ACTIVE))', // Query used to retrieve each user %1$s => iTop login + // For Windows AD use (samaccountname=%1$s) or (userprincipalname=%1$s) + + // Some extra LDAP options, refer to: http://www.php.net/manual/en/function.ldap-set-option.php for more info + 'options' => array( + LDAP_OPT_PROTOCOL_VERSION => 3, + LDAP_OPT_REFERRALS => 0, + ), + 'start_tls' => false, + 'debug' => false, + 'servers' => array(), + ), + ) +); + +// Module installation handler +// +class AuthentLDAPInstaller extends ModuleInstallerAPI +{ + public static function AfterDataLoad(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + // Create missing table entries + $sUserLDAPTable = MetaModel::DBGetTable('UserLDAP'); + $sUserTable = MetaModel::DBGetTable('User'); + $sSQL = "insert into $sUserLDAPTable (id) select U.id from $sUserTable as U left join $sUserLDAPTable as L on U.id = L.id where U.finalclass='UserLDAP' and isnull(L.id);"; + CMDBSource::Query($sSQL); + } + + public static function BeforeWritingConfig(Config $oConfiguration) + { + $sURI = $oConfiguration->GetModuleSetting('authent-ldap', 'uri'); + if (empty($sURI)) { + $sLDAPHost = MetaModel::GetModuleSetting('authent-ldap', 'host', 'localhost'); + $iLDAPPort = MetaModel::GetModuleSetting('authent-ldap', 'port', 389); + $sURI = preg_match('#^ldaps?://#i', $sLDAPHost) ? $sLDAPHost : 'ldap://'.$sLDAPHost.':'.$iLDAPPort; + $oConfiguration->SetModuleSetting('authent-ldap', 'uri', $sURI); + } + + $aServers = $oConfiguration->GetModuleSetting('authent-ldap', 'servers', []); + foreach ($aServers as &$aServer) { + if (!array_key_exists($aServer, 'uri')) { + $sLDAPHost = $aServerParams['host'] ?? 'localhost'; + $iLDAPPort = $aServerParams['port'] ?? 389; + $aServer['uri'] = preg_match('#^ldaps?://#i', $sLDAPHost) ? $sLDAPHost : 'ldap://'.$sLDAPHost.':'.$iLDAPPort; + } + } + $oConfiguration->SetModuleSetting('authent-ldap', 'servers', $aServers); + } +} + +} // if (function_exists('ldap_connect'))