From 1bc14f97e1c2b656ff72696846fc4f1f492bc6d8 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 20 Aug 2025 12:22:16 +0200 Subject: [PATCH 01/23] =?UTF-8?q?N=C2=B04789=20-=20Parse=20datamodel=20mod?= =?UTF-8?q?ule.xxx.php=20files=20instead=20of=20interpreting=20them=20-=20?= =?UTF-8?q?refactoring=20all=20in=20a=20dedicated=20service=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 149 ++++++------------ setup/modulediscovery.class.inc.php | 16 +- .../ModuleDiscoveryService.php | 86 ++++++++++ setup/runtimeenv.class.inc.php | 15 +- .../InstallationFileService.php | 3 +- setup/wizardsteps.class.inc.php | 30 ++-- 6 files changed, 163 insertions(+), 136 deletions(-) create mode 100644 setup/modulediscovery/ModuleDiscoveryService.php diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 440608f12e..20eb016763 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -11,52 +11,52 @@ class iTopExtension const SOURCE_WIZARD = 'datamodels'; const SOURCE_MANUAL = 'extensions'; const SOURCE_REMOTE = 'data'; - + /** * @var string */ public $sCode; - + /** * @var string */ public $sVersion; - + /** * @var string */ public $sInstalledVersion; - + /** * @var string */ public $sLabel; - + /** * @var string */ public $sDescription; - + /** * @var string */ public $sSource; - + /** * @var bool */ public $bMandatory; - + /** * @var string */ public $sMoreInfoUrl; - + /** * @var bool */ public $bMarkedAsChosen; - + /** * @var bool */ @@ -87,7 +87,7 @@ class iTopExtension * @var string[] */ public $aMissingDependencies; - + public function __construct() { $this->sCode = ''; @@ -120,13 +120,13 @@ class iTopExtensionsMap * @return void */ protected $aExtensions; - + /** * The list of directories browsed using the ReadDir method when building the map * @var string[] */ protected $aScannedDirs; - + public function __construct($sFromEnvironment = 'production', $bNormalizeOldExtensions = true, $aExtraDirs = array()) { $this->aExtensions = array(); @@ -142,7 +142,7 @@ public function __construct($sFromEnvironment = 'production', $bNormalizeOldExte $this->NormalizeOldExtensions(); } } - + /** * Populate the list of available (pseudo)extensions by scanning the disk * where the iTop files are located @@ -158,7 +158,7 @@ protected function ScanDisk($sEnvironment) $this->ReadDir(APPROOT.'/extensions', iTopExtension::SOURCE_MANUAL); $this->ReadDir(APPROOT.'/data/'.$sEnvironment.'-modules', iTopExtension::SOURCE_REMOTE); } - + /** * Read the information contained in the "installation.xml" file in the given directory * and create pseudo extensions from the list of choices described in this file @@ -168,7 +168,7 @@ protected function ScanDisk($sEnvironment) protected function ReadInstallationWizard($sDir) { if (!is_readable($sDir.'/installation.xml')) return false; - + $oXml = new XMLParameters($sDir.'/installation.xml'); foreach($oXml->Get('steps') as $aStepInfo) { @@ -183,7 +183,7 @@ protected function ReadInstallationWizard($sDir) } return true; } - + /** * Helper to process a "choice" array read from the installation.xml file * @param array $aChoices @@ -218,7 +218,7 @@ protected function ProcessWizardChoices($aChoices) } } } - + /** * Add an extension to the list of existing extensions, taking care of removing duplicates * (only the latest/greatest version is kept) @@ -248,7 +248,7 @@ protected function AddExtension(iTopExtension $oNewExtension) // Finally it's not a duplicate, let's add it to the list $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; } - + /** * Read (recursively) a directory to find if it contains extensions (or modules) * @@ -266,7 +266,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) { if ($sParentExtensionId == null) { - // We're not recursing, let's add the directory to the list of scanned dirs + // We're not recursing, let's add the directory to the list of scanned dirs $this->aScannedDirs[] = $sSearchDir; } $sExtensionId = null; @@ -285,7 +285,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) $oExtension->sMoreInfoUrl = $oXml->Get('more_info_url'); $oExtension->sSource = $sSource; $oExtension->sSourceDir = $sSearchDir; - + $sParentExtensionId = $sExtensionId = $oExtension->sCode.'/'.$oExtension->sVersion; $this->AddExtension($oExtension); } @@ -303,7 +303,8 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) { // Found a module - $aModuleInfo = $this->GetModuleInfo($sSearchDir.'/'.$sFile); + $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sSearchDir.'/'.$sFile); + // If we are not already inside a formal extension, then the module itself is considered // as an extension, otherwise, the module is just added to the list of modules belonging // to this extension @@ -314,7 +315,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) // Provide a default module version since version is mandatory when recording ExtensionInstallation $sModuleVersion = '0.0.1'; } - + if (($sParentExtensionId !== null) && (array_key_exists($sParentExtensionId, $this->aExtensions)) && ($this->aExtensions[$sParentExtensionId] instanceof iTopExtension)) { // Already inside an extension, let's add this module the list of modules belonging to this extension $this->aExtensions[$sParentExtensionId]->aModules[] = $sModuleName; @@ -324,7 +325,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) else { // Not already inside an folder containing an 'extension.xml' file - + // Ignore non-visible modules and auto-select ones, since these are never prompted // as a choice to the end-user $bVisible = true; @@ -332,7 +333,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) { $bVisible = false; } - + // Let's create a "fake" extension from this module (containing just this module) for backwards compatibility $oExtension = new iTopExtension(); $oExtension->sCode = $sModuleName; @@ -366,7 +367,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) } return false; } - + /** * Check if some extension contains a module with missing dependencies... * If so, populate the aMissingDepenencies array @@ -376,7 +377,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) protected function CheckDependencies($sFromEnvironment) { $aSearchDirs = array(); - + if (is_dir(APPROOT.'/datamodels/2.x')) { $aSearchDirs[] = APPROOT.'/datamodels/2.x'; @@ -386,7 +387,7 @@ protected function CheckDependencies($sFromEnvironment) $aSearchDirs[] = APPROOT.'/datamodels/1.x'; } $aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs); - + try { $aAllModules = ModuleDiscovery::GetAvailableModules($aSearchDirs, true); @@ -404,7 +405,7 @@ protected function CheckDependencies($sFromEnvironment) // This information is not available for pseudo modules defined in the installation wizard, but let's ignore them $sVersion = $oExtension->aModuleVersion[$sModuleName]; $sModuleId = $sModuleName.'/'.$sVersion; - + if (array_key_exists($sModuleId, $e->aModulesInfo)) { // The extension actually contains a module which has unmet dependencies @@ -416,61 +417,7 @@ protected function CheckDependencies($sFromEnvironment) } } } - - /** - * Read the information from a module file (module.xxx.php) - * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles - * @param string $sModuleFile - * @return array - */ - protected function GetModuleInfo($sModuleFile) - { - static $iDummyClassIndex = 0; - - $aModuleInfo = array(); // will be filled by the "eval" line below... - try - { - $aMatches = array(); - $sModuleFileContents = file_get_contents($sModuleFile); - $sModuleFileContents = str_replace(array(''), '', $sModuleFileContents); - $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFile)."'", $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_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); - } - $idx++; - } - // Replace the main function call by an assignment to a variable, as an array... - $sModuleFileContents = str_replace(array('SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'), '$aModuleInfo = array', $sModuleFileContents); - - eval($sModuleFileContents); // Assigns $aModuleInfo - - if (count($aModuleInfo) === 0) - { - SetupLog::Warning("Eval of $sModuleFile did not return the expected information..."); - } - } - catch(ParseError $e) - { - // Continue... - SetupLog::Warning("Eval of $sModuleFile caused a parse error: ".$e->getMessage()." at line ".$e->getLine()); - } - catch(Exception $e) - { - // Continue... - SetupLog::Warning("Eval of $sModuleFile caused an exception: ".$e->getMessage()); - } - return $aModuleInfo; - } - + /** * Get all available extensions * @return iTopExtension[] @@ -479,7 +426,7 @@ public function GetAllExtensions() { return $this->aExtensions; } - + /** * Mark the given extension as chosen * @param string $sExtensionCode The code of the extension (code without verison number) @@ -497,7 +444,7 @@ public function MarkAsChosen($sExtensionCode, $bMark = true) } } } - + /** * Tells if a given extension(code) is marked as chosen * @param string $sExtensionCode @@ -532,7 +479,7 @@ protected function SetInstalledVersion($sExtensionCode, $sInstalledVersion) } } } - + /** * Get the list of the "chosen" extensions * @return iTopExtension[] @@ -549,7 +496,7 @@ public function GetChoices() } return $aResult; } - + /** * Load the choices (i.e. MarkedAsChosen) from the database defined in the supplied Config * @param Config $oConfig @@ -572,7 +519,7 @@ public function LoadChoicesFromDatabase(Config $oConfig) // No database or erroneous information return false; } - + foreach($aInstalledExtensions as $aDBInfo) { $this->MarkAsChosen($aDBInfo['code']); @@ -580,7 +527,7 @@ public function LoadChoicesFromDatabase(Config $oConfig) } return true; } - + /** * Find is a single-module extension is contained within another extension * @param iTopExtension $oExtension @@ -590,7 +537,7 @@ public function IsExtensionObsoletedByAnother(iTopExtension $oExtension) { // Complex extensions (more than 1 module) are never considered as obsolete if (count($oExtension->aModules) != 1) return null; - + foreach($this->GetAllExtensions() as $oOtherExtension) { if (($oOtherExtension->sSourceDir != $oExtension->sSourceDir) && ($oOtherExtension->sSource != iTopExtension::SOURCE_WIZARD)) @@ -603,12 +550,12 @@ public function IsExtensionObsoletedByAnother(iTopExtension $oExtension) } } } - + // No match at all return null; - + } - + /** * Search for multi-module extensions that are NOT deployed as an extension (i.e. shipped with an extension.xml file) * but as a bunch of un-related modules based on the signature of some well-known extensions. If such an extension is found, @@ -639,7 +586,7 @@ public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MA } if ($bFound) break; // The current version matches the signature } - + if ($bFound) { $oExtension = new iTopExtension(); @@ -663,7 +610,7 @@ public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MA } } } - + /** * Check if the given module-code/version is present on the disk * @param string $sModuleIdToFind The module ID (code/version) to search for @@ -674,7 +621,7 @@ protected function ModuleIsPresent($sModuleIdToFind, $sInSourceOnly) { return (array_key_exists($sModuleIdToFind, $this->aExtensions) && ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly)); } - + /** * Check if the given module-code/version is currently installed * @param string $sModuleIdToFind The module ID (code/version) to search for @@ -687,7 +634,7 @@ protected function ModuleIsInstalled($sModuleIdToFind, $sInSourceOnly) ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly) && ($this->aExtensions[$sModuleIdToFind]->sInstalledVersion !== '') ); } - + /** * Tells if the given module name is "chosen" since it is part of a "chosen" extension (in the specified source dir) * @param string $sModuleNameToFind @@ -697,7 +644,7 @@ protected function ModuleIsInstalled($sModuleIdToFind, $sInSourceOnly) public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSourceOnly = iTopExtension::SOURCE_REMOTE) { $bChosen = false; - + foreach($this->GetAllExtensions() as $oExtension) { if (($oExtension->sSource == $sInSourceOnly) && @@ -709,7 +656,7 @@ public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSource } return false; } - + /** * Replace a given set of stand-alone modules by one single "extension" * @param string[] $aModules @@ -723,7 +670,7 @@ protected function ReplaceModulesByNormalizedExtension($aModules, iTopExtension } $this->AddExtension($oNewExtension); } - + /** * Get the list of signatures of some well-known multi-module extensions without extension.xml file (should not exist anymore) * diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 96822a982d..f36423f0cf 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -19,6 +19,8 @@ * */ +require_once(APPROOT.'setup/modulediscovery/ModuleDiscoveryService.php'); + class MissingDependencyException extends CoreException { /** @@ -385,7 +387,7 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a else { $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); - $bOk = @eval('$bResult = '.$sBooleanExpr.'; return true;'); + $bOk = ModuleDiscoveryService::GetInstance()->ComputeDependencyExpression($sBooleanExpr); if ($bOk == false) { SetupLog::Warning("Eval of '$sBooleanExpr' returned false"); @@ -514,24 +516,22 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) } $idx++; } - $bRet = eval($sModuleFileContents); - if ($bRet === false) - { - SetupLog::Warning("Eval of $sRelDir/$sFile returned false"); - } + $sModuleFilePath = $sDirectory.'/'.$sFile; + $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo); //echo "

Done.

\n"; } catch(ParseError $e) { // PHP 7 - SetupLog::Warning("Eval of $sRelDir/$sFile caused an exception: ".$e->getMessage()." at line ".$e->getLine()); + SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()." at line ".$e->getLine()); } catch(Exception $e) { // Continue... - SetupLog::Warning("Eval of $sRelDir/$sFile caused an exception: ".$e->getMessage()); + SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()); } } } diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php new file mode 100644 index 0000000000..5b82ac4abc --- /dev/null +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -0,0 +1,86 @@ +'), '', $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 the class 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 + $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); + } + $idx++; + } + + // Replace the main function call by an assignment to a variable, as an array... + $sModuleFileContents = str_replace(array('SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'), '$aModuleInfo = array', $sModuleFileContents); + + eval($sModuleFileContents); // Assigns $aModuleInfo + + if (count($aModuleInfo) === 0) + { + SetupLog::Warning("Eval of $sModuleFilePath did not return the expected information..."); + } + + //echo "

Done.

\n"; + } + catch(ParseError $e) + { + // PHP 7 + SetupLog::Warning("Eval of $sModuleFilePath caused a parse exception: ".$e->getMessage()." at line ".$e->getLine()); + } + catch(Exception $e) + { + // Continue... + SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()); + } + + return $aModuleInfo; + } + + public function ComputeDependencyExpression(string $sBooleanExpr) : bool + { + return @eval('$bResult = '.$sBooleanExpr.'; return $bResult;'); + } + + + public function ComputeAutoSelectExpression(string $sBooleanExpr) : bool + { + return eval('$bSelected = ('.$sBooleanExpr.'); return $bSelected'); + } +} \ No newline at end of file diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index bf5cc50c65..3872392719 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -459,18 +459,17 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { try { - $bSelected = false; SetupInfo::SetSelectedModules($aRet); - eval('$bSelected = ('.$oModule->GetAutoSelect().');'); + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($oModule->GetAutoSelect()); + + if ($bSelected) + { + $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module + $bModuleAdded = true; + } } catch(Exception $e) { - $bSelected = false; - } - if ($bSelected) - { - $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module - $bModuleAdded = true; } } } diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index e44a8d0460..2aa6e28678 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -269,9 +269,8 @@ public function ProcessAutoSelectModules() : void { foreach($this->GetAutoSelectModules() as $sModuleId => $aModule) { try { - $bSelected = false; SetupInfo::SetSelectedModules($this->aSelectedModules); - eval('$bSelected = ('.$aModule['auto_select'].');'); + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aModule['auto_select']); if ($bSelected) { // Modules in data/production-modules/ are considered as mandatory and always installed diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index a6bbd1012f..3e7b398e80 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -604,7 +604,7 @@ public function Display(WebPage $oPage) // No changes detected... or no way to tell because of the lack of a manifest or previous source dir // Use the "compatible" datamodel as-is. $sCompatibleDMDirToDisplay = utils::HtmlEntities($sCompatibleDMDir); - $sUpgradeDMVersionToDisplay = utils::HtmlEntities($sUpgradeDMVersion); + $sUpgradeDMVersionToDisplay = utils::HtmlEntities($sUpgradeDMVersion); $oPage->add( <<The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion. @@ -1786,19 +1786,17 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP if (isset($aInfo['auto_select'])) { // Check the module selection try { - $bSelected = false; SetupInfo::SetSelectedModules($aModules); - eval('$bSelected = ('.$aInfo['auto_select'].');'); + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aInfo['auto_select']); + if ($bSelected) { + $aModules[$sModuleId] = true; // store the Id of the selected module + SetupInfo::SetSelectedModules($aModules); + } } catch (Exception $e) { - $bSelected = false; } } } - if ($bSelected) { - $aModules[$sModuleId] = true; // store the Id of the selected module - SetupInfo::SetSelectedModules($aModules); - } } } $sChoiceType = isset($aChoice['type']) ? $aChoice['type'] : 'wizard_option'; @@ -1864,20 +1862,18 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP { try { - $bSelected = false; SetupInfo::SetSelectedModules($aModules); - eval('$bSelected = ('.$aModule['auto_select'].');'); + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aModule['auto_select']); + if ($bSelected) + { + $aModules[$sModuleId] = true; // store the Id of the selected module + $sDisplayChoices .= '
  • '.$aModule['label'].' (auto_select)
  • '; + $bModuleAdded = true; + } } catch(Exception $e) { $sDisplayChoices .= '
  • Warning: auto_select failed with exception ('.$e->getMessage().') for module "'.$sModuleId.'"
  • '; - $bSelected = false; - } - if ($bSelected) - { - $aModules[$sModuleId] = true; // store the Id of the selected module - $sDisplayChoices .= '
  • '.$aModule['label'].' (auto_select)
  • '; - $bModuleAdded = true; } } } From 07d7995a519f83d971511e42477d51e3d97df5a0 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 20 Aug 2025 16:26:03 +0200 Subject: [PATCH 02/23] =?UTF-8?q?N=C2=B04789=20-=20fix=20broken=20setup=20?= =?UTF-8?q?+=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 1395 +++++++++-------- setup/modulediscovery.class.inc.php | 58 +- .../ModuleDiscoveryService.php | 64 +- setup/runtimeenv.class.inc.php | 293 ++-- .../InstallationFileService.php | 5 +- setup/wizardsteps.class.inc.php | 271 ++-- .../ModuleDiscoveryServiceTest.php | 55 + .../resources/module.itop-full-itil.php | 41 + 8 files changed, 1141 insertions(+), 1041 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-full-itil.php diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 20eb016763..afc5ba6a3b 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -22,12 +22,12 @@ class iTopExtension */ public $sVersion; - /** + /** * @var string */ public $sInstalledVersion; -/** + /** * @var string */ public $sLabel; @@ -134,7 +134,7 @@ public function __construct($sFromEnvironment = 'production', $bNormalizeOldExte $this->ScanDisk($sFromEnvironment); foreach($aExtraDirs as $sDir) { - $this->ReadDir($sDir, iTopExtension::SOURCE_REMOTE); + $this->ReadDir($sDir, iTopExtension::SOURCE_REMOTE); } $this->CheckDependencies($sFromEnvironment); if ($bNormalizeOldExtensions) @@ -264,11 +264,11 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) $hDir = opendir($sSearchDir); if ($hDir !== false) { - if ($sParentExtensionId == null) - { - // We're not recursing, let's add the directory to the list of scanned dirs - $this->aScannedDirs[] = $sSearchDir; - } + if ($sParentExtensionId == null) + { + // We're not recursing, let's add the directory to the list of scanned dirs + $this->aScannedDirs[] = $sSearchDir; + } $sExtensionId = null; $aSubDirectories = array(); @@ -303,8 +303,11 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) { // Found a module - $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sSearchDir.'/'.$sFile); - + try { + $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sSearchDir.'/'.$sFile); + } catch(ModuleDiscoveryServiceException $e){ + continue; + } // If we are not already inside a formal extension, then the module itself is considered // as an extension, otherwise, the module is just added to the list of modules belonging // to this extension @@ -331,7 +334,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) $bVisible = true; if (!$aModuleInfo[2]['visible'] || isset($aModuleInfo[2]['auto_select'])) { - $bVisible = false; + $bVisible = false; } // Let's create a "fake" extension from this module (containing just this module) for backwards compatibility @@ -564,51 +567,51 @@ public function IsExtensionObsoletedByAnother(iTopExtension $oExtension) */ public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MANUAL) { - $aSignatures = $this->GetOldExtensionsSignatures(); - foreach($aSignatures as $sExtensionCode => $aExtensionSignatures) - { - $bFound = false; - foreach($aExtensionSignatures['versions'] as $sVersion => $aModules) - { - $bInstalled = true; - foreach($aModules as $sModuleId) - { - if(!$this->ModuleIsPresent($sModuleId, $sInSourceOnly)) - { - $bFound = false; - break; // One missing module is enough to determine that the extension/version is not present - } - else - { - $bInstalled = $bInstalled && (!$this->ModuleIsInstalled($sModuleId, $sInSourceOnly)); - $bFound = true; - } - } - if ($bFound) break; // The current version matches the signature - } + $aSignatures = $this->GetOldExtensionsSignatures(); + foreach($aSignatures as $sExtensionCode => $aExtensionSignatures) + { + $bFound = false; + foreach($aExtensionSignatures['versions'] as $sVersion => $aModules) + { + $bInstalled = true; + foreach($aModules as $sModuleId) + { + if(!$this->ModuleIsPresent($sModuleId, $sInSourceOnly)) + { + $bFound = false; + break; // One missing module is enough to determine that the extension/version is not present + } + else + { + $bInstalled = $bInstalled && (!$this->ModuleIsInstalled($sModuleId, $sInSourceOnly)); + $bFound = true; + } + } + if ($bFound) break; // The current version matches the signature + } - if ($bFound) - { - $oExtension = new iTopExtension(); - $oExtension->sCode = $sExtensionCode; - $oExtension->sLabel = $aExtensionSignatures['label']; - $oExtension->sSource = $sInSourceOnly; - $oExtension->sDescription = $aExtensionSignatures['description']; - $oExtension->sVersion = $sVersion; - $oExtension->aModules = array(); - if ($bInstalled) - { - $oExtension->sInstalledVersion = $sVersion; - $oExtension->bMarkedAsChosen = true; - } - foreach($aModules as $sModuleId) - { - list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); - $oExtension->aModules[] = $sModuleName; - } - $this->ReplaceModulesByNormalizedExtension($aExtensionSignatures['versions'][$sVersion], $oExtension); - } - } + if ($bFound) + { + $oExtension = new iTopExtension(); + $oExtension->sCode = $sExtensionCode; + $oExtension->sLabel = $aExtensionSignatures['label']; + $oExtension->sSource = $sInSourceOnly; + $oExtension->sDescription = $aExtensionSignatures['description']; + $oExtension->sVersion = $sVersion; + $oExtension->aModules = array(); + if ($bInstalled) + { + $oExtension->sInstalledVersion = $sVersion; + $oExtension->bMarkedAsChosen = true; + } + foreach($aModules as $sModuleId) + { + list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); + $oExtension->aModules[] = $sModuleName; + } + $this->ReplaceModulesByNormalizedExtension($aExtensionSignatures['versions'][$sVersion], $oExtension); + } + } } /** @@ -619,7 +622,7 @@ public function NormalizeOldExtensions($sInSourceOnly = iTopExtension::SOURCE_MA */ protected function ModuleIsPresent($sModuleIdToFind, $sInSourceOnly) { - return (array_key_exists($sModuleIdToFind, $this->aExtensions) && ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly)); + return (array_key_exists($sModuleIdToFind, $this->aExtensions) && ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly)); } /** @@ -630,9 +633,9 @@ protected function ModuleIsPresent($sModuleIdToFind, $sInSourceOnly) */ protected function ModuleIsInstalled($sModuleIdToFind, $sInSourceOnly) { - return (array_key_exists($sModuleIdToFind, $this->aExtensions) && - ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly) && - ($this->aExtensions[$sModuleIdToFind]->sInstalledVersion !== '') ); + return (array_key_exists($sModuleIdToFind, $this->aExtensions) && + ($this->aExtensions[$sModuleIdToFind]->sSource == $sInSourceOnly) && + ($this->aExtensions[$sModuleIdToFind]->sInstalledVersion !== '') ); } /** @@ -664,11 +667,11 @@ public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSource */ protected function ReplaceModulesByNormalizedExtension($aModules, iTopExtension $oNewExtension) { - foreach($aModules as $sModuleId) - { - unset($this->aExtensions[$sModuleId]); - } - $this->AddExtension($oNewExtension); + foreach($aModules as $sModuleId) + { + unset($this->aExtensions[$sModuleId]); + } + $this->AddExtension($oNewExtension); } /** @@ -678,637 +681,637 @@ protected function ReplaceModulesByNormalizedExtension($aModules, iTopExtension */ protected function GetOldExtensionsSignatures() { - // Generated by the Factory using the page export_component_versions_for_normalisation.php - return array ( - 'combodo-approval-process-light' => - array ( - 'label' => 'Approval process light', - 'description' => 'Approve a request via a simple email', - 'versions' => - array ( - '1.0.1' => - array ( - 0 => 'approval-base/2.1.0', - 1 => 'combodo-approval-light/1.0.1', - ), - '1.0.2' => - array ( - 0 => 'approval-base/2.1.1', - 1 => 'combodo-approval-light/1.0.2', - ), - '1.0.3' => - array ( - 0 => 'approval-base/2.1.2', - 1 => 'combodo-approval-light/1.0.2', - ), - '1.1.0' => - array ( - 0 => 'approval-base/2.2.2', - 1 => 'combodo-approval-light/1.0.2', - ), - '1.1.1' => - array ( - 0 => 'approval-base/2.2.3', - 1 => 'combodo-approval-light/1.0.2', - ), - '1.1.2' => - array ( - 0 => 'approval-base/2.2.6', - 1 => 'combodo-approval-light/1.0.2', - ), - '1.1.3' => - array ( - 0 => 'approval-base/2.2.6', - 1 => 'combodo-approval-light/1.0.3', - ), - '1.2.0' => - array ( - 0 => 'approval-base/2.3.0', - 1 => 'combodo-approval-light/1.0.3', - ), - '1.2.1' => - array ( - 0 => 'approval-base/2.4.0', - 1 => 'combodo-approval-light/1.0.4', - ), - '1.3.0' => - array ( - 0 => 'approval-base/2.4.2', - 1 => 'combodo-approval-light/1.1.1', - ), - '1.3.1' => - array ( - 0 => 'approval-base/2.5.0', - 1 => 'combodo-approval-light/1.1.1', - ), - '1.3.2' => - array ( - 0 => 'approval-base/2.5.0', - 1 => 'combodo-approval-light/1.1.2', - ), - '1.2.2' => - array ( - 0 => 'approval-base/2.4.2', - 1 => 'combodo-approval-light/1.0.5', - ), - '1.3.3' => - array ( - 0 => 'approval-base/2.5.1', - 1 => 'combodo-approval-light/1.1.2', - ), - '1.3.4' => - array ( - 0 => 'approval-base/2.5.2', - 1 => 'combodo-approval-light/1.1.2', - ), - '1.3.5' => - array ( - 0 => 'approval-base/2.5.3', - 1 => 'combodo-approval-light/1.1.2', - ), - '1.4.0' => - array ( - 0 => 'approval-base/2.5.3', - 1 => 'combodo-approval-light/1.1.2', - 2 => 'itop-approval-portal/1.0.0', - ), - ), - ), - 'combodo-approval-process-automation' => - array ( - 'label' => 'Approval process automation', - 'description' => 'Control your approval process with predefined rules based on service catalog', - 'versions' => - array ( - '1.0.1' => - array ( - 0 => 'approval-base/2.1.0', - 1 => 'combodo-approval-extended/1.0.2', - ), - '1.0.2' => - array ( - 0 => 'approval-base/2.1.1', - 1 => 'combodo-approval-extended/1.0.4', - ), - '1.0.3' => - array ( - 0 => 'approval-base/2.1.2', - 1 => 'combodo-approval-extended/1.0.4', - ), - '1.1.0' => - array ( - 0 => 'approval-base/2.2.2', - 1 => 'combodo-approval-extended/1.0.4', - ), - '1.1.1' => - array ( - 0 => 'approval-base/2.2.3', - 1 => 'combodo-approval-extended/1.0.4', - ), - '1.1.2' => - array ( - 0 => 'approval-base/2.2.6', - 1 => 'combodo-approval-extended/1.0.5', - ), - '1.1.3' => - array ( - 0 => 'approval-base/2.2.6', - 1 => 'combodo-approval-extended/1.0.6', - ), - '1.2.0' => - array ( - 0 => 'approval-base/2.3.0', - 1 => 'combodo-approval-extended/1.0.7', - ), - '1.2.1' => - array ( - 0 => 'approval-base/2.4.0', - 1 => 'combodo-approval-extended/1.0.8', - ), - '1.3.0' => - array ( - 0 => 'approval-base/2.4.2', - 1 => 'combodo-approval-extended/1.2.1', - ), - '1.3.1' => - array ( - 0 => 'approval-base/2.5.0', - 1 => 'combodo-approval-extended/1.2.1', - ), - '1.3.2' => - array ( - 0 => 'approval-base/2.5.0', - 1 => 'combodo-approval-extended/1.2.2', - ), - '1.2.2' => - array ( - 0 => 'approval-base/2.4.2', - 1 => 'combodo-approval-extended/1.0.9', - ), - '1.3.3' => - array ( - 0 => 'approval-base/2.5.1', - 1 => 'combodo-approval-extended/1.2.3', - ), - '1.3.4' => - array ( - 0 => 'approval-base/2.5.2', - 1 => 'combodo-approval-extended/1.2.3', - ), - '1.3.5' => - array ( - 0 => 'approval-base/2.5.3', - 1 => 'combodo-approval-extended/1.2.3', - ), - '1.4.0' => - array ( - 0 => 'approval-base/2.5.3', - 1 => 'combodo-approval-extended/1.2.3', - 3 => 'itop-approval-portal/1.0.0', - ), - ), - ), - 'combodo-predefined-response-models' => - array ( - 'label' => 'Predefined response models', - 'description' => 'Pick common answers from a list of predefined replies grouped by categories to update tickets log', - 'versions' => - array ( - '1.0.0' => - array ( - 0 => 'precanned-replies/1.0.0', - 1 => 'precanned-replies-pro/1.0.0', - ), - '1.0.1' => - array ( - 0 => 'precanned-replies/1.0.1', - 1 => 'precanned-replies-pro/1.0.1', - ), - '1.0.2' => - array ( - 0 => 'precanned-replies/1.0.2', - 1 => 'precanned-replies-pro/1.0.1', - ), - '1.0.3' => - array ( - 0 => 'precanned-replies/1.0.3', - 1 => 'precanned-replies-pro/1.0.1', - ), - '1.0.4' => - array ( - 0 => 'precanned-replies/1.0.3', - 1 => 'precanned-replies-pro/1.0.2', - ), - '1.0.5' => - array ( - 0 => 'precanned-replies/1.0.4', - 1 => 'precanned-replies-pro/1.0.2', - ), - '1.1.0' => - array ( - 0 => 'precanned-replies/1.1.0', - 1 => 'precanned-replies-pro/1.0.2', - ), - '1.1.1' => - array ( - 0 => 'precanned-replies/1.1.1', - 1 => 'precanned-replies-pro/1.0.2', - ), - ), - ), - 'combodo-customized-request-forms' => - array ( - 'label' => 'Customized request forms', - 'description' => 'Define personalized request forms based on the service catalog. Add extra fields for a given type of request.', - 'versions' => - array ( - '1.0.1' => - array ( - 0 => 'templates-base/2.1.1', - 1 => 'itop-request-template/1.0.0', - ), - '1.0.2' => - array ( - 0 => 'templates-base/2.1.2', - 1 => 'itop-request-template/1.0.0', - ), - '1.0.3' => - array ( - 0 => 'templates-base/2.1.2', - 1 => 'itop-request-template/1.0.1', - ), - '1.0.4' => - array ( - 0 => 'templates-base/2.1.3', - 1 => 'itop-request-template/1.0.1', - ), - '1.0.5' => - array ( - 0 => 'templates-base/2.1.4', - 1 => 'itop-request-template/1.0.1', - ), - '2.0.0' => - array ( - 0 => 'templates-base/3.0.0', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.1' => - array ( - 0 => 'templates-base/3.0.1', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.2' => - array ( - 0 => 'templates-base/3.0.2', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.3' => - array ( - 0 => 'templates-base/3.0.4', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.4' => - array ( - 0 => 'templates-base/3.0.5', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.5' => - array ( - 0 => 'templates-base/3.0.6', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.6' => - array ( - 0 => 'templates-base/3.0.8', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.7' => - array ( - 0 => 'templates-base/3.0.9', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - '2.0.8' => - array ( - 0 => 'templates-base/3.0.12', - 1 => 'itop-request-template/2.0.0', - 2 => 'itop-request-template-portal/1.0.0', - ), - ), - ), - 'combodo-sla-considering-business-hours' => - array ( - 'label' => 'SLA considering business hours', - 'description' => 'Compute SLAs taking into account service coverage window and holidays', - 'versions' => - array ( - '2.0.1' => - array ( - 0 => 'combodo-sla-computation/2.0.1', - 1 => 'combodo-coverage-windows-computation/2.0.0', - ), - '2.1.0' => - array ( - 0 => 'combodo-sla-computation/2.1.0', - 1 => 'combodo-coverage-windows-computation/2.0.0', - ), - '2.1.1' => - array ( - 0 => 'combodo-sla-computation/2.1.1', - 1 => 'combodo-coverage-windows-computation/2.0.0', - ), - '2.1.2' => - array ( - 0 => 'combodo-sla-computation/2.1.2', - 1 => 'combodo-coverage-windows-computation/2.0.0', - ), - '2.1.3' => - array ( - 0 => 'combodo-sla-computation/2.1.2', - 1 => 'combodo-coverage-windows-computation/2.0.1', - ), - '2.0.2' => - array ( - 0 => 'combodo-sla-computation/2.0.1', - 1 => 'combodo-coverage-windows-computation/2.0.1', - ), - '2.1.4' => - array ( - 0 => 'combodo-sla-computation/2.1.3', - 1 => 'combodo-coverage-windows-computation/2.0.1', - ), - '2.1.5' => - array ( - 0 => 'combodo-sla-computation/2.1.5', - 1 => 'combodo-coverage-windows-computation/2.0.1', - ), - '2.1.6' => - array ( - 0 => 'combodo-sla-computation/2.1.5', - 1 => 'combodo-coverage-windows-computation/2.0.2', - ), - '2.1.7' => - array ( - 0 => 'combodo-sla-computation/2.1.6', - 1 => 'combodo-coverage-windows-computation/2.0.2', - ), - '2.1.8' => - array ( - 0 => 'combodo-sla-computation/2.1.7', - 1 => 'combodo-coverage-windows-computation/2.0.2', - ), - '2.1.9' => - array ( - 0 => 'combodo-sla-computation/2.1.8', - 1 => 'combodo-coverage-windows-computation/2.0.2', - ), - ), - ), - 'combodo-mail-to-ticket-automation' => - array ( - 'label' => 'Mail to ticket automation', - 'description' => 'Scan several mailboxes to create or update tickets.', - 'versions' => - array ( - '2.6.0' => - array ( - 0 => 'combodo-email-synchro/2.6.0', - 1 => 'itop-standard-email-synchro/2.6.0', - ), - '2.6.1' => - array ( - 0 => 'combodo-email-synchro/2.6.1', - 1 => 'itop-standard-email-synchro/2.6.0', - ), - '2.6.2' => - array ( - 0 => 'combodo-email-synchro/2.6.2', - 1 => 'itop-standard-email-synchro/2.6.0', - ), - '2.6.3' => - array ( - 0 => 'combodo-email-synchro/2.6.2', - 1 => 'itop-standard-email-synchro/2.6.1', - ), - '2.6.4' => - array ( - 0 => 'combodo-email-synchro/2.6.3', - 1 => 'itop-standard-email-synchro/2.6.2', - ), - '2.6.5' => - array ( - 0 => 'combodo-email-synchro/2.6.4', - 1 => 'itop-standard-email-synchro/2.6.2', - ), - '2.6.6' => - array ( - 0 => 'combodo-email-synchro/2.6.5', - 1 => 'itop-standard-email-synchro/2.6.3', - ), - '2.6.7' => - array ( - 0 => 'combodo-email-synchro/2.6.6', - 1 => 'itop-standard-email-synchro/2.6.4', - ), - '2.6.8' => - array ( - 0 => 'combodo-email-synchro/2.6.7', - 1 => 'itop-standard-email-synchro/2.6.4', - ), - '2.6.9' => - array ( - 0 => 'combodo-email-synchro/2.6.8', - 1 => 'itop-standard-email-synchro/2.6.5', - ), - '2.6.10' => - array ( - 0 => 'combodo-email-synchro/2.6.9', - 1 => 'itop-standard-email-synchro/2.6.6', - ), - '2.6.11' => - array ( - 0 => 'combodo-email-synchro/2.6.10', - 1 => 'itop-standard-email-synchro/2.6.6', - ), - '2.6.12' => - array ( - 0 => 'combodo-email-synchro/2.6.11', - 1 => 'itop-standard-email-synchro/2.6.6', - ), - '3.0.0' => - array ( - 0 => 'combodo-email-synchro/3.0.0', - 1 => 'itop-standard-email-synchro/3.0.0', - ), - '3.0.1' => - array ( - 0 => 'combodo-email-synchro/3.0.1', - 1 => 'itop-standard-email-synchro/3.0.1', - ), - '3.0.2' => - array ( - 0 => 'combodo-email-synchro/3.0.2', - 1 => 'itop-standard-email-synchro/3.0.1', - ), - '3.0.3' => - array ( - 0 => 'combodo-email-synchro/3.0.3', - 1 => 'itop-standard-email-synchro/3.0.3', - ), - '3.0.4' => - array ( - 0 => 'combodo-email-synchro/3.0.3', - 1 => 'itop-standard-email-synchro/3.0.4', - ), - '3.0.5' => - array ( - 0 => 'combodo-email-synchro/3.0.4', - 1 => 'itop-standard-email-synchro/3.0.4', - ), - '3.0.6' => - array ( - 0 => 'combodo-email-synchro/3.0.5', - 1 => 'itop-standard-email-synchro/3.0.4', - ), - '3.0.7' => - array ( - 0 => 'combodo-email-synchro/3.0.5', - 1 => 'itop-standard-email-synchro/3.0.5', - ), - ), - ), - 'combodo-configurator-for-automatic-object-creation' => - array ( - 'label' => 'Configurator for automatic object creation', - 'description' => 'Templating based on existing objects.', - 'versions' => - array ( - '1.0.13' => - array ( - 1 => 'itop-stencils/1.0.6', - ), - ), - ), - 'combodo-user-actions-configurator' => - array ( - 'label' => 'User actions configurator', - 'description' => 'Configure user actions to simplify and automate processes (e.g. create an incident from a CI).', - 'versions' => - array ( - '1.0.0' => - array ( - 0 => 'itop-object-copier/1.0.0', - ), - '1.0.1' => - array ( - 0 => 'itop-object-copier/1.0.1', - ), - '1.0.2' => - array ( - 0 => 'itop-object-copier/1.0.2', - ), - '1.0.3' => - array ( - 0 => 'itop-object-copier/1.0.3', - ), - '1.1.0' => - array ( - 0 => 'itop-object-copier/1.1.0', - ), - '1.1.1' => - array ( - 0 => 'itop-object-copier/1.1.1', - ), - '1.1.2' => - array ( - 0 => 'itop-object-copier/1.1.2', - ), - '1.1.3' => - array ( - 0 => 'itop-object-copier/1.1.3', - ), - '1.1.4' => - array ( - 0 => 'itop-object-copier/1.1.4', - ), - '1.1.5' => - array ( - 0 => 'itop-object-copier/1.1.5', - ), - '1.1.6' => - array ( - 0 => 'itop-object-copier/1.1.6', - ), - '1.1.7' => - array ( - 0 => 'itop-object-copier/1.1.7', - ), - '1.1.8' => - array ( - 0 => 'itop-object-copier/1.1.8', - ), - ), - ), - 'combodo-send-updates-by-email' => - array ( - 'label' => 'Send updates by email', - 'description' => 'Send an email to pre-configured contacts when a ticket log is updated.', - 'versions' => - array ( - '1.0.1' => - array ( - 0 => 'email-reply/1.0.1', - ), - '1.0.3' => - array ( - 0 => 'email-reply/1.0.3', - ), - '1.1.1' => - array ( - 0 => 'email-reply/1.1.1', - ), - '1.1.2' => - array ( - 0 => 'email-reply/1.1.2', - ), - '1.1.3' => - array ( - 0 => 'email-reply/1.1.3', - ), - '1.1.4' => - array ( - 0 => 'email-reply/1.1.4', - ), - '1.1.5' => - array ( - 0 => 'email-reply/1.1.5', - ), - '1.1.6' => - array ( - 0 => 'email-reply/1.1.6', - ), - '1.1.7' => - array ( - 0 => 'email-reply/1.1.7', - ), - // 1.1.8 was never released - '1.1.9' => - array ( - 0 => 'email-reply/1.1.9', - ), - '1.1.10' => - array ( - 0 => 'email-reply/1.1.10', - ), - ), - ), - ); + // Generated by the Factory using the page export_component_versions_for_normalisation.php + return array ( + 'combodo-approval-process-light' => + array ( + 'label' => 'Approval process light', + 'description' => 'Approve a request via a simple email', + 'versions' => + array ( + '1.0.1' => + array ( + 0 => 'approval-base/2.1.0', + 1 => 'combodo-approval-light/1.0.1', + ), + '1.0.2' => + array ( + 0 => 'approval-base/2.1.1', + 1 => 'combodo-approval-light/1.0.2', + ), + '1.0.3' => + array ( + 0 => 'approval-base/2.1.2', + 1 => 'combodo-approval-light/1.0.2', + ), + '1.1.0' => + array ( + 0 => 'approval-base/2.2.2', + 1 => 'combodo-approval-light/1.0.2', + ), + '1.1.1' => + array ( + 0 => 'approval-base/2.2.3', + 1 => 'combodo-approval-light/1.0.2', + ), + '1.1.2' => + array ( + 0 => 'approval-base/2.2.6', + 1 => 'combodo-approval-light/1.0.2', + ), + '1.1.3' => + array ( + 0 => 'approval-base/2.2.6', + 1 => 'combodo-approval-light/1.0.3', + ), + '1.2.0' => + array ( + 0 => 'approval-base/2.3.0', + 1 => 'combodo-approval-light/1.0.3', + ), + '1.2.1' => + array ( + 0 => 'approval-base/2.4.0', + 1 => 'combodo-approval-light/1.0.4', + ), + '1.3.0' => + array ( + 0 => 'approval-base/2.4.2', + 1 => 'combodo-approval-light/1.1.1', + ), + '1.3.1' => + array ( + 0 => 'approval-base/2.5.0', + 1 => 'combodo-approval-light/1.1.1', + ), + '1.3.2' => + array ( + 0 => 'approval-base/2.5.0', + 1 => 'combodo-approval-light/1.1.2', + ), + '1.2.2' => + array ( + 0 => 'approval-base/2.4.2', + 1 => 'combodo-approval-light/1.0.5', + ), + '1.3.3' => + array ( + 0 => 'approval-base/2.5.1', + 1 => 'combodo-approval-light/1.1.2', + ), + '1.3.4' => + array ( + 0 => 'approval-base/2.5.2', + 1 => 'combodo-approval-light/1.1.2', + ), + '1.3.5' => + array ( + 0 => 'approval-base/2.5.3', + 1 => 'combodo-approval-light/1.1.2', + ), + '1.4.0' => + array ( + 0 => 'approval-base/2.5.3', + 1 => 'combodo-approval-light/1.1.2', + 2 => 'itop-approval-portal/1.0.0', + ), + ), + ), + 'combodo-approval-process-automation' => + array ( + 'label' => 'Approval process automation', + 'description' => 'Control your approval process with predefined rules based on service catalog', + 'versions' => + array ( + '1.0.1' => + array ( + 0 => 'approval-base/2.1.0', + 1 => 'combodo-approval-extended/1.0.2', + ), + '1.0.2' => + array ( + 0 => 'approval-base/2.1.1', + 1 => 'combodo-approval-extended/1.0.4', + ), + '1.0.3' => + array ( + 0 => 'approval-base/2.1.2', + 1 => 'combodo-approval-extended/1.0.4', + ), + '1.1.0' => + array ( + 0 => 'approval-base/2.2.2', + 1 => 'combodo-approval-extended/1.0.4', + ), + '1.1.1' => + array ( + 0 => 'approval-base/2.2.3', + 1 => 'combodo-approval-extended/1.0.4', + ), + '1.1.2' => + array ( + 0 => 'approval-base/2.2.6', + 1 => 'combodo-approval-extended/1.0.5', + ), + '1.1.3' => + array ( + 0 => 'approval-base/2.2.6', + 1 => 'combodo-approval-extended/1.0.6', + ), + '1.2.0' => + array ( + 0 => 'approval-base/2.3.0', + 1 => 'combodo-approval-extended/1.0.7', + ), + '1.2.1' => + array ( + 0 => 'approval-base/2.4.0', + 1 => 'combodo-approval-extended/1.0.8', + ), + '1.3.0' => + array ( + 0 => 'approval-base/2.4.2', + 1 => 'combodo-approval-extended/1.2.1', + ), + '1.3.1' => + array ( + 0 => 'approval-base/2.5.0', + 1 => 'combodo-approval-extended/1.2.1', + ), + '1.3.2' => + array ( + 0 => 'approval-base/2.5.0', + 1 => 'combodo-approval-extended/1.2.2', + ), + '1.2.2' => + array ( + 0 => 'approval-base/2.4.2', + 1 => 'combodo-approval-extended/1.0.9', + ), + '1.3.3' => + array ( + 0 => 'approval-base/2.5.1', + 1 => 'combodo-approval-extended/1.2.3', + ), + '1.3.4' => + array ( + 0 => 'approval-base/2.5.2', + 1 => 'combodo-approval-extended/1.2.3', + ), + '1.3.5' => + array ( + 0 => 'approval-base/2.5.3', + 1 => 'combodo-approval-extended/1.2.3', + ), + '1.4.0' => + array ( + 0 => 'approval-base/2.5.3', + 1 => 'combodo-approval-extended/1.2.3', + 3 => 'itop-approval-portal/1.0.0', + ), + ), + ), + 'combodo-predefined-response-models' => + array ( + 'label' => 'Predefined response models', + 'description' => 'Pick common answers from a list of predefined replies grouped by categories to update tickets log', + 'versions' => + array ( + '1.0.0' => + array ( + 0 => 'precanned-replies/1.0.0', + 1 => 'precanned-replies-pro/1.0.0', + ), + '1.0.1' => + array ( + 0 => 'precanned-replies/1.0.1', + 1 => 'precanned-replies-pro/1.0.1', + ), + '1.0.2' => + array ( + 0 => 'precanned-replies/1.0.2', + 1 => 'precanned-replies-pro/1.0.1', + ), + '1.0.3' => + array ( + 0 => 'precanned-replies/1.0.3', + 1 => 'precanned-replies-pro/1.0.1', + ), + '1.0.4' => + array ( + 0 => 'precanned-replies/1.0.3', + 1 => 'precanned-replies-pro/1.0.2', + ), + '1.0.5' => + array ( + 0 => 'precanned-replies/1.0.4', + 1 => 'precanned-replies-pro/1.0.2', + ), + '1.1.0' => + array ( + 0 => 'precanned-replies/1.1.0', + 1 => 'precanned-replies-pro/1.0.2', + ), + '1.1.1' => + array ( + 0 => 'precanned-replies/1.1.1', + 1 => 'precanned-replies-pro/1.0.2', + ), + ), + ), + 'combodo-customized-request-forms' => + array ( + 'label' => 'Customized request forms', + 'description' => 'Define personalized request forms based on the service catalog. Add extra fields for a given type of request.', + 'versions' => + array ( + '1.0.1' => + array ( + 0 => 'templates-base/2.1.1', + 1 => 'itop-request-template/1.0.0', + ), + '1.0.2' => + array ( + 0 => 'templates-base/2.1.2', + 1 => 'itop-request-template/1.0.0', + ), + '1.0.3' => + array ( + 0 => 'templates-base/2.1.2', + 1 => 'itop-request-template/1.0.1', + ), + '1.0.4' => + array ( + 0 => 'templates-base/2.1.3', + 1 => 'itop-request-template/1.0.1', + ), + '1.0.5' => + array ( + 0 => 'templates-base/2.1.4', + 1 => 'itop-request-template/1.0.1', + ), + '2.0.0' => + array ( + 0 => 'templates-base/3.0.0', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.1' => + array ( + 0 => 'templates-base/3.0.1', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.2' => + array ( + 0 => 'templates-base/3.0.2', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.3' => + array ( + 0 => 'templates-base/3.0.4', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.4' => + array ( + 0 => 'templates-base/3.0.5', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.5' => + array ( + 0 => 'templates-base/3.0.6', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.6' => + array ( + 0 => 'templates-base/3.0.8', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.7' => + array ( + 0 => 'templates-base/3.0.9', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + '2.0.8' => + array ( + 0 => 'templates-base/3.0.12', + 1 => 'itop-request-template/2.0.0', + 2 => 'itop-request-template-portal/1.0.0', + ), + ), + ), + 'combodo-sla-considering-business-hours' => + array ( + 'label' => 'SLA considering business hours', + 'description' => 'Compute SLAs taking into account service coverage window and holidays', + 'versions' => + array ( + '2.0.1' => + array ( + 0 => 'combodo-sla-computation/2.0.1', + 1 => 'combodo-coverage-windows-computation/2.0.0', + ), + '2.1.0' => + array ( + 0 => 'combodo-sla-computation/2.1.0', + 1 => 'combodo-coverage-windows-computation/2.0.0', + ), + '2.1.1' => + array ( + 0 => 'combodo-sla-computation/2.1.1', + 1 => 'combodo-coverage-windows-computation/2.0.0', + ), + '2.1.2' => + array ( + 0 => 'combodo-sla-computation/2.1.2', + 1 => 'combodo-coverage-windows-computation/2.0.0', + ), + '2.1.3' => + array ( + 0 => 'combodo-sla-computation/2.1.2', + 1 => 'combodo-coverage-windows-computation/2.0.1', + ), + '2.0.2' => + array ( + 0 => 'combodo-sla-computation/2.0.1', + 1 => 'combodo-coverage-windows-computation/2.0.1', + ), + '2.1.4' => + array ( + 0 => 'combodo-sla-computation/2.1.3', + 1 => 'combodo-coverage-windows-computation/2.0.1', + ), + '2.1.5' => + array ( + 0 => 'combodo-sla-computation/2.1.5', + 1 => 'combodo-coverage-windows-computation/2.0.1', + ), + '2.1.6' => + array ( + 0 => 'combodo-sla-computation/2.1.5', + 1 => 'combodo-coverage-windows-computation/2.0.2', + ), + '2.1.7' => + array ( + 0 => 'combodo-sla-computation/2.1.6', + 1 => 'combodo-coverage-windows-computation/2.0.2', + ), + '2.1.8' => + array ( + 0 => 'combodo-sla-computation/2.1.7', + 1 => 'combodo-coverage-windows-computation/2.0.2', + ), + '2.1.9' => + array ( + 0 => 'combodo-sla-computation/2.1.8', + 1 => 'combodo-coverage-windows-computation/2.0.2', + ), + ), + ), + 'combodo-mail-to-ticket-automation' => + array ( + 'label' => 'Mail to ticket automation', + 'description' => 'Scan several mailboxes to create or update tickets.', + 'versions' => + array ( + '2.6.0' => + array ( + 0 => 'combodo-email-synchro/2.6.0', + 1 => 'itop-standard-email-synchro/2.6.0', + ), + '2.6.1' => + array ( + 0 => 'combodo-email-synchro/2.6.1', + 1 => 'itop-standard-email-synchro/2.6.0', + ), + '2.6.2' => + array ( + 0 => 'combodo-email-synchro/2.6.2', + 1 => 'itop-standard-email-synchro/2.6.0', + ), + '2.6.3' => + array ( + 0 => 'combodo-email-synchro/2.6.2', + 1 => 'itop-standard-email-synchro/2.6.1', + ), + '2.6.4' => + array ( + 0 => 'combodo-email-synchro/2.6.3', + 1 => 'itop-standard-email-synchro/2.6.2', + ), + '2.6.5' => + array ( + 0 => 'combodo-email-synchro/2.6.4', + 1 => 'itop-standard-email-synchro/2.6.2', + ), + '2.6.6' => + array ( + 0 => 'combodo-email-synchro/2.6.5', + 1 => 'itop-standard-email-synchro/2.6.3', + ), + '2.6.7' => + array ( + 0 => 'combodo-email-synchro/2.6.6', + 1 => 'itop-standard-email-synchro/2.6.4', + ), + '2.6.8' => + array ( + 0 => 'combodo-email-synchro/2.6.7', + 1 => 'itop-standard-email-synchro/2.6.4', + ), + '2.6.9' => + array ( + 0 => 'combodo-email-synchro/2.6.8', + 1 => 'itop-standard-email-synchro/2.6.5', + ), + '2.6.10' => + array ( + 0 => 'combodo-email-synchro/2.6.9', + 1 => 'itop-standard-email-synchro/2.6.6', + ), + '2.6.11' => + array ( + 0 => 'combodo-email-synchro/2.6.10', + 1 => 'itop-standard-email-synchro/2.6.6', + ), + '2.6.12' => + array ( + 0 => 'combodo-email-synchro/2.6.11', + 1 => 'itop-standard-email-synchro/2.6.6', + ), + '3.0.0' => + array ( + 0 => 'combodo-email-synchro/3.0.0', + 1 => 'itop-standard-email-synchro/3.0.0', + ), + '3.0.1' => + array ( + 0 => 'combodo-email-synchro/3.0.1', + 1 => 'itop-standard-email-synchro/3.0.1', + ), + '3.0.2' => + array ( + 0 => 'combodo-email-synchro/3.0.2', + 1 => 'itop-standard-email-synchro/3.0.1', + ), + '3.0.3' => + array ( + 0 => 'combodo-email-synchro/3.0.3', + 1 => 'itop-standard-email-synchro/3.0.3', + ), + '3.0.4' => + array ( + 0 => 'combodo-email-synchro/3.0.3', + 1 => 'itop-standard-email-synchro/3.0.4', + ), + '3.0.5' => + array ( + 0 => 'combodo-email-synchro/3.0.4', + 1 => 'itop-standard-email-synchro/3.0.4', + ), + '3.0.6' => + array ( + 0 => 'combodo-email-synchro/3.0.5', + 1 => 'itop-standard-email-synchro/3.0.4', + ), + '3.0.7' => + array ( + 0 => 'combodo-email-synchro/3.0.5', + 1 => 'itop-standard-email-synchro/3.0.5', + ), + ), + ), + 'combodo-configurator-for-automatic-object-creation' => + array ( + 'label' => 'Configurator for automatic object creation', + 'description' => 'Templating based on existing objects.', + 'versions' => + array ( + '1.0.13' => + array ( + 1 => 'itop-stencils/1.0.6', + ), + ), + ), + 'combodo-user-actions-configurator' => + array ( + 'label' => 'User actions configurator', + 'description' => 'Configure user actions to simplify and automate processes (e.g. create an incident from a CI).', + 'versions' => + array ( + '1.0.0' => + array ( + 0 => 'itop-object-copier/1.0.0', + ), + '1.0.1' => + array ( + 0 => 'itop-object-copier/1.0.1', + ), + '1.0.2' => + array ( + 0 => 'itop-object-copier/1.0.2', + ), + '1.0.3' => + array ( + 0 => 'itop-object-copier/1.0.3', + ), + '1.1.0' => + array ( + 0 => 'itop-object-copier/1.1.0', + ), + '1.1.1' => + array ( + 0 => 'itop-object-copier/1.1.1', + ), + '1.1.2' => + array ( + 0 => 'itop-object-copier/1.1.2', + ), + '1.1.3' => + array ( + 0 => 'itop-object-copier/1.1.3', + ), + '1.1.4' => + array ( + 0 => 'itop-object-copier/1.1.4', + ), + '1.1.5' => + array ( + 0 => 'itop-object-copier/1.1.5', + ), + '1.1.6' => + array ( + 0 => 'itop-object-copier/1.1.6', + ), + '1.1.7' => + array ( + 0 => 'itop-object-copier/1.1.7', + ), + '1.1.8' => + array ( + 0 => 'itop-object-copier/1.1.8', + ), + ), + ), + 'combodo-send-updates-by-email' => + array ( + 'label' => 'Send updates by email', + 'description' => 'Send an email to pre-configured contacts when a ticket log is updated.', + 'versions' => + array ( + '1.0.1' => + array ( + 0 => 'email-reply/1.0.1', + ), + '1.0.3' => + array ( + 0 => 'email-reply/1.0.3', + ), + '1.1.1' => + array ( + 0 => 'email-reply/1.1.1', + ), + '1.1.2' => + array ( + 0 => 'email-reply/1.1.2', + ), + '1.1.3' => + array ( + 0 => 'email-reply/1.1.3', + ), + '1.1.4' => + array ( + 0 => 'email-reply/1.1.4', + ), + '1.1.5' => + array ( + 0 => 'email-reply/1.1.5', + ), + '1.1.6' => + array ( + 0 => 'email-reply/1.1.6', + ), + '1.1.7' => + array ( + 0 => 'email-reply/1.1.7', + ), + // 1.1.8 was never released + '1.1.9' => + array ( + 0 => 'email-reply/1.1.9', + ), + '1.1.10' => + array ( + 0 => 'email-reply/1.1.10', + ), + ), + ), + ); } } diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index f36423f0cf..f77d75c824 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -117,7 +117,7 @@ public static function AddModule($sFilePath, $sId, $aArgs) if (!array_key_exists($sArgName, $aArgs)) { throw new Exception("Module '$sId': missing argument '$sArgName'"); - } + } } $aArgs['root_dir'] = dirname($sFilePath); @@ -220,7 +220,7 @@ protected static function GetModules($bAbortOnMissingDependency = false, $aModul * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted * @return array * @throws \MissingDependencyException -*/ + */ public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null) { // Order the modules to take into account their inter-dependencies @@ -351,19 +351,19 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) { $aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error + // a function call that results in a runtime fatal error } else { $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error + // a function call that results in a runtime fatal error } } else { // module is not present $aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing - // a function call that results in a runtime fatal error + // a function call that results in a runtime fatal error } } } @@ -387,10 +387,10 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a else { $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); - $bOk = ModuleDiscoveryService::GetInstance()->ComputeDependencyExpression($sBooleanExpr); - if ($bOk == false) - { - SetupLog::Warning("Eval of '$sBooleanExpr' returned false"); + try{ + $bResult = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpr); + } catch(ModuleDiscoveryServiceException $e){ + //logged already echo "Failed to parse the boolean Expression = '$sBooleanExpr'
    "; } } @@ -498,40 +498,12 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) { self::SetModulePath($sRelDir); - try - { - $sModuleFileContents = file_get_contents($sDirectory.'/'.$sFile); - $sModuleFileContents = str_replace(array(''), '', $sModuleFileContents); - $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sDirectory.'/'.$sFile)."'", $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 the class 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 - $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); - } - $idx++; - } - - $sModuleFilePath = $sDirectory.'/'.$sFile; - $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo); - - //echo "

    Done.

    \n"; - } - catch(ParseError $e) - { - // PHP 7 - SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()." at line ".$e->getLine()); - } - catch(Exception $e) - { - // Continue... - SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()); + $sModuleFilePath = $sDirectory.'/'.$sFile; + try { + $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sDirectory.'/'.$sFile); + SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo[2]); + } catch(ModuleDiscoveryServiceException $e){ + continue; } } } diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 5b82ac4abc..a279e5bdb8 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -2,6 +2,7 @@ class ModuleDiscoveryService { private static ModuleDiscoveryService $oInstance; + private static int $iDummyClassIndex = 0; protected function __construct() { } @@ -23,13 +24,14 @@ final public static function SetInstance(?ModuleDiscoveryService $oInstance): vo * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles * @param string $sModuleFile * @return array + * @throws ModuleDiscoveryServiceException */ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array { - static $iDummyClassIndex = 0; - $aModuleInfo = []; // will be filled by the "eval" line below... + $aModuleInfo = array(); // will be filled by the "eval" line below... try { + $aMatches = array(); $sModuleFileContents = file_get_contents($sModuleFilePath); $sModuleFileContents = str_replace(array(''), '', $sModuleFileContents); $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFilePath)."'", $sModuleFileContents); @@ -40,47 +42,73 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array { if (class_exists($sClassName)) { - // rename the class inside the code to prevent a "duplicate class" declaration + // 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 - $sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents); + // 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(array('SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'), '$aModuleInfo = array', $sModuleFileContents); - eval($sModuleFileContents); // Assigns $aModuleInfo if (count($aModuleInfo) === 0) { - SetupLog::Warning("Eval of $sModuleFilePath did not return the expected information..."); + throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath did not return the expected information..."); } - - //echo "

    Done.

    \n"; + } + catch(ModuleDiscoveryServiceException $e) + { + // Continue... + throw $e; } catch(ParseError $e) { - // PHP 7 - SetupLog::Warning("Eval of $sModuleFilePath caused a parse exception: ".$e->getMessage()." at line ".$e->getLine()); + // Continue... + throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused a parse error: ".$e->getMessage()." at line ".$e->getLine()); } catch(Exception $e) { // Continue... - SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()); + throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e); } - return $aModuleInfo; } - public function ComputeDependencyExpression(string $sBooleanExpr) : bool + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + public function ComputeBooleanExpression(string $sBooleanExpr) : bool { - return @eval('$bResult = '.$sBooleanExpr.'; return $bResult;'); - } + $bResult = false; + try{ + @eval('$bResult = '.$sBooleanExpr.';'); + } catch (Throwable $t) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); + } + return $bResult; + } +} - public function ComputeAutoSelectExpression(string $sBooleanExpr) : bool +class ModuleDiscoveryServiceException extends Exception +{ + /** + * ModuleDiscoveryServiceException constructor. + * + * @param string $sMessage + * @param int $iHttpCode + * @param Exception|null $oPrevious + */ + public function __construct($sMessage, $iHttpCode = 0, Exception $oPrevious = null) { - return eval('$bSelected = ('.$sBooleanExpr.'); return $bSelected'); + $e = new \Exception(""); + + SetupLog::Warning($sMessage, null, ['previous' => $oPrevious?->getMessage(), 'stack' => $e->getTraceAsString()]); + parent::__construct($sMessage, $iHttpCode, $oPrevious); } } \ No newline at end of file diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 3872392719..3adcfb6ce6 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -202,10 +202,10 @@ public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDepe if (!in_array($sModuleAppVersion, array('1.0.0', '1.0.1', '1.0.2'))) { // This module is NOT compatible with the current version - $aModuleInfo['install'] = array( - 'flag' => MODULE_ACTION_IMPOSSIBLE, - 'message' => 'the module is not compatible with the current version of the application' - ); + $aModuleInfo['install'] = array( + 'flag' => MODULE_ACTION_IMPOSSIBLE, + 'message' => 'the module is not compatible with the current version of the application' + ); } elseif ($aModuleInfo['mandatory']) { @@ -457,19 +457,16 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { if (!array_key_exists($oModule->GetName(), $aRet) && $oModule->IsAutoSelect()) { - try - { - SetupInfo::SetSelectedModules($aRet); - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($oModule->GetAutoSelect()); - + SetupInfo::SetSelectedModules($aRet); + try{ + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($oModule->GetAutoSelect()); if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module $bModuleAdded = true; } - } - catch(Exception $e) - { + } catch(ModuleDiscoveryServiceException $e){ + //do nothing. logged already } } } @@ -976,8 +973,8 @@ public function Commit() $this->CommitDir( APPROOT.'env-'.$this->sTargetEnv, APPROOT.'env-'.$this->sFinalEnv, - true, - false + true, + false ); // Move the config file @@ -1044,7 +1041,7 @@ protected function CommitFile($sSource, $sDest, $bSourceMustExist = true) * @param $sSource * @param $sDest * @param boolean $bSourceMustExist - * @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied + * @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied * @throws Exception */ protected function CommitDir($sSource, $sDest, $bSourceMustExist = true, $bRemoveSource = true) @@ -1079,41 +1076,41 @@ public function Rollback() } } - /** - * Call the given handler method for all selected modules having an installation handler - * @param array[] $aAvailableModules - * @param string[] $aSelectedModules - * @param string $sHandlerName - * @throws CoreException - */ + /** + * Call the given handler method for all selected modules having an installation handler + * @param array[] $aAvailableModules + * @param string[] $aSelectedModules + * @param string $sHandlerName + * @throws CoreException + */ public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName) { - foreach($aAvailableModules as $sModuleId => $aModule) - { - if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) && - isset($aAvailableModules[$sModuleId]['installer']) ) - { - $sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer']; - SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})"); - $aCallSpec = array($sModuleInstallerClass, $sHandlerName); - if (is_callable($aCallSpec)) - { - try { - call_user_func_array($aCallSpec, array(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); - } - } - } - } + foreach($aAvailableModules as $sModuleId => $aModule) + { + if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) && + isset($aAvailableModules[$sModuleId]['installer']) ) + { + $sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer']; + SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})"); + $aCallSpec = array($sModuleInstallerClass, $sHandlerName); + if (is_callable($aCallSpec)) + { + try { + call_user_func_array($aCallSpec, array(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); + } + } + } + } } /** @@ -1142,64 +1139,64 @@ public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) if ($aModule['version_db'] != '') { // Simulate the load of the previously loaded XML files to get the mapping of the keys if ($bSampleData) { - $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); - } - else - { - // Load only structural data - $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - } - } - else - { - if ($bSampleData) - { - $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); - } - else - { - // Load only structural data - $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - } - } - } - } - } - - // Simulate the load of the previously loaded files, in order to initialize - // the mapping between the identifiers in the XML and the actual identifiers - // in the current database - foreach($aPreviouslyLoadedFiles as $sFileRelativePath) - { - $sFileName = APPROOT.$sFileRelativePath; - SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)"); - if (empty($sFileName) || !file_exists($sFileName)) - { - throw(new Exception("File $sFileName does not exist")); - } - - $oDataLoader->LoadFile($sFileName, true); - $sResult = sprintf("loading of %s done.", basename($sFileName)); - SetupLog::Info($sResult); - } - - foreach($aFiles as $sFileRelativePath) - { - $sFileName = APPROOT.$sFileRelativePath; - SetupLog::Info("Loading file: $sFileName"); - if (empty($sFileName) || !file_exists($sFileName)) - { - throw(new Exception("File $sFileName does not exist")); - } - - $oDataLoader->LoadFile($sFileName); - $sResult = sprintf("loading of %s done.", basename($sFileName)); - SetupLog::Info($sResult); - } - - $oDataLoader->EndSession(); + $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); + } + else + { + // Load only structural data + $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + } + } + else + { + if ($bSampleData) + { + $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); + } + else + { + // Load only structural data + $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + } + } + } + } + } + + // Simulate the load of the previously loaded files, in order to initialize + // the mapping between the identifiers in the XML and the actual identifiers + // in the current database + foreach($aPreviouslyLoadedFiles as $sFileRelativePath) + { + $sFileName = APPROOT.$sFileRelativePath; + SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)"); + if (empty($sFileName) || !file_exists($sFileName)) + { + throw(new Exception("File $sFileName does not exist")); + } + + $oDataLoader->LoadFile($sFileName, true); + $sResult = sprintf("loading of %s done.", basename($sFileName)); + SetupLog::Info($sResult); + } + + foreach($aFiles as $sFileRelativePath) + { + $sFileName = APPROOT.$sFileRelativePath; + SetupLog::Info("Loading file: $sFileName"); + if (empty($sFileName) || !file_exists($sFileName)) + { + throw(new Exception("File $sFileName does not exist")); + } + + $oDataLoader->LoadFile($sFileName); + $sResult = sprintf("loading of %s done.", basename($sFileName)); + SetupLog::Info($sResult); + } + + $oDataLoader->EndSession(); SetupLog::Info("ending data load session"); } @@ -1212,12 +1209,12 @@ public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) */ protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFilesToMerge) { - $aToMerge = array(); - foreach($aFilesToMerge as $sFile) - { - $aToMerge[] = $sBaseDir.'/'.$sFile; - } - return array_merge($aSourceArray, $aToMerge); + $aToMerge = array(); + foreach($aFilesToMerge as $sFile) + { + $aToMerge[] = $sBaseDir.'/'.$sFile; + } + return array_merge($aSourceArray, $aToMerge); } /** @@ -1226,40 +1223,40 @@ protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFiles * @throws Exception * @return string */ - public function CheckMetaModel() - { - $iCount = 0; - $fStart = microtime(true); - foreach(MetaModel::GetClasses() as $sClass) - { - if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass)) - { - //if a class is not persisted and is abstract, the code below would crash. Needed by the class AbstractRessource. This is tolerable to skip this because we check the setup process integrity, not the datamodel integrity. - continue; - } - - $oSearch = new DBObjectSearch($sClass); - $oSearch->SetShowObsoleteData(false); - $oSQLQuery = $oSearch->GetSQLQueryStructure(null, false); - $sViewName = MetaModel::DBGetView($sClass); - if (strlen($sViewName) > 64) - { - throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters)."); - } - $sTableName = MetaModel::DBGetTable($sClass); - if (strlen($sTableName) > 64) - { - throw new Exception("Table name too long for class: '$sClass'. The name of the corresponding MySQL table ($sTableName) would exceed MySQL's limit for the name of a table (64 characters)."); - } - $iTableCount = $oSQLQuery->CountTables(); - if ($iTableCount > 61) - { - throw new Exception("Class requiring too many tables: '$sClass'. The structure of the class ($sClass) would require a query with more than 61 JOINS (MySQL's limitation)."); - } - $iCount++; - } - $fDuration = microtime(true) - $fStart; - - return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0); - } + public function CheckMetaModel() + { + $iCount = 0; + $fStart = microtime(true); + foreach(MetaModel::GetClasses() as $sClass) + { + if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass)) + { + //if a class is not persisted and is abstract, the code below would crash. Needed by the class AbstractRessource. This is tolerable to skip this because we check the setup process integrity, not the datamodel integrity. + continue; + } + + $oSearch = new DBObjectSearch($sClass); + $oSearch->SetShowObsoleteData(false); + $oSQLQuery = $oSearch->GetSQLQueryStructure(null, false); + $sViewName = MetaModel::DBGetView($sClass); + if (strlen($sViewName) > 64) + { + throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters)."); + } + $sTableName = MetaModel::DBGetTable($sClass); + if (strlen($sTableName) > 64) + { + throw new Exception("Table name too long for class: '$sClass'. The name of the corresponding MySQL table ($sTableName) would exceed MySQL's limit for the name of a table (64 characters)."); + } + $iTableCount = $oSQLQuery->CountTables(); + if ($iTableCount > 61) + { + throw new Exception("Class requiring too many tables: '$sClass'. The structure of the class ($sClass) would require a query with more than 61 JOINS (MySQL's limitation)."); + } + $iCount++; + } + $fDuration = microtime(true) - $fStart; + + return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0); + } } // End of class diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index 2aa6e28678..a86f1179d2 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -270,14 +270,15 @@ public function ProcessAutoSelectModules() : void { { try { SetupInfo::SetSelectedModules($this->aSelectedModules); - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aModule['auto_select']); + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aModule['auto_select']); if ($bSelected) { // Modules in data/production-modules/ are considered as mandatory and always installed $this->aSelectedModules[$sModuleId] = true; } } - catch (Exception $e) { + catch (ModuleDiscoveryServiceException $e) { + //logged already } } } diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 3e7b398e80..6b36d960ec 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -90,7 +90,7 @@ public function Display(WebPage $oPage) $oPage->add(""); $oPage->add_ready_script( -<< 0) { alert("Internet Explorer version 10 or older is NOT supported! (Check that IE is not running in compatibility mode)"); @@ -144,7 +144,7 @@ public function Display(WebPage $oPage) $sH2Class = 'text-valid'; } $oPage->add( -<<Prerequisites validation: $sTitle
    HTML @@ -271,12 +271,12 @@ public function Display(WebPage $oPage) } $oPage->add('
    What do you want to do?
    '); $sChecked = ($sInstallMode == 'install') ? ' checked ' : ''; - $oPage->p(''); + $oPage->p(''); $sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : ''; $sDisabled = (($sInstallMode == 'install') && (empty($sPreviousVersionDir))) ? ' disabled' : ''; - $oPage->p(''); + $oPage->p(''); - $sUpgradeDir = utils::HtmlEntities($sPreviousVersionDir); + $sUpgradeDir = utils::HtmlEntities($sPreviousVersionDir); $oPage->add(<<
    Location on the disk: @@ -319,7 +319,7 @@ public function Display(WebPage $oPage) $oPage->add(''); //$oPage->add(''); $oPage->add_ready_script( -<<oWizard->SetParameter('source_dir', $this->oWizard->GetParameter('previous_version_dir').'/'.$sSourceDir); - $this->oWizard->SetParameter('datamodel_version', utils::ReadParam('datamodel_previous_version', '', false, 'raw_data')); - break; + $sSourceDir = utils::ReadParam('relative_source_dir', '', false, 'raw_data'); + $this->oWizard->SetParameter('source_dir', $this->oWizard->GetParameter('previous_version_dir').'/'.$sSourceDir); + $this->oWizard->SetParameter('datamodel_version', utils::ReadParam('datamodel_previous_version', '', false, 'raw_data')); + break; case 'use-compatible': - $sDataModelPath = utils::ReadParam('datamodel_path', '', false, 'raw_data'); - $this->oWizard->SetParameter('source_dir', $sDataModelPath); - $this->oWizard->SaveParameter('datamodel_version', ''); - break; + $sDataModelPath = utils::ReadParam('datamodel_path', '', false, 'raw_data'); + $this->oWizard->SetParameter('source_dir', $sDataModelPath); + $this->oWizard->SaveParameter('datamodel_version', ''); + break; default: - // Do nothing, maybe the user pressed the Back button + // Do nothing, maybe the user pressed the Back button } if ($bDisplayLicense) { @@ -469,7 +469,7 @@ public function ProcessParams($bMoveForward = true) public function Display(WebPage $oPage) { $oPage->add_style( -<<add( -<<The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion.
    @@ -617,7 +617,7 @@ public function Display(WebPage $oPage) } $oPage->add_ready_script( -<< 0); return bRet; EOF - ; + ; } } @@ -695,55 +695,55 @@ private function NeedsGdprConsent() return (($sMode === 'install') && SetupUtils::IsConnectableToITopHub($aModules)); } - /** - * @param WebPage $oPage - */ - public function Display(WebPage $oPage) - { - $aLicenses = SetupUtils::GetLicenses(); - $oPage->add_style( - <<add_style( + <<add('

    Licenses agreements for the components of '.ITOP_APPLICATION.'

    '); - $oPage->add_style('div a.no-arrow { background:transparent; padding-left:0;}'); - $oPage->add_style('.toggle { cursor:pointer; text-decoration:underline; color:#1C94C4; }'); - $oPage->add('
    '); - $oPage->add('Components of '.ITOP_APPLICATION.''); - $oPage->add('
      '); - $index = 0; - foreach ($aLicenses as $oLicense) { - $oPage->add('
    • '.$oLicense->product.', © '.$oLicense->author.' is licensed under the '.$oLicense->license_type.' license. (Details)'); - $oPage->add(''); - $oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");'); - $oPage->add_ready_script('$("#toggle_'.$index.'").on("click", function() { $("#license_'.$index.'").toggle(); } );'); - $index++; - } - $oPage->add('
    '); - $oPage->add('
    '); - $sChecked = ($this->oWizard->GetParameter('accept_license', 'no') == 'yes') ? ' checked ' : ''; - $oPage->add('
    '); - if ($this->NeedsGdprConsent()) { - $oPage->add('
    '); - $oPage->add('
    '); - $oPage->add('European General Data Protection Regulation'); - $oPage->add('
    '.ITOP_APPLICATION.' software is compliant with the processing of personal data according to the European General Data Protection Regulation (GDPR).

    + ); + + $oPage->add('

    Licenses agreements for the components of '.ITOP_APPLICATION.'

    '); + $oPage->add_style('div a.no-arrow { background:transparent; padding-left:0;}'); + $oPage->add_style('.toggle { cursor:pointer; text-decoration:underline; color:#1C94C4; }'); + $oPage->add('
    '); + $oPage->add('Components of '.ITOP_APPLICATION.''); + $oPage->add('
      '); + $index = 0; + foreach ($aLicenses as $oLicense) { + $oPage->add('
    • '.$oLicense->product.', © '.$oLicense->author.' is licensed under the '.$oLicense->license_type.' license. (Details)'); + $oPage->add(''); + $oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");'); + $oPage->add_ready_script('$("#toggle_'.$index.'").on("click", function() { $("#license_'.$index.'").toggle(); } );'); + $index++; + } + $oPage->add('
    '); + $oPage->add('
    '); + $sChecked = ($this->oWizard->GetParameter('accept_license', 'no') == 'yes') ? ' checked ' : ''; + $oPage->add('
    '); + if ($this->NeedsGdprConsent()) { + $oPage->add('
    '); + $oPage->add('
    '); + $oPage->add('European General Data Protection Regulation'); + $oPage->add('
    '.ITOP_APPLICATION.' software is compliant with the processing of personal data according to the European General Data Protection Regulation (GDPR).

    By installing '.ITOP_APPLICATION.' you agree that some information will be collected by Combodo to help you manage your instances and for statistical purposes. This data remains anonymous until it is associated to a user account on iTop Hub.

    List of collected data available in our Data privacy section.


    '); - $oPage->add(''); - $oPage->add(''); - $oPage->add('
    '); - } - $oPage->add_ready_script('$(".check_select").on("click change", function() { WizardUpdateButtons(); });'); - - $oPage->add_script( - <<add(''); + $oPage->add(''); + $oPage->add('
    '); + } + $oPage->add_ready_script('$(".check_select").on("click change", function() { WizardUpdateButtons(); });'); + + $oPage->add_script( + <<sLabel; - $sMessage = json_encode('
    '.$sErrorExplanation.'
    '); - break; + $sStatus = 'ko'; + $sErrorExplanation = $oCheck->sLabel; + $sMessage = json_encode('
    '.$sErrorExplanation.'
    '); + break; } if ($oCheck->iSeverity !== CheckResult::TRACE) { @@ -1134,7 +1134,7 @@ public function AsyncAction(WebPage $oPage, $sCode, $aParameters) { public function JSCanMoveForward() { return -<<add('
    '); $oPage->add_script( -<<add_ready_script( -<<ComputeAutoSelectExpression($aInfo['auto_select']); - if ($bSelected) { - $aModules[$sModuleId] = true; // store the Id of the selected module - SetupInfo::SetSelectedModules($aModules); - } + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aInfo['auto_select']); } - catch (Exception $e) { + catch (ModuleDiscoveryServiceException $e) { + //logged already + $bSelected = false; } } } + if ($bSelected) { + $aModules[$sModuleId] = true; // store the Id of the selected module + SetupInfo::SetSelectedModules($aModules); + } } } $sChoiceType = isset($aChoice['type']) ? $aChoice['type'] : 'wizard_option'; @@ -1823,7 +1825,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP $sChoiceName = $sChoiceId; } if ( (isset($aChoice['mandatory']) && $aChoice['mandatory']) || - (isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId)) ) + (isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId)) ) { $sDisplayChoices .= '
  • '.$aChoice['title'].'
  • '; if ($aSelectedExtensions !== null) @@ -1863,7 +1865,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aModule['auto_select']); + $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aModule['auto_select']); if ($bSelected) { $aModules[$sModuleId] = true; // store the Id of the selected module @@ -1871,8 +1873,9 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP $bModuleAdded = true; } } - catch(Exception $e) + catch(ModuleDiscoveryServiceException $e) { + //logged already $sDisplayChoices .= '
  • Warning: auto_select failed with exception ('.$e->getMessage().') for module "'.$sModuleId.'"
  • '; } } @@ -1890,11 +1893,11 @@ protected function GetStepIndex() { case 'start_install': case 'start_upgrade': - $index = 0; - break; + $index = 0; + break; default: - $index = (integer)$this->sCurrentState; + $index = (integer)$this->sCurrentState; } return $index; } @@ -1921,10 +1924,10 @@ protected function GetStepInfo($idx = null) // Additional step for the "extensions" $aStepDefinition = array( - 'title' => 'Extensions', - 'description' => '

    Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.

    ', - 'banner' => '/images/icons/icons8-puzzle.svg', - 'options' => array() + 'title' => 'Extensions', + 'description' => '

    Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.

    ', + 'banner' => '/images/icons/icons8-puzzle.svg', + 'options' => array() ); foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) @@ -1932,14 +1935,14 @@ protected function GetStepInfo($idx = null) if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) { $aStepDefinition['options'][] = array( - 'extension_code' => $oExtension->sCode, - 'title' => $oExtension->sLabel, - 'description' => $oExtension->sDescription, - 'more_info' => $oExtension->sMoreInfoUrl, - 'default' => true, // by default offer to install all modules - 'modules' => $oExtension->aModules, - 'mandatory' => $oExtension->bMandatory || ($oExtension->sSource === iTopExtension::SOURCE_REMOTE), - 'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource), + 'extension_code' => $oExtension->sCode, + 'title' => $oExtension->sLabel, + 'description' => $oExtension->sDescription, + 'more_info' => $oExtension->sMoreInfoUrl, + 'default' => true, // by default offer to install all modules + 'modules' => $oExtension->aModules, + 'mandatory' => $oExtension->bMandatory || ($oExtension->sSource === iTopExtension::SOURCE_REMOTE), + 'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource), ); } } @@ -1954,24 +1957,24 @@ protected function GetStepInfo($idx = null) { // No wizard configuration provided, build a standard one with just one big list $aStepDefinition = array( - 'title' => 'Modules Selection', - 'description' => '

    Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.

    ', - 'banner' => '/images/icons/icons8-apps-tab.svg', - 'options' => array() + 'title' => 'Modules Selection', + 'description' => '

    Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.

    ', + 'banner' => '/images/icons/icons8-apps-tab.svg', + 'options' => array() ); foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) { if (($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) { $aStepDefinition['options'][] = array( - 'extension_code' => $oExtension->sCode, - 'title' => $oExtension->sLabel, - 'description' => $oExtension->sDescription, - 'more_info' => $oExtension->sMoreInfoUrl, - 'default' => true, // by default offer to install all modules - 'modules' => $oExtension->aModules, - 'mandatory' => $oExtension->bMandatory || ($oExtension->sSource !== iTopExtension::SOURCE_REMOTE), - 'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource), + 'extension_code' => $oExtension->sCode, + 'title' => $oExtension->sLabel, + 'description' => $oExtension->sDescription, + 'more_info' => $oExtension->sMoreInfoUrl, + 'default' => true, // by default offer to install all modules + 'modules' => $oExtension->aModules, + 'mandatory' => $oExtension->bMandatory || ($oExtension->sSource !== iTopExtension::SOURCE_REMOTE), + 'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource), ); } } @@ -1992,17 +1995,17 @@ protected function GetExtensionSourceLabel($sSource) switch($sSource) { case iTopExtension::SOURCE_MANUAL: - $sResult = 'Local extensions folder'; - $sDecorationClass = 'fas fa-folder'; - break; + $sResult = 'Local extensions folder'; + $sDecorationClass = 'fas fa-folder'; + break; case iTopExtension::SOURCE_REMOTE: - $sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer'; - $sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler'; - break; + $sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer'; + $sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler'; + break; default: - $sResult = ''; + $sResult = ''; } if ($sResult == '') { @@ -2587,10 +2590,10 @@ public function Display(WebPage $oPage) $oProductionEnv->InitDataModel($oConfig, true); $sIframeUrl = $oConfig->GetModuleSetting('itop-hub-connector', 'setup_url', ''); - $sSetupTokenFile = APPROOT.'data/.setup'; - $sSetupToken = bin2hex(random_bytes(12)); - file_put_contents($sSetupTokenFile, $sSetupToken); - $sIframeUrl.= "&setup_token=$sSetupToken"; + $sSetupTokenFile = APPROOT.'data/.setup'; + $sSetupToken = bin2hex(random_bytes(12)); + file_put_contents($sSetupTokenFile, $sSetupToken); + $sIframeUrl.= "&setup_token=$sSetupToken"; if ($sIframeUrl != '') { diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php new file mode 100644 index 0000000000..415a4aa197 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -0,0 +1,55 @@ +RequireOnceItopFile('setup/modulediscovery/ModuleDiscoveryService.php'); + } + + public function test() + { + $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; + $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + + var_dump($aRes); + + $this->assertCount(3, $aRes); + $this->assertEquals($sModuleFilePath, $aRes[0]); + $this->assertEquals('itop-full-itil/3.3.0', $aRes[1]); + $this->assertIsArray($aRes[2]); + $this->assertArrayHasKey('label', $aRes[2]); + $this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null); + } + + public static function ComputeBooleanExpressionProvider() + { + return [ + "true" => [ "expr" => "true", "expected" => true], + "(true)" => [ "expr" => "(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], + ]; + } + + /** + * @dataProvider ComputeBooleanExpressionProvider + */ + public function testComputeBooleanExpression(string $sBooleanExpression, bool $expected){ + $this->assertEquals($expected, ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpression), $sBooleanExpression); + } + + public function testComputeBooleanExpression_BrokenBooleanExpression(){ + $this->expectException(\ModuleDiscoveryServiceException::class); + $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error: Undefined constant "a"'); + $this->assertTrue(ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression("(a || true)")); + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-full-itil.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-full-itil.php new file mode 100644 index 0000000000..daa46e6910 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-full-itil.php @@ -0,0 +1,41 @@ + 'Bridge - Request management ITIL + Incident management ITIL', + 'category' => 'business', + // Setup + // + 'dependencies' => array( + 'itop-request-mgmt-itil/2.3.0', + 'itop-incident-mgmt-itil/2.3.0', + ), + 'mandatory' => false, + 'visible' => false, + 'auto_select' => 'SetupInfo::ModuleIsSelected("itop-request-mgmt-itil") && SetupInfo::ModuleIsSelected("itop-incident-mgmt-itil")', + // Components + // + 'datamodel' => array(), + '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 + ), + ) +); From 6d80b2e5ed815a11048e38eb56dd5d431424b884 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 25 Aug 2025 06:45:17 +0200 Subject: [PATCH 03/23] =?UTF-8?q?N=C2=B04789=20-=20replace=20legacy=20eval?= =?UTF-8?q?=20by=20module=20file=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModuleDiscoveryService.php | 134 +++++++++++++++++- .../ModuleDiscoveryServiceTest.php | 13 +- 2 files changed, 139 insertions(+), 8 deletions(-) diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index a279e5bdb8..7af6130f98 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -1,5 +1,8 @@ '), '', $sModuleFileContents); + $sModuleFileContents = str_replace([''], '', $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); @@ -50,7 +53,7 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array $idx++; } // Replace the main function call by an assignment to a variable, as an array... - $sModuleFileContents = str_replace(array('SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'), '$aModuleInfo = array', $sModuleFileContents); + $sModuleFileContents = str_replace(['SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'], '$aModuleInfo = array', $sModuleFileContents); eval($sModuleFileContents); // Assigns $aModuleInfo if (count($aModuleInfo) === 0) @@ -76,6 +79,96 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array 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 + { + $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); + } + + try { + foreach ($aNodes as $sKey => $oNode) { + if (false === ($oNode instanceof \PhpParser\Node\Stmt\Expression)) { + continue; + } + + /** @var Assign $oAssignation */ + $oAssignation = $oNode->expr; + + if (false === ($oAssignation instanceof PhpParser\Node\Expr\StaticCall)) { + continue; + } + + /** @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); + } + } + /** * @param string $sBooleanExpr * @@ -93,6 +186,37 @@ public function ComputeBooleanExpression(string $sBooleanExpr) : bool return $bResult; } + + private function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleConfig) : 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 { + $sKey = $iIndex++; + } + + $oValue = $oArrayItem->value; + + if ($oValue instanceof PhpParser\Node\Expr\Array_) { + $aSubConfig=[]; + $this->BrowseArrayStructure($oValue, $aSubConfig); + $aModuleConfig[$sKey]=$aSubConfig; + } + + if ($oValue instanceof PhpParser\Node\Scalar\String_) { + $aModuleConfig[$sKey]=$oValue->value; + continue; + } + + if ($oValue instanceof \PhpParser\Node\Expr\ConstFetch) { + $aModuleConfig[$sKey]= filter_var($oValue->name->name, FILTER_VALIDATE_BOOLEAN); + } + } + } } class ModuleDiscoveryServiceException extends Exception diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php index 415a4aa197..ee6ecbecf8 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -13,13 +13,11 @@ protected function setUp(): void $this->RequireOnceItopFile('setup/modulediscovery/ModuleDiscoveryService.php'); } - public function test() + public function testReadModuleFileConfigurationLegacy() { $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - var_dump($aRes); - $this->assertCount(3, $aRes); $this->assertEquals($sModuleFilePath, $aRes[0]); $this->assertEquals('itop-full-itil/3.3.0', $aRes[1]); @@ -28,6 +26,15 @@ public function test() $this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null); } + public function testReadModuleFileConfiguration() + { + $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; + $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + + $this->assertEquals($aExpected, $aRes); + } + public static function ComputeBooleanExpressionProvider() { return [ From 788b23a485f73d1fcd6aa9eb5f8f4b56e10eb2f8 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 25 Aug 2025 11:45:38 +0200 Subject: [PATCH 04/23] =?UTF-8?q?N=C2=B04789=20-=20handle=20constants=20an?= =?UTF-8?q?d=20if=20conditional=20structures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/modulediscovery.class.inc.php | 3 + .../ModuleDiscoveryService.php | 233 +++++++++++++----- .../ModuleDiscoveryServiceTest.php | 20 ++ .../resources/module.__MODULE__.php | 56 +++++ .../resources/module.authent-ldap.php | 100 ++++++++ 5 files changed, 347 insertions(+), 65 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.__MODULE__.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.authent-ldap.php 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 @@ protected static function SetModulePath($sModulePath) */ 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 @@ public function ReadModuleFileConfigurationLegacy(string $sModuleFilePath) : arr */ 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; - } - - /** @var Assign $oAssignation */ - $oAssignation = $oNode->expr; - - if (false === ($oAssignation instanceof PhpParser\Node\Expr\StaticCall)) { - continue; + if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { + $aModuleConfig = $this->ParseCallToAddModuleAndReturnModuleConfiguration($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; + if ($oNode instanceof PhpParser\Node\Stmt\If_) { + $aModuleConfig = $this->BrowseIfStructure($sModuleFilePath, $oNode); + if (! is_null($aModuleConfig)){ + return $aModuleConfig; + } } - - $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 @@ private function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array 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 @@ private function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array $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 @@ public function testReadModuleFileConfiguration() $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')) From 8af748bd3ea99c6429e4437033568d18f17307c8 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 25 Aug 2025 18:51:56 +0200 Subject: [PATCH 05/23] =?UTF-8?q?N=C2=B04789=20-=20compute=20boolean=20exp?= =?UTF-8?q?ressions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModuleDiscoveryService.php | 146 +++++++--- .../ModuleDiscoveryServiceTest.php | 259 ++++++++++++++++++ 2 files changed, 374 insertions(+), 31 deletions(-) diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 5d4cbf00e0..c1e4be8958 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -79,6 +79,17 @@ public function ReadModuleFileConfigurationLegacy(string $sModuleFilePath) : arr return $aModuleInfo; } + /** + * @param string $sPhpContent + * + * @return \PhpParser\Node\Stmt[]|null + */ + public function parsePhpCode(string $sPhpContent): ?array + { + $oParser = (new ParserFactory())->createForNewestSupportedVersion(); + return $oParser->parse($sPhpContent); + } + /** * Read the information from a module file (module.xxx.php) * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles @@ -90,8 +101,7 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array { try { - $oParser = (new ParserFactory())->createForNewestSupportedVersion(); - $aNodes = $oParser->parse(file_get_contents($sModuleFilePath)); + $aNodes = $this->parsePhpCode(file_get_contents($sModuleFilePath)); } catch (PhpParser\Error $e) { throw new \ModuleDiscoveryServiceException($e->getMessage(), 0, $e, $sModuleFilePath); @@ -124,24 +134,6 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array throw new ModuleDiscoveryServiceException("No proper call to SetupWebPage::AddModule found in module file", 0, null, $sModuleFilePath); } - /** - * @param string $sBooleanExpr - * - * @return bool - * @throws ModuleDiscoveryServiceException - */ - public function ComputeBooleanExpression(string $sBooleanExpr) : bool - { - $bResult = false; - try{ - @eval('$bResult = '.$sBooleanExpr.';'); - } catch (Throwable $t) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); - } - - return $bResult; - } - private function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleConfig) : void { $iIndex=0; @@ -242,7 +234,7 @@ private function ParseCallToAddModuleAndReturnModuleConfiguration(string $sModul return [ $sModuleFilePath, $sModuleId, - $aModuleConfig + $aModuleConfig, ]; } @@ -268,20 +260,27 @@ private function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt return null; } - foreach ($oNode->elseifs as $oElseIfSubNode) { - /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode*/ - $bCondition = $this->EvaluateBooleanExpression($oElseIfSubNode->cond); - if($bCondition){ - $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->stmts); - if (!is_null($aModuleConfig)) { - return $aModuleConfig; + if (! is_null($oNode->elseifs)) { + foreach ($oNode->elseifs as $oElseIfSubNode) { + /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ + $bCondition = $this->EvaluateBooleanExpression($oElseIfSubNode->cond); + if ($bCondition) { + $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oElseIfSubNode->stmts); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + break; } - break; } } - $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); - return $aModuleConfig; + if (! is_null($oNode->else)) { + $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); + + return $aModuleConfig; + } + + return null; } @@ -311,11 +310,96 @@ private function EvaluateConstantExpression(\PhpParser\Node\Expr\ArrayItem|\PhpP return $bResult; } + private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr $oExpr) : string + { + if ($oExpr instanceof \PhpParser\Node\Scalar\Int_ || $oExpr instanceof \PhpParser\Node\Scalar\Float_){ + return "" . $oExpr->value; + } + + return $this->EvaluateBooleanExpression($oExpr) ? "true" : "false"; + } + + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + public function ComputeBooleanExpression(string $sBooleanExpr) : bool + { + $bResult = false; + try{ + @eval('$bResult = '.$sBooleanExpr.';'); + } catch (Throwable $t) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); + } + + return $bResult; + } + + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + public function ComputeBooleanExpression3(string $sBooleanExpr) : bool + { + $sPhpContent = <<parsePhpCode($sPhpContent); + $oExpr = $aNodes[0]; + return $this->EvaluateBooleanExpression($oExpr->expr); + } + private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression) : bool { //var_dump($oCondExpression); + + if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ + $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) + . " " + . $oCondExpression->getOperatorSigil() + . " " + . $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->right); + return $this->ComputeBooleanExpression($sExpr); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\BooleanNot){ + return ! $this->EvaluateBooleanExpression($oCondExpression->expr); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\FuncCall){ + return $this->CallFunction($oCondExpression); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\ConstFetch){ + return $this->EvaluateConstantExpression($oCondExpression); + } + return true; } + + private function CallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool + { + $sFunction = $oFunct->name->name; + $aWhiteList = ["function_exists"]; + if (! in_array($sFunction, $aWhiteList)){ + throw new ModuleDiscoveryServiceException("FuncCall $sFunction not supported"); + //return false; + } + + $aArgs=[]; + foreach ($oFunct->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $oReflectionFunction = new ReflectionFunction($sFunction); + return (bool)$oReflectionFunction->invoke(...$aArgs); + } } class ModuleDiscoveryServiceException extends Exception diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php index 52d5be98ea..da760f094d 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -4,9 +4,11 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use ModuleDiscoveryService; +use PhpParser\ParserFactory; class ModuleDiscoveryServiceTest extends ItopDataTestCase { + private string $sTempModuleFilePath; protected function setUp(): void { parent::setUp(); @@ -26,6 +28,16 @@ public function testReadModuleFileConfigurationLegacy() $this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null); } + /*public function testAllReadModuleFileConfiguration() + { + foreach (glob(__DIR__.'/resources/all/module.*.php') as $sModuleFilePath){ + $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + + $this->assertEquals($aExpected, $aRes); + } + }*/ + public function testReadModuleFileConfiguration() { $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; @@ -79,4 +91,251 @@ public function testComputeBooleanExpression_BrokenBooleanExpression(){ $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error: Undefined constant "a"'); $this->assertTrue(ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression("(a || true)")); } + + public function testEvaluateConstantExpression() + { + $sPHP = <<parsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateConstantExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->expr]); + $this->assertEquals(APPROOT, $val); + } + + public function CallReadModuleFileConfiguration($sPHpCode) + { + $this->sTempModuleFilePath = tempnam(__DIR__, "test"); + file_put_contents($this->sTempModuleFilePath, $sPHpCode); + try { + return $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "ReadModuleFileConfiguration", ModuleDiscoveryService::GetInstance(), [$this->sTempModuleFilePath]); + } + finally { + @unlink($this->sTempModuleFilePath); + } + + } + + public function testReadModuleFileConfigurationCheckBasicStatementWithoutIf() + { + $sPHP = << "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "noif", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_IfConditionVerified() + { + $sPHP = << "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "if", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_IfNoConditionVerifiedAndNoElse() + { + $sPHP = << "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "outsideif", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_ElseApplied() + { + $sPHP = << "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "else", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_FirstElseIfApplied() + { + $sPHP = << "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "elseif1", ["c" => "d"]], $val); + } + + public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApplied() + { + $sPHP = << "d"]); +} elseif (false){ + SetupWebPage::AddModule("a", "elseif1", ["c" => "d"]); +} elseif (true){ + SetupWebPage::AddModule("a", "elseif2", ["c" => "d"]); +} else { + SetupWebPage::AddModule("a", "else", ["c" => "d"]); +} +SetupWebPage::AddModule("a", "outsideif", ["c" => "d"]); +\$b=2; +PHP; + $val = $this->CallReadModuleFileConfiguration($sPHP); + $this->assertEquals([$this->sTempModuleFilePath, "elseif2", ["c" => "d"]], $val); + } + + public static function EvaluateExpressionBooleanProvider() { + $sTruePHP = << [ + "code" => str_replace("COND", "true", $sTruePHP), + "bool_expected" => true + + ], + "false" => [ + "code" => str_replace("COND", "false", $sTruePHP), + "bool_expected" => false + + ], + "not ok" => [ + "code" => str_replace("COND", "! false", $sTruePHP), + "bool_expected" => true + + ], + "not ko" => [ + "code" => str_replace("COND", "! (true)", $sTruePHP), + "bool_expected" => false + + ], + "AND ko" => [ + "code" => str_replace("COND", "true && false", $sTruePHP), + "bool_expected" => false + + ], + "AND ok1" => [ + "code" => str_replace("COND", "true && true", $sTruePHP), + "bool_expected" => true + + ], + "AND ko2" => [ + "code" => str_replace("COND", "true && true && false", $sTruePHP), + "bool_expected" => false + + ], + "OR ko" => [ + "code" => str_replace("COND", "false || false", $sTruePHP), + "bool_expected" => false + + ], + "OR ok" => [ + "code" => str_replace("COND", "false ||true", $sTruePHP), + "bool_expected" => true + + ], + "OR ok2" => [ + "code" => str_replace("COND", "false ||false||true", $sTruePHP), + "bool_expected" => true + + ], + "function_exists('ldap_connect')" => [ + "code" => str_replace("COND", "function_exists('ldap_connect')", $sTruePHP), + "bool_expected" => function_exists('ldap_connect') + + ], + "function_exists('gabuzomeushouldnotexist')" => [ + "code" => str_replace("COND", "function_exists('gabuzomeushouldnotexist')", $sTruePHP), + "bool_expected" => function_exists('gabuzomeushouldnotexist') + + ], + "1 > 2" => [ + "code" => str_replace("COND", "1 > 2", $sTruePHP), + "bool_expected" => false + + ], + "1 == 1" => [ + "code" => str_replace("COND", "1 == 1", $sTruePHP), + "bool_expected" => true + + ], + "1 < 2" => [ + "code" => str_replace("COND", "1 < 2", $sTruePHP), + "bool_expected" => true + ], + "PHP_VERSION_ID == PHP_VERSION_ID" => [ + "code" => str_replace("COND", "PHP_VERSION_ID == PHP_VERSION_ID", $sTruePHP), + "bool_expected" => true + ], + "PHP_VERSION_ID != PHP_VERSION_ID" => [ + "code" => str_replace("COND", "PHP_VERSION_ID != PHP_VERSION_ID", $sTruePHP), + "bool_expected" => false + ], + ]; + } + + /** + * @dataProvider EvaluateExpressionBooleanProvider + */ + public function testEvaluateExpressionBoolean($sPHP, $bExpected) + { + $aNodes = ModuleDiscoveryService::GetInstance()->parsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateBooleanExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->cond]); + $this->assertEquals($bExpected, $val); + } } \ No newline at end of file From 812e24b4024eb73e887e8126a02183315b58ce54 Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 26 Aug 2025 11:29:19 +0200 Subject: [PATCH 06/23] =?UTF-8?q?N=C2=B04789=20-=20make=20autoselect=20and?= =?UTF-8?q?=20dependencies=20work=20as=20well?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModuleDiscoveryService.php | 59 ++++++++++++++++--- .../ModuleDiscoveryServiceTest.php | 50 ++++++++++++++-- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index c1e4be8958..0b6381b82b 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -325,7 +325,7 @@ private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr * @return bool * @throws ModuleDiscoveryServiceException */ - public function ComputeBooleanExpression(string $sBooleanExpr) : bool + public function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : bool { $bResult = false; try{ @@ -343,20 +343,28 @@ public function ComputeBooleanExpression(string $sBooleanExpr) : bool * @return bool * @throws ModuleDiscoveryServiceException */ - public function ComputeBooleanExpression3(string $sBooleanExpr) : bool + public function ComputeBooleanExpression(string $sBooleanExpr, $bProtected=true) : bool { + if (! $bProtected){ + return $this->UnprotectedComputeBooleanExpression($sBooleanExpr); + } + $sPhpContent = <<parsePhpCode($sPhpContent); - $oExpr = $aNodes[0]; - return $this->EvaluateBooleanExpression($oExpr->expr); + try{ + $aNodes = ModuleDiscoveryService::GetInstance()->parsePhpCode($sPhpContent); + $oExpr = $aNodes[0]; + return $this->EvaluateBooleanExpression($oExpr->expr); + } catch (ModuleDiscoveryServiceException $previous) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error", 0, $previous); + } } private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression) : bool { - //var_dump($oCondExpression); + #var_dump($oCondExpression); if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) @@ -364,7 +372,7 @@ private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression . $oCondExpression->getOperatorSigil() . " " . $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->right); - return $this->ComputeBooleanExpression($sExpr); + return $this->ComputeBooleanExpression($sExpr, false); } if ($oCondExpression instanceof \PhpParser\Node\Expr\BooleanNot){ @@ -375,6 +383,10 @@ private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression return $this->CallFunction($oCondExpression); } + if ($oCondExpression instanceof \PhpParser\Node\Expr\StaticCall){ + return $this->StaticCallFunction($oCondExpression); + } + if ($oCondExpression instanceof \PhpParser\Node\Expr\ConstFetch){ return $this->EvaluateConstantExpression($oCondExpression); } @@ -400,6 +412,39 @@ private function CallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool $oReflectionFunction = new ReflectionFunction($sFunction); return (bool)$oReflectionFunction->invoke(...$aArgs); } + + /** + * @param \PhpParser\Node\Expr\StaticCall $oStaticCall + * + * @return bool + * @throws \ModuleDiscoveryServiceException + * @throws \ReflectionException + */ + private function StaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : bool + { + var_dump($oStaticCall); + $sClassName = $oStaticCall->class->name; + $sMethodName = $oStaticCall->name->name; + $aWhiteList = ["SetupInfo::ModuleIsSelected"]; + $sStaticCallDescription = "$sClassName::$sMethodName"; + if (! in_array($sStaticCallDescription, $aWhiteList)){ + throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not supported"); + } + + $aArgs=[]; + foreach ($oStaticCall->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $class = new \ReflectionClass($sClassName); + $method = $class->getMethod($sMethodName); + if (! $method->isPublic()){ + throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not public"); + } + + return (bool) $method->invokeArgs(null, $aArgs); + } } class ModuleDiscoveryServiceException extends Exception diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php index da760f094d..4044e12206 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -31,10 +31,15 @@ public function testReadModuleFileConfigurationLegacy() /*public function testAllReadModuleFileConfiguration() { foreach (glob(__DIR__.'/resources/all/module.*.php') as $sModuleFilePath){ - $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); - $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); + $aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath); + $aExpected = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfigurationLegacy($sModuleFilePath); - $this->assertEquals($aExpected, $aRes); + $this->assertEquals($aExpected, $aRes); + + $aAutoselect = $aRes[2]['auto_select'] ?? ""; + if (strlen($aAutoselect) >0){ + var_dump($aAutoselect); + } } }*/ @@ -88,10 +93,47 @@ public function testComputeBooleanExpression(string $sBooleanExpression, bool $e public function testComputeBooleanExpression_BrokenBooleanExpression(){ $this->expectException(\ModuleDiscoveryServiceException::class); - $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error: Undefined constant "a"'); + $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); $this->assertTrue(ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression("(a || true)")); } + + public static function ComputeBooleanExpressionAutoselectProvider() + { + $sSimpleCallToModuleIsSelected = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\")"; + $sSimpleCallToModuleIsSelected2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\")"; + $sCallToModuleIsSelectedCombinedWithAndOperator = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; + $sCallToModuleIsSelectedCombinedWithAndOperator2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; + + return [ + "simple call to SetupInfo::ModuleIsSelected SELECTED" => [ + "expr" => $sSimpleCallToModuleIsSelected, + "expected" => true + ], + "simple call to SetupInfo::ModuleIsSelected NOT SELECTED" => [ + "expr" => $sSimpleCallToModuleIsSelected2, + "expected" => false + ], + "call to SetupInfo::ModuleIsSelected + OR => SELECTED" => [ + "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator, + "expected" => true + ], + "simple call to SetupInfo::ModuleIsSelected + OR => NOT SELECTED" => [ + "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator2, + "expected" => false + ], + ]; + } + + + /** + * @dataProvider ComputeBooleanExpressionAutoselectProvider + */ + public function testComputeBooleanExpressionAutoselect(string $sBooleanExpression, bool $expected){ + \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); + $this->assertEquals($expected, ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpression), $sBooleanExpression); + } + public function testEvaluateConstantExpression() { $sPHP = << Date: Tue, 26 Aug 2025 11:30:52 +0200 Subject: [PATCH 07/23] cleanup --- setup/modulediscovery/ModuleDiscoveryService.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 0b6381b82b..1e8263de84 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -357,15 +357,13 @@ public function ComputeBooleanExpression(string $sBooleanExpr, $bProtected=true) $aNodes = ModuleDiscoveryService::GetInstance()->parsePhpCode($sPhpContent); $oExpr = $aNodes[0]; return $this->EvaluateBooleanExpression($oExpr->expr); - } catch (ModuleDiscoveryServiceException $previous) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error", 0, $previous); + } catch (Throwable $t) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); } } private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression) : bool { - #var_dump($oCondExpression); - if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) . " " @@ -422,7 +420,6 @@ private function CallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool */ private function StaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : bool { - var_dump($oStaticCall); $sClassName = $oStaticCall->class->name; $sMethodName = $oStaticCall->name->name; $aWhiteList = ["SetupInfo::ModuleIsSelected"]; From f47309f535384a13b9c106b6751ba54cf6593a85 Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 26 Aug 2025 16:54:36 +0200 Subject: [PATCH 08/23] =?UTF-8?q?N=C2=B04789=20-=20fix=20BeforeWritingConf?= =?UTF-8?q?ig=20calls=20during=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/config.class.inc.php | 16 +-- .../ModuleDiscoveryService.php | 49 ++++++++ .../ModuleDiscoveryServiceTest.php | 80 ++++++++----- .../resources/module.itop-tickets.php | 107 ++++++++++++++++++ 4 files changed, 209 insertions(+), 43 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-tickets.php diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 3622c41930..1aafd90d08 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -2856,20 +2856,8 @@ public function UpdateIncludes($sModulesDir, $aSelectedModules = null) } } } - if (isset($aModuleInfo['installer'])) - { - $sModuleInstallerClass = $aModuleInfo['installer']; - if (!class_exists($sModuleInstallerClass)) - { - throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); - } - if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) - { - throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); - } - $aCallSpec = array($sModuleInstallerClass, 'BeforeWritingConfig'); - call_user_func_array($aCallSpec, array($this)); - } + + ModuleDiscoveryService::GetInstance()->CallInstallerBeforeWritingConfigMethod($this, $aModuleInfo); } } $this->SetAddOns($aAddOns); diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 1e8263de84..9f37adca4f 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -60,6 +60,8 @@ public function ReadModuleFileConfigurationLegacy(string $sModuleFilePath) : arr { throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath did not return the expected information..."); } + + $this->AddModuleFilePath($aModuleInfo); } catch(ModuleDiscoveryServiceException $e) { @@ -79,6 +81,20 @@ public function ReadModuleFileConfigurationLegacy(string $sModuleFilePath) : arr return $aModuleInfo; } + /** + * 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 AddModuleFilePath(array &$aModuleInfo) + { + if (count($aModuleInfo)==3) { + $aModuleInfo[2]['module_file_path'] = $aModuleInfo[0]; + } + } + /** * @param string $sPhpContent * @@ -112,6 +128,7 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { $aModuleConfig = $this->ParseCallToAddModuleAndReturnModuleConfiguration($sModuleFilePath, $oNode); if (! is_null($aModuleConfig)){ + $this->AddModuleFilePath($aModuleConfig); return $aModuleConfig; } } @@ -119,6 +136,7 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array if ($oNode instanceof PhpParser\Node\Stmt\If_) { $aModuleConfig = $this->BrowseIfStructure($sModuleFilePath, $oNode); if (! is_null($aModuleConfig)){ + $this->AddModuleFilePath($aModuleConfig); return $aModuleConfig; } } @@ -442,6 +460,37 @@ private function StaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall return (bool) $method->invokeArgs(null, $aArgs); } + + /** + * + * @param \Config $oConfig + * @param array $aModuleInfo + * + * @return void + * @throws \ModuleDiscoveryServiceException + */ + public function CallInstallerBeforeWritingConfigMethod(Config $oConfig, array $aModuleInfo) + { + if (isset($aModuleInfo['installer'])) + { + $sModuleInstallerClass = $aModuleInfo['installer']; + if (!class_exists($sModuleInstallerClass)) { + $sModuleFilePath = $aModuleInfo['module_file_path']; + $this->ReadModuleFileConfigurationLegacy($sModuleFilePath); + } + + if (!class_exists($sModuleInstallerClass)) + { + throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); + } + if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) + { + throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); + } + $aCallSpec = array($sModuleInstallerClass, 'BeforeWritingConfig'); + call_user_func_array($aCallSpec, array($oConfig)); + } + } } class ModuleDiscoveryServiceException extends Exception diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php index 4044e12206..e68b3cbf86 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -108,19 +108,19 @@ public static function ComputeBooleanExpressionAutoselectProvider() return [ "simple call to SetupInfo::ModuleIsSelected SELECTED" => [ "expr" => $sSimpleCallToModuleIsSelected, - "expected" => true + "expected" => true, ], "simple call to SetupInfo::ModuleIsSelected NOT SELECTED" => [ "expr" => $sSimpleCallToModuleIsSelected2, - "expected" => false + "expected" => false, ], "call to SetupInfo::ModuleIsSelected + OR => SELECTED" => [ "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator, - "expected" => true + "expected" => true, ], "simple call to SetupInfo::ModuleIsSelected + OR => NOT SELECTED" => [ "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator2, - "expected" => false + "expected" => false, ], ]; } @@ -152,12 +152,11 @@ public function CallReadModuleFileConfiguration($sPHpCode) $this->sTempModuleFilePath = tempnam(__DIR__, "test"); file_put_contents($this->sTempModuleFilePath, $sPHpCode); try { - return $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "ReadModuleFileConfiguration", ModuleDiscoveryService::GetInstance(), [$this->sTempModuleFilePath]); + return ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); } finally { @unlink($this->sTempModuleFilePath); } - } public function testReadModuleFileConfigurationCheckBasicStatementWithoutIf() @@ -169,7 +168,7 @@ public function testReadModuleFileConfigurationCheckBasicStatementWithoutIf() \$b=2; PHP; $val = $this->CallReadModuleFileConfiguration($sPHP); - $this->assertEquals([$this->sTempModuleFilePath, "noif", ["c" => "d"]], $val); + $this->assertEquals([$this->sTempModuleFilePath, "noif", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } public function testReadModuleFileConfigurationCheckBasicStatement_IfConditionVerified() @@ -190,7 +189,7 @@ public function testReadModuleFileConfigurationCheckBasicStatement_IfConditionVe \$b=2; PHP; $val = $this->CallReadModuleFileConfiguration($sPHP); - $this->assertEquals([$this->sTempModuleFilePath, "if", ["c" => "d"]], $val); + $this->assertEquals([$this->sTempModuleFilePath, "if", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } public function testReadModuleFileConfigurationCheckBasicStatement_IfNoConditionVerifiedAndNoElse() @@ -209,7 +208,7 @@ public function testReadModuleFileConfigurationCheckBasicStatement_IfNoCondition \$b=2; PHP; $val = $this->CallReadModuleFileConfiguration($sPHP); - $this->assertEquals([$this->sTempModuleFilePath, "outsideif", ["c" => "d"]], $val); + $this->assertEquals([$this->sTempModuleFilePath, "outsideif", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } public function testReadModuleFileConfigurationCheckBasicStatement_ElseApplied() @@ -230,7 +229,7 @@ public function testReadModuleFileConfigurationCheckBasicStatement_ElseApplied() \$b=2; PHP; $val = $this->CallReadModuleFileConfiguration($sPHP); - $this->assertEquals([$this->sTempModuleFilePath, "else", ["c" => "d"]], $val); + $this->assertEquals([$this->sTempModuleFilePath, "else", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } public function testReadModuleFileConfigurationCheckBasicStatement_FirstElseIfApplied() @@ -251,7 +250,7 @@ public function testReadModuleFileConfigurationCheckBasicStatement_FirstElseIfAp \$b=2; PHP; $val = $this->CallReadModuleFileConfiguration($sPHP); - $this->assertEquals([$this->sTempModuleFilePath, "elseif1", ["c" => "d"]], $val); + $this->assertEquals([$this->sTempModuleFilePath, "elseif1", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApplied() @@ -272,7 +271,7 @@ public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApp \$b=2; PHP; $val = $this->CallReadModuleFileConfiguration($sPHP); - $this->assertEquals([$this->sTempModuleFilePath, "elseif2", ["c" => "d"]], $val); + $this->assertEquals([$this->sTempModuleFilePath, "elseif2", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } public static function EvaluateExpressionBooleanProvider() { @@ -286,85 +285,85 @@ public static function EvaluateExpressionBooleanProvider() { return [ "true" => [ "code" => str_replace("COND", "true", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "false" => [ "code" => str_replace("COND", "false", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], "not ok" => [ "code" => str_replace("COND", "! false", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "not ko" => [ "code" => str_replace("COND", "! (true)", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], "AND ko" => [ "code" => str_replace("COND", "true && false", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], "AND ok1" => [ "code" => str_replace("COND", "true && true", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "AND ko2" => [ "code" => str_replace("COND", "true && true && false", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], "OR ko" => [ "code" => str_replace("COND", "false || false", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], "OR ok" => [ "code" => str_replace("COND", "false ||true", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "OR ok2" => [ "code" => str_replace("COND", "false ||false||true", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "function_exists('ldap_connect')" => [ "code" => str_replace("COND", "function_exists('ldap_connect')", $sTruePHP), - "bool_expected" => function_exists('ldap_connect') + "bool_expected" => function_exists('ldap_connect'), ], "function_exists('gabuzomeushouldnotexist')" => [ "code" => str_replace("COND", "function_exists('gabuzomeushouldnotexist')", $sTruePHP), - "bool_expected" => function_exists('gabuzomeushouldnotexist') + "bool_expected" => function_exists('gabuzomeushouldnotexist'), ], "1 > 2" => [ "code" => str_replace("COND", "1 > 2", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], "1 == 1" => [ "code" => str_replace("COND", "1 == 1", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "1 < 2" => [ "code" => str_replace("COND", "1 < 2", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "PHP_VERSION_ID == PHP_VERSION_ID" => [ "code" => str_replace("COND", "PHP_VERSION_ID == PHP_VERSION_ID", $sTruePHP), - "bool_expected" => true + "bool_expected" => true, ], "PHP_VERSION_ID != PHP_VERSION_ID" => [ "code" => str_replace("COND", "PHP_VERSION_ID != PHP_VERSION_ID", $sTruePHP), - "bool_expected" => false + "bool_expected" => false, ], ]; } @@ -380,4 +379,27 @@ public function testEvaluateExpressionBoolean($sPHP, $bExpected) $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateBooleanExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->cond]); $this->assertEquals($bExpected, $val); } + + public function testCallDeclaredInstaller() + { + $sModuleInstallerClass = "TicketsInstaller" . uniqid(); + $sPHpCode = file_get_contents(__DIR__.'/resources/module.itop-tickets.php'); + $sPHpCode = str_replace("TicketsInstaller", $sModuleInstallerClass, $sPHpCode); + $this->sTempModuleFilePath = tempnam(__DIR__, "test"); + file_put_contents($this->sTempModuleFilePath, $sPHpCode); + var_dump($sPHpCode); + + try { + $this->assertFalse(class_exists($sModuleInstallerClass)); + $aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($this->sTempModuleFilePath); + $this->assertFalse(class_exists($sModuleInstallerClass)); + + ModuleDiscoveryService::GetInstance()->CallInstallerBeforeWritingConfigMethod(\MetaModel::GetConfig(), $aModuleInfo[2]); + } + finally { + @unlink($this->sTempModuleFilePath); + } + + $this->assertTrue(class_exists($sModuleInstallerClass)); + } } \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-tickets.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-tickets.php new file mode 100644 index 0000000000..f46fc1f231 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-tickets.php @@ -0,0 +1,107 @@ + 'Tickets Management', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-structure/2.7.1', + ), + 'mandatory' => false, + 'visible' => true, + 'installer' => 'TicketsInstaller', + + // Components + // + 'datamodel' => array( + 'main.itop-tickets.php', + ), + 'data.struct' => array( + // 'data.struct.ta-actions.xml', + ), + 'data.sample' => array( + ), + + // Documentation + // + 'doc.manual_setup' => '', + 'doc.more_information' => '', + + // Default settings + // + 'settings' => array( + ), + ) +); + +// Module installation handler +// +class TicketsInstaller extends ModuleInstallerAPI +{ + public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + // Delete all Triggers corresponding to a no more valid class + CMDBObject::SetTrackInfo('Uninstallation'); + $oSearch = new DBObjectSearch('TriggerOnObject'); + $oSet = new DBObjectSet($oSearch); + while($oTrigger = $oSet->Fetch()) + { + try + { + if (!MetaModel::IsValidClass($oTrigger->Get('target_class'))) + { + $oTrigger->DBDelete(); + } + } + catch(Exception $e) + { + utils::EnrichRaisedException($oTrigger, $e); + } + } + // It's not very clear if it make sense to test a particular version, + // as the loading mechanism checks object existence using reconc_keys + // and do not recreate them, nor update existing. + // Without test, new entries added to the data files, would be automatically loaded + if (($sPreviousVersion === '') || + (version_compare($sPreviousVersion, $sCurrentVersion, '<') + && version_compare($sPreviousVersion, '3.0.0', '<'))) { + $oDataLoader = new XMLDataLoader(); + + CMDBObject::SetTrackInfo("Initialization TicketsInstaller"); + $oMyChange = CMDBObject::GetCurrentChange(); + + $sLang = null; + // - Try to get app. language from configuration fil (app. upgrade) + $sConfigFileName = APPCONF.'production/'.ITOP_CONFIG_FILE; + if (file_exists($sConfigFileName)) { + $oFileConfig = new Config($sConfigFileName); + if (is_object($oFileConfig)) { + $sLang = str_replace(' ', '_', strtolower($oFileConfig->GetDefaultLanguage())); + } + } + + // - I still no language, get the default one + if (null === $sLang) { + $sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage())); + } + + $sFileName = dirname(__FILE__)."/data/{$sLang}.data.itop-tickets.xml"; + SetupLog::Info("Searching file: $sFileName"); + if (!file_exists($sFileName)) { + $sFileName = dirname(__FILE__)."/data/en_us.data.itop-tickets.xml"; + } + SetupLog::Info("Loading file: $sFileName"); + $oDataLoader->StartSession($oMyChange); + $oDataLoader->LoadFile($sFileName, false, true); + $oDataLoader->EndSession(); + } + } +} From 61c2b71f1f76ae3096e3bb0e4ecac4a868d615e0 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 27 Aug 2025 11:47:09 +0200 Subject: [PATCH 09/23] =?UTF-8?q?N=C2=B04789=20-=20refactor=20and=20split?= =?UTF-8?q?=20in=20ModuleDiscoveryEvaluationService=20+=20handle=20ModuleI?= =?UTF-8?q?nstallerAPI=20methods=20calls=20during=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/modulediscovery.class.inc.php | 2 +- .../ModuleDiscoveryEvaluationService.php | 343 ++++++++++++++ .../ModuleDiscoveryService.php | 425 +++--------------- .../ModuleDiscoveryServiceException.php | 23 + setup/runtimeenv.class.inc.php | 25 +- .../InstallationFileService.php | 2 +- setup/wizardsteps.class.inc.php | 4 +- .../ModuleDiscoveryEvaluationServiceTest.php | 199 ++++++++ .../ModuleDiscoveryServiceTest.php | 182 +------- 9 files changed, 642 insertions(+), 563 deletions(-) create mode 100644 setup/modulediscovery/ModuleDiscoveryEvaluationService.php create mode 100644 setup/modulediscovery/ModuleDiscoveryServiceException.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 64b39c07d3..899099bf02 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -391,7 +391,7 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a { $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); try{ - $bResult = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpr); + $bResult = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($sBooleanExpr); } catch(ModuleDiscoveryServiceException $e){ //logged already echo "Failed to parse the boolean Expression = '$sBooleanExpr'
    "; diff --git a/setup/modulediscovery/ModuleDiscoveryEvaluationService.php b/setup/modulediscovery/ModuleDiscoveryEvaluationService.php new file mode 100644 index 0000000000..1d5cb357db --- /dev/null +++ b/setup/modulediscovery/ModuleDiscoveryEvaluationService.php @@ -0,0 +1,343 @@ +createForNewestSupportedVersion(); + return $oParser->parse($sPhpContent); + } + + /** + * @param string $sModuleFilePath + * @param \PhpParser\Node\Expr\Assign $oAssignation + * + * @return array|null + * @throws \ModuleDiscoveryServiceException + */ + public function BrowseAddModuleCallAndReturnModuleConfiguration(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, + ]; + } + + public function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleConfig) : 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) { + $sKey = $this->EvaluateConstantExpression($oArrayItem->key); + if (is_null($sKey)){ + continue; + } + }else { + $sKey = $iIndex++; + } + + $oValue = $oArrayItem->value; + + if ($oValue instanceof PhpParser\Node\Expr\Array_) { + $aSubConfig=[]; + $this->BrowseArrayStructure($oValue, $aSubConfig); + $aModuleConfig[$sKey]=$aSubConfig; + } + + 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) { + $oEvaluatedConstant = $this->EvaluateConstantExpression($oValue); + $aModuleConfig[$sKey]= $oEvaluatedConstant; + } + } + } + + /** + * @param string $sModuleFilePath + * @param \PhpParser\Node\Stmt\If_ $oNode + * + * @return array|null + * @throws \ModuleDiscoveryServiceException + */ + public function BrowseIfStructure(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); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + } + } + return null; + } + + if (! is_null($oNode->elseifs)) { + foreach ($oNode->elseifs as $oElseIfSubNode) { + /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ + $bCondition = $this->EvaluateExpression($oElseIfSubNode->cond); + if ($bCondition) { + $aModuleConfig = $this->BrowseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oElseIfSubNode->stmts); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + break; + } + } + } + + if (! is_null($oNode->else)) { + $aModuleConfig = $this->BrowseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); + + return $aModuleConfig; + } + + return null; + } + + public function BrowseStatementsAndReturnModuleConfiguration(string $sModuleFilePath, array $aStmts) : ?array + { + foreach ($aStmts as $oSubNode) { + if ($oSubNode instanceof \PhpParser\Node\Stmt\Expression) { + $aModuleConfig = $this->BrowseAddModuleCallAndReturnModuleConfiguration($sModuleFilePath, $oSubNode); + if (!is_null($aModuleConfig)) { + return $aModuleConfig; + } + } + } + + return null; + } + + 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()); + } + + return $bResult; + } + + private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr $oExpr) : string + { + if ($oExpr instanceof \PhpParser\Node\Scalar\Int_ || $oExpr instanceof \PhpParser\Node\Scalar\Float_){ + return "" . $oExpr->value; + } + + return $this->EvaluateExpression($oExpr) ? "true" : "false"; + } + + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : bool + { + $bResult = false; + try{ + @eval('$bResult = '.$sBooleanExpr.';'); + } catch (Throwable $t) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); + } + + return $bResult; + } + + /** + * @param string $sBooleanExpr + * @param bool $bSafe: when true, evaluation relies on unsafe eval() call + * + * @return bool + * @throws ModuleDiscoveryServiceException + */ + public function EvaluateBooleanExpression(string $sBooleanExpr, $bSafe=true) : bool + { + if (! $bSafe){ + return $this->UnprotectedComputeBooleanExpression($sBooleanExpr); + } + + $sPhpContent = <<ParsePhpCode($sPhpContent); + $oExpr = $aNodes[0]; + return $this->EvaluateExpression($oExpr->expr); + } catch (Throwable $t) { + throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); + } + } + + private function EvaluateExpression(\PhpParser\Node\Expr $oCondExpression) : bool + { + if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ + $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) + . " " + . $oCondExpression->getOperatorSigil() + . " " + . $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->right); + return $this->EvaluateBooleanExpression($sExpr, false); + } + + if ($oCondExpression instanceof \PhpParser\Node\Expr\BooleanNot){ + return ! $this->EvaluateExpression($oCondExpression->expr); + } + + 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; + } + + private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool + { + $sFunction = $oFunct->name->name; + $aWhiteList = ["function_exists"]; + if (! in_array($sFunction, $aWhiteList)){ + throw new ModuleDiscoveryServiceException("FuncCall $sFunction not supported"); + //return false; + } + + $aArgs=[]; + foreach ($oFunct->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $oReflectionFunction = new ReflectionFunction($sFunction); + return (bool)$oReflectionFunction->invoke(...$aArgs); + } + + /** + * @param \PhpParser\Node\Expr\StaticCall $oStaticCall + * + * @return bool + * @throws \ModuleDiscoveryServiceException + * @throws \ReflectionException + */ + private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : bool + { + $sClassName = $oStaticCall->class->name; + $sMethodName = $oStaticCall->name->name; + $aWhiteList = ["SetupInfo::ModuleIsSelected"]; + $sStaticCallDescription = "$sClassName::$sMethodName"; + if (! in_array($sStaticCallDescription, $aWhiteList)){ + throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not supported"); + } + + $aArgs=[]; + foreach ($oStaticCall->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $class = new \ReflectionClass($sClassName); + $method = $class->getMethod($sMethodName); + if (! $method->isPublic()){ + throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not public"); + } + + return (bool) $method->invokeArgs(null, $aArgs); + } +} \ No newline at end of file diff --git a/setup/modulediscovery/ModuleDiscoveryService.php b/setup/modulediscovery/ModuleDiscoveryService.php index 9f37adca4f..e2668e0d93 100644 --- a/setup/modulediscovery/ModuleDiscoveryService.php +++ b/setup/modulediscovery/ModuleDiscoveryService.php @@ -3,6 +3,9 @@ use PhpParser\ParserFactory; use PhpParser\Node\Expr\Assign; +require_once __DIR__ . '/ModuleDiscoveryEvaluationService.php'; +require_once __DIR__ . '/ModuleDiscoveryServiceException.php'; + class ModuleDiscoveryService { private static ModuleDiscoveryService $oInstance; private static int $iDummyClassIndex = 0; @@ -61,7 +64,7 @@ public function ReadModuleFileConfigurationLegacy(string $sModuleFilePath) : arr throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath did not return the expected information..."); } - $this->AddModuleFilePath($aModuleInfo); + $this->CompleteConfigWithModuleFilePath($aModuleInfo); } catch(ModuleDiscoveryServiceException $e) { @@ -81,30 +84,6 @@ public function ReadModuleFileConfigurationLegacy(string $sModuleFilePath) : arr return $aModuleInfo; } - /** - * 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 AddModuleFilePath(array &$aModuleInfo) - { - if (count($aModuleInfo)==3) { - $aModuleInfo[2]['module_file_path'] = $aModuleInfo[0]; - } - } - - /** - * @param string $sPhpContent - * - * @return \PhpParser\Node\Stmt[]|null - */ - public function parsePhpCode(string $sPhpContent): ?array - { - $oParser = (new ParserFactory())->createForNewestSupportedVersion(); - return $oParser->parse($sPhpContent); - } /** * Read the information from a module file (module.xxx.php) @@ -117,7 +96,7 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array { try { - $aNodes = $this->parsePhpCode(file_get_contents($sModuleFilePath)); + $aNodes = ModuleDiscoveryEvaluationService::GetInstance()->ParsePhpCode(file_get_contents($sModuleFilePath)); } catch (PhpParser\Error $e) { throw new \ModuleDiscoveryServiceException($e->getMessage(), 0, $e, $sModuleFilePath); @@ -126,17 +105,17 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array try { foreach ($aNodes as $sKey => $oNode) { if ($oNode instanceof \PhpParser\Node\Stmt\Expression) { - $aModuleConfig = $this->ParseCallToAddModuleAndReturnModuleConfiguration($sModuleFilePath, $oNode); + $aModuleConfig = ModuleDiscoveryEvaluationService::GetInstance()->BrowseAddModuleCallAndReturnModuleConfiguration($sModuleFilePath, $oNode); if (! is_null($aModuleConfig)){ - $this->AddModuleFilePath($aModuleConfig); + $this->CompleteConfigWithModuleFilePath($aModuleConfig); return $aModuleConfig; } } if ($oNode instanceof PhpParser\Node\Stmt\If_) { - $aModuleConfig = $this->BrowseIfStructure($sModuleFilePath, $oNode); + $aModuleConfig = ModuleDiscoveryEvaluationService::GetInstance()->BrowseIfStructure($sModuleFilePath, $oNode); if (! is_null($aModuleConfig)){ - $this->AddModuleFilePath($aModuleConfig); + $this->CompleteConfigWithModuleFilePath($aModuleConfig); return $aModuleConfig; } } @@ -152,365 +131,97 @@ public function ReadModuleFileConfiguration(string $sModuleFilePath) : array throw new ModuleDiscoveryServiceException("No proper call to SetupWebPage::AddModule found in module file", 0, null, $sModuleFilePath); } - private function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array &$aModuleConfig) : 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) { - $sKey = $this->EvaluateConstantExpression($oArrayItem->key); - if (is_null($sKey)){ - continue; - } - }else { - $sKey = $iIndex++; - } - - $oValue = $oArrayItem->value; - - if ($oValue instanceof PhpParser\Node\Expr\Array_) { - $aSubConfig=[]; - $this->BrowseArrayStructure($oValue, $aSubConfig); - $aModuleConfig[$sKey]=$aSubConfig; - } - - 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) { - $oEvaluatedConstant = $this->EvaluateConstantExpression($oValue); - $aModuleConfig[$sKey]= $oEvaluatedConstant; - } - } - } - /** - * @param string $sModuleFilePath - * @param \PhpParser\Node\Expr\Assign $oAssignation + * 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 array|null - * @throws \ModuleDiscoveryServiceException + * @return void */ - private function ParseCallToAddModuleAndReturnModuleConfiguration(string $sModuleFilePath, \PhpParser\Node\Stmt\Expression $oExpression) : ?array + private function CompleteConfigWithModuleFilePath(array &$aModuleInfo) { - /** @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); + if (count($aModuleInfo)==3) { + $aModuleInfo[2]['module_file_path'] = $aModuleInfo[0]; } - return [ - $sModuleFilePath, - $sModuleId, - $aModuleConfig, - ]; } /** - * @param string $sModuleFilePath - * @param \PhpParser\Node\Stmt\If_ $oNode * - * @return array|null + * @param \Config $oConfig + * @param array $aModuleConfig + * + * @return void * @throws \ModuleDiscoveryServiceException */ - private function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt\If_ $oNode) : ?array + public function CallInstallerBeforeWritingConfigMethod(Config $oConfig, array $aModuleConfig) { - $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; - } - - if (! is_null($oNode->elseifs)) { - foreach ($oNode->elseifs as $oElseIfSubNode) { - /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ - $bCondition = $this->EvaluateBooleanExpression($oElseIfSubNode->cond); - if ($bCondition) { - $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oElseIfSubNode->stmts); - if (!is_null($aModuleConfig)) { - return $aModuleConfig; - } - break; - } - } - } - - if (! is_null($oNode->else)) { - $aModuleConfig = $this->ParseStatementsAndReturnModuleConfiguration($sModuleFilePath, $oNode->else->stmts); - - return $aModuleConfig; + $sModuleInstallerClass = $this->DeclareModuleInstallerAPI($aModuleConfig); + if (is_null($sModuleInstallerClass)){ + return; } - return null; - } - - - 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 GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr $oExpr) : string - { - if ($oExpr instanceof \PhpParser\Node\Scalar\Int_ || $oExpr instanceof \PhpParser\Node\Scalar\Float_){ - return "" . $oExpr->value; - } - - return $this->EvaluateBooleanExpression($oExpr) ? "true" : "false"; + $aCallSpec = [$sModuleInstallerClass, 'BeforeWritingConfig']; + call_user_func_array($aCallSpec, [$oConfig]); } /** - * @param string $sBooleanExpr + * Call the given handler method for all selected modules having an installation handler * - * @return bool - * @throws ModuleDiscoveryServiceException - */ - public function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : bool - { - $bResult = false; - try{ - @eval('$bResult = '.$sBooleanExpr.';'); - } catch (Throwable $t) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); - } - - return $bResult; - } - - /** - * @param string $sBooleanExpr + * @param Config $oConfig + * @param array $aModuleConfig + * @param array $aModule + * @param string $sHandlerName * - * @return bool - * @throws ModuleDiscoveryServiceException + * @throws CoreException */ - public function ComputeBooleanExpression(string $sBooleanExpr, $bProtected=true) : bool - { - if (! $bProtected){ - return $this->UnprotectedComputeBooleanExpression($sBooleanExpr); - } - - $sPhpContent = <<parsePhpCode($sPhpContent); - $oExpr = $aNodes[0]; - return $this->EvaluateBooleanExpression($oExpr->expr); - } catch (Throwable $t) { - throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); - } - } - - private function EvaluateBooleanExpression(\PhpParser\Node\Expr $oCondExpression) : bool - { - if ($oCondExpression instanceof \PhpParser\Node\Expr\BinaryOp){ - $sExpr = $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->left) - . " " - . $oCondExpression->getOperatorSigil() - . " " - . $this->GetMixedValueForBooleanOperatorEvaluation($oCondExpression->right); - return $this->ComputeBooleanExpression($sExpr, false); - } - - if ($oCondExpression instanceof \PhpParser\Node\Expr\BooleanNot){ - return ! $this->EvaluateBooleanExpression($oCondExpression->expr); - } - - if ($oCondExpression instanceof \PhpParser\Node\Expr\FuncCall){ - return $this->CallFunction($oCondExpression); - } - - if ($oCondExpression instanceof \PhpParser\Node\Expr\StaticCall){ - return $this->StaticCallFunction($oCondExpression); - } - - if ($oCondExpression instanceof \PhpParser\Node\Expr\ConstFetch){ - return $this->EvaluateConstantExpression($oCondExpression); - } - - return true; - } - - private function CallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : bool + public function CallInstallerHandler(Config $oConfig, array $aModuleConfig, array $aModule, $sHandlerName) { - $sFunction = $oFunct->name->name; - $aWhiteList = ["function_exists"]; - if (! in_array($sFunction, $aWhiteList)){ - throw new ModuleDiscoveryServiceException("FuncCall $sFunction not supported"); - //return false; + $sModuleInstallerClass = $this->DeclareModuleInstallerAPI($aModuleConfig); + if (is_null($sModuleInstallerClass)){ + return; } - $aArgs=[]; - foreach ($oFunct->args as $arg){ - /** @var \PhpParser\Node\Arg $arg */ - $aArgs[]=$arg->value->value; + 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); + } } - - $oReflectionFunction = new ReflectionFunction($sFunction); - return (bool)$oReflectionFunction->invoke(...$aArgs); } - - /** - * @param \PhpParser\Node\Expr\StaticCall $oStaticCall - * - * @return bool - * @throws \ModuleDiscoveryServiceException - * @throws \ReflectionException - */ - private function StaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : bool + + private function DeclareModuleInstallerAPI($aModuleConfig) : ?string { - $sClassName = $oStaticCall->class->name; - $sMethodName = $oStaticCall->name->name; - $aWhiteList = ["SetupInfo::ModuleIsSelected"]; - $sStaticCallDescription = "$sClassName::$sMethodName"; - if (! in_array($sStaticCallDescription, $aWhiteList)){ - throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not supported"); + if (! isset($aModuleConfig['installer'])){ + return null; } - $aArgs=[]; - foreach ($oStaticCall->args as $arg){ - /** @var \PhpParser\Node\Arg $arg */ - $aArgs[]=$arg->value->value; + $sModuleInstallerClass = $aModuleConfig['installer']; + if (!class_exists($sModuleInstallerClass)) { + $sModuleFilePath = $aModuleConfig['module_file_path']; + $this->ReadModuleFileConfigurationLegacy($sModuleFilePath); } - $class = new \ReflectionClass($sClassName); - $method = $class->getMethod($sMethodName); - if (! $method->isPublic()){ - throw new ModuleDiscoveryServiceException("StaticCall $sStaticCallDescription not public"); + if (!class_exists($sModuleInstallerClass)) + { + throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleConfig['label']); } - - return (bool) $method->invokeArgs(null, $aArgs); - } - - /** - * - * @param \Config $oConfig - * @param array $aModuleInfo - * - * @return void - * @throws \ModuleDiscoveryServiceException - */ - public function CallInstallerBeforeWritingConfigMethod(Config $oConfig, array $aModuleInfo) - { - if (isset($aModuleInfo['installer'])) + if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) { - $sModuleInstallerClass = $aModuleInfo['installer']; - if (!class_exists($sModuleInstallerClass)) { - $sModuleFilePath = $aModuleInfo['module_file_path']; - $this->ReadModuleFileConfigurationLegacy($sModuleFilePath); - } - - if (!class_exists($sModuleInstallerClass)) - { - throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); - } - if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) - { - throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); - } - $aCallSpec = array($sModuleInstallerClass, 'BeforeWritingConfig'); - call_user_func_array($aCallSpec, array($oConfig)); + throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleConfig['label']); } - } -} - -class ModuleDiscoveryServiceException extends Exception -{ - /** - * ModuleDiscoveryServiceException constructor. - * - * @param string $sMessage - * @param int $iHttpCode - * @param Exception|null $oPrevious - */ - public function __construct($sMessage, $iHttpCode = 0, Exception $oPrevious = null, $sModuleFile=null) - { - $e = new \Exception(""); - $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); + return $sModuleInstallerClass; } -} \ No newline at end of file +} diff --git a/setup/modulediscovery/ModuleDiscoveryServiceException.php b/setup/modulediscovery/ModuleDiscoveryServiceException.php new file mode 100644 index 0000000000..99ab3bb479 --- /dev/null +++ b/setup/modulediscovery/ModuleDiscoveryServiceException.php @@ -0,0 +1,23 @@ + $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/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 3adcfb6ce6..d4834c0609 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -459,7 +459,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { SetupInfo::SetSelectedModules($aRet); try{ - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($oModule->GetAutoSelect()); + $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($oModule->GetAutoSelect()); if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module @@ -1087,28 +1087,9 @@ public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sH { foreach($aAvailableModules as $sModuleId => $aModule) { - if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) && - isset($aAvailableModules[$sModuleId]['installer']) ) + if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) { - $sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer']; - SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})"); - $aCallSpec = array($sModuleInstallerClass, $sHandlerName); - if (is_callable($aCallSpec)) - { - try { - call_user_func_array($aCallSpec, array(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); - } - } + ModuleDiscoveryService::GetInstance()->CallInstallerHandler(MetaModel::GetConfig(), $aAvailableModules[$sModuleId], $aModule, $sHandlerName); } } } diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index a86f1179d2..fdb6788e7f 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -270,7 +270,7 @@ public function ProcessAutoSelectModules() : void { { try { SetupInfo::SetSelectedModules($this->aSelectedModules); - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aModule['auto_select']); + $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { // Modules in data/production-modules/ are considered as mandatory and always installed diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 6b36d960ec..f1209d0cec 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1787,7 +1787,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP // Check the module selection try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aInfo['auto_select']); + $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($aInfo['auto_select']); } catch (ModuleDiscoveryServiceException $e) { //logged already @@ -1865,7 +1865,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aModule['auto_select']); + $bSelected = ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { $aModules[$sModuleId] = true; // store the Id of the selected module diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php new file mode 100644 index 0000000000..18317fe66e --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryEvaluationServiceTest.php @@ -0,0 +1,199 @@ +RequireOnceItopFile('setup/modulediscovery/ModuleDiscoveryService.php'); + } + + public static function EvaluateBooleanExpressionProvider() + { + return [ + "true" => [ "expr" => "true", "expected" => true], + "(true)" => [ "expr" => "(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], + ]; + } + + /** + * @dataProvider EvaluateBooleanExpressionProvider + */ + public function testEvaluateBooleanExpression(string $sBooleanExpression, bool $expected){ + $this->assertEquals($expected, ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); + } + + public function testEvaluateBooleanExpression_BrokenBooleanExpression(){ + $this->expectException(\ModuleDiscoveryServiceException::class); + $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); + $this->assertTrue(ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression("(a || true)")); + } + + + public static function EvaluateBooleanExpressionAutoselectProvider() + { + $sSimpleCallToModuleIsSelected = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\")"; + $sSimpleCallToModuleIsSelected2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\")"; + $sCallToModuleIsSelectedCombinedWithAndOperator = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; + $sCallToModuleIsSelectedCombinedWithAndOperator2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; + + return [ + "simple call to SetupInfo::ModuleIsSelected SELECTED" => [ + "expr" => $sSimpleCallToModuleIsSelected, + "expected" => true, + ], + "simple call to SetupInfo::ModuleIsSelected NOT SELECTED" => [ + "expr" => $sSimpleCallToModuleIsSelected2, + "expected" => false, + ], + "call to SetupInfo::ModuleIsSelected + OR => SELECTED" => [ + "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator, + "expected" => true, + ], + "simple call to SetupInfo::ModuleIsSelected + OR => NOT SELECTED" => [ + "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator2, + "expected" => false, + ], + ]; + } + + + /** + * @dataProvider EvaluateBooleanExpressionAutoselectProvider + */ + public function testEvaluateBooleanExpression_Autoselect(string $sBooleanExpression, bool $expected){ + \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); + $this->assertEquals($expected, ModuleDiscoveryEvaluationService::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); + } + + public function testEvaluateConstantExpression() + { + $sPHP = <<ParsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleDiscoveryEvaluationService::class, "EvaluateConstantExpression", ModuleDiscoveryEvaluationService::GetInstance(), [$oExpr->expr]); + $this->assertEquals(APPROOT, $val); + } + + public static function EvaluateExpressionBooleanProvider() { + $sTruePHP = << [ + "code" => str_replace("COND", "true", $sTruePHP), + "bool_expected" => true, + + ], + "false" => [ + "code" => str_replace("COND", "false", $sTruePHP), + "bool_expected" => false, + + ], + "not ok" => [ + "code" => str_replace("COND", "! false", $sTruePHP), + "bool_expected" => true, + + ], + "not ko" => [ + "code" => str_replace("COND", "! (true)", $sTruePHP), + "bool_expected" => false, + + ], + "AND ko" => [ + "code" => str_replace("COND", "true && false", $sTruePHP), + "bool_expected" => false, + + ], + "AND ok1" => [ + "code" => str_replace("COND", "true && true", $sTruePHP), + "bool_expected" => true, + + ], + "AND ko2" => [ + "code" => str_replace("COND", "true && true && false", $sTruePHP), + "bool_expected" => false, + + ], + "OR ko" => [ + "code" => str_replace("COND", "false || false", $sTruePHP), + "bool_expected" => false, + + ], + "OR ok" => [ + "code" => str_replace("COND", "false ||true", $sTruePHP), + "bool_expected" => true, + + ], + "OR ok2" => [ + "code" => str_replace("COND", "false ||false||true", $sTruePHP), + "bool_expected" => true, + + ], + "function_exists('ldap_connect')" => [ + "code" => str_replace("COND", "function_exists('ldap_connect')", $sTruePHP), + "bool_expected" => function_exists('ldap_connect'), + + ], + "function_exists('gabuzomeushouldnotexist')" => [ + "code" => str_replace("COND", "function_exists('gabuzomeushouldnotexist')", $sTruePHP), + "bool_expected" => function_exists('gabuzomeushouldnotexist'), + + ], + "1 > 2" => [ + "code" => str_replace("COND", "1 > 2", $sTruePHP), + "bool_expected" => false, + + ], + "1 == 1" => [ + "code" => str_replace("COND", "1 == 1", $sTruePHP), + "bool_expected" => true, + + ], + "1 < 2" => [ + "code" => str_replace("COND", "1 < 2", $sTruePHP), + "bool_expected" => true, + ], + "PHP_VERSION_ID == PHP_VERSION_ID" => [ + "code" => str_replace("COND", "PHP_VERSION_ID == PHP_VERSION_ID", $sTruePHP), + "bool_expected" => true, + ], + "PHP_VERSION_ID != PHP_VERSION_ID" => [ + "code" => str_replace("COND", "PHP_VERSION_ID != PHP_VERSION_ID", $sTruePHP), + "bool_expected" => false, + ], + ]; + } + + /** + * @dataProvider EvaluateExpressionBooleanProvider + */ + public function testEvaluateExpression($sPHP, $bExpected) + { + $aNodes = ModuleDiscoveryEvaluationService::GetInstance()->ParsePhpCode($sPHP); + /** @var \PhpParser\Node\Expr $oExpr */ + $oExpr = $aNodes[0]; + $val = $this->InvokeNonPublicMethod(ModuleDiscoveryEvaluationService::class, "EvaluateExpression", ModuleDiscoveryEvaluationService::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/ModuleDiscoveryServiceTest.php index e68b3cbf86..e4105e1712 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleDiscoveryServiceTest.php @@ -72,82 +72,10 @@ public function testReadModuleFileConfigurationParsingIssue() } - public static function ComputeBooleanExpressionProvider() - { - return [ - "true" => [ "expr" => "true", "expected" => true], - "(true)" => [ "expr" => "(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], - ]; - } - /** - * @dataProvider ComputeBooleanExpressionProvider + * local tool function */ - public function testComputeBooleanExpression(string $sBooleanExpression, bool $expected){ - $this->assertEquals($expected, ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpression), $sBooleanExpression); - } - - public function testComputeBooleanExpression_BrokenBooleanExpression(){ - $this->expectException(\ModuleDiscoveryServiceException::class); - $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); - $this->assertTrue(ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression("(a || true)")); - } - - - public static function ComputeBooleanExpressionAutoselectProvider() - { - $sSimpleCallToModuleIsSelected = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\")"; - $sSimpleCallToModuleIsSelected2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\")"; - $sCallToModuleIsSelectedCombinedWithAndOperator = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; - $sCallToModuleIsSelectedCombinedWithAndOperator2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; - - return [ - "simple call to SetupInfo::ModuleIsSelected SELECTED" => [ - "expr" => $sSimpleCallToModuleIsSelected, - "expected" => true, - ], - "simple call to SetupInfo::ModuleIsSelected NOT SELECTED" => [ - "expr" => $sSimpleCallToModuleIsSelected2, - "expected" => false, - ], - "call to SetupInfo::ModuleIsSelected + OR => SELECTED" => [ - "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator, - "expected" => true, - ], - "simple call to SetupInfo::ModuleIsSelected + OR => NOT SELECTED" => [ - "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator2, - "expected" => false, - ], - ]; - } - - - /** - * @dataProvider ComputeBooleanExpressionAutoselectProvider - */ - public function testComputeBooleanExpressionAutoselect(string $sBooleanExpression, bool $expected){ - \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); - $this->assertEquals($expected, ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpression), $sBooleanExpression); - } - - public function testEvaluateConstantExpression() - { - $sPHP = <<parsePhpCode($sPHP); - /** @var \PhpParser\Node\Expr $oExpr */ - $oExpr = $aNodes[0]; - $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateConstantExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->expr]); - $this->assertEquals(APPROOT, $val); - } - - public function CallReadModuleFileConfiguration($sPHpCode) + private function CallReadModuleFileConfiguration($sPHpCode) { $this->sTempModuleFilePath = tempnam(__DIR__, "test"); file_put_contents($this->sTempModuleFilePath, $sPHpCode); @@ -274,112 +202,6 @@ public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApp $this->assertEquals([$this->sTempModuleFilePath, "elseif2", ["c" => "d", 'module_file_path' => $this->sTempModuleFilePath]], $val); } - public static function EvaluateExpressionBooleanProvider() { - $sTruePHP = << [ - "code" => str_replace("COND", "true", $sTruePHP), - "bool_expected" => true, - - ], - "false" => [ - "code" => str_replace("COND", "false", $sTruePHP), - "bool_expected" => false, - - ], - "not ok" => [ - "code" => str_replace("COND", "! false", $sTruePHP), - "bool_expected" => true, - - ], - "not ko" => [ - "code" => str_replace("COND", "! (true)", $sTruePHP), - "bool_expected" => false, - - ], - "AND ko" => [ - "code" => str_replace("COND", "true && false", $sTruePHP), - "bool_expected" => false, - - ], - "AND ok1" => [ - "code" => str_replace("COND", "true && true", $sTruePHP), - "bool_expected" => true, - - ], - "AND ko2" => [ - "code" => str_replace("COND", "true && true && false", $sTruePHP), - "bool_expected" => false, - - ], - "OR ko" => [ - "code" => str_replace("COND", "false || false", $sTruePHP), - "bool_expected" => false, - - ], - "OR ok" => [ - "code" => str_replace("COND", "false ||true", $sTruePHP), - "bool_expected" => true, - - ], - "OR ok2" => [ - "code" => str_replace("COND", "false ||false||true", $sTruePHP), - "bool_expected" => true, - - ], - "function_exists('ldap_connect')" => [ - "code" => str_replace("COND", "function_exists('ldap_connect')", $sTruePHP), - "bool_expected" => function_exists('ldap_connect'), - - ], - "function_exists('gabuzomeushouldnotexist')" => [ - "code" => str_replace("COND", "function_exists('gabuzomeushouldnotexist')", $sTruePHP), - "bool_expected" => function_exists('gabuzomeushouldnotexist'), - - ], - "1 > 2" => [ - "code" => str_replace("COND", "1 > 2", $sTruePHP), - "bool_expected" => false, - - ], - "1 == 1" => [ - "code" => str_replace("COND", "1 == 1", $sTruePHP), - "bool_expected" => true, - - ], - "1 < 2" => [ - "code" => str_replace("COND", "1 < 2", $sTruePHP), - "bool_expected" => true, - ], - "PHP_VERSION_ID == PHP_VERSION_ID" => [ - "code" => str_replace("COND", "PHP_VERSION_ID == PHP_VERSION_ID", $sTruePHP), - "bool_expected" => true, - ], - "PHP_VERSION_ID != PHP_VERSION_ID" => [ - "code" => str_replace("COND", "PHP_VERSION_ID != PHP_VERSION_ID", $sTruePHP), - "bool_expected" => false, - ], - ]; - } - - /** - * @dataProvider EvaluateExpressionBooleanProvider - */ - public function testEvaluateExpressionBoolean($sPHP, $bExpected) - { - $aNodes = ModuleDiscoveryService::GetInstance()->parsePhpCode($sPHP); - /** @var \PhpParser\Node\Expr $oExpr */ - $oExpr = $aNodes[0]; - $val = $this->InvokeNonPublicMethod(ModuleDiscoveryService::class, "EvaluateBooleanExpression", ModuleDiscoveryService::GetInstance(), [$oExpr->cond]); - $this->assertEquals($bExpected, $val); - } - public function testCallDeclaredInstaller() { $sModuleInstallerClass = "TicketsInstaller" . uniqid(); From 08c77f8106eff63dc36c95d16d0f9edc84ff8c01 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 27 Aug 2025 21:45:56 +0200 Subject: [PATCH 10/23] =?UTF-8?q?N=C2=B04789=20-=20PR=20review=20changes?= =?UTF-8?q?=20with=20Romain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/config.class.inc.php | 2 +- setup/extensionsmap.class.inc.php | 4 +- setup/modulediscovery.class.inc.php | 12 +- .../ModuleDiscoveryService.php | 227 ------------------ ...uationService.php => ModuleFileParser.php} | 72 +++--- setup/modulediscovery/ModuleFileReader.php | 170 +++++++++++++ ...tion.php => ModuleFileReaderException.php} | 4 +- setup/runtimeenv.class.inc.php | 43 +++- .../InstallationFileService.php | 4 +- setup/wizardsteps.class.inc.php | 8 +- ...rviceTest.php => ModuleFileParserTest.php} | 25 +- ...rviceTest.php => ModuleFileReaderTest.php} | 32 +-- 12 files changed, 291 insertions(+), 312 deletions(-) delete mode 100644 setup/modulediscovery/ModuleDiscoveryService.php rename setup/modulediscovery/{ModuleDiscoveryEvaluationService.php => ModuleFileParser.php} (69%) create mode 100644 setup/modulediscovery/ModuleFileReader.php rename setup/modulediscovery/{ModuleDiscoveryServiceException.php => ModuleFileReaderException.php} (83%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/{ModuleDiscoveryEvaluationServiceTest.php => ModuleFileParserTest.php} (80%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/{ModuleDiscoveryServiceTest.php => ModuleFileReaderTest.php} (81%) diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 1aafd90d08..0025c9addd 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -2857,7 +2857,7 @@ public function UpdateIncludes($sModulesDir, $aSelectedModules = null) } } - 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 afc5ba6a3b..84ef710d95 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -304,8 +304,8 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) { // 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 899099bf02..234b534613 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 @@ protected static function SetModulePath($sModulePath) 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 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a { $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 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) 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 e2668e0d93..0000000000 --- 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 1d5cb357db..41ef5d250c 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 @@ final public static function GetInstance(): 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 @@ public function ParsePhpCode(string $sPhpContent): ?array * @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 @@ public function BrowseAddModuleCallAndReturnModuleConfiguration(string $sModuleF $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 @@ public function BrowseAddModuleCallAndReturnModuleConfiguration(string $sModuleF $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 @@ public function BrowseAddModuleCallAndReturnModuleConfiguration(string $sModuleF ]; } - 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 @@ public function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array & 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 @@ public function BrowseArrayStructure(PhpParser\Node\Expr\Array_ $oArray, array & * @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 @@ public function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt\ /** @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 @@ public function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt\ } 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 @@ public function BrowseIfStructure(string $sModuleFilePath, \PhpParser\Node\Stmt\ 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 @@ public function BrowseStatementsAndReturnModuleConfiguration(string $sModuleFile 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 @@ private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr * @param string $sBooleanExpr * * @return bool - * @throws ModuleDiscoveryServiceException + * @throws ModuleFileReaderException */ private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : bool { @@ -228,7 +229,7 @@ private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : boo 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 @@ private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : boo * @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 @@ public function EvaluateBooleanExpression(string $sBooleanExpr, $bSafe=true) : b $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 @@ private function EvaluateExpression(\PhpParser\Node\Expr $oCondExpression) : boo 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 @@ private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : b * @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 @@ private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oSt $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 @@ private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oSt $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 0000000000..b6c23be149 --- /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 99ab3bb479..00f074819a 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 @@ public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sH { 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 fdb6788e7f..45c2ffaee5 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -270,14 +270,14 @@ public function ProcessAutoSelectModules() : void { { 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 f1209d0cec..0defe72477 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1787,9 +1787,9 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP // 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 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP 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 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP $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 18317fe66e..550d34232a 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 @@ public static function EvaluateBooleanExpressionProvider() * @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 @@ public static function EvaluateBooleanExpressionAutoselectProvider() */ 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 @@ public function testEvaluateConstantExpression() 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 @@ public static function EvaluateExpressionBooleanProvider() { */ 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 e4105e1712..d2af4c8684 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 @@ public function testReadModuleFileConfigurationLegacy() /*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 @@ public function testReadModuleFileConfigurationLegacy() 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 @@ public function testReadModuleFileConfiguration() 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 @@ public function testReadModuleFileConfigurationParsingIssue() { $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 @@ private function CallReadModuleFileConfiguration($sPHpCode) $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 @@ public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApp $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 @@ public function testCallDeclaredInstaller() 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); From a587bd68eb37c31f89f24b19b7b2ab75975691ad Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 1 Sep 2025 21:19:24 +0200 Subject: [PATCH 11/23] PR review + code cleanup + added usecases and test cover --- setup/extensionsmap.class.inc.php | 2 +- setup/modulediscovery.class.inc.php | 2 +- setup/modulediscovery/ModuleFileParser.php | 165 +++++++++++------- setup/modulediscovery/ModuleFileReader.php | 98 +++++------ .../modulediscovery/ModuleFileParserTest.php | 81 ++++++++- .../modulediscovery/ModuleFileReaderTest.php | 77 +++++--- .../module.combodo-email-synchro.php | 123 +++++++++++++ .../resources/module.combodo-make-it-vip.php | 57 ++++++ ...files-bridge-for-combodo-email-synchro.php | 51 ++++++ .../module.itop-admin-delegation-profiles.php | 52 ++++++ .../module.itop-global-requests-mgmt.php | 114 ++++++++++++ 11 files changed, 683 insertions(+), 139 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-email-synchro.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-make-it-vip.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-global-requests-mgmt.php diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 84ef710d95..851a63bdd6 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -304,7 +304,7 @@ protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) { // 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 234b534613..8d2aaf1e13 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -503,7 +503,7 @@ protected static function ListModuleFiles($sRelDir, $sRootDir) 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 41ef5d250c..7432a621bf 100644 --- a/setup/modulediscovery/ModuleFileParser.php +++ b/setup/modulediscovery/ModuleFileParser.php @@ -72,9 +72,7 @@ public function GetModuleInformationFromAddModuleCall(string $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 */ - $sModuleIdStringObj = $oModuleId->value; - $sModuleId = $sModuleIdStringObj->value; + $sModuleId = $oModuleId->value->value; $oModuleConfigInfo = $aArgs[2]; if (false === ($oModuleConfigInfo instanceof PhpParser\Node\Arg)) { @@ -92,6 +90,7 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ 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 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ 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; + continue; } - if ($oValue instanceof PhpParser\Node\Scalar\String_||$oValue instanceof PhpParser\Node\Scalar\Int_) { - $aModuleInformation[$sKey]=$oValue->value; + 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; } - if ($oValue instanceof \PhpParser\Node\Expr\ConstFetch) { - $oEvaluatedConstant = $this->EvaluateConstantExpression($oValue); - $aModuleInformation[$sKey]= $oEvaluatedConstant; - } + $aModuleInformation[$sKey]=$oEvaluatuedValue; } } @@ -155,6 +157,7 @@ public function GetModuleInformationFromIf(string $sModuleFilePath, \PhpParser\N } } } + return null; } @@ -163,19 +166,13 @@ public function GetModuleInformationFromIf(string $sModuleFilePath, \PhpParser\N /** @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 @@ public function GetModuleConfigurationFromStatement(string $sModuleFilePath, arr 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 $this->UnprotectedComputeBooleanExpression($oValue->name); + } + + public function EvaluateClassConstantExpression(\PhpParser\Node\Expr\ClassConstFetch $oValue) : mixed + { + $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 $bResult; + if ('class' === $sProperty){ + return $sClassName; + } + + return null; } - private function GetMixedValueForBooleanOperatorEvaluation(\PhpParser\Node\Expr $oExpr) : string + public function EvaluateStaticPropertyExpression(\PhpParser\Node\Expr\StaticPropertyFetch $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->getStaticProperties())) { + $oReflectionProperty = $class->getProperty($sProperty); + if ($oReflectionProperty->isPublic()){ + return $class->getStaticPropertyValue($sProperty); + } + } } - return $this->EvaluateExpression($oExpr) ? "true" : "false"; + 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); + return $oExpr; + } + + 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 ($oCondExpression instanceof \PhpParser\Node\Expr\StaticCall){ - return $this->EvaluateStaticCallFunction($oCondExpression); + if ($oExpression instanceof \PhpParser\Node\Expr\StaticCall){ + return $this->EvaluateStaticCallFunction($oExpression); } - if ($oCondExpression instanceof \PhpParser\Node\Expr\ConstFetch){ - return $this->EvaluateConstantExpression($oCondExpression); + if ($oExpression instanceof \PhpParser\Node\Expr\ConstFetch){ + return $this->EvaluateConstantExpression($oExpression); } - return true; + 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) : bool + private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : mixed { $sFunction = $oFunct->name->name; $aWhiteList = ["function_exists", "class_exists", "method_exists"]; @@ -306,17 +343,17 @@ private function EvaluateCallFunction(\PhpParser\Node\Expr\FuncCall $oFunct) : b } $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 @@ private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oSt 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 b6c23be149..e2229f6805 100644 --- a/setup/modulediscovery/ModuleFileReader.php +++ b/setup/modulediscovery/ModuleFileReader.php @@ -27,12 +27,58 @@ final public static function SetInstance(?ModuleFileReader $oInstance): void { /** * 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 @@ public function ReadModuleFileConfigurationUnsafe(string $sModuleFilePath) : arr 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 @@ public function GetAndCheckModuleInstallerClass($aModuleInfo) : ?string $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 550d34232a..87f15d607f 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 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 @@ public static function EvaluateBooleanExpressionProvider() 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 @@ public function testEvaluateConstantExpression() $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 @@ public static function EvaluateExpressionBooleanProvider() { "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 d2af4c8684..aeeffd7a06 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 @@ protected function setUp(): void $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 @@ public function testReadModuleFileConfigurationLegacy() /*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 @@ public function testReadModuleFileConfigurationParsingIssue() $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 @@ private function CallReadModuleFileConfiguration($sPHpCode) $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 @@ public function testGetAndCheckModuleInstallerClass() 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 0000000000..c4d0675a88 --- /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 0000000000..3ee370f0ee --- /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 0000000000..e94a8d770e --- /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 0000000000..fa29d1ff82 --- /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 0000000000..64cf608f1b --- /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); + } + } +} + + From f7b5091b39224c4d4db5ec5540bad4cac027aa61 Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 2 Sep 2025 12:23:46 +0200 Subject: [PATCH 12/23] temp evaluation work --- setup/modulediscovery/ModuleFileParser.php | 226 +----------------- .../evaluation/expression/ArrayEvaluator.php | 47 ++++ .../expression/BinaryOpEvaluator.php | 20 ++ .../expression/BitwiseAndEvaluator.php | 16 ++ .../expression/BitwiseOrEvaluator.php | 16 ++ .../expression/BooleanAndEvaluator.php | 16 ++ .../expression/BooleanNotEvaluator.php | 18 ++ .../expression/BooleanOrEvaluator.php | 16 ++ .../expression/ClassConstFetchEvaluator.php | 35 +++ .../expression/ConstFetchEvaluator.php | 21 ++ .../expression/FuncCallEvaluator.php | 33 +++ .../evaluation/expression/MulEvaluator.php | 16 ++ .../expression/PhpExpressionEvaluator.php | 79 ++++++ .../expression/StaticCallEvaluator.php | 41 ++++ .../StaticPropertyFetchEvaluator.php | 31 +++ .../evaluation/expression/iExprEvaluator.php | 11 + .../modulediscovery/ModuleFileReaderTest.php | 4 - .../PhpExpressionEvaluatorTest.php | 13 + 18 files changed, 438 insertions(+), 221 deletions(-) create mode 100644 setup/modulediscovery/evaluation/expression/ArrayEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/BitwiseAndEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/BitwiseOrEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/BooleanAndEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/BooleanNotEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/BooleanOrEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/ClassConstFetchEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/ConstFetchEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/FuncCallEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/MulEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/PhpExpressionEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/StaticCallEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/StaticPropertyFetchEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/iExprEvaluator.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php diff --git a/setup/modulediscovery/ModuleFileParser.php b/setup/modulediscovery/ModuleFileParser.php index 7432a621bf..65bf732381 100644 --- a/setup/modulediscovery/ModuleFileParser.php +++ b/setup/modulediscovery/ModuleFileParser.php @@ -1,8 +1,11 @@ value), 0, null, $sModuleFilePath); } - $sModuleId = $oModuleId->value->value; + $sModuleId = $this->EvaluateExpression($oModuleId->value); $oModuleConfigInfo = $aArgs[2]; if (false === ($oModuleConfigInfo instanceof PhpParser\Node\Arg)) { @@ -84,8 +87,7 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); } - $aModuleConfig=[]; - $this->FillModuleInformationFromArray($oModuleConfigInfo->value, $aModuleConfig); + $aModuleConfig = $this->EvaluateExpression($oModuleConfigInfo->value); if (! is_array($aModuleConfig)){ throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); @@ -98,46 +100,6 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ ]; } - 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 { - //array - $sKey = $iIndex++; - } - - $oValue = $oArrayItem->value; - if ($oValue instanceof PhpParser\Node\Expr\Array_) { - $aSubConfig=[]; - $this->FillModuleInformationFromArray($oValue, $aSubConfig); - $aModuleInformation[$sKey]=$aSubConfig; - continue; - } - - 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; - } - } - /** * @param string $sModuleFilePath * @param \PhpParser\Node\Stmt\If_ $oNode @@ -192,66 +154,6 @@ public function GetModuleConfigurationFromStatement(string $sModuleFilePath, arr return null; } - public function EvaluateConstantExpression(\PhpParser\Node\Expr\ArrayItem|\PhpParser\Node\Expr\ConstFetch $oValue) : mixed - { - return $this->UnprotectedComputeBooleanExpression($oValue->name); - } - - public function EvaluateClassConstantExpression(\PhpParser\Node\Expr\ClassConstFetch $oValue) : mixed - { - $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); - } - } - } - - 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 mixed - * @throws ModuleFileReaderException - */ - private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : mixed - { - try{ - $bResult = null; - @eval('$bResult = '.$sBooleanExpr.';'); - return $bResult; - } catch (Throwable $t) { - throw new ModuleFileReaderException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage()); - } - } - /** * @param string $sBooleanExpr * @@ -260,121 +162,11 @@ private function UnprotectedComputeBooleanExpression(string $sBooleanExpr) : mix */ public function EvaluateBooleanExpression(string $sBooleanExpr) : bool { - $sPhpContent = <<ParsePhpCode($sPhpContent); - $oExpr = $aNodes[0]; - $oRes = $this->EvaluateExpression($oExpr->expr); - - return (bool) $oRes; - - } catch (Throwable $t) { - throw new ModuleFileReaderException("Eval of '$sBooleanExpr' caused an error:".$t->getMessage()); - } - } - - private function GetMixedValueToString(mixed $oExpr) : string - { - if (false === $oExpr){ - return "false"; - } - - if (true === $oExpr){ - return "true"; - } - - return $oExpr; - } - - 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"]; - if (! in_array($sFunction, $aWhiteList)){ - throw new ModuleFileReaderException("FuncCall $sFunction not supported"); - } - - $aArgs=[]; - foreach ($oFunct->args as $arg){ - /** @var \PhpParser\Node\Arg $arg */ - $aArgs[]=$arg->value->value; - } - - $oReflectionFunction = new ReflectionFunction($sFunction); - return $oReflectionFunction->invoke(...$aArgs); + return PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpr); } - /** - * @param \PhpParser\Node\Expr\StaticCall $oStaticCall - * - * @return mixed - * @throws \ModuleFileReaderException - * @throws \ReflectionException - */ - private function EvaluateStaticCallFunction(\PhpParser\Node\Expr\StaticCall $oStaticCall) : mixed + private function EvaluateExpression(Expr $oExpression) : mixed { - $sClassName = $oStaticCall->class->name; - $sMethodName = $oStaticCall->name->name; - $aWhiteList = ["SetupInfo::ModuleIsSelected"]; - $sStaticCallDescription = "$sClassName::$sMethodName"; - if (! in_array($sStaticCallDescription, $aWhiteList)){ - throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not supported"); - } - - $aArgs=[]; - foreach ($oStaticCall->args as $arg){ - /** @var \PhpParser\Node\Arg $arg */ - $aArgs[]=$arg->value->value; - } - - $class = new \ReflectionClass($sClassName); - $method = $class->getMethod($sMethodName); - if (! $method->isPublic()){ - throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not public"); - } - - return $method->invokeArgs(null, $aArgs); + return PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpression); } } \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php b/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php new file mode 100644 index 0000000000..cd3b761102 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php @@ -0,0 +1,47 @@ +items as $oArrayItem){ + if ($oArrayItem->key instanceof String_||$oArrayItem->key instanceof ConstFetch) { + //dictionnary + $sKey = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oArrayItem->key); + if (is_null($sKey)){ + continue; + } + } else { + //array + $sKey = $iIndex++; + } + + try { + $oValue = $oArrayItem->value; + $oEvaluatuedValue = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oValue); + $aModuleInformation[$sKey]=$oEvaluatuedValue; + } catch(ModuleFileReaderException $e){ + //required to support legacy below dump dependency + //'dependencies' => ['itop-config-mgmt/2.0.0'||'itop-structure/3.0.0'] + continue; + } + } + + return $aModuleInformation; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php b/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php new file mode 100644 index 0000000000..95d9ad7b10 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php @@ -0,0 +1,20 @@ +EvaluateBinaryOperation( + PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->left), + PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->right)); + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/BitwiseAndEvaluator.php b/setup/modulediscovery/evaluation/expression/BitwiseAndEvaluator.php new file mode 100644 index 0000000000..68a7e4878e --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/BitwiseAndEvaluator.php @@ -0,0 +1,16 @@ +EvaluateExpression($oExpr->expr); + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/BooleanOrEvaluator.php b/setup/modulediscovery/evaluation/expression/BooleanOrEvaluator.php new file mode 100644 index 0000000000..38abe1b8e5 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/BooleanOrEvaluator.php @@ -0,0 +1,16 @@ +class->name; + $sProperty = $oExpr->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); + } + } + } + + if ('class' === $sProperty){ + return $sClassName; + } + + return null; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/ConstFetchEvaluator.php b/setup/modulediscovery/evaluation/expression/ConstFetchEvaluator.php new file mode 100644 index 0000000000..402778c06e --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/ConstFetchEvaluator.php @@ -0,0 +1,21 @@ +name)){ + return constant($oExpr->name); + } + + return null; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/FuncCallEvaluator.php b/setup/modulediscovery/evaluation/expression/FuncCallEvaluator.php new file mode 100644 index 0000000000..b50cee75a9 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/FuncCallEvaluator.php @@ -0,0 +1,33 @@ +name->name; + $aWhiteList = ["function_exists", "class_exists", "method_exists"]; + if (! in_array($sFunction, $aWhiteList)){ + throw new ModuleFileReaderException("FuncCall $sFunction not supported"); + } + + $aArgs=[]; + foreach ($oExpr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $oReflectionFunction = new ReflectionFunction($sFunction); + return $oReflectionFunction->invoke(...$aArgs); + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/MulEvaluator.php b/setup/modulediscovery/evaluation/expression/MulEvaluator.php new file mode 100644 index 0000000000..001f020014 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/MulEvaluator.php @@ -0,0 +1,16 @@ +isInstantiable() + && $oReflectionClass->implementsInterface("{$sNamespace}iExprEvaluator")){ + $oClass = new $sClass; + static::$aPhpParserEvaluators[$oClass->GetHandledExpressionType()] = $oClass; + } + } + } + + return static::$oInstance; + } + + final public static function SetInstance(?PhpExpressionEvaluator $oInstance): void { + static::$oInstance = $oInstance; + } + + public function EvaluateExpression(Expr $oExpression) : mixed + { + $sClass = get_class($oExpression); + $oPhpParserEvaluator = static::$aPhpParserEvaluators[$sClass] ?? null; + if (is_null($oPhpParserEvaluator)){ + return $oExpression->value; + } + + return $oPhpParserEvaluator->Evaluate($oExpression); + } + + /** + * @param string $sBooleanExpr + * + * @return bool + * @throws \ModuleFileReaderException + */ + public function ParseAndEvaluateBooleanExpression(string $sBooleanExpr) : bool + { + return $this->ParseAndEvaluateExpression($sBooleanExpr); + } + + public function ParseAndEvaluateExpression(string $sExpr) : bool + { + $sPhpContent = <<ParsePhpCode($sPhpContent); + $oExpr = $aNodes[0]; + return $this->EvaluateExpression($oExpr->expr); + + } catch (\Throwable $t) { + throw new \ModuleFileReaderException("Eval of '$sExpr' caused an error:".$t->getMessage()); + } + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/StaticCallEvaluator.php b/setup/modulediscovery/evaluation/expression/StaticCallEvaluator.php new file mode 100644 index 0000000000..27985db392 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/StaticCallEvaluator.php @@ -0,0 +1,41 @@ +class->name; + $sMethodName = $oExpr->name->name; + + $aWhiteList = ["SetupInfo::ModuleIsSelected"]; + $sStaticCallDescription = "$sClassName::$sMethodName"; + if (! in_array($sStaticCallDescription, $aWhiteList)){ + throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not supported"); + } + + $aArgs=[]; + foreach ($oExpr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } + + $class = new \ReflectionClass($sClassName); + $method = $class->getMethod($sMethodName); + if (! $method->isPublic()){ + throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not public"); + } + + return $method->invokeArgs(null, $aArgs); + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/StaticPropertyFetchEvaluator.php b/setup/modulediscovery/evaluation/expression/StaticPropertyFetchEvaluator.php new file mode 100644 index 0000000000..076854953b --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/StaticPropertyFetchEvaluator.php @@ -0,0 +1,31 @@ +class->name; + $sProperty = $oExpr->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; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/iExprEvaluator.php b/setup/modulediscovery/evaluation/expression/iExprEvaluator.php new file mode 100644 index 0000000000..27932e26cc --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/iExprEvaluator.php @@ -0,0 +1,11 @@ +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); } diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php new file mode 100644 index 0000000000..a2eaa97146 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php @@ -0,0 +1,13 @@ +ParseAndEvaluateExpression('false'); + $this->assertEquals(false, $res); + } +} \ No newline at end of file From 1962cd7a881933023601d6bddca07104aeda960b Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 2 Sep 2025 17:03:02 +0200 Subject: [PATCH 13/23] replace eval by iTop custom evaluation classes --- setup/modulediscovery.class.inc.php | 2 +- setup/modulediscovery/ModuleFileParser.php | 24 +- .../expression/ArrayDimFetchEvaluator.php | 28 ++ .../evaluation/expression/ArrayEvaluator.php | 3 +- .../expression/BinaryOpEvaluator.php | 2 - .../evaluation/expression/ConcatEvaluator.php | 28 ++ .../evaluation/expression/EqualEvaluator.php | 16 + .../expression/GreaterEvaluator.php | 16 + .../expression/GreaterOrEqualEvaluator.php | 16 + .../expression/NotEqualEvaluator.php | 16 + .../expression/PhpExpressionEvaluator.php | 3 +- .../expression/SmallerEvaluator.php | 16 + .../expression/SmallerOrEqualEvaluator.php | 16 + .../expression/StaticCallEvaluator.php | 2 +- .../expression/UnaryMinusEvaluator.php | 19 ++ .../expression/VariableEvaluator.php | 32 ++ setup/runtimeenv.class.inc.php | 2 +- .../InstallationFileService.php | 2 +- setup/wizardsteps.class.inc.php | 4 +- .../modulediscovery/ModuleFileParserTest.php | 277 ------------------ .../modulediscovery/ModuleFileReaderTest.php | 18 +- .../PhpExpressionEvaluatorTest.php | 129 +++++++- 22 files changed, 357 insertions(+), 314 deletions(-) create mode 100644 setup/modulediscovery/evaluation/expression/ArrayDimFetchEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/ConcatEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/EqualEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/GreaterEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/GreaterOrEqualEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/NotEqualEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/SmallerEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/SmallerOrEqualEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/UnaryMinusEvaluator.php create mode 100644 setup/modulediscovery/evaluation/expression/VariableEvaluator.php delete mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 8d2aaf1e13..bf9af1973c 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -391,7 +391,7 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a { $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); try{ - $bResult = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpr); + $bResult = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpr); } catch(ModuleFileReaderException $e){ //logged already echo "Failed to parse the boolean Expression = '$sBooleanExpr'
    "; diff --git a/setup/modulediscovery/ModuleFileParser.php b/setup/modulediscovery/ModuleFileParser.php index 65bf732381..08f36496ce 100644 --- a/setup/modulediscovery/ModuleFileParser.php +++ b/setup/modulediscovery/ModuleFileParser.php @@ -75,7 +75,7 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ throw new ModuleFileReaderException("2nd parameter to SetupWebPage::AddModule not a string: " . get_class($oModuleId->value), 0, null, $sModuleFilePath); } - $sModuleId = $this->EvaluateExpression($oModuleId->value); + $sModuleId = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oModuleId->value); $oModuleConfigInfo = $aArgs[2]; if (false === ($oModuleConfigInfo instanceof PhpParser\Node\Arg)) { @@ -87,7 +87,7 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); } - $aModuleConfig = $this->EvaluateExpression($oModuleConfigInfo->value); + $aModuleConfig = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oModuleConfigInfo->value); if (! is_array($aModuleConfig)){ throw new ModuleFileReaderException("3rd parameter to SetupWebPage::AddModule not an array: " . get_class($oModuleConfigInfo->value), 0, null, $sModuleFilePath); @@ -109,7 +109,7 @@ public function GetModuleInformationFromAddModuleCall(string $sModuleFilePath, \ */ public function GetModuleInformationFromIf(string $sModuleFilePath, \PhpParser\Node\Stmt\If_ $oNode) : ?array { - $bCondition = $this->EvaluateExpression($oNode->cond); + $bCondition = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oNode->cond); if ($bCondition) { foreach ($oNode->stmts as $oSubNode) { if ($oSubNode instanceof \PhpParser\Node\Stmt\Expression) { @@ -126,7 +126,7 @@ public function GetModuleInformationFromIf(string $sModuleFilePath, \PhpParser\N if (! is_null($oNode->elseifs)) { foreach ($oNode->elseifs as $oElseIfSubNode) { /** @var \PhpParser\Node\Stmt\ElseIf_ $oElseIfSubNode */ - $bCondition = $this->EvaluateExpression($oElseIfSubNode->cond); + $bCondition = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oElseIfSubNode->cond); if ($bCondition) { return $this->GetModuleConfigurationFromStatement($sModuleFilePath, $oElseIfSubNode->stmts); } @@ -153,20 +153,4 @@ public function GetModuleConfigurationFromStatement(string $sModuleFilePath, arr return null; } - - /** - * @param string $sBooleanExpr - * - * @return bool - * @throws ModuleFileReaderException - */ - public function EvaluateBooleanExpression(string $sBooleanExpr) : bool - { - return PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpr); - } - - private function EvaluateExpression(Expr $oExpression) : mixed - { - return PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpression); - } } \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/ArrayDimFetchEvaluator.php b/setup/modulediscovery/evaluation/expression/ArrayDimFetchEvaluator.php new file mode 100644 index 0000000000..b6787696f4 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/ArrayDimFetchEvaluator.php @@ -0,0 +1,28 @@ +EvaluateExpression($oExpr->var); + if (is_null($var)){ + return null; + } + + $dim = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->dim); + if (is_null($var)){ + return $dim; + } + + return $var[$dim] ?? null; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php b/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php index cd3b761102..a65ca11a43 100644 --- a/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php +++ b/setup/modulediscovery/evaluation/expression/ArrayEvaluator.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Scalar\String_; class ArrayEvaluator implements iExprEvaluator { @@ -20,7 +21,7 @@ public function Evaluate(Expr $oExpr): mixed { $aModuleInformation=[]; /** @var \PhpParser\Node\Expr\ArrayItem $oValue */ foreach ($oExpr->items as $oArrayItem){ - if ($oArrayItem->key instanceof String_||$oArrayItem->key instanceof ConstFetch) { + if ($oArrayItem->key instanceof Int_||$oArrayItem->key instanceof String_||$oArrayItem->key instanceof ConstFetch) { //dictionnary $sKey = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oArrayItem->key); if (is_null($sKey)){ diff --git a/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php b/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php index 95d9ad7b10..cb7ce4b27a 100644 --- a/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php +++ b/setup/modulediscovery/evaluation/expression/BinaryOpEvaluator.php @@ -2,10 +2,8 @@ namespace evaluation\expression; -use ModuleFileReaderException; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp; -use Throwable; abstract class BinaryOpEvaluator implements iExprEvaluator { abstract function EvaluateBinaryOperation(mixed $left, mixed $right) : mixed; diff --git a/setup/modulediscovery/evaluation/expression/ConcatEvaluator.php b/setup/modulediscovery/evaluation/expression/ConcatEvaluator.php new file mode 100644 index 0000000000..9008f4cfd8 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/ConcatEvaluator.php @@ -0,0 +1,28 @@ + $right; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/GreaterOrEqualEvaluator.php b/setup/modulediscovery/evaluation/expression/GreaterOrEqualEvaluator.php new file mode 100644 index 0000000000..b4e7b9e9ce --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/GreaterOrEqualEvaluator.php @@ -0,0 +1,16 @@ += $right; + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/NotEqualEvaluator.php b/setup/modulediscovery/evaluation/expression/NotEqualEvaluator.php new file mode 100644 index 0000000000..469c77fadd --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/NotEqualEvaluator.php @@ -0,0 +1,16 @@ +ParseAndEvaluateExpression($sBooleanExpr); } - public function ParseAndEvaluateExpression(string $sExpr) : bool + public function ParseAndEvaluateExpression(string $sExpr) : mixed { $sPhpContent = <<class->name; $sMethodName = $oExpr->name->name; - $aWhiteList = ["SetupInfo::ModuleIsSelected"]; + $aWhiteList = ["SetupInfo::ModuleIsSelected", "utils::GetItopVersionWikiSyntax"]; $sStaticCallDescription = "$sClassName::$sMethodName"; if (! in_array($sStaticCallDescription, $aWhiteList)){ throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not supported"); diff --git a/setup/modulediscovery/evaluation/expression/UnaryMinusEvaluator.php b/setup/modulediscovery/evaluation/expression/UnaryMinusEvaluator.php new file mode 100644 index 0000000000..8932263161 --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/UnaryMinusEvaluator.php @@ -0,0 +1,19 @@ +EvaluateExpression($oExpr->expr); + } +} \ No newline at end of file diff --git a/setup/modulediscovery/evaluation/expression/VariableEvaluator.php b/setup/modulediscovery/evaluation/expression/VariableEvaluator.php new file mode 100644 index 0000000000..e47f6f1fbf --- /dev/null +++ b/setup/modulediscovery/evaluation/expression/VariableEvaluator.php @@ -0,0 +1,32 @@ +name)){ + return null; + } + + if (! isset($oExpr->name)) { + return null; + } + + $sVarname=$oExpr->name; + + $bResult = null; + @eval('$bResult = $'.$sVarname.';'); + + return $bResult; + + } +} \ No newline at end of file diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 86f90c59a2..45f52dacd0 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -459,7 +459,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { SetupInfo::SetSelectedModules($aRet); try{ - $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($oModule->GetAutoSelect()); + $bSelected = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($oModule->GetAutoSelect()); if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index 45c2ffaee5..030b41f579 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -270,7 +270,7 @@ public function ProcessAutoSelectModules() : void { { try { SetupInfo::SetSelectedModules($this->aSelectedModules); - $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); + $bSelected =\evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { // Modules in data/production-modules/ are considered as mandatory and always installed diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 0defe72477..a64156851f 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1787,7 +1787,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP // Check the module selection try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($aInfo['auto_select']); + $bSelected = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aInfo['auto_select']); } catch (ModuleFileReaderException $e) { //logged already @@ -1865,7 +1865,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP try { SetupInfo::SetSelectedModules($aModules); - $bSelected = ModuleFileParser::GetInstance()->EvaluateBooleanExpression($aModule['auto_select']); + $bSelected = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { $aModules[$sModuleId] = true; // store the Id of the selected module diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php deleted file mode 100644 index 87f15d607f..0000000000 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileParserTest.php +++ /dev/null @@ -1,277 +0,0 @@ -RequireOnceItopFile('setup/modulediscovery/ModuleFileReader.php'); - } - - public static function EvaluateBooleanExpressionProvider() - { - 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, $expected){ - $this->assertEquals($expected, ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); - } - - public function testEvaluateBooleanExpression_BrokenBooleanExpression(){ - $this->expectException(\ModuleFileReaderException::class); - $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); - $this->assertTrue(ModuleFileParser::GetInstance()->EvaluateBooleanExpression("(a || true)")); - } - - - public static function EvaluateBooleanExpressionAutoselectProvider() - { - $sSimpleCallToModuleIsSelected = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\")"; - $sSimpleCallToModuleIsSelected2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\")"; - $sCallToModuleIsSelectedCombinedWithAndOperator = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; - $sCallToModuleIsSelectedCombinedWithAndOperator2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; - - return [ - "simple call to SetupInfo::ModuleIsSelected SELECTED" => [ - "expr" => $sSimpleCallToModuleIsSelected, - "expected" => true, - ], - "simple call to SetupInfo::ModuleIsSelected NOT SELECTED" => [ - "expr" => $sSimpleCallToModuleIsSelected2, - "expected" => false, - ], - "call to SetupInfo::ModuleIsSelected + OR => SELECTED" => [ - "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator, - "expected" => true, - ], - "simple call to SetupInfo::ModuleIsSelected + OR => NOT SELECTED" => [ - "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator2, - "expected" => false, - ], - ]; - } - - - /** - * @dataProvider EvaluateBooleanExpressionAutoselectProvider - */ - public function testEvaluateBooleanExpression_Autoselect(string $sBooleanExpression, bool $expected){ - \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); - $this->assertEquals($expected, ModuleFileParser::GetInstance()->EvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); - } - - public function testEvaluateConstantExpression() - { - $sPHP = <<ParsePhpCode($sPHP); - /** @var \PhpParser\Node\Expr $oExpr */ - $oExpr = $aNodes[0]; - $val = $this->InvokeNonPublicMethod(ModuleFileParser::class, "EvaluateConstantExpression", ModuleFileParser::GetInstance(), [$oExpr->expr]); - $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, - - ], - "false" => [ - "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), - "bool_expected" => true, - - ], - "not ko" => [ - "code" => str_replace("COND", "! (true)", $sTruePHP), - "bool_expected" => false, - - ], - "AND ko" => [ - "code" => str_replace("COND", "true && false", $sTruePHP), - "bool_expected" => false, - - ], - "AND ok1" => [ - "code" => str_replace("COND", "true && true", $sTruePHP), - "bool_expected" => true, - - ], - "AND ko2" => [ - "code" => str_replace("COND", "true && true && false", $sTruePHP), - "bool_expected" => false, - - ], - "OR ko" => [ - "code" => str_replace("COND", "false || false", $sTruePHP), - "bool_expected" => false, - - ], - "OR ok" => [ - "code" => str_replace("COND", "false ||true", $sTruePHP), - "bool_expected" => true, - - ], - "OR ok2" => [ - "code" => str_replace("COND", "false ||false||true", $sTruePHP), - "bool_expected" => true, - - ], - "function_exists('ldap_connect')" => [ - "code" => str_replace("COND", "function_exists('ldap_connect')", $sTruePHP), - "bool_expected" => function_exists('ldap_connect'), - - ], - "function_exists('gabuzomeushouldnotexist')" => [ - "code" => str_replace("COND", "function_exists('gabuzomeushouldnotexist')", $sTruePHP), - "bool_expected" => function_exists('gabuzomeushouldnotexist'), - - ], - "1 > 2" => [ - "code" => str_replace("COND", "1 > 2", $sTruePHP), - "bool_expected" => false, - - ], - "1 == 1" => [ - "code" => str_replace("COND", "1 == 1", $sTruePHP), - "bool_expected" => true, - - ], - "1 < 2" => [ - "code" => str_replace("COND", "1 < 2", $sTruePHP), - "bool_expected" => true, - ], - "PHP_VERSION_ID == PHP_VERSION_ID" => [ - "code" => str_replace("COND", "PHP_VERSION_ID == PHP_VERSION_ID", $sTruePHP), - "bool_expected" => true, - ], - "PHP_VERSION_ID != PHP_VERSION_ID" => [ - "code" => str_replace("COND", "PHP_VERSION_ID != PHP_VERSION_ID", $sTruePHP), - "bool_expected" => false, - ], - ]; - } - - /** - * @dataProvider EvaluateExpressionBooleanProvider - */ - public function testEvaluateExpression($sPHP, $bExpected) - { - $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode($sPHP); - /** @var \PhpParser\Node\Expr $oExpr */ - $oExpr = $aNodes[0]; - $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/ModuleFileReaderTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php index 64cb84cd11..a427575238 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php @@ -30,16 +30,28 @@ public function testReadModuleFileInformationUnsafe() /*public function testAllReadModuleFileConfiguration() { + $_SERVER=[ + 'SERVER_NAME' => 'titi' + ]; + $aErrors=[]; - foreach (glob(__DIR__.'/resources/all_factory/module.*.php') as $sModuleFilePath){ - $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); + foreach (glob(__DIR__.'/resources/all_designer/**.php') as $sModuleFilePath){ + //var_dump($sModuleFilePath); + try{ + $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); + } catch(\Exception $e){ + $aErrors[]=basename($sModuleFilePath); + continue; + } + $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); if ($aExpected !== $aRes){ $aErrors[]=basename($sModuleFilePath); continue; } - //$this->assertEquals($aExpected, $aRes); + //break; + //$this->assertEquals($aExpected, $aRes, $sModuleFilePath); } $this->assertEquals([], $aErrors); diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php index a2eaa97146..569f7b3d8e 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/PhpExpressionEvaluatorTest.php @@ -5,9 +5,130 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use evaluation\expression\PhpExpressionEvaluator; -class PhpExpressionEvaluatorTest extends ItopDataTestCase{ - public function testEvaluateExpression(){ - $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression('false'); - $this->assertEquals(false, $res); +class PhpExpressionEvaluatorTest extends ItopDataTestCase { + public static $STATIC_PROPERTY = 123; + private static $PRIVATE_STATIC_PROPERTY = 123; + private const PRIVATE_CONSTANT = 123; + + public static function EvaluateExpressionProvider() { + return [ + 'ConstFetch: false' => [ 'sExpression' => 'false'], + 'ConstFetch: (false)' => [ 'sExpression' => 'false'], + 'ConstFetch: true' => [ 'sExpression' => 'true'], + //'ConstFetch: __FILE__' => [ 'sExpression' => __FILE__], + 'ConstFetch: (true)' => [ 'sExpression' => 'true'], + 'ClassConstFetch: public existing constant' => [ 'sExpression' => 'SetupUtils::PHP_MIN_VERSION'], + 'ClassConstFetch: unknown constant' => [ 'sExpression' => 'SetupUtils::UNKNOWN_CONSTANT'], + 'ClassConstFetch: unknown class:constant' => [ 'sExpression' => 'GabuZomeuUnknownClass::UNKNOWN_CONSTANT'], + 'ClassConstFetch: unknown class:class' => [ 'sExpression' => 'GabuZomeuUnknownClass::class'], + 'ClassConstFetch: private existing constant' => [ + 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::PRIVATE_CONSTANT', + 'forced_expected' => null + ], + 'StaticProperty: public existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$STATIC_PROPERTY'], + 'StaticProperty: private existing constant' => [ + 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$PRIVATE_STATIC_PROPERTY', + 'forced_expected' => null + ], + 'BinaryOperator: false|true' => [ 'sExpression' => 'false|true'], + 'BinaryOperator: false||true' => [ 'sExpression' => 'false||true'], + 'BinaryOperator: false&&true' => [ 'sExpression' => 'false&&true'], + 'BinaryOperator: true&&true&&true&&false' => [ 'sExpression' => 'true&&true&&true&&false'], + 'BinaryOperator: false&true' => [ 'sExpression' => 'false&true'], + 'BinaryOperator: ! true' => [ 'sExpression' => '! true'], + 'BinaryOperator: 10 * 5' => [ 'sExpression' => '10 * 5'], + 'BinaryOperator: 1 > 2' => [ 'sExpression' => '1 > 2'], + 'BinaryOperator: 1 >= 1' => [ 'sExpression' => '1 >= 1'], + 'BinaryOperator: 1 <= 1' => [ 'sExpression' => '1 <= 1'], + 'BinaryOperator: PHP_VERSION_ID == PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID == PHP_VERSION_ID'], + 'BinaryOperator: PHP_VERSION_ID != PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID != PHP_VERSION_ID'], + 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], + 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], + 'UnaryMinus: -1' => ['sExpression' => '-1'], + 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], + 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], + 'Variable: $_SERVER' => ['sExpression' => '$_SERVER'], + 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], + 'Array: ["a"]' => ['sExpression' => '["a"]'], + 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], + 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'] + ]; + } + + /** + * @dataProvider EvaluateExpressionProvider + */ + public function testEvaluateExpression($sExpression, $forced_expected="NOTPROVIDED") + { + $_SERVER=[ + 'toto' => 'titi' + ]; + + $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression); + if ($forced_expected === "NOTPROVIDED"){ + $this->assertEquals($this->UnprotectedComputeExpression($sExpression), $res, $sExpression); + } else { + $this->assertEquals($forced_expected, $res, $sExpression); + } + } + + /** + * @param string $sBooleanExpr + * + * @return mixed + * @throws \ModuleFileReaderException + */ + private function UnprotectedComputeExpression(string $sExpr) : mixed + { + try { + $bResult = null; + @eval('$bResult = '.$sExpr.';'); + + return $bResult; + } catch (\Throwable $t){ + return null; + } + } + + public function testParseAndEvaluateBooleanExpression_BrokenBooleanExpression(){ + $this->expectException(\ModuleFileReaderException::class); + $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); + $this->assertTrue(PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression("(a || true)")); + } + + public static function ParseAndEvaluateBooleanExpression_AutoselectProvider() + { + $sSimpleCallToModuleIsSelected = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\")"; + $sSimpleCallToModuleIsSelected2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\")"; + $sCallToModuleIsSelectedCombinedWithAndOperator = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; + $sCallToModuleIsSelectedCombinedWithAndOperator2 = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt-notselected\") || SetupInfo::ModuleIsSelected(\"itop-virtualization-mgmt\")"; + + return [ + "simple call to SetupInfo::ModuleIsSelected SELECTED" => [ + "expr" => $sSimpleCallToModuleIsSelected, + "expected" => true, + ], + "simple call to SetupInfo::ModuleIsSelected NOT SELECTED" => [ + "expr" => $sSimpleCallToModuleIsSelected2, + "expected" => false, + ], + "call to SetupInfo::ModuleIsSelected + OR => SELECTED" => [ + "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator, + "expected" => true, + ], + "simple call to SetupInfo::ModuleIsSelected + OR => NOT SELECTED" => [ + "expr" => $sCallToModuleIsSelectedCombinedWithAndOperator2, + "expected" => false, + ], + ]; + } + + + /** + * @dataProvider ParseAndEvaluateBooleanExpression_AutoselectProvider + */ + public function testEvaluateBooleanExpression_Autoselect(string $sBooleanExpression, bool $expected){ + \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); + $this->assertEquals($expected, PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); } } \ No newline at end of file From ac2b787e09388a444ea05608038e0ab53d0747e0 Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 2 Sep 2025 19:19:56 +0200 Subject: [PATCH 14/23] move PhpParser/Evaluation classes in a specific namespave + composer dumpautoload --- lib/autoload.php | 5 +++- lib/composer/autoload_classmap.php | 25 ++++++++++++++++ lib/composer/autoload_psr4.php | 2 +- lib/composer/autoload_static.php | 29 +++++++++++++++++-- lib/composer/platform_check.php | 5 ++-- setup/modulediscovery/ModuleFileParser.php | 5 ++-- setup/modulediscovery/ModuleFileReader.php | 3 -- .../Evaluation}/ArrayDimFetchEvaluator.php | 2 +- .../PhpParser/Evaluation}/ArrayEvaluator.php | 2 +- .../Evaluation}/BinaryOpEvaluator.php | 2 +- .../Evaluation}/BitwiseAndEvaluator.php | 2 +- .../Evaluation}/BitwiseOrEvaluator.php | 2 +- .../Evaluation}/BooleanAndEvaluator.php | 2 +- .../Evaluation}/BooleanNotEvaluator.php | 2 +- .../Evaluation}/BooleanOrEvaluator.php | 2 +- .../Evaluation}/ClassConstFetchEvaluator.php | 2 +- .../PhpParser/Evaluation}/ConcatEvaluator.php | 2 +- .../Evaluation}/ConstFetchEvaluator.php | 2 +- .../PhpParser/Evaluation}/EqualEvaluator.php | 2 +- .../Evaluation}/FuncCallEvaluator.php | 2 +- .../Evaluation}/GreaterEvaluator.php | 2 +- .../Evaluation}/GreaterOrEqualEvaluator.php | 2 +- .../PhpParser/Evaluation}/MulEvaluator.php | 2 +- .../Evaluation}/NotEqualEvaluator.php | 2 +- .../Evaluation}/PhpExpressionEvaluator.php | 8 ++--- .../Evaluation}/SmallerEvaluator.php | 2 +- .../Evaluation}/SmallerOrEqualEvaluator.php | 2 +- .../Evaluation}/StaticCallEvaluator.php | 2 +- .../StaticPropertyFetchEvaluator.php | 2 +- .../Evaluation}/UnaryMinusEvaluator.php | 2 +- .../Evaluation}/VariableEvaluator.php | 2 +- .../PhpParser/Evaluation}/iExprEvaluator.php | 2 +- .../PhpExpressionEvaluatorTest.php | 4 +-- 33 files changed, 92 insertions(+), 42 deletions(-) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/ArrayDimFetchEvaluator.php (92%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/ArrayEvaluator.php (96%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/BinaryOpEvaluator.php (91%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/BitwiseAndEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/BitwiseOrEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/BooleanAndEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/BooleanNotEvaluator.php (89%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/BooleanOrEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/ClassConstFetchEvaluator.php (94%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/ConcatEvaluator.php (90%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/ConstFetchEvaluator.php (89%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/EqualEvaluator.php (85%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/FuncCallEvaluator.php (94%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/GreaterEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/GreaterOrEqualEvaluator.php (87%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/MulEvaluator.php (85%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/NotEqualEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/PhpExpressionEvaluator.php (90%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/SmallerEvaluator.php (86%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/SmallerOrEqualEvaluator.php (87%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/StaticCallEvaluator.php (95%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/StaticPropertyFetchEvaluator.php (94%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/UnaryMinusEvaluator.php (89%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/VariableEvaluator.php (92%) rename {setup/modulediscovery/evaluation/expression => sources/PhpParser/Evaluation}/iExprEvaluator.php (78%) rename tests/php-unit-tests/unitary-tests/{setup/modulediscovery => sources/PhpParser/Evaluation}/PhpExpressionEvaluatorTest.php (97%) diff --git a/lib/autoload.php b/lib/autoload.php index 9ee03077e4..db10dc8675 100644 --- a/lib/autoload.php +++ b/lib/autoload.php @@ -14,7 +14,10 @@ echo $err; } } - throw new RuntimeException($err); + trigger_error( + $err, + E_USER_ERROR + ); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index eccb877e48..b35dca37ce 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -469,6 +469,31 @@ 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => $baseDir . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => $baseDir . '/sources/Form/Validator/SelectObjectValidator.php', 'Combodo\\iTop\\Kernel' => $baseDir . '/sources/Kernel.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayDimFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ArrayDimFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ArrayEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BinaryOpEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BinaryOpEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseAndEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BitwiseAndEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseOrEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanAndEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BooleanAndEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanNotEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BooleanNotEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanOrEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BooleanOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ClassConstFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ConcatEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ConcatEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ConstFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ConstFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\EqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/EqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\FuncCallEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/FuncCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/GreaterEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterOrEqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/GreaterOrEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\MulEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/MulEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\NotEqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/NotEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/SmallerEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerOrEqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/SmallerOrEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticCallEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/StaticCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticPropertyFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\UnaryMinusEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\VariableEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/VariableEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\iExprEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/iExprEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php', 'Combodo\\iTop\\Renderer\\Bootstrap\\BsFieldRendererMappings' => $baseDir . '/sources/Renderer/Bootstrap/BsFieldRendererMappings.php', 'Combodo\\iTop\\Renderer\\Bootstrap\\BsFormRenderer' => $baseDir . '/sources/Renderer/Bootstrap/BsFormRenderer.php', diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 18f7002d40..d57e598609 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -56,7 +56,7 @@ 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'), - 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-google/src', $vendorDir . '/league/oauth2-client/src'), + 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src'), 'Laminas\\Validator\\' => array($vendorDir . '/laminas/laminas-validator/src'), 'Laminas\\Stdlib\\' => array($vendorDir . '/laminas/laminas-stdlib/src'), 'Laminas\\ServiceManager\\' => array($vendorDir . '/laminas/laminas-servicemanager/src'), diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 2d9aa046ed..1c3aca7fd6 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -317,8 +317,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f ), 'League\\OAuth2\\Client\\' => array ( - 0 => __DIR__ . '/..' . '/league/oauth2-google/src', - 1 => __DIR__ . '/..' . '/league/oauth2-client/src', + 0 => __DIR__ . '/..' . '/league/oauth2-client/src', + 1 => __DIR__ . '/..' . '/league/oauth2-google/src', ), 'Laminas\\Validator\\' => array ( @@ -847,6 +847,31 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/SelectObjectValidator.php', 'Combodo\\iTop\\Kernel' => __DIR__ . '/../..' . '/sources/Kernel.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayDimFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ArrayDimFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ArrayEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BinaryOpEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BinaryOpEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseAndEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BitwiseAndEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseOrEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanAndEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BooleanAndEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanNotEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BooleanNotEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanOrEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BooleanOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ClassConstFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ConcatEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ConcatEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ConstFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ConstFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\EqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/EqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\FuncCallEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/FuncCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/GreaterEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterOrEqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/GreaterOrEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\MulEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/MulEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\NotEqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/NotEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/SmallerEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerOrEqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/SmallerOrEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticCallEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/StaticCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticPropertyFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\UnaryMinusEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\VariableEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/VariableEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\iExprEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/iExprEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php', 'Combodo\\iTop\\Renderer\\Bootstrap\\BsFieldRendererMappings' => __DIR__ . '/../..' . '/sources/Renderer/Bootstrap/BsFieldRendererMappings.php', 'Combodo\\iTop\\Renderer\\Bootstrap\\BsFormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/Bootstrap/BsFormRenderer.php', diff --git a/lib/composer/platform_check.php b/lib/composer/platform_check.php index 72145773d0..dee74e1736 100644 --- a/lib/composer/platform_check.php +++ b/lib/composer/platform_check.php @@ -36,7 +36,8 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - throw new \RuntimeException( - 'Composer detected issues in your platform: ' . implode(' ', $issues) + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR ); } diff --git a/setup/modulediscovery/ModuleFileParser.php b/setup/modulediscovery/ModuleFileParser.php index 08f36496ce..5064c34818 100644 --- a/setup/modulediscovery/ModuleFileParser.php +++ b/setup/modulediscovery/ModuleFileParser.php @@ -1,11 +1,10 @@ isInstantiable() @@ -69,7 +69,7 @@ public function ParseAndEvaluateExpression(string $sExpr) : mixed $sExpr; PHP; try{ - $aNodes = \ModuleFileParser::GetInstance()->ParsePhpCode($sPhpContent); + $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode($sPhpContent); $oExpr = $aNodes[0]; return $this->EvaluateExpression($oExpr->expr); diff --git a/setup/modulediscovery/evaluation/expression/SmallerEvaluator.php b/sources/PhpParser/Evaluation/SmallerEvaluator.php similarity index 86% rename from setup/modulediscovery/evaluation/expression/SmallerEvaluator.php rename to sources/PhpParser/Evaluation/SmallerEvaluator.php index ba276f0174..5f5543e3f3 100644 --- a/setup/modulediscovery/evaluation/expression/SmallerEvaluator.php +++ b/sources/PhpParser/Evaluation/SmallerEvaluator.php @@ -1,6 +1,6 @@ Date: Tue, 2 Sep 2025 19:23:12 +0200 Subject: [PATCH 15/23] fix broken setup --- setup/unattended-install/InstallationFileService.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index 030b41f579..7cf0dfce40 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -1,5 +1,7 @@ aSelectedModules); - $bSelected =\evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); + $bSelected = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { // Modules in data/production-modules/ are considered as mandatory and always installed From 794a9afe3ea320f6d30bcc90153cf19c438b55d0 Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 2 Sep 2025 19:23:34 +0200 Subject: [PATCH 16/23] fix broken setup --- setup/modulediscovery.class.inc.php | 4 +++- setup/runtimeenv.class.inc.php | 4 +++- setup/wizardsteps.class.inc.php | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index bf9af1973c..1021c93b2e 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -19,6 +19,8 @@ * */ +use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; + require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php'); class MissingDependencyException extends CoreException @@ -391,7 +393,7 @@ protected static function DependencyIsResolved($sDepString, $aOrderedModules, $a { $sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString); try{ - $bResult = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpr); + $bResult = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpr); } catch(ModuleFileReaderException $e){ //logged already echo "Failed to parse the boolean Expression = '$sBooleanExpr'
    "; diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 45f52dacd0..7cf0129b3d 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -24,6 +24,8 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; + require_once APPROOT."setup/modulediscovery.class.inc.php"; require_once APPROOT.'setup/modelfactory.class.inc.php'; require_once APPROOT.'setup/compiler.class.inc.php'; @@ -459,7 +461,7 @@ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { SetupInfo::SetSelectedModules($aRet); try{ - $bSelected = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($oModule->GetAutoSelect()); + $bSelected = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($oModule->GetAutoSelect()); if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index a64156851f..b99111d465 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -40,6 +40,7 @@ */ use Combodo\iTop\Application\WebPage\WebPage; +use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; require_once(APPROOT.'setup/setuputils.class.inc.php'); require_once(APPROOT.'setup/parameters.class.inc.php'); @@ -1787,7 +1788,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP // Check the module selection try { SetupInfo::SetSelectedModules($aModules); - $bSelected = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aInfo['auto_select']); + $bSelected = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aInfo['auto_select']); } catch (ModuleFileReaderException $e) { //logged already @@ -1865,7 +1866,7 @@ protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sP try { SetupInfo::SetSelectedModules($aModules); - $bSelected = \evaluation\expression\PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); + $bSelected = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); if ($bSelected) { $aModules[$sModuleId] = true; // store the Id of the selected module From c14ac90a13207d20953c046cd16a47131e79182c Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 3 Sep 2025 16:48:40 +0200 Subject: [PATCH 17/23] complete Evaluators list + autoload --- lib/composer/autoload_classmap.php | 13 ++++ lib/composer/autoload_static.php | 13 ++++ .../Evaluation/AbstractExprEvaluator.php | 9 +++ .../Evaluation/ArrayDimFetchEvaluator.php | 6 +- .../PhpParser/Evaluation/ArrayEvaluator.php | 6 +- .../Evaluation/BinaryOpEvaluator.php | 4 +- .../Evaluation/BitwiseAndEvaluator.php | 4 +- .../Evaluation/BitwiseNotEvaluator.php | 18 ++++++ .../Evaluation/BitwiseOrEvaluator.php | 4 +- .../Evaluation/BitwiseXorEvaluator.php | 16 +++++ .../Evaluation/BooleanAndEvaluator.php | 4 +- .../Evaluation/BooleanNotEvaluator.php | 6 +- .../Evaluation/BooleanOrEvaluator.php | 4 +- .../PhpParser/Evaluation/CastEvaluator.php | 59 ++++++++++++++++++ .../Evaluation/ClassConstFetchEvaluator.php | 6 +- .../Evaluation/CoalesceEvaluator.php | 23 +++++++ .../PhpParser/Evaluation/ConcatEvaluator.php | 4 +- .../Evaluation/ConstFetchEvaluator.php | 6 +- .../PhpParser/Evaluation/EqualEvaluator.php | 4 +- .../Evaluation/FuncCallEvaluator.php | 6 +- .../PhpParser/Evaluation/GreaterEvaluator.php | 4 +- .../Evaluation/GreaterOrEqualEvaluator.php | 4 +- .../PhpParser/Evaluation/IssetEvaluator.php | 26 ++++++++ .../Evaluation/MethodCallEvaluator.php | 36 +++++++++++ sources/PhpParser/Evaluation/ModEvaluator.php | 16 +++++ sources/PhpParser/Evaluation/MulEvaluator.php | 4 +- .../Evaluation/NotEqualEvaluator.php | 4 +- .../NullsafeMethodCallEvaluator.php | 36 +++++++++++ .../NullsafePropertyFetchEvaluator.php | 35 +++++++++++ .../Evaluation/PhpExpressionEvaluator.php | 26 ++++++-- .../Evaluation/PropertyFetchEvaluator.php | 47 ++++++++++++++ .../PhpParser/Evaluation/SmallerEvaluator.php | 4 +- .../Evaluation/SmallerOrEqualEvaluator.php | 4 +- .../Evaluation/StaticCallEvaluator.php | 7 +-- .../StaticPropertyFetchEvaluator.php | 6 +- .../PhpParser/Evaluation/TernaryEvaluator.php | 24 ++++++++ .../Evaluation/UnaryMinusEvaluator.php | 7 +-- .../Evaluation/UnaryPlusEvaluator.php | 18 ++++++ .../Evaluation/VariableEvaluator.php | 7 +-- .../PhpParser/Evaluation/iExprEvaluator.php | 6 +- .../modulediscovery/ModuleFileReaderTest.php | 5 +- .../Evaluation/PhpExpressionEvaluatorTest.php | 61 ++++++++++++++++--- 42 files changed, 522 insertions(+), 80 deletions(-) create mode 100644 sources/PhpParser/Evaluation/AbstractExprEvaluator.php create mode 100644 sources/PhpParser/Evaluation/BitwiseNotEvaluator.php create mode 100644 sources/PhpParser/Evaluation/BitwiseXorEvaluator.php create mode 100644 sources/PhpParser/Evaluation/CastEvaluator.php create mode 100644 sources/PhpParser/Evaluation/CoalesceEvaluator.php create mode 100644 sources/PhpParser/Evaluation/IssetEvaluator.php create mode 100644 sources/PhpParser/Evaluation/MethodCallEvaluator.php create mode 100644 sources/PhpParser/Evaluation/ModEvaluator.php create mode 100644 sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php create mode 100644 sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php create mode 100644 sources/PhpParser/Evaluation/PropertyFetchEvaluator.php create mode 100644 sources/PhpParser/Evaluation/TernaryEvaluator.php create mode 100644 sources/PhpParser/Evaluation/UnaryPlusEvaluator.php diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index b35dca37ce..90782d2bb6 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -469,29 +469,42 @@ 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => $baseDir . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => $baseDir . '/sources/Form/Validator/SelectObjectValidator.php', 'Combodo\\iTop\\Kernel' => $baseDir . '/sources/Kernel.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\AbstractExprEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/AbstractExprEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayDimFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ArrayDimFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ArrayEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BinaryOpEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BinaryOpEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseAndEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BitwiseAndEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseNotEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BitwiseNotEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseOrEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseXorEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BitwiseXorEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanAndEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BooleanAndEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanNotEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BooleanNotEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanOrEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/BooleanOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\CastEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/CastEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ClassConstFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\CoalesceEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/CoalesceEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ConcatEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ConcatEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ConstFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ConstFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\EqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/EqualEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\FuncCallEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/FuncCallEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/GreaterEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterOrEqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/GreaterOrEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\IssetEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/IssetEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\MethodCallEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/MethodCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ModEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/ModEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\MulEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/MulEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\NotEqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/NotEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\NullsafeMethodCallEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\NullsafePropertyFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\PropertyFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/SmallerEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerOrEqualEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/SmallerOrEqualEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticCallEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/StaticCallEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticPropertyFetchEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\TernaryEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/TernaryEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\UnaryMinusEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\UnaryPlusEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/UnaryPlusEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\VariableEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/VariableEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\iExprEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/iExprEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 1c3aca7fd6..ede77adf1d 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -847,29 +847,42 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/SelectObjectValidator.php', 'Combodo\\iTop\\Kernel' => __DIR__ . '/../..' . '/sources/Kernel.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\AbstractExprEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/AbstractExprEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayDimFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ArrayDimFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ArrayEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ArrayEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BinaryOpEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BinaryOpEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseAndEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BitwiseAndEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseNotEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BitwiseNotEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseOrEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\BitwiseXorEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BitwiseXorEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanAndEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BooleanAndEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanNotEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BooleanNotEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\BooleanOrEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/BooleanOrEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\CastEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/CastEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ClassConstFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\CoalesceEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/CoalesceEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ConcatEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ConcatEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\ConstFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ConstFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\EqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/EqualEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\FuncCallEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/FuncCallEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/GreaterEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\GreaterOrEqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/GreaterOrEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\IssetEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/IssetEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\MethodCallEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/MethodCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\ModEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/ModEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\MulEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/MulEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\NotEqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/NotEqualEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\NullsafeMethodCallEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\NullsafePropertyFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\PropertyFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/SmallerEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\SmallerOrEqualEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/SmallerOrEqualEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticCallEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/StaticCallEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\StaticPropertyFetchEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\TernaryEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/TernaryEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\UnaryMinusEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php', + 'Combodo\\iTop\\PhpParser\\Evaluation\\UnaryPlusEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/UnaryPlusEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\VariableEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/VariableEvaluator.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\iExprEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/iExprEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php', diff --git a/sources/PhpParser/Evaluation/AbstractExprEvaluator.php b/sources/PhpParser/Evaluation/AbstractExprEvaluator.php new file mode 100644 index 0000000000..16631d9047 --- /dev/null +++ b/sources/PhpParser/Evaluation/AbstractExprEvaluator.php @@ -0,0 +1,9 @@ +EvaluateExpression($oExpr->expr); + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php b/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php index 9c8a6be3a1..a2b4c04fb4 100644 --- a/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php +++ b/sources/PhpParser/Evaluation/BitwiseOrEvaluator.php @@ -1,11 +1,11 @@ EvaluateExpression($oExpr->expr); + switch (get_class($oExpr)){ + case Cast\Array_::class: + return (array) $oSubExpr; + + case Cast\Bool_::class: + return (bool) $oSubExpr; + + case Cast\Double::class: + /** @var Cast\Double $oExpr */ + switch ($oExpr->getAttribute("kind")){ + case Cast\Double::KIND_DOUBLE: + return (double) $oSubExpr; + + case Cast\Double::KIND_FLOAT: + case Cast\Double::KIND_REAL: + return (float) $oSubExpr; + } + + break; + + case Cast\Int_::class: + return (int) $oSubExpr; + + case Cast\Object_::class: + return (object) $oSubExpr; + + case Cast\String_::class: + return (string) $oSubExpr; + } + + return null; + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php b/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php index 2f4b0bc9c3..e3e67378b5 100644 --- a/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php +++ b/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php @@ -1,12 +1,12 @@ EvaluateExpression($oExpr->left); + if (! is_null($oLeftEval)) { + return $oLeftEval; + } + + return PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->right); + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/ConcatEvaluator.php b/sources/PhpParser/Evaluation/ConcatEvaluator.php index 4605eb0bd6..a4d2a12163 100644 --- a/sources/PhpParser/Evaluation/ConcatEvaluator.php +++ b/sources/PhpParser/Evaluation/ConcatEvaluator.php @@ -1,11 +1,11 @@ vars as $oVar){ + $var = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oVar); + if (! isset($var)){ + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/MethodCallEvaluator.php b/sources/PhpParser/Evaluation/MethodCallEvaluator.php new file mode 100644 index 0000000000..025f015d38 --- /dev/null +++ b/sources/PhpParser/Evaluation/MethodCallEvaluator.php @@ -0,0 +1,36 @@ +EvaluateExpression($oExpr->var); + if (is_null($oVar)) { + return null; + } + + $aArgs = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->args); + $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + $oReflectionClass = new ReflectionClass(get_class($oVar)); + + $oMethods = $oReflectionClass->getMethods(); + if (array_key_exists($sName, $oMethods)){ + $oMethods = $oMethods[$sName]; + if ($oMethods->isPublic()){ + return $oMethods->invokeArgs($oVar, $aArgs); + } + } + + return null; + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/ModEvaluator.php b/sources/PhpParser/Evaluation/ModEvaluator.php new file mode 100644 index 0000000000..8dc7ca5866 --- /dev/null +++ b/sources/PhpParser/Evaluation/ModEvaluator.php @@ -0,0 +1,16 @@ +EvaluateExpression($oExpr->var); + if (is_null($oVar)) { + return null; + } + + $aArgs = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->args); + $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + $oReflectionClass = new ReflectionClass(get_class($oVar)); + + $oMethods = $oReflectionClass->getMethods(); + if (array_key_exists($sName, $oMethods)){ + $oMethods = $oMethods[$sName]; + if ($oMethods->isPublic()){ + return $oMethods->invokeArgs($oVar, $aArgs); + } + } + + return null; + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php b/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php new file mode 100644 index 0000000000..8b956390bf --- /dev/null +++ b/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php @@ -0,0 +1,35 @@ +EvaluateExpression($oExpr->var); + if (is_null($oVar)) { + return null; + } + + $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + $oReflectionClass = new ReflectionClass(get_class($oVar)); + + $oProperties = $oReflectionClass->getProperties(); + if (array_key_exists($sName, $oProperties)){ + $oProperty = $oProperties[$sName]; + if ($oProperty->isPublic()){ + return $oProperty->getValue($oVar); + } + } + + return null; + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php index c4cffcdde0..f047995037 100644 --- a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php +++ b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php @@ -3,9 +3,9 @@ namespace Combodo\iTop\PhpParser\Evaluation; use ModuleFileParser; +use ModuleFileReaderException; use PhpParser\Node\Expr; -require_once __DIR__ . '/iExprEvaluator.php'; class PhpExpressionEvaluator { private static PhpExpressionEvaluator $oInstance; @@ -21,14 +21,23 @@ final public static function GetInstance(): PhpExpressionEvaluator { static::$aPhpParserEvaluators=[]; foreach (glob(__DIR__ . "/**Evaluator.php") as $sFile){ + require_once $sFile; require_once $sFile; $sNamespace = 'Combodo\\iTop\PhpParser\\Evaluation\\'; $sClass = $sNamespace. str_replace(".php", "", basename($sFile)); $oReflectionClass = new \ReflectionClass($sClass); if ($oReflectionClass->isInstantiable() - && $oReflectionClass->implementsInterface("{$sNamespace}iExprEvaluator")){ + && $oReflectionClass->implementsInterface(iExprEvaluator::class)){ $oClass = new $sClass; - static::$aPhpParserEvaluators[$oClass->GetHandledExpressionType()] = $oClass; + + if (! is_null($oClass->GetHandledExpressionType())){ + static::RegisterEvaluator($oClass, $oClass->GetHandledExpressionType()); + } + if (! is_null($oClass->GetHandledExpressionTypes())) { + foreach ($oClass->GetHandledExpressionTypes() as $sHandledExpressionType){ + static::RegisterEvaluator($oClass, $sHandledExpressionType); + } + } } } } @@ -36,6 +45,14 @@ final public static function GetInstance(): PhpExpressionEvaluator { return static::$oInstance; } + private static function RegisterEvaluator(iExprEvaluator $oClass, string $sHandledExpressionType) + { + if (array_key_exists($sHandledExpressionType, static::$aPhpParserEvaluators)){ + throw new \CoreException("Another Evaluator class already deals with $sHandledExpressionType"); + } + static::$aPhpParserEvaluators[$sHandledExpressionType] = $oClass; + } + final public static function SetInstance(?PhpExpressionEvaluator $oInstance): void { static::$oInstance = $oInstance; } @@ -72,9 +89,8 @@ public function ParseAndEvaluateExpression(string $sExpr) : mixed $aNodes = ModuleFileParser::GetInstance()->ParsePhpCode($sPhpContent); $oExpr = $aNodes[0]; return $this->EvaluateExpression($oExpr->expr); - } catch (\Throwable $t) { - throw new \ModuleFileReaderException("Eval of '$sExpr' caused an error:".$t->getMessage()); + throw new ModuleFileReaderException("Eval of '$sExpr' caused an error:".$t->getMessage()); } } } \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php b/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php new file mode 100644 index 0000000000..0724eb6510 --- /dev/null +++ b/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php @@ -0,0 +1,47 @@ +EvaluateExpression($oExpr->var); + if (is_null($oVar)) { + return null; + } + + $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + $oReflectionClass = new ReflectionClass(get_class($oVar)); + + $oProperties = $oReflectionClass->getProperties(); + if (array_key_exists($sName, $oProperties)){ + $oProperty = $oProperties[$sName]; + if ($oProperty->isPublic()){ + return $oProperty->getValue($oVar); + } + + return null; + } + + $aArgs=[]; + + $oMethods = $oReflectionClass->getMethods(); + if (array_key_exists($sName, $oMethods)){ + $oMethod = $oMethods[$sName]; + if ($oMethod->isPublic()){ + return $oMethod->invokeArgs(null, $aArgs); + } + + return null; + } + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/SmallerEvaluator.php b/sources/PhpParser/Evaluation/SmallerEvaluator.php index 5f5543e3f3..b85b688461 100644 --- a/sources/PhpParser/Evaluation/SmallerEvaluator.php +++ b/sources/PhpParser/Evaluation/SmallerEvaluator.php @@ -1,11 +1,11 @@ EvaluateExpression($oExpr->cond); + + if ($cond){ + return PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->if); + } + + return PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->else); + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php b/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php index 3b69b3303c..e376a849ce 100644 --- a/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php +++ b/sources/PhpParser/Evaluation/UnaryMinusEvaluator.php @@ -1,13 +1,12 @@ EvaluateExpression($oExpr->expr); + } +} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/VariableEvaluator.php b/sources/PhpParser/Evaluation/VariableEvaluator.php index 244446499f..c05e93c91c 100644 --- a/sources/PhpParser/Evaluation/VariableEvaluator.php +++ b/sources/PhpParser/Evaluation/VariableEvaluator.php @@ -1,13 +1,12 @@ ReadModuleFileInformation($sModuleFilePath); @@ -54,7 +53,7 @@ public function testReadModuleFileInformationUnsafe() //$this->assertEquals($aExpected, $aRes, $sModuleFilePath); } - $this->assertEquals([], $aErrors); + $this->assertEquals([], $aErrors, var_export($aErrors, true)); }*/ public static function ReadModuleFileConfigurationFileNameProvider() diff --git a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php index bb3b1f3a6c..bede0865e8 100644 --- a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php @@ -23,12 +23,12 @@ public static function EvaluateExpressionProvider() { 'ClassConstFetch: unknown class:class' => [ 'sExpression' => 'GabuZomeuUnknownClass::class'], 'ClassConstFetch: private existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::PRIVATE_CONSTANT', - 'forced_expected' => null + 'forced_expected' => null, ], 'StaticProperty: public existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$STATIC_PROPERTY'], 'StaticProperty: private existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$PRIVATE_STATIC_PROPERTY', - 'forced_expected' => null + 'forced_expected' => null, ], 'BinaryOperator: false|true' => [ 'sExpression' => 'false|true'], 'BinaryOperator: false||true' => [ 'sExpression' => 'false||true'], @@ -45,13 +45,40 @@ public static function EvaluateExpressionProvider() { 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], 'UnaryMinus: -1' => ['sExpression' => '-1'], + 'UnaryPlus: +1' => ['sExpression' => '+1'], 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], 'Variable: $_SERVER' => ['sExpression' => '$_SERVER'], 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], 'Array: ["a"]' => ['sExpression' => '["a"]'], 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], - 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'] + 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'], + 'NullsafePropertyFetch: $oNullVar?->b' => ['sExpression' => '$oNullVar?->b'], + 'NullsafePropertyFetch: $oEvaluationFakeClass?->bIsOk' => ['sExpression' => '$oEvaluationFakeClass?->bIsOk'], + 'PropertyFetch: $oEvaluationFakeClass->bIsOk' => ['sExpression' => '$oEvaluationFakeClass->bIsOk'], + 'NullsafeMethodCall: $oEvaluationFakeClass?->GetName()' => ['sExpression' => '$oEvaluationFakeClass?->GetName()'], + 'NullsafeMethodCall: $oEvaluationFakeClass?->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass?->GetLongName("aa")'], + 'MethodCall: $oEvaluationFakeClass->GetName()' => ['sExpression' => '$oEvaluationFakeClass->GetName()'], + 'MethodCall: $oEvaluationFakeClass->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass->GetLongName("aa")'], + 'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1'], + 'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1'], + 'Isset: isset($a)' => ['sExpression' => 'isset($a)'], + 'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)'], + 'Isset: isset($_SERVER)' => ['sExpression' => 'isset($_SERVER)'], + 'Isset: isset($_SERVER, $a)' => ['sExpression' => 'isset($_SERVER, $a)'], + 'BitwiseNot: ~3' => ['sExpression' => '~3'], + 'Mod: 3%2' => ['sExpression' => '3%2'], + 'BitwiseXor: 3^2' => ['sExpression' => '3^2'], + 'Ternary: (true) ? 1 : 2' => ['sExpression' => '(true) ? 1 : 2'], + 'Ternary: (false) ? 1 : 2' => ['sExpression' => '(false) ? 1 : 2'], + 'Cast: (array)3' => ['sExpression' => '(array)3'], + 'Cast: (bool)1' => ['sExpression' => '(bool)1'], + 'Cast: (bool)0' => ['sExpression' => '(bool)0'], + 'Cast: (double)3' => ['sExpression' => '(double)3'], + 'Cast: (float)3' => ['sExpression' => '(float)3'], + 'Cast: (int)3' => ['sExpression' => '(int)3'], + 'Cast: (object)3' => ['sExpression' => '(object)3'], + 'Cast: (string)3' => ['sExpression' => '(string)3'], ]; } @@ -60,10 +87,16 @@ public static function EvaluateExpressionProvider() { */ public function testEvaluateExpression($sExpression, $forced_expected="NOTPROVIDED") { + $oNullVar=null; + $oNonNullVar="a"; $_SERVER=[ - 'toto' => 'titi' + 'toto' => 'titi', ]; + $oEvaluationFakeClass = new EvaluationFakeClass(); + $oEvaluationFakeClass->bIsOk; + $oEvaluationFakeClass->GetName(); + $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression); if ($forced_expected === "NOTPROVIDED"){ $this->assertEquals($this->UnprotectedComputeExpression($sExpression), $res, $sExpression); @@ -90,12 +123,6 @@ private function UnprotectedComputeExpression(string $sExpr) : mixed } } - public function testParseAndEvaluateBooleanExpression_BrokenBooleanExpression(){ - $this->expectException(\ModuleFileReaderException::class); - $this->expectExceptionMessage('Eval of \'(a || true)\' caused an error'); - $this->assertTrue(PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression("(a || true)")); - } - public static function ParseAndEvaluateBooleanExpression_AutoselectProvider() { $sSimpleCallToModuleIsSelected = "SetupInfo::ModuleIsSelected(\"itop-storage-mgmt\")"; @@ -131,4 +158,18 @@ public function testEvaluateBooleanExpression_Autoselect(string $sBooleanExpress \SetupInfo::SetSelectedModules(["itop-storage-mgmt" => "123"]); $this->assertEquals($expected, PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateBooleanExpression($sBooleanExpression), $sBooleanExpression); } +} + +class EvaluationFakeClass { + public bool $bIsOk=true; + + public function GetName() + { + return "gabuzomeu"; + } + + public function GetLongName($suffix) + { + return "gabuzomeu_" . $suffix; + } } \ No newline at end of file From cdbe331c350eed7031c7f6011851ee4d64320766 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 3 Sep 2025 17:11:49 +0200 Subject: [PATCH 18/23] cleanup useless testing resources --- .../resources/module.authent-ldap.php | 26 +-------- .../resources/module.combodo-make-it-vip.php | 57 ------------------- .../module.itop-global-requests-mgmt.php | 41 +------------ 3 files changed, 4 insertions(+), 120 deletions(-) delete mode 100644 tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-make-it-vip.php 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 index d1f7cf7151..6124cd2f8b 100644 --- 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 @@ -34,7 +34,7 @@ 'data.sample' => array( //'data.sample.authent-ldap.xml', ), - + // Documentation // 'doc.manual_setup' => '', @@ -49,7 +49,7 @@ '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, @@ -68,32 +68,12 @@ 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); } } 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 deleted file mode 100644 index 3ee370f0ee..0000000000 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-make-it-vip.php +++ /dev/null @@ -1,57 +0,0 @@ - '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-global-requests-mgmt.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-global-requests-mgmt.php index 64cf608f1b..023b1046f3 100644 --- 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 @@ -21,15 +21,6 @@ // '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, @@ -39,12 +30,6 @@ // '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, @@ -83,31 +68,7 @@ class GlobalRequestInstaller extends ModuleInstallerAPI */ 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); - } + //code } } From 86fe9d6a2b2adec64711e8b1aaff189ad2a257fa Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 3 Sep 2025 18:11:34 +0200 Subject: [PATCH 19/23] cleanup + replace last eval call in VariableEvaluator --- sources/PhpParser/Evaluation/VariableEvaluator.php | 7 ++----- .../Evaluation/PhpExpressionEvaluatorTest.php | 13 ++++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sources/PhpParser/Evaluation/VariableEvaluator.php b/sources/PhpParser/Evaluation/VariableEvaluator.php index c05e93c91c..92444ec5b3 100644 --- a/sources/PhpParser/Evaluation/VariableEvaluator.php +++ b/sources/PhpParser/Evaluation/VariableEvaluator.php @@ -22,10 +22,7 @@ public function Evaluate(Expr $oExpr): mixed { $sVarname=$oExpr->name; - $bResult = null; - @eval('$bResult = $'.$sVarname.';'); - - return $bResult; - + global $$sVarname; + return $$sVarname; } } \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php index bede0865e8..9c6ed56b83 100644 --- a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php @@ -15,7 +15,6 @@ public static function EvaluateExpressionProvider() { 'ConstFetch: false' => [ 'sExpression' => 'false'], 'ConstFetch: (false)' => [ 'sExpression' => 'false'], 'ConstFetch: true' => [ 'sExpression' => 'true'], - //'ConstFetch: __FILE__' => [ 'sExpression' => __FILE__], 'ConstFetch: (true)' => [ 'sExpression' => 'true'], 'ClassConstFetch: public existing constant' => [ 'sExpression' => 'SetupUtils::PHP_MIN_VERSION'], 'ClassConstFetch: unknown constant' => [ 'sExpression' => 'SetupUtils::UNKNOWN_CONSTANT'], @@ -48,7 +47,8 @@ public static function EvaluateExpressionProvider() { 'UnaryPlus: +1' => ['sExpression' => '+1'], 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], - 'Variable: $_SERVER' => ['sExpression' => '$_SERVER'], + //'Variable: $_SERVER' => ['sExpression' => '$_SERVER'], + 'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar'], 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], 'Array: ["a"]' => ['sExpression' => '["a"]'], 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], @@ -78,7 +78,7 @@ public static function EvaluateExpressionProvider() { 'Cast: (float)3' => ['sExpression' => '(float)3'], 'Cast: (int)3' => ['sExpression' => '(int)3'], 'Cast: (object)3' => ['sExpression' => '(object)3'], - 'Cast: (string)3' => ['sExpression' => '(string)3'], + 'Cast: (string)$oEvaluationFakeClass' => ['sExpression' => '(string)$oEvaluationFakeClass'], ]; } @@ -94,8 +94,6 @@ public function testEvaluateExpression($sExpression, $forced_expected="NOTPROVID ]; $oEvaluationFakeClass = new EvaluationFakeClass(); - $oEvaluationFakeClass->bIsOk; - $oEvaluationFakeClass->GetName(); $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression); if ($forced_expected === "NOTPROVIDED"){ @@ -172,4 +170,9 @@ public function GetLongName($suffix) { return "gabuzomeu_" . $suffix; } + + public function __toString(): string + { + return "a"; + } } \ No newline at end of file From af790269f07cf77894a8e266544457e931bf77e8 Mon Sep 17 00:00:00 2001 From: odain Date: Thu, 4 Sep 2025 22:41:14 +0200 Subject: [PATCH 20/23] fix few Evaluators code --- .../Evaluation/ClassConstFetchEvaluator.php | 8 ++-- .../Evaluation/FuncCallEvaluator.php | 17 +++++++-- .../Evaluation/MethodCallEvaluator.php | 38 ++++++++++++------- .../NullsafeMethodCallEvaluator.php | 36 ------------------ .../NullsafePropertyFetchEvaluator.php | 35 ----------------- .../Evaluation/PhpExpressionEvaluator.php | 27 +++++++++++-- .../Evaluation/PropertyFetchEvaluator.php | 38 ++++++++----------- .../Evaluation/StaticCallEvaluator.php | 15 ++++++-- .../StaticPropertyFetchEvaluator.php | 7 +++- .../Evaluation/VariableEvaluator.php | 1 - 10 files changed, 101 insertions(+), 121 deletions(-) delete mode 100644 sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php delete mode 100644 sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php diff --git a/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php b/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php index e3e67378b5..5217dc0991 100644 --- a/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php +++ b/sources/PhpParser/Evaluation/ClassConstFetchEvaluator.php @@ -16,6 +16,10 @@ public function Evaluate(Expr $oExpr): mixed { $sClassName = $oExpr->class->name; $sProperty = $oExpr->name->name; + if ('class' === $sProperty){ + return $sClassName; + } + if (class_exists($sClassName)){ $class = new \ReflectionClass($sClassName); if (array_key_exists($sProperty, $class->getConstants())) { @@ -26,10 +30,6 @@ public function Evaluate(Expr $oExpr): mixed { } } - if ('class' === $sProperty){ - return $sClassName; - } - return null; } } \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/FuncCallEvaluator.php b/sources/PhpParser/Evaluation/FuncCallEvaluator.php index 8d185eef1f..a206cf36a8 100644 --- a/sources/PhpParser/Evaluation/FuncCallEvaluator.php +++ b/sources/PhpParser/Evaluation/FuncCallEvaluator.php @@ -5,9 +5,17 @@ use ModuleFileReaderException; use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use ReflectionFunction; class FuncCallEvaluator extends AbstractExprEvaluator { + public const WHITELIST=[ + "function_exists", + "class_exists", + "method_exists" + ]; + public function GetHandledExpressionType(): ?string { return FuncCall::class; } @@ -15,9 +23,12 @@ public function GetHandledExpressionType(): ?string { public function Evaluate(Expr $oExpr): mixed { /** @var FuncCall $oExpr */ - $sFunction = $oExpr->name->name; - $aWhiteList = ["function_exists", "class_exists", "method_exists"]; - if (! in_array($sFunction, $aWhiteList)){ + if ($oExpr->name instanceof Name){ + $sFunction = $oExpr->name->name; + } else { + $sFunction = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + } + if (! in_array($sFunction, self::WHITELIST)){ throw new ModuleFileReaderException("FuncCall $sFunction not supported"); } diff --git a/sources/PhpParser/Evaluation/MethodCallEvaluator.php b/sources/PhpParser/Evaluation/MethodCallEvaluator.php index 025f015d38..0e03406bc2 100644 --- a/sources/PhpParser/Evaluation/MethodCallEvaluator.php +++ b/sources/PhpParser/Evaluation/MethodCallEvaluator.php @@ -4,33 +4,45 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Identifier; use ReflectionClass; -class MethodCallEvaluator extends AbstractExprEvaluator { +class MethodCallEvaluator extends AbstractExprEvaluator { public function GetHandledExpressionType(): ?string { - return MethodCall::class; + return null; } - public function Evaluate(Expr $oExpr): mixed { - /** @var MethodCall $oExpr */ + public function GetHandledExpressionTypes(): ?array { + return [MethodCall::class, Expr\NullsafeMethodCall::class]; + } + public function Evaluate(Expr $oExpr): mixed { $oVar = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->var); if (is_null($oVar)) { return null; } - $aArgs = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->args); - $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); - $oReflectionClass = new ReflectionClass(get_class($oVar)); + $aArgs=[]; + foreach ($oExpr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $aArgs[]=$arg->value->value; + } - $oMethods = $oReflectionClass->getMethods(); - if (array_key_exists($sName, $oMethods)){ - $oMethods = $oMethods[$sName]; - if ($oMethods->isPublic()){ - return $oMethods->invokeArgs($oVar, $aArgs); - } + if ($oExpr->name instanceof Identifier){ + $sName = $oExpr->name->name; + } else { + $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); } + $oReflectionClass = new ReflectionClass(get_class($oVar)); + try{ + $oMethod = $oReflectionClass->getMethod($sName); + if ($oMethod->isPublic()){ + return $oMethod->invokeArgs($oVar, $aArgs); + } + } catch (\ReflectionException $t) {} + return null; } } \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php b/sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php deleted file mode 100644 index 77788ee039..0000000000 --- a/sources/PhpParser/Evaluation/NullsafeMethodCallEvaluator.php +++ /dev/null @@ -1,36 +0,0 @@ -EvaluateExpression($oExpr->var); - if (is_null($oVar)) { - return null; - } - - $aArgs = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->args); - $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); - $oReflectionClass = new ReflectionClass(get_class($oVar)); - - $oMethods = $oReflectionClass->getMethods(); - if (array_key_exists($sName, $oMethods)){ - $oMethods = $oMethods[$sName]; - if ($oMethods->isPublic()){ - return $oMethods->invokeArgs($oVar, $aArgs); - } - } - - return null; - } -} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php b/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php deleted file mode 100644 index 8b956390bf..0000000000 --- a/sources/PhpParser/Evaluation/NullsafePropertyFetchEvaluator.php +++ /dev/null @@ -1,35 +0,0 @@ -EvaluateExpression($oExpr->var); - if (is_null($oVar)) { - return null; - } - - $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); - $oReflectionClass = new ReflectionClass(get_class($oVar)); - - $oProperties = $oReflectionClass->getProperties(); - if (array_key_exists($sName, $oProperties)){ - $oProperty = $oProperties[$sName]; - if ($oProperty->isPublic()){ - return $oProperty->getValue($oVar); - } - } - - return null; - } -} \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php index f047995037..8abad02632 100644 --- a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php +++ b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php @@ -4,6 +4,7 @@ use ModuleFileParser; use ModuleFileReaderException; +use PhpParser\ConstExprEvaluator; use PhpParser\Node\Expr; class PhpExpressionEvaluator { @@ -57,7 +58,24 @@ final public static function SetInstance(?PhpExpressionEvaluator $oInstance): vo static::$oInstance = $oInstance; } - public function EvaluateExpression(Expr $oExpression) : mixed + public function EvaluateExpression(Expr $oExpression, int $iMode=self::LIB_AND_FALLBACK) : mixed + { + if ($iMode==self::ITOP_ALGO){ + return $this->EvaluateExpressionLocally($oExpression); + } + + if ($iMode==self::LIB_ONLY){ + $oConstExprEvaluator = new ConstExprEvaluator(); + } else { + $oConstExprEvaluator = new ConstExprEvaluator([$this, "EvaluateExpressionLocally"]); + } + + $oConstExprEvaluator->setFunctionsWhitelist(FuncCallEvaluator::WHITELIST); + $oConstExprEvaluator->setStaticcallsWhitelist(StaticCallEvaluator::WHITELIST); + return $oConstExprEvaluator->evaluateDirectly($oExpression); + } + + public function EvaluateExpressionLocally(Expr $oExpression) : mixed { $sClass = get_class($oExpression); $oPhpParserEvaluator = static::$aPhpParserEvaluators[$sClass] ?? null; @@ -79,7 +97,10 @@ public function ParseAndEvaluateBooleanExpression(string $sBooleanExpr) : bool return $this->ParseAndEvaluateExpression($sBooleanExpr); } - public function ParseAndEvaluateExpression(string $sExpr) : mixed + const LIB_AND_FALLBACK=1; + const LIB_ONLY=2; + const ITOP_ALGO=3; + public function ParseAndEvaluateExpression(string $sExpr, int $iMode=self::LIB_AND_FALLBACK) : mixed { $sPhpContent = <<ParsePhpCode($sPhpContent); $oExpr = $aNodes[0]; - return $this->EvaluateExpression($oExpr->expr); + return $this->EvaluateExpression($oExpr->expr, $iMode); } catch (\Throwable $t) { throw new ModuleFileReaderException("Eval of '$sExpr' caused an error:".$t->getMessage()); } diff --git a/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php b/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php index 0724eb6510..543b626f01 100644 --- a/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php +++ b/sources/PhpParser/Evaluation/PropertyFetchEvaluator.php @@ -4,44 +4,38 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Identifier; use ReflectionClass; class PropertyFetchEvaluator extends AbstractExprEvaluator { public function GetHandledExpressionType(): ?string { - return PropertyFetch::class; + return null; } - public function Evaluate(Expr $oExpr): mixed { - /** @var PropertyFetch $oExpr */ + public function GetHandledExpressionTypes(): ?array { + return [PropertyFetch::class, Expr\NullsafePropertyFetch::class]; + } + public function Evaluate(Expr $oExpr): mixed { $oVar = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->var); if (is_null($oVar)) { return null; } - $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); - $oReflectionClass = new ReflectionClass(get_class($oVar)); + if ($oExpr->name instanceof Identifier){ + $sName = $oExpr->name->name; + } else { + $sName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + } - $oProperties = $oReflectionClass->getProperties(); - if (array_key_exists($sName, $oProperties)){ - $oProperty = $oProperties[$sName]; + $oReflectionClass = new ReflectionClass(get_class($oVar)); + try{ + $oProperty = $oReflectionClass->getProperty($sName); if ($oProperty->isPublic()){ return $oProperty->getValue($oVar); } + } catch (\ReflectionException $t) {} - return null; - } - - $aArgs=[]; - - $oMethods = $oReflectionClass->getMethods(); - if (array_key_exists($sName, $oMethods)){ - $oMethod = $oMethods[$sName]; - if ($oMethod->isPublic()){ - return $oMethod->invokeArgs(null, $aArgs); - } - - return null; - } + return null; } } \ No newline at end of file diff --git a/sources/PhpParser/Evaluation/StaticCallEvaluator.php b/sources/PhpParser/Evaluation/StaticCallEvaluator.php index f66c37162e..3d5bc5f35d 100644 --- a/sources/PhpParser/Evaluation/StaticCallEvaluator.php +++ b/sources/PhpParser/Evaluation/StaticCallEvaluator.php @@ -5,8 +5,14 @@ use ModuleFileReaderException; use PhpParser\Node\Expr; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; class StaticCallEvaluator extends AbstractExprEvaluator { + public const WHITELIST=[ + "SetupInfo::ModuleIsSelected", + "utils::GetItopVersionWikiSyntax" + ]; + public function GetHandledExpressionType(): ?string { return StaticCall::class; } @@ -15,11 +21,14 @@ public function Evaluate(Expr $oExpr): mixed { /** @var StaticCall $oExpr */ $sClassName = $oExpr->class->name; - $sMethodName = $oExpr->name->name; + if ($oExpr->name instanceof Identifier){ + $sMethodName = $oExpr->name->name; + } else { + $sMethodName = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + } - $aWhiteList = ["SetupInfo::ModuleIsSelected", "utils::GetItopVersionWikiSyntax"]; $sStaticCallDescription = "$sClassName::$sMethodName"; - if (! in_array($sStaticCallDescription, $aWhiteList)){ + if (! in_array($sStaticCallDescription, self::WHITELIST)){ throw new ModuleFileReaderException("StaticCall $sStaticCallDescription not supported"); } diff --git a/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php b/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php index 93090342a2..8ebd5350e1 100644 --- a/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php +++ b/sources/PhpParser/Evaluation/StaticPropertyFetchEvaluator.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\StaticPropertyFetch; +use PhpParser\Node\Identifier; class StaticPropertyFetchEvaluator extends AbstractExprEvaluator { public function GetHandledExpressionType(): ?string { @@ -14,7 +15,11 @@ public function Evaluate(Expr $oExpr): mixed { /** @var StaticPropertyFetch $oExpr */ $sClassName = $oExpr->class->name; - $sProperty = $oExpr->name->name; + if ($oExpr->name instanceof Identifier){ + $sProperty = $oExpr->name->name; + } else { + $sProperty = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oExpr->name); + } if (class_exists($sClassName)){ $class = new \ReflectionClass($sClassName); diff --git a/sources/PhpParser/Evaluation/VariableEvaluator.php b/sources/PhpParser/Evaluation/VariableEvaluator.php index 92444ec5b3..967224c9b4 100644 --- a/sources/PhpParser/Evaluation/VariableEvaluator.php +++ b/sources/PhpParser/Evaluation/VariableEvaluator.php @@ -21,7 +21,6 @@ public function Evaluate(Expr $oExpr): mixed { } $sVarname=$oExpr->name; - global $$sVarname; return $$sVarname; } From 11f142b782c94bbc8fa66c3eaa97912d1b619a4d Mon Sep 17 00:00:00 2001 From: odain Date: Thu, 4 Sep 2025 22:42:02 +0200 Subject: [PATCH 21/23] enhance nikic evaluators + test with/without nikic lib --- .../lib/PhpParser/ConstExprEvaluator.php | 315 +++++++++++++++++- .../Evaluation/PhpExpressionEvaluatorTest.php | 133 +++++--- 2 files changed, 390 insertions(+), 58 deletions(-) diff --git a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php index 9526787142..79de55005b 100644 --- a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php @@ -3,7 +3,14 @@ namespace PhpParser; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use PhpParser\Node\Scalar; +use Exception; use function array_merge; @@ -30,7 +37,13 @@ class ConstExprEvaluator { /** @var callable|null */ private $fallbackEvaluator; - /** + /** @var array $functions_whitelist */ + private $functions_whitelist; + + /** @var array staticcalls_whitelist */ + private $staticcalls_whitelist; + + /** * Create a constant expression evaluator. * * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See @@ -44,8 +57,21 @@ public function __construct(?callable $fallbackEvaluator = null) { "Expression of type {$expr->getType()} cannot be evaluated" ); }; + + $this->functions_whitelist=[]; + $this->staticcalls_whitelist=[]; } + public function setFunctionsWhitelist(array $functions_whitelist): void + { + $this->functions_whitelist = $functions_whitelist; + } + + public function setStaticcallsWhitelist(array $staticcalls_whitelist): void + { + $this->staticcalls_whitelist = $staticcalls_whitelist; + } + /** * Silently evaluates a constant expression into a PHP value. * @@ -145,6 +171,41 @@ private function evaluate(Expr $expr) { return $this->evaluateConstFetch($expr); } + if ($expr instanceof Expr\Isset_) { + return $this->evaluateIsset($expr); + } + + if ($expr instanceof Expr\ClassConstFetch) { + return $this->evaluateClassConstFetch($expr); + } + + if ($expr instanceof Expr\Cast) { + return $this->evaluateCast($expr); + } + + if ($expr instanceof Expr\StaticPropertyFetch) { + return $this->evaluateStaticPropertyFetch($expr); + } + + if ($expr instanceof Expr\FuncCall) { + return $this->evaluateFuncCall($expr); + } + + if ($expr instanceof Expr\Variable) { + return $this->evaluateVariable($expr); + } + + if ($expr instanceof Expr\StaticCall) { + return $this->evaluateStaticCall($expr); + } + + if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { + return $this->evaluatePropertyFetch($expr); + } + + if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { + return $this->evaluateMethodCall($expr); + } return ($this->fallbackEvaluator)($expr); } @@ -225,13 +286,253 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { /** @return mixed */ private function evaluateConstFetch(Expr\ConstFetch $expr) { - $name = $expr->name->toLowerString(); - switch ($name) { - case 'null': return null; - case 'false': return false; - case 'true': return true; - } + try { + $name = $expr->name; + if(! is_string($name)){ + //PHP_VERSION_ID usecase + $name = $name->name; + } + + if (defined($name)){ + return constant($name); + } + } catch(\Throwable $t){} return ($this->fallbackEvaluator)($expr); } + + /** @return mixed */ + private function evaluateIsset(Expr\Isset_ $expr) { + try { + foreach ($expr->vars as $var){ + $var = $this->evaluate($var); + if (! isset($var)){ + return false; + } + } + + return true; + } catch(\Throwable $t){ + return false; + }; + } + + /** @return mixed */ + private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { + try { + $classname = $expr->class->name; + $property = $expr->name->name; + + if ('class' === $property){ + return $classname; + } + + if (class_exists($classname)){ + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getConstants())) { + $oReflectionConstant = $class->getReflectionConstant($property); + if ($oReflectionConstant->isPublic()){ + return $class->getConstant($property); + } + } + } + } catch(\Throwable $t){} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateCast(Expr\Cast $expr) { + try { + $subexpr = $this->evaluate($expr->expr); + $type = get_class($expr); + switch ($type){ + case Expr\Cast\Array_::class: + return (array) $subexpr; + + case Expr\Cast\Bool_::class: + return (bool) $subexpr; + + case Expr\Cast\Double::class: + switch ($expr->getAttribute("kind")){ + case Expr\Cast\Double::KIND_DOUBLE: + return (double) $subexpr; + + case Expr\Cast\Double::KIND_FLOAT: + case Expr\Cast\Double::KIND_REAL: + return (float) $subexpr; + } + + break; + + case Expr\Cast\Int_::class: + return (int) $subexpr; + + case Expr\Cast\Object_::class: + return (object) $subexpr; + + case Expr\Cast\String_::class: + return (string) $subexpr; + } + } catch(\Throwable $t){ + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) + { + try { + $classname = $expr->class->name; + if ($expr->name instanceof Identifier){ + $property = $expr->name->name; + } else { + $property = $this->evaluate($expr->name); + } + + if (class_exists($classname)){ + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getStaticProperties())) { + $oReflectionProperty = $class->getProperty($property); + if ($oReflectionProperty->isPublic()){ + return $class->getStaticPropertyValue($property); + } + } + } + } + catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateFuncCall(Expr\FuncCall $expr) + { + try { + if ($expr->name instanceof Name){ + $function = $expr->name->name; + } else { + $function = $this->evaluate($expr->name); + } + + if (! in_array($function, $this->functions_whitelist)){ + throw new Exception("FuncCall $function not supported"); + } + + $args=[]; + foreach ($expr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $args[]=$arg->value->value; + } + + $reflection_function = new \ReflectionFunction($function); + return $reflection_function->invoke(...$args); + } + catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateVariable(Expr\Variable $expr) + { + try { + $name = $expr->name; + if (! is_null($name) && isset($name)) { + global $$name; + return $$name; + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateStaticCall(Expr\StaticCall $expr) + { + try { + $classname = $expr->class->name; + if ($expr->name instanceof Identifier){ + $methodname = $expr->name->name; + } else { + $methodname = $this->evaluate($expr->name); + } + + $static_call_description = "$classname::$methodname"; + if (! in_array($static_call_description, $this->staticcalls_whitelist)){ + throw new Exception("StaticCall $static_call_description not supported"); + } + + $args=[]; + foreach ($expr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $args[]=$arg->value->value; + } + + $class = new \ReflectionClass($classname); + $method = $class->getMethod($methodname); + if ($method->isPublic()){ + return $method->invokeArgs(null, $args); + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluatePropertyFetch(Expr\NullsafePropertyFetch|Expr\PropertyFetch $expr) + { + try { + $var = $this->evaluateVariable($expr->var); + if (is_null($var)) { + return null; + } + + if ($expr->name instanceof Identifier){ + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $property = $reflectionClass->getProperty($name); + if ($property->isPublic()){ + return $property->getValue($var); + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateMethodCall(Expr\MethodCall|Expr\NullsafeMethodCall $expr) + { + try { + $var = $this->evaluateVariable($expr->var); + if (is_null($var)) { + return null; + } + + $args=[]; + foreach ($expr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $args[]=$arg->value->value; + } + + if ($expr->name instanceof Identifier){ + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + $reflectionClass = new \ReflectionClass(get_class($var)); + $method = $reflectionClass->getMethod($name); + if ($method->isPublic()){ + return $method->invokeArgs($var, $args); + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + } diff --git a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php index 9c6ed56b83..ed1d07fd7c 100644 --- a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php @@ -12,23 +12,10 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase { public static function EvaluateExpressionProvider() { return [ - 'ConstFetch: false' => [ 'sExpression' => 'false'], - 'ConstFetch: (false)' => [ 'sExpression' => 'false'], - 'ConstFetch: true' => [ 'sExpression' => 'true'], - 'ConstFetch: (true)' => [ 'sExpression' => 'true'], - 'ClassConstFetch: public existing constant' => [ 'sExpression' => 'SetupUtils::PHP_MIN_VERSION'], - 'ClassConstFetch: unknown constant' => [ 'sExpression' => 'SetupUtils::UNKNOWN_CONSTANT'], - 'ClassConstFetch: unknown class:constant' => [ 'sExpression' => 'GabuZomeuUnknownClass::UNKNOWN_CONSTANT'], - 'ClassConstFetch: unknown class:class' => [ 'sExpression' => 'GabuZomeuUnknownClass::class'], - 'ClassConstFetch: private existing constant' => [ - 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::PRIVATE_CONSTANT', - 'forced_expected' => null, - ], - 'StaticProperty: public existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$STATIC_PROPERTY'], - 'StaticProperty: private existing constant' => [ - 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$PRIVATE_STATIC_PROPERTY', - 'forced_expected' => null, - ], + 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], + 'Array: ["a"]' => ['sExpression' => '["a"]'], + 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], + 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], 'BinaryOperator: false|true' => [ 'sExpression' => 'false|true'], 'BinaryOperator: false||true' => [ 'sExpression' => 'false||true'], 'BinaryOperator: false&&true' => [ 'sExpression' => 'false&&true'], @@ -41,36 +28,8 @@ public static function EvaluateExpressionProvider() { 'BinaryOperator: 1 <= 1' => [ 'sExpression' => '1 <= 1'], 'BinaryOperator: PHP_VERSION_ID == PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID == PHP_VERSION_ID'], 'BinaryOperator: PHP_VERSION_ID != PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID != PHP_VERSION_ID'], - 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], - 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], - 'UnaryMinus: -1' => ['sExpression' => '-1'], - 'UnaryPlus: +1' => ['sExpression' => '+1'], - 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], - 'ArrayDimFetch: $_SERVER[\'toto\']' => ['sExpression' => '$_SERVER[\'toto\']'], - //'Variable: $_SERVER' => ['sExpression' => '$_SERVER'], - 'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar'], - 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], - 'Array: ["a"]' => ['sExpression' => '["a"]'], - 'Array dict: ["a"=>"b"]' => ['sExpression' => '["a"=>"b"]'], - 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'], - 'NullsafePropertyFetch: $oNullVar?->b' => ['sExpression' => '$oNullVar?->b'], - 'NullsafePropertyFetch: $oEvaluationFakeClass?->bIsOk' => ['sExpression' => '$oEvaluationFakeClass?->bIsOk'], - 'PropertyFetch: $oEvaluationFakeClass->bIsOk' => ['sExpression' => '$oEvaluationFakeClass->bIsOk'], - 'NullsafeMethodCall: $oEvaluationFakeClass?->GetName()' => ['sExpression' => '$oEvaluationFakeClass?->GetName()'], - 'NullsafeMethodCall: $oEvaluationFakeClass?->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass?->GetLongName("aa")'], - 'MethodCall: $oEvaluationFakeClass->GetName()' => ['sExpression' => '$oEvaluationFakeClass->GetName()'], - 'MethodCall: $oEvaluationFakeClass->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass->GetLongName("aa")'], - 'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1'], - 'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1'], - 'Isset: isset($a)' => ['sExpression' => 'isset($a)'], - 'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)'], - 'Isset: isset($_SERVER)' => ['sExpression' => 'isset($_SERVER)'], - 'Isset: isset($_SERVER, $a)' => ['sExpression' => 'isset($_SERVER, $a)'], 'BitwiseNot: ~3' => ['sExpression' => '~3'], - 'Mod: 3%2' => ['sExpression' => '3%2'], 'BitwiseXor: 3^2' => ['sExpression' => '3^2'], - 'Ternary: (true) ? 1 : 2' => ['sExpression' => '(true) ? 1 : 2'], - 'Ternary: (false) ? 1 : 2' => ['sExpression' => '(false) ? 1 : 2'], 'Cast: (array)3' => ['sExpression' => '(array)3'], 'Cast: (bool)1' => ['sExpression' => '(bool)1'], 'Cast: (bool)0' => ['sExpression' => '(bool)0'], @@ -78,24 +37,96 @@ public static function EvaluateExpressionProvider() { 'Cast: (float)3' => ['sExpression' => '(float)3'], 'Cast: (int)3' => ['sExpression' => '(int)3'], 'Cast: (object)3' => ['sExpression' => '(object)3'], - 'Cast: (string)$oEvaluationFakeClass' => ['sExpression' => '(string)$oEvaluationFakeClass'], + 'Cast: (string) $oEvaluationFakeClass' => ['sExpression' => '(string) $oEvaluationFakeClass', "toString"], + 'ClassConstFetch: public existing constant' => [ 'sExpression' => 'SetupUtils::PHP_MIN_VERSION'], + 'ClassConstFetch: unknown constant' => [ 'sExpression' => 'SetupUtils::UNKNOWN_CONSTANT'], + 'ClassConstFetch: unknown class:constant' => [ 'sExpression' => 'GabuZomeuUnknownClass::UNKNOWN_CONSTANT'], + 'ClassConstFetch: unknown class:class' => [ 'sExpression' => 'GabuZomeuUnknownClass::class'], + 'ClassConstFetch: private existing constant' => [ + 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::PRIVATE_CONSTANT', + 'forced_expected' => null, + ], + 'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1', 1], + 'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1', 1], + 'Coalesce: $oGlobalNonNullVar ?? 1' => ['sExpression' => '$oGlobalNonNullVar ?? 1', "a"], + 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], + 'ConstFetch: false' => [ 'sExpression' => 'false'], + 'ConstFetch: (false)' => [ 'sExpression' => 'false'], + 'ConstFetch: true' => [ 'sExpression' => 'true'], + 'ConstFetch: (true)' => [ 'sExpression' => 'true'], + 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], + 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], + 'Isset: isset($oNonNullVar)' => ['sExpression' => 'isset($oNonNullVar)', false], + 'Isset: isset($oGlobalNonNullVar)' => ['sExpression' => 'isset($oGlobalNonNullVar)', true], + 'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)', false], + 'Isset: isset($_SERVER)' => ['sExpression' => 'isset($_SERVER)', true], + 'Isset: isset($_SERVER, $a)' => ['sExpression' => 'isset($_SERVER, $a)', false], + 'Isset: isset($oGlobalNonNullVar, $_SERVER)' => ['sExpression' => 'isset($oGlobalNonNullVar, $_SERVER)', true], + 'MethodCall: $oEvaluationFakeClass->GetName()' => ['sExpression' => '$oEvaluationFakeClass->GetName()', "gabuzomeu"], + 'MethodCall: $oEvaluationFakeClass->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass->GetLongName("aa")', "gabuzomeu_aa"], + 'Mod: 3%2' => ['sExpression' => '3%2'], + 'NullsafeMethodCall: $oNullVar?->GetName()' => ['sExpression' => '$oNullVar?->GetName()', null], + 'NullsafeMethodCall: $oNullVar?->GetLongName("aa")' => ['sExpression' => '$oNullVar?->GetLongName("aa")', null], + 'NullsafeMethodCall: $oEvaluationFakeClass?->GetName()' => ['sExpression' => '$oEvaluationFakeClass?->GetName()', "gabuzomeu"], + 'NullsafeMethodCall: $oEvaluationFakeClass?->GetLongName("aa")' => ['sExpression' => '$oEvaluationFakeClass?->GetLongName("aa")', "gabuzomeu_aa"], + 'NullsafePropertyFetch: $oNullVar?->b' => ['sExpression' => '$oNullVar?->b', null], + 'NullsafePropertyFetch: $oEvaluationFakeClass?->iIsOk' => ['sExpression' => '$oEvaluationFakeClass?->iIsOk', "IsOkValue"], + 'PropertyFetch: $oEvaluationFakeClass->iIsOk' => ['sExpression' => '$oEvaluationFakeClass->iIsOk', "IsOkValue"], + 'StaticCall utils::GetItopVersionWikiSyntax()' => ['sExpression' => 'utils::GetItopVersionWikiSyntax()'], + 'StaticProperty: public existing constant' => [ 'sExpression' => 'Combodo\iTop\Test\UnitTest\Sources\PhpParser\Evaluation\PhpExpressionEvaluatorTest::$STATIC_PROPERTY'], + 'StaticProperty: private existing constant' => [ + 'sExpression' => 'Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery\PhpExpressionEvaluatorTest::$PRIVATE_STATIC_PROPERTY', + 'forced_expected' => null, + ], + 'Ternary: (true) ? 1 : 2' => ['sExpression' => '(true) ? 1 : 2'], + 'Ternary: (false) ? 1 : 2' => ['sExpression' => '(false) ? 1 : 2'], + 'UnaryMinus: -1' => ['sExpression' => '-1'], + 'UnaryPlus: +1' => ['sExpression' => '+1'], + 'Variable: $_SERVER' => ['sExpression' => '$_SERVER', ['toto' => 'titi']], + 'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar', null], + 'Variable: $oGlobalNonNullVar' => ['sExpression' => '$oGlobalNonNullVar', "a"], ]; } /** * @dataProvider EvaluateExpressionProvider */ - public function testEvaluateExpression($sExpression, $forced_expected="NOTPROVIDED") + public function testEvaluateExpressionWithItopAlgo($sExpression, $forced_expected="NOTPROVIDED") { - $oNullVar=null; + $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::ITOP_ALGO); + } + + /** + * @dataProvider EvaluateExpressionProvider + */ + public function testEvaluateExpressionWithLibAndItopFallback($sExpression, $forced_expected="NOTPROVIDED") + { + $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::LIB_AND_FALLBACK); + } + + /** + * @dataProvider EvaluateExpressionProvider + */ + public function testEvaluateExpressionWithLibOnly($sExpression, $forced_expected="NOTPROVIDED") + { + $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::LIB_ONLY); + } + + public function evaluateExpressionWithMode($sExpression, $forced_expected, $iMode) + { + global $oGlobalNonNullVar; + $oGlobalNonNullVar="a"; $oNonNullVar="a"; + + $oNullVar=null; $_SERVER=[ 'toto' => 'titi', ]; + global $oEvaluationFakeClass; $oEvaluationFakeClass = new EvaluationFakeClass(); - $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression); + $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression, $iMode); if ($forced_expected === "NOTPROVIDED"){ $this->assertEquals($this->UnprotectedComputeExpression($sExpression), $res, $sExpression); } else { @@ -159,7 +190,7 @@ public function testEvaluateBooleanExpression_Autoselect(string $sBooleanExpress } class EvaluationFakeClass { - public bool $bIsOk=true; + public string $iIsOk="IsOkValue"; public function GetName() { @@ -173,6 +204,6 @@ public function GetLongName($suffix) public function __toString(): string { - return "a"; + return "toString"; } } \ No newline at end of file From 7e7b5874a648d8d2a011994b3b9e811d85e570c6 Mon Sep 17 00:00:00 2001 From: odain Date: Fri, 5 Sep 2025 15:21:15 +0200 Subject: [PATCH 22/23] Evaluator fixes/enhancements + tests --- .../lib/PhpParser/ConstExprEvaluator.php | 166 ++++++++++-------- .../Evaluation/IdenticalEvaluator.php | 16 ++ .../PhpParser/Evaluation/IssetEvaluator.php | 8 +- .../Evaluation/PhpExpressionEvaluator.php | 22 ++- .../Evaluation/VariableEvaluator.php | 17 +- .../modulediscovery/ModuleFileReaderTest.php | 4 +- .../Evaluation/PhpExpressionEvaluatorTest.php | 24 ++- 7 files changed, 164 insertions(+), 93 deletions(-) create mode 100644 sources/PhpParser/Evaluation/IdenticalEvaluator.php diff --git a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php index 79de55005b..74fcc2fb89 100644 --- a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php @@ -3,10 +3,6 @@ namespace PhpParser; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ClassConstFetch; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\StaticCall; -use PhpParser\Node\Expr\Variable; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Scalar; @@ -37,13 +33,13 @@ class ConstExprEvaluator { /** @var callable|null */ private $fallbackEvaluator; - /** @var array $functions_whitelist */ - private $functions_whitelist; + /** @var array $functionsWhiteList */ + private $functionsWhiteList; - /** @var array staticcalls_whitelist */ - private $staticcalls_whitelist; + /** @var array $staticCallsWhitelist */ + private $staticCallsWhitelist; - /** + /** * Create a constant expression evaluator. * * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See @@ -58,19 +54,19 @@ public function __construct(?callable $fallbackEvaluator = null) { ); }; - $this->functions_whitelist=[]; - $this->staticcalls_whitelist=[]; + $this->functionsWhiteList = []; + $this->staticCallsWhitelist = []; } - public function setFunctionsWhitelist(array $functions_whitelist): void + public function setFunctionsWhitelist(array $functionsWhiteList): void { - $this->functions_whitelist = $functions_whitelist; + $this->functionsWhiteList = $functionsWhiteList; } - public function setStaticcallsWhitelist(array $staticcalls_whitelist): void + public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { - $this->staticcalls_whitelist = $staticcalls_whitelist; - } + $this->staticCallsWhitelist = $staticCallsWhitelist; + } /** * Silently evaluates a constant expression into a PHP value. @@ -141,6 +137,10 @@ private function evaluate(Expr $expr) { return $this->evaluateArray($expr); } + if ($expr instanceof Expr\Variable) { + return $this->evaluateVariable($expr); + } + // Unary operators if ($expr instanceof Expr\UnaryPlus) { return +$this->evaluate($expr->expr); @@ -191,10 +191,6 @@ private function evaluate(Expr $expr) { return $this->evaluateFuncCall($expr); } - if ($expr instanceof Expr\Variable) { - return $this->evaluateVariable($expr); - } - if ($expr instanceof Expr\StaticCall) { return $this->evaluateStaticCall($expr); } @@ -236,12 +232,14 @@ private function evaluateTernary(Expr\Ternary $expr) { /** @return mixed */ private function evaluateBinaryOp(Expr\BinaryOp $expr) { - if ($expr instanceof Expr\BinaryOp\Coalesce - && $expr->left instanceof Expr\ArrayDimFetch - ) { - // This needs to be special cased to respect BP_VAR_IS fetch semantics - return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)] - ?? $this->evaluate($expr->right); + if ($expr instanceof Expr\BinaryOp\Coalesce) { + try { + $var = $this->evaluate($expr->left); + return $var ?? $this->evaluate($expr->right); + } catch(\Throwable $t){ + //handle when isset($expr->left->var)===false + return $this->evaluate($expr->right); + } } // The evaluate() calls are repeated in each branch, because some of the operators are @@ -291,7 +289,7 @@ private function evaluateConstFetch(Expr\ConstFetch $expr) { if(! is_string($name)){ //PHP_VERSION_ID usecase $name = $name->name; - } + } if (defined($name)){ return constant($name); @@ -314,7 +312,7 @@ private function evaluateIsset(Expr\Isset_ $expr) { return true; } catch(\Throwable $t){ return false; - }; + } } /** @return mixed */ @@ -410,13 +408,14 @@ private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) private function evaluateFuncCall(Expr\FuncCall $expr) { try { - if ($expr->name instanceof Name){ - $function = $expr->name->name; + $name = $expr->name; + if ($name instanceof Name){ + $function = $name->name; } else { - $function = $this->evaluate($expr->name); + $function = $this->evaluate($name); } - if (! in_array($function, $this->functions_whitelist)){ + if (! in_array($function, $this->functionsWhiteList)){ throw new Exception("FuncCall $function not supported"); } @@ -439,11 +438,16 @@ private function evaluateVariable(Expr\Variable $expr) { try { $name = $expr->name; - if (! is_null($name) && isset($name)) { + if (array_key_exists($name, get_defined_vars())) { + return $$name; + } + + if (array_key_exists($name, $GLOBALS)) { global $$name; return $$name; } - } catch (\Throwable $t) {} + } catch (\Throwable $t) { + } return ($this->fallbackEvaluator)($expr); } @@ -452,15 +456,15 @@ private function evaluateVariable(Expr\Variable $expr) private function evaluateStaticCall(Expr\StaticCall $expr) { try { - $classname = $expr->class->name; + $class = $expr->class->name; if ($expr->name instanceof Identifier){ - $methodname = $expr->name->name; + $method = $expr->name->name; } else { - $methodname = $this->evaluate($expr->name); + $method = $this->evaluate($expr->name); } - $static_call_description = "$classname::$methodname"; - if (! in_array($static_call_description, $this->staticcalls_whitelist)){ + $static_call_description = "$class::$method"; + if (! in_array($static_call_description, $this->staticCallsWhitelist)){ throw new Exception("StaticCall $static_call_description not supported"); } @@ -470,8 +474,8 @@ private function evaluateStaticCall(Expr\StaticCall $expr) $args[]=$arg->value->value; } - $class = new \ReflectionClass($classname); - $method = $class->getMethod($methodname); + $class = new \ReflectionClass($class); + $method = $class->getMethod($method); if ($method->isPublic()){ return $method->invokeArgs(null, $args); } @@ -480,59 +484,79 @@ private function evaluateStaticCall(Expr\StaticCall $expr) return ($this->fallbackEvaluator)($expr); } - /** @return mixed */ - private function evaluatePropertyFetch(Expr\NullsafePropertyFetch|Expr\PropertyFetch $expr) + /** + * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr + * + * @return mixed + */ + private function evaluatePropertyFetch($expr) { try { - $var = $this->evaluateVariable($expr->var); - if (is_null($var)) { - return null; - } + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } - if ($expr->name instanceof Identifier){ - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } + if (! is_null($var)) { + try { + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } - $reflectionClass = new \ReflectionClass(get_class($var)); - $property = $reflectionClass->getProperty($name); - if ($property->isPublic()){ - return $property->getValue($var); + $reflectionClass = new \ReflectionClass(get_class($var)); + $property = $reflectionClass->getProperty($name); + if ($property->isPublic()) { + return $property->getValue($var); + } } - } catch (\Throwable $t) {} + catch (\Throwable $t) {} + } else if ($expr instanceof Expr\NullsafePropertyFetch){ + return null; + } return ($this->fallbackEvaluator)($expr); } - /** @return mixed */ - private function evaluateMethodCall(Expr\MethodCall|Expr\NullsafeMethodCall $expr) + /** + * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr + * + * @return mixed + */ + private function evaluateMethodCall($expr) { try { - $var = $this->evaluateVariable($expr->var); - if (is_null($var)) { - return null; - } + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } - $args=[]; - foreach ($expr->args as $arg){ + if (! is_null($var)) { + try { + $args = []; + foreach ($expr->args as $arg) { /** @var \PhpParser\Node\Arg $arg */ - $args[]=$arg->value->value; + $args[] = $arg->value->value; } - if ($expr->name instanceof Identifier){ + if ($expr->name instanceof Identifier) { $name = $expr->name->name; } else { $name = $this->evaluate($expr->name); } + $reflectionClass = new \ReflectionClass(get_class($var)); $method = $reflectionClass->getMethod($name); - if ($method->isPublic()){ - return $method->invokeArgs($var, $args); + if ($method->isPublic()) { + return $method->invokeArgs($var, $args); + } } - } catch (\Throwable $t) {} + catch (\Throwable $t) {} + } else if ($expr instanceof Expr\NullsafeMethodCall){ + return null; + } return ($this->fallbackEvaluator)($expr); } - } diff --git a/sources/PhpParser/Evaluation/IdenticalEvaluator.php b/sources/PhpParser/Evaluation/IdenticalEvaluator.php new file mode 100644 index 0000000000..9798eedbe8 --- /dev/null +++ b/sources/PhpParser/Evaluation/IdenticalEvaluator.php @@ -0,0 +1,16 @@ +vars as $oVar){ - $var = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oVar); - if (! isset($var)){ + try{ + $var = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oVar); + if (is_null($var)){ + return false; + } + } catch (\Throwable $t) { return false; } } diff --git a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php index 8abad02632..4b870c31dd 100644 --- a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php +++ b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php @@ -12,10 +12,19 @@ class PhpExpressionEvaluator { /** @var iExprEvaluator[] $aPhpParserEvaluators */ private static array $aPhpParserEvaluators; + private int $iMode=self::ITOP_ALGO; protected function __construct() { } + const LIB_AND_FALLBACK=1; + const LIB_ONLY=2; + const ITOP_ALGO=3; + public function SetMode($iMode) + { + $this->iMode =$iMode; + } + final public static function GetInstance(): PhpExpressionEvaluator { if (!isset(static::$oInstance)) { static::$oInstance = new static(); @@ -58,13 +67,13 @@ final public static function SetInstance(?PhpExpressionEvaluator $oInstance): vo static::$oInstance = $oInstance; } - public function EvaluateExpression(Expr $oExpression, int $iMode=self::LIB_AND_FALLBACK) : mixed + public function EvaluateExpression(Expr $oExpression) : mixed { - if ($iMode==self::ITOP_ALGO){ + if ($this->iMode===self::ITOP_ALGO){ return $this->EvaluateExpressionLocally($oExpression); } - if ($iMode==self::LIB_ONLY){ + if ($this->iMode==self::LIB_ONLY){ $oConstExprEvaluator = new ConstExprEvaluator(); } else { $oConstExprEvaluator = new ConstExprEvaluator([$this, "EvaluateExpressionLocally"]); @@ -97,10 +106,7 @@ public function ParseAndEvaluateBooleanExpression(string $sBooleanExpr) : bool return $this->ParseAndEvaluateExpression($sBooleanExpr); } - const LIB_AND_FALLBACK=1; - const LIB_ONLY=2; - const ITOP_ALGO=3; - public function ParseAndEvaluateExpression(string $sExpr, int $iMode=self::LIB_AND_FALLBACK) : mixed + public function ParseAndEvaluateExpression(string $sExpr) : mixed { $sPhpContent = <<ParsePhpCode($sPhpContent); $oExpr = $aNodes[0]; - return $this->EvaluateExpression($oExpr->expr, $iMode); + return $this->EvaluateExpression($oExpr->expr); } catch (\Throwable $t) { throw new ModuleFileReaderException("Eval of '$sExpr' caused an error:".$t->getMessage()); } diff --git a/sources/PhpParser/Evaluation/VariableEvaluator.php b/sources/PhpParser/Evaluation/VariableEvaluator.php index 967224c9b4..f1b8acea11 100644 --- a/sources/PhpParser/Evaluation/VariableEvaluator.php +++ b/sources/PhpParser/Evaluation/VariableEvaluator.php @@ -9,19 +9,20 @@ class VariableEvaluator extends AbstractExprEvaluator { public function GetHandledExpressionType(): ?string { return Variable::class; } - public function Evaluate(Expr $oExpr): mixed { /** @var Variable $oExpr */ - if (is_null($oExpr->name)){ - return null; + $sName = $oExpr->name; + + if (array_key_exists($sName, get_defined_vars())) { + return $$sName; } - if (! isset($oExpr->name)) { - return null; + if (array_key_exists($sName, $GLOBALS)) { + global $$sName; + return $$sName; } - $sVarname=$oExpr->name; - global $$sVarname; - return $$sVarname; + return null; } + } \ No newline at end of file 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 b3b7c99415..7b0c52b937 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php @@ -27,7 +27,7 @@ public function testReadModuleFileInformationUnsafe() $this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null); } - /*public function testAllReadModuleFileConfiguration() + public function testAllReadModuleFileConfiguration() { $_SERVER=[ 'SERVER_NAME' => 'titi' @@ -54,7 +54,7 @@ public function testReadModuleFileInformationUnsafe() } $this->assertEquals([], $aErrors, var_export($aErrors, true)); - }*/ + } public static function ReadModuleFileConfigurationFileNameProvider() { diff --git a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php index ed1d07fd7c..1e722dc8f7 100644 --- a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php @@ -10,6 +10,12 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase { private static $PRIVATE_STATIC_PROPERTY = 123; private const PRIVATE_CONSTANT = 123; + protected function tearDown(): void + { + parent::tearDown(); // TODO: Change the autogenerated stub + PhpExpressionEvaluator::GetInstance()->SetMode(PhpExpressionEvaluator::ITOP_ALGO); + } + public static function EvaluateExpressionProvider() { return [ 'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'], @@ -19,7 +25,7 @@ public static function EvaluateExpressionProvider() { 'BinaryOperator: false|true' => [ 'sExpression' => 'false|true'], 'BinaryOperator: false||true' => [ 'sExpression' => 'false||true'], 'BinaryOperator: false&&true' => [ 'sExpression' => 'false&&true'], - 'BinaryOperator: true&&true&&true&&false' => [ 'sExpression' => 'true&&true&&true&&false'], + 'BinaryOperator: true&&true&&true&&false' => [ 'sExpression' => 'true && true && true && false'], 'BinaryOperator: false&true' => [ 'sExpression' => 'false&true'], 'BinaryOperator: ! true' => [ 'sExpression' => '! true'], 'BinaryOperator: 10 * 5' => [ 'sExpression' => '10 * 5'], @@ -30,6 +36,7 @@ public static function EvaluateExpressionProvider() { 'BinaryOperator: PHP_VERSION_ID != PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID != PHP_VERSION_ID'], 'BitwiseNot: ~3' => ['sExpression' => '~3'], 'BitwiseXor: 3^2' => ['sExpression' => '3^2'], + 'BooleanAnd: true && false' => ['sExpression' => 'true && false'], 'Cast: (array)3' => ['sExpression' => '(array)3'], 'Cast: (bool)1' => ['sExpression' => '(bool)1'], 'Cast: (bool)0' => ['sExpression' => '(bool)0'], @@ -48,14 +55,21 @@ public static function EvaluateExpressionProvider() { ], 'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1', 1], 'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1', 1], + 'Coalesce: $_SERVER["toto"] ?? 1' => ['sExpression' => '$_SERVER["toto"] ?? 1', "titi"], + 'Coalesce: $_SERVER["unknown_key"] ?? 1' => ['sExpression' => '$_SERVER["unknown_key"] ?? 1', 1], 'Coalesce: $oGlobalNonNullVar ?? 1' => ['sExpression' => '$oGlobalNonNullVar ?? 1', "a"], + 'Coalesce: $oGlobalNullVar ?? 1' => ['sExpression' => '$oGlobalNullVar ?? 1', 1], 'Concat: "a"."b"' => ['sExpression' => '"a"."b"'], 'ConstFetch: false' => [ 'sExpression' => 'false'], 'ConstFetch: (false)' => [ 'sExpression' => 'false'], 'ConstFetch: true' => [ 'sExpression' => 'true'], 'ConstFetch: (true)' => [ 'sExpression' => 'true'], + 'Equal: 1 == true' => [ 'sExpression' => '1 == true', true], + 'Equal: 1 == false' => [ 'sExpression' => '1 == false', false], 'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'], 'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'], + 'Identical: 1==="1"' => ['sExpression' => '1==="1"', false], + 'Identical: "1"==="1"' => ['sExpression' => '"1"==="1"', true], 'Isset: isset($oNonNullVar)' => ['sExpression' => 'isset($oNonNullVar)', false], 'Isset: isset($oGlobalNonNullVar)' => ['sExpression' => 'isset($oGlobalNonNullVar)', true], 'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)', false], @@ -85,6 +99,7 @@ public static function EvaluateExpressionProvider() { 'Variable: $_SERVER' => ['sExpression' => '$_SERVER', ['toto' => 'titi']], 'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar', null], 'Variable: $oGlobalNonNullVar' => ['sExpression' => '$oGlobalNonNullVar', "a"], + 'Variable: $oEvaluationFakeClass' => ['sExpression' => '$oEvaluationFakeClass', new EvaluationFakeClass()], ]; } @@ -116,6 +131,10 @@ public function evaluateExpressionWithMode($sExpression, $forced_expected, $iMod { global $oGlobalNonNullVar; $oGlobalNonNullVar="a"; + + global $oGlobalNullVar; + $oGlobalNullVar=null; + $oNonNullVar="a"; $oNullVar=null; @@ -126,7 +145,8 @@ public function evaluateExpressionWithMode($sExpression, $forced_expected, $iMod global $oEvaluationFakeClass; $oEvaluationFakeClass = new EvaluationFakeClass(); - $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression, $iMode); + PhpExpressionEvaluator::GetInstance()->SetMode($iMode); + $res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression); if ($forced_expected === "NOTPROVIDED"){ $this->assertEquals($this->UnprotectedComputeExpression($sExpression), $res, $sExpression); } else { From 9872702f59bb3bf143997fdd7560b668a1a0d302 Mon Sep 17 00:00:00 2001 From: odain Date: Fri, 5 Sep 2025 15:35:38 +0200 Subject: [PATCH 23/23] revert nikic enhancements and keep them for later + finalize work and tests --- .../lib/PhpParser/ConstExprEvaluator.php | 651 +++++------------- .../Evaluation/PhpExpressionEvaluator.php | 4 +- .../modulediscovery/ModuleFileReaderTest.php | 54 +- .../{ => all}/module.authent-ldap.php | 0 .../module.combodo-email-synchro.php | 0 ...files-bridge-for-combodo-email-synchro.php | 0 .../module.itop-admin-delegation-profiles.php | 0 .../{ => all}/module.itop-full-itil.php | 0 .../module.itop-global-requests-mgmt.php | 0 .../{ => all}/module.itop-tickets.php | 0 .../Evaluation/PhpExpressionEvaluatorTest.php | 8 - 11 files changed, 176 insertions(+), 541 deletions(-) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.authent-ldap.php (100%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.combodo-email-synchro.php (100%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php (100%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.itop-admin-delegation-profiles.php (100%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.itop-full-itil.php (100%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.itop-global-requests-mgmt.php (100%) rename tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/{ => all}/module.itop-tickets.php (100%) diff --git a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php index 74fcc2fb89..c99d0f82d3 100644 --- a/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php @@ -3,10 +3,7 @@ namespace PhpParser; use PhpParser\Node\Expr; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; use PhpParser\Node\Scalar; -use Exception; use function array_merge; @@ -30,531 +27,209 @@ * affected by the LC_NUMERIC locale. */ class ConstExprEvaluator { - /** @var callable|null */ - private $fallbackEvaluator; + /** @var callable|null */ + private $fallbackEvaluator; - /** @var array $functionsWhiteList */ - private $functionsWhiteList; - - /** @var array $staticCallsWhitelist */ - private $staticCallsWhitelist; - - /** - * Create a constant expression evaluator. - * - * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See - * class doc comment for more information. - * - * @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated - */ - public function __construct(?callable $fallbackEvaluator = null) { - $this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) { - throw new ConstExprEvaluationException( - "Expression of type {$expr->getType()} cannot be evaluated" - ); - }; - - $this->functionsWhiteList = []; - $this->staticCallsWhitelist = []; - } - - public function setFunctionsWhitelist(array $functionsWhiteList): void - { - $this->functionsWhiteList = $functionsWhiteList; + /** + * Create a constant expression evaluator. + * + * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See + * class doc comment for more information. + * + * @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated + */ + public function __construct(?callable $fallbackEvaluator = null) { + $this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) { + throw new ConstExprEvaluationException( + "Expression of type {$expr->getType()} cannot be evaluated" + ); + }; } - public function setStaticCallsWhitelist(array $staticCallsWhitelist): void - { - $this->staticCallsWhitelist = $staticCallsWhitelist; - } - - /** - * Silently evaluates a constant expression into a PHP value. - * - * Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException. - * The original source of the exception is available through getPrevious(). - * - * If some part of the expression cannot be evaluated, the fallback evaluator passed to the - * constructor will be invoked. By default, if no fallback is provided, an exception of type - * ConstExprEvaluationException is thrown. - * - * See class doc comment for caveats and limitations. - * - * @param Expr $expr Constant expression to evaluate - * @return mixed Result of evaluation - * - * @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred - */ - public function evaluateSilently(Expr $expr) { - set_error_handler(function ($num, $str, $file, $line) { - throw new \ErrorException($str, 0, $num, $file, $line); - }); - - try { - return $this->evaluate($expr); - } catch (\Throwable $e) { - if (!$e instanceof ConstExprEvaluationException) { - $e = new ConstExprEvaluationException( - "An error occurred during constant expression evaluation", 0, $e); - } - throw $e; - } finally { - restore_error_handler(); - } - } - - /** - * Directly evaluates a constant expression into a PHP value. - * - * May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these - * into a ConstExprEvaluationException. - * - * If some part of the expression cannot be evaluated, the fallback evaluator passed to the - * constructor will be invoked. By default, if no fallback is provided, an exception of type - * ConstExprEvaluationException is thrown. - * - * See class doc comment for caveats and limitations. - * - * @param Expr $expr Constant expression to evaluate - * @return mixed Result of evaluation - * - * @throws ConstExprEvaluationException if the expression cannot be evaluated - */ - public function evaluateDirectly(Expr $expr) { - return $this->evaluate($expr); - } - - /** @return mixed */ - private function evaluate(Expr $expr) { - if ($expr instanceof Scalar\Int_ - || $expr instanceof Scalar\Float_ - || $expr instanceof Scalar\String_ - ) { - return $expr->value; - } - - if ($expr instanceof Expr\Array_) { - return $this->evaluateArray($expr); - } - - if ($expr instanceof Expr\Variable) { - return $this->evaluateVariable($expr); - } - - // Unary operators - if ($expr instanceof Expr\UnaryPlus) { - return +$this->evaluate($expr->expr); - } - if ($expr instanceof Expr\UnaryMinus) { - return -$this->evaluate($expr->expr); - } - if ($expr instanceof Expr\BooleanNot) { - return !$this->evaluate($expr->expr); - } - if ($expr instanceof Expr\BitwiseNot) { - return ~$this->evaluate($expr->expr); - } - - if ($expr instanceof Expr\BinaryOp) { - return $this->evaluateBinaryOp($expr); - } - - if ($expr instanceof Expr\Ternary) { - return $this->evaluateTernary($expr); - } - - if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) { - return $this->evaluate($expr->var)[$this->evaluate($expr->dim)]; - } - - if ($expr instanceof Expr\ConstFetch) { - return $this->evaluateConstFetch($expr); - } - - if ($expr instanceof Expr\Isset_) { - return $this->evaluateIsset($expr); - } - - if ($expr instanceof Expr\ClassConstFetch) { - return $this->evaluateClassConstFetch($expr); - } - - if ($expr instanceof Expr\Cast) { - return $this->evaluateCast($expr); - } - - if ($expr instanceof Expr\StaticPropertyFetch) { - return $this->evaluateStaticPropertyFetch($expr); - } - - if ($expr instanceof Expr\FuncCall) { - return $this->evaluateFuncCall($expr); - } - - if ($expr instanceof Expr\StaticCall) { - return $this->evaluateStaticCall($expr); - } - - if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { - return $this->evaluatePropertyFetch($expr); - } - - if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { - return $this->evaluateMethodCall($expr); - } - return ($this->fallbackEvaluator)($expr); - } - - private function evaluateArray(Expr\Array_ $expr): array { - $array = []; - foreach ($expr->items as $item) { - if (null !== $item->key) { - $array[$this->evaluate($item->key)] = $this->evaluate($item->value); - } elseif ($item->unpack) { - $array = array_merge($array, $this->evaluate($item->value)); - } else { - $array[] = $this->evaluate($item->value); - } - } - return $array; - } - - /** @return mixed */ - private function evaluateTernary(Expr\Ternary $expr) { - if (null === $expr->if) { - return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else); - } - - return $this->evaluate($expr->cond) - ? $this->evaluate($expr->if) - : $this->evaluate($expr->else); - } - - /** @return mixed */ - private function evaluateBinaryOp(Expr\BinaryOp $expr) { - if ($expr instanceof Expr\BinaryOp\Coalesce) { - try { - $var = $this->evaluate($expr->left); - return $var ?? $this->evaluate($expr->right); - } catch(\Throwable $t){ - //handle when isset($expr->left->var)===false - return $this->evaluate($expr->right); - } - } - - // The evaluate() calls are repeated in each branch, because some of the operators are - // short-circuiting and evaluating the RHS in advance may be illegal in that case - $l = $expr->left; - $r = $expr->right; - switch ($expr->getOperatorSigil()) { - case '&': return $this->evaluate($l) & $this->evaluate($r); - case '|': return $this->evaluate($l) | $this->evaluate($r); - case '^': return $this->evaluate($l) ^ $this->evaluate($r); - case '&&': return $this->evaluate($l) && $this->evaluate($r); - case '||': return $this->evaluate($l) || $this->evaluate($r); - case '??': return $this->evaluate($l) ?? $this->evaluate($r); - case '.': return $this->evaluate($l) . $this->evaluate($r); - case '/': return $this->evaluate($l) / $this->evaluate($r); - case '==': return $this->evaluate($l) == $this->evaluate($r); - case '>': return $this->evaluate($l) > $this->evaluate($r); - case '>=': return $this->evaluate($l) >= $this->evaluate($r); - case '===': return $this->evaluate($l) === $this->evaluate($r); - case 'and': return $this->evaluate($l) and $this->evaluate($r); - case 'or': return $this->evaluate($l) or $this->evaluate($r); - case 'xor': return $this->evaluate($l) xor $this->evaluate($r); - case '-': return $this->evaluate($l) - $this->evaluate($r); - case '%': return $this->evaluate($l) % $this->evaluate($r); - case '*': return $this->evaluate($l) * $this->evaluate($r); - case '!=': return $this->evaluate($l) != $this->evaluate($r); - case '!==': return $this->evaluate($l) !== $this->evaluate($r); - case '+': return $this->evaluate($l) + $this->evaluate($r); - case '**': return $this->evaluate($l) ** $this->evaluate($r); - case '<<': return $this->evaluate($l) << $this->evaluate($r); - case '>>': return $this->evaluate($l) >> $this->evaluate($r); - case '<': return $this->evaluate($l) < $this->evaluate($r); - case '<=': return $this->evaluate($l) <= $this->evaluate($r); - case '<=>': return $this->evaluate($l) <=> $this->evaluate($r); - case '|>': - $lval = $this->evaluate($l); - return $this->evaluate($r)($lval); - } - - throw new \Exception('Should not happen'); - } - - /** @return mixed */ - private function evaluateConstFetch(Expr\ConstFetch $expr) { - try { - $name = $expr->name; - if(! is_string($name)){ - //PHP_VERSION_ID usecase - $name = $name->name; - } - - if (defined($name)){ - return constant($name); - } - } catch(\Throwable $t){} - - return ($this->fallbackEvaluator)($expr); - } + /** + * Silently evaluates a constant expression into a PHP value. + * + * Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException. + * The original source of the exception is available through getPrevious(). + * + * If some part of the expression cannot be evaluated, the fallback evaluator passed to the + * constructor will be invoked. By default, if no fallback is provided, an exception of type + * ConstExprEvaluationException is thrown. + * + * See class doc comment for caveats and limitations. + * + * @param Expr $expr Constant expression to evaluate + * @return mixed Result of evaluation + * + * @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred + */ + public function evaluateSilently(Expr $expr) { + set_error_handler(function ($num, $str, $file, $line) { + throw new \ErrorException($str, 0, $num, $file, $line); + }); - /** @return mixed */ - private function evaluateIsset(Expr\Isset_ $expr) { try { - foreach ($expr->vars as $var){ - $var = $this->evaluate($var); - if (! isset($var)){ - return false; - } - } - - return true; - } catch(\Throwable $t){ - return false; + return $this->evaluate($expr); + } catch (\Throwable $e) { + if (!$e instanceof ConstExprEvaluationException) { + $e = new ConstExprEvaluationException( + "An error occurred during constant expression evaluation", 0, $e); + } + throw $e; + } finally { + restore_error_handler(); } } - /** @return mixed */ - private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { - try { - $classname = $expr->class->name; - $property = $expr->name->name; - - if ('class' === $property){ - return $classname; - } - - if (class_exists($classname)){ - $class = new \ReflectionClass($classname); - if (array_key_exists($property, $class->getConstants())) { - $oReflectionConstant = $class->getReflectionConstant($property); - if ($oReflectionConstant->isPublic()){ - return $class->getConstant($property); - } - } - } - } catch(\Throwable $t){} - - return ($this->fallbackEvaluator)($expr); + /** + * Directly evaluates a constant expression into a PHP value. + * + * May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these + * into a ConstExprEvaluationException. + * + * If some part of the expression cannot be evaluated, the fallback evaluator passed to the + * constructor will be invoked. By default, if no fallback is provided, an exception of type + * ConstExprEvaluationException is thrown. + * + * See class doc comment for caveats and limitations. + * + * @param Expr $expr Constant expression to evaluate + * @return mixed Result of evaluation + * + * @throws ConstExprEvaluationException if the expression cannot be evaluated + */ + public function evaluateDirectly(Expr $expr) { + return $this->evaluate($expr); } /** @return mixed */ - private function evaluateCast(Expr\Cast $expr) { - try { - $subexpr = $this->evaluate($expr->expr); - $type = get_class($expr); - switch ($type){ - case Expr\Cast\Array_::class: - return (array) $subexpr; - - case Expr\Cast\Bool_::class: - return (bool) $subexpr; - - case Expr\Cast\Double::class: - switch ($expr->getAttribute("kind")){ - case Expr\Cast\Double::KIND_DOUBLE: - return (double) $subexpr; - - case Expr\Cast\Double::KIND_FLOAT: - case Expr\Cast\Double::KIND_REAL: - return (float) $subexpr; - } - - break; + private function evaluate(Expr $expr) { + if ($expr instanceof Scalar\Int_ + || $expr instanceof Scalar\Float_ + || $expr instanceof Scalar\String_ + ) { + return $expr->value; + } - case Expr\Cast\Int_::class: - return (int) $subexpr; + if ($expr instanceof Expr\Array_) { + return $this->evaluateArray($expr); + } - case Expr\Cast\Object_::class: - return (object) $subexpr; + // Unary operators + if ($expr instanceof Expr\UnaryPlus) { + return +$this->evaluate($expr->expr); + } + if ($expr instanceof Expr\UnaryMinus) { + return -$this->evaluate($expr->expr); + } + if ($expr instanceof Expr\BooleanNot) { + return !$this->evaluate($expr->expr); + } + if ($expr instanceof Expr\BitwiseNot) { + return ~$this->evaluate($expr->expr); + } - case Expr\Cast\String_::class: - return (string) $subexpr; - } - } catch(\Throwable $t){ + if ($expr instanceof Expr\BinaryOp) { + return $this->evaluateBinaryOp($expr); } - return ($this->fallbackEvaluator)($expr); - } + if ($expr instanceof Expr\Ternary) { + return $this->evaluateTernary($expr); + } - /** @return mixed */ - private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) - { - try { - $classname = $expr->class->name; - if ($expr->name instanceof Identifier){ - $property = $expr->name->name; - } else { - $property = $this->evaluate($expr->name); - } + if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) { + return $this->evaluate($expr->var)[$this->evaluate($expr->dim)]; + } - if (class_exists($classname)){ - $class = new \ReflectionClass($classname); - if (array_key_exists($property, $class->getStaticProperties())) { - $oReflectionProperty = $class->getProperty($property); - if ($oReflectionProperty->isPublic()){ - return $class->getStaticPropertyValue($property); - } - } - } + if ($expr instanceof Expr\ConstFetch) { + return $this->evaluateConstFetch($expr); } - catch (\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } - /** @return mixed */ - private function evaluateFuncCall(Expr\FuncCall $expr) - { - try { - $name = $expr->name; - if ($name instanceof Name){ - $function = $name->name; + private function evaluateArray(Expr\Array_ $expr): array { + $array = []; + foreach ($expr->items as $item) { + if (null !== $item->key) { + $array[$this->evaluate($item->key)] = $this->evaluate($item->value); + } elseif ($item->unpack) { + $array = array_merge($array, $this->evaluate($item->value)); } else { - $function = $this->evaluate($name); - } - - if (! in_array($function, $this->functionsWhiteList)){ - throw new Exception("FuncCall $function not supported"); - } - - $args=[]; - foreach ($expr->args as $arg){ - /** @var \PhpParser\Node\Arg $arg */ - $args[]=$arg->value->value; + $array[] = $this->evaluate($item->value); } - - $reflection_function = new \ReflectionFunction($function); - return $reflection_function->invoke(...$args); } - catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); + return $array; } /** @return mixed */ - private function evaluateVariable(Expr\Variable $expr) - { - try { - $name = $expr->name; - if (array_key_exists($name, get_defined_vars())) { - return $$name; - } - - if (array_key_exists($name, $GLOBALS)) { - global $$name; - return $$name; - } - } catch (\Throwable $t) { + private function evaluateTernary(Expr\Ternary $expr) { + if (null === $expr->if) { + return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else); } - return ($this->fallbackEvaluator)($expr); + return $this->evaluate($expr->cond) + ? $this->evaluate($expr->if) + : $this->evaluate($expr->else); } /** @return mixed */ - private function evaluateStaticCall(Expr\StaticCall $expr) - { - try { - $class = $expr->class->name; - if ($expr->name instanceof Identifier){ - $method = $expr->name->name; - } else { - $method = $this->evaluate($expr->name); - } - - $static_call_description = "$class::$method"; - if (! in_array($static_call_description, $this->staticCallsWhitelist)){ - throw new Exception("StaticCall $static_call_description not supported"); - } - - $args=[]; - foreach ($expr->args as $arg){ - /** @var \PhpParser\Node\Arg $arg */ - $args[]=$arg->value->value; - } - - $class = new \ReflectionClass($class); - $method = $class->getMethod($method); - if ($method->isPublic()){ - return $method->invokeArgs(null, $args); - } - } catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** - * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr - * - * @return mixed - */ - private function evaluatePropertyFetch($expr) - { - try { - $var = $this->evaluate($expr->var); - } catch (\Throwable $t) { - $var = null; + private function evaluateBinaryOp(Expr\BinaryOp $expr) { + if ($expr instanceof Expr\BinaryOp\Coalesce + && $expr->left instanceof Expr\ArrayDimFetch + ) { + // This needs to be special cased to respect BP_VAR_IS fetch semantics + return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)] + ?? $this->evaluate($expr->right); } - if (! is_null($var)) { - try { - if ($expr->name instanceof Identifier) { - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } - - $reflectionClass = new \ReflectionClass(get_class($var)); - $property = $reflectionClass->getProperty($name); - if ($property->isPublic()) { - return $property->getValue($var); - } - } - catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafePropertyFetch){ - return null; + // The evaluate() calls are repeated in each branch, because some of the operators are + // short-circuiting and evaluating the RHS in advance may be illegal in that case + $l = $expr->left; + $r = $expr->right; + switch ($expr->getOperatorSigil()) { + case '&': return $this->evaluate($l) & $this->evaluate($r); + case '|': return $this->evaluate($l) | $this->evaluate($r); + case '^': return $this->evaluate($l) ^ $this->evaluate($r); + case '&&': return $this->evaluate($l) && $this->evaluate($r); + case '||': return $this->evaluate($l) || $this->evaluate($r); + case '??': return $this->evaluate($l) ?? $this->evaluate($r); + case '.': return $this->evaluate($l) . $this->evaluate($r); + case '/': return $this->evaluate($l) / $this->evaluate($r); + case '==': return $this->evaluate($l) == $this->evaluate($r); + case '>': return $this->evaluate($l) > $this->evaluate($r); + case '>=': return $this->evaluate($l) >= $this->evaluate($r); + case '===': return $this->evaluate($l) === $this->evaluate($r); + case 'and': return $this->evaluate($l) and $this->evaluate($r); + case 'or': return $this->evaluate($l) or $this->evaluate($r); + case 'xor': return $this->evaluate($l) xor $this->evaluate($r); + case '-': return $this->evaluate($l) - $this->evaluate($r); + case '%': return $this->evaluate($l) % $this->evaluate($r); + case '*': return $this->evaluate($l) * $this->evaluate($r); + case '!=': return $this->evaluate($l) != $this->evaluate($r); + case '!==': return $this->evaluate($l) !== $this->evaluate($r); + case '+': return $this->evaluate($l) + $this->evaluate($r); + case '**': return $this->evaluate($l) ** $this->evaluate($r); + case '<<': return $this->evaluate($l) << $this->evaluate($r); + case '>>': return $this->evaluate($l) >> $this->evaluate($r); + case '<': return $this->evaluate($l) < $this->evaluate($r); + case '<=': return $this->evaluate($l) <= $this->evaluate($r); + case '<=>': return $this->evaluate($l) <=> $this->evaluate($r); + case '|>': + $lval = $this->evaluate($l); + return $this->evaluate($r)($lval); } - return ($this->fallbackEvaluator)($expr); + throw new \Exception('Should not happen'); } - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr - * - * @return mixed - */ - private function evaluateMethodCall($expr) - { - try { - $var = $this->evaluate($expr->var); - } catch (\Throwable $t) { - $var = null; - } - - if (! is_null($var)) { - try { - $args = []; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[] = $arg->value->value; - } - - if ($expr->name instanceof Identifier) { - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } - - $reflectionClass = new \ReflectionClass(get_class($var)); - $method = $reflectionClass->getMethod($name); - if ($method->isPublic()) { - return $method->invokeArgs($var, $args); - } - } - catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafeMethodCall){ - return null; + /** @return mixed */ + private function evaluateConstFetch(Expr\ConstFetch $expr) { + $name = $expr->name->toLowerString(); + switch ($name) { + case 'null': return null; + case 'false': return false; + case 'true': return true; } return ($this->fallbackEvaluator)($expr); diff --git a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php index 4b870c31dd..1d36bb8b79 100644 --- a/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php +++ b/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php @@ -12,7 +12,7 @@ class PhpExpressionEvaluator { /** @var iExprEvaluator[] $aPhpParserEvaluators */ private static array $aPhpParserEvaluators; - private int $iMode=self::ITOP_ALGO; + private int $iMode=self::LIB_AND_FALLBACK; protected function __construct() { } @@ -79,8 +79,6 @@ public function EvaluateExpression(Expr $oExpression) : mixed $oConstExprEvaluator = new ConstExprEvaluator([$this, "EvaluateExpressionLocally"]); } - $oConstExprEvaluator->setFunctionsWhitelist(FuncCallEvaluator::WHITELIST); - $oConstExprEvaluator->setStaticcallsWhitelist(StaticCallEvaluator::WHITELIST); return $oConstExprEvaluator->evaluateDirectly($oExpression); } 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 7b0c52b937..6b74b2c35f 100644 --- a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/ModuleFileReaderTest.php @@ -16,7 +16,7 @@ protected function setUp(): void public function testReadModuleFileInformationUnsafe() { - $sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php'; + $sModuleFilePath = __DIR__.'/resources/all/module.itop-full-itil.php'; $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); $this->assertCount(3, $aRes); @@ -27,52 +27,23 @@ public function testReadModuleFileInformationUnsafe() $this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null); } - public function testAllReadModuleFileConfiguration() - { - $_SERVER=[ - 'SERVER_NAME' => 'titi' - ]; - - $aErrors=[]; - foreach (glob(__DIR__.'/resources/all_designer/*.php') as $sModuleFilePath){ - //var_dump($sModuleFilePath); - try{ - $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); - } catch(\Exception $e){ - $aErrors[]=basename($sModuleFilePath); - continue; - } - - $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); - - if ($aExpected !== $aRes){ - $aErrors[]=basename($sModuleFilePath); - continue; - } - //break; - //$this->assertEquals($aExpected, $aRes, $sModuleFilePath); - } - - $this->assertEquals([], $aErrors, var_export($aErrors, true)); - } - public static function ReadModuleFileConfigurationFileNameProvider() { - 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'], - ]; + $aUsecases=[]; + foreach (glob(__DIR__.'/resources/all/*.php') as $sModuleFilePath){ + $aUsecases[basename($sModuleFilePath)]=[$sModuleFilePath]; + } + return $aUsecases; } /** * @dataProvider ReadModuleFileConfigurationFileNameProvider */ - public function testReadModuleFileConfigurationVsLegacyMethod(string $sModuleBasename) + public function testReadModuleFileConfigurationVsLegacyMethod(string $sModuleFilePath) { - $sModuleFilePath = __DIR__."/resources/$sModuleBasename"; + $_SERVER=[ + 'SERVER_NAME' => 'titi' + ]; $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); @@ -92,8 +63,7 @@ public function testReadModuleFileConfigurationVsLegacyMethod(string $sModuleBas * @throws \ModuleFileReaderException */ public function testReadModuleFileConfiguration_BadlyWrittenDependencies(){ - //$sModuleFilePath = __DIR__."/resources/module.combodo-make-it-vip.php"; - $sModuleFilePath = __DIR__."/resources/module.itop-admin-delegation-profiles.php"; + $sModuleFilePath = __DIR__."/resources/all/module.itop-admin-delegation-profiles.php"; $aRes = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); $aExpected = ModuleFileReader::GetInstance()->ReadModuleFileInformationUnsafe($sModuleFilePath); @@ -243,7 +213,7 @@ public function testReadModuleFileConfigurationCheckBasicStatement_LastElseIfApp public function testGetAndCheckModuleInstallerClass() { $sModuleInstallerClass = "TicketsInstaller" . uniqid(); - $sPHpCode = file_get_contents(__DIR__.'/resources/module.itop-tickets.php'); + $sPHpCode = file_get_contents(__DIR__.'/resources/all/module.itop-tickets.php'); $sPHpCode = str_replace("TicketsInstaller", $sModuleInstallerClass, $sPHpCode); $this->sTempModuleFilePath = tempnam(__DIR__, "test"); file_put_contents($this->sTempModuleFilePath, $sPHpCode); 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/all/module.authent-ldap.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.authent-ldap.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.authent-ldap.php 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/all/module.combodo-email-synchro.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.combodo-email-synchro.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.combodo-email-synchro.php 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/all/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-admin-delegation-profiles-bridge-for-combodo-email-synchro.php 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/all/module.itop-admin-delegation-profiles.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-admin-delegation-profiles.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-admin-delegation-profiles.php diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-full-itil.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-full-itil.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-full-itil.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-full-itil.php 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/all/module.itop-global-requests-mgmt.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-global-requests-mgmt.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-global-requests-mgmt.php diff --git a/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-tickets.php b/tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-tickets.php similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/module.itop-tickets.php rename to tests/php-unit-tests/unitary-tests/setup/modulediscovery/resources/all/module.itop-tickets.php diff --git a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php index 1e722dc8f7..d305e80e95 100644 --- a/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/PhpParser/Evaluation/PhpExpressionEvaluatorTest.php @@ -119,14 +119,6 @@ public function testEvaluateExpressionWithLibAndItopFallback($sExpression, $forc $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::LIB_AND_FALLBACK); } - /** - * @dataProvider EvaluateExpressionProvider - */ - public function testEvaluateExpressionWithLibOnly($sExpression, $forced_expected="NOTPROVIDED") - { - $this->evaluateExpressionWithMode($sExpression, $forced_expected, PhpExpressionEvaluator::LIB_ONLY); - } - public function evaluateExpressionWithMode($sExpression, $forced_expected, $iMode) { global $oGlobalNonNullVar;