Compare commits

..

18 Commits

Author SHA1 Message Date
Anne-Cath
f032710d7e N°9751 - Issue with AttributeImage in AttributeLinkedSetIndirect in the portal 2026-07-06 15:20:16 +02:00
Eric Espié
6b396adeea N°9571 - Do not allow DB change or install when upgrading iTop (#958)
* N°9571 - Do not allow DB change or install when upgrading iTop

* N°9571 - Fix unit tests

* N°9711 - Removed step Install or Upgrade

* N°9711 - After revue
2026-07-06 10:31:43 +02:00
odain
c448f8411c Merge branch 'support/3.2' into develop 2026-07-03 11:52:36 +02:00
odain
4b4fe55060 N°9689 - Fix itop-portal-base dependancy 2026-07-03 11:49:47 +02:00
Eric Espie
dedcd4af19 N°9711 - Update WizStepSummary to conditionally display database parameters based on return_application parameter 2026-07-03 09:23:13 +02:00
Eric Espie
0556a03839 N°9711 - Clear setup parameters on menu node rendering in UI.php (default page display) 2026-07-03 09:10:11 +02:00
Molkobain
64df429715 Merge remote-tracking branch 'origin/support/3.2' into develop 2026-07-02 21:12:38 +02:00
Molkobain
9ae813db2b Merge remote-tracking branch 'origin/support/3.2.3' into support/3.2 2026-07-02 21:08:45 +02:00
Molkobain
768423a6aa N°7371 - Simplify initial approach by using common API, and revert modification of unnecessary js/utils.js API 2026-07-02 20:51:30 +02:00
odain-cbd
921e7c88fe N°9675 - Set up crash - cannot get class from production (#953)
* N°9675 - raise first exception encountered when starting metamodel

* N°9675 - enhance setup data consistency error feedback

* N°9675 - raise PHP CLI version issue as well

* N°9675 - geptile PR fixes + enhance regexp for cli php version

* cleanup

* N°9675 - remove maintenance mode before any setup compilation

* N°9675 - share php cli check in SetupUtils

* N°9675 - exit maintenance only before audit
2026-07-02 16:59:04 +02:00
Molkobain
d6ef4fb7bb N°9687 - Fix incorrect return type for the DBObjectSearch::ApplyDataFilter() function (#939)
* N°9687 - Fix incorrect return type for the DBObjectSearch::ApplyDataFilter() function

* N°9687 - Add unit test

* N°9687 - Improve unit test after code review

* N°9687 - Improve unit test after code review
2026-07-02 16:11:32 +02:00
Eric Espié
a335e1004b N°9711 - Block user from going back to the setup (#954)
* N°9711 - Store setup parameters in session

* N°9711 - Restrict database parameters display to administrators in setup summary
2026-07-02 12:00:22 +02:00
Molkobain
e882df96d9 📝 Remove duplicated table in PR template 2026-07-02 11:55:34 +02:00
Eric Espié
3a7439e970 N°9639 - Uninstallation Analysis result doesn't list all the object (#950)
* N°9639 - Uninstallation Analysis better count method

* N°9639 - Fix tests

* N°9639 - Refactor deletion plan handling with new entity classes for improved structure and clarity

* N°9639 - Refactor deletion plan methods to use class names instead of table names and add TotalCount method

* N°9639 - Add messages to tests

* N°9639 - Refactor data feature removal logic and update database queries for consistency

* N°9639 - Refactor SQL queries to use backticks for table and field names

* N°9639 - Enhance deletion logic and add circular reference handling in cleanup process

* N°9639 - Add filtering methods to DeletionPlan classes for improved data cleanup

* N°9639 - Add filtering methods to DeletionPlan classes for improved data cleanup

* N°9639 - Refactor DeletionPlanItem to remove unused queries and simplify constructor

* N°9639 - Refactor variable name for clarity in hierarchical key update process

* Update tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* N°9639 - Clarify comment on deletion plan to indicate prevention of infinite loops

* N°9639 - Remove unused deletion plan from DataFeatureRemovalController

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-30 09:37:30 +02:00
jf-cbd
7fca6424ab N°9723 - IsActionAllowed should work with non instantiated users objects (#947)
* Replace references to user objects keys since objects can be non-instantiated (so no key available) and replace them by equivalent code

* Rename test + use self variable for profile

---------

Co-authored-by: jf-cbd <jf-cbd@users.noreply.github.com>
2026-06-25 17:45:43 +02:00
jf-cbd
f09a73c45a N°7060 - create ReadOnly profiles (#935) 2026-06-25 17:38:48 +02:00
Vincent Dumas
5a174dd8fa N°9658 - Fix Profiles retrieval in User sample data loading (#948) 2026-06-25 12:08:21 +02:00
Molkobain
1d9e7f9b55 Update test so it works on both ITIL and standard request management 2026-06-20 17:47:19 +02:00
57 changed files with 1166 additions and 1153 deletions

View File

@@ -9,11 +9,6 @@ Any PRs not following the guidelines or with missing information will not be con
## Base information
| Question | Answer
|----------------------------------------------------------------|--------
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |
| Type of change? | Bug fix / Enhancement / Translations
| Question | Answer |
|---------------------------------------------------------------------------------|--------------------------------------|
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |

View File

@@ -582,16 +582,26 @@ class UserRightsProfile extends UserRightsAddOnAPI
*/
public function ListProfiles($oUser)
{
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
$oSearch->NoContextParameters();
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
$oProfiles = new DBObjectSet($oSearch);
while ($oUserProfile = $oProfiles->Fetch()) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
if (count($oUser->ListChanges()) === 0) { // backward compatibility
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
$oSearch->NoContextParameters();
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
$oProfiles = new DBObjectSet($oSearch);
while ($oUserProfile = $oProfiles->Fetch()) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
} else {
$aRet = [];
$oProfilesSet = $oUser->Get('profile_list');
foreach ($oProfilesSet as $oUserProfile) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
}
return $aRet;
}
public function GetSelectFilter($oUser, $sClass, $aSettings = [])
@@ -705,26 +715,23 @@ class UserRightsProfile extends UserRightsAddOnAPI
protected function GetUserActionGrant($oUser, $sClass, $iActionCode)
{
$this->LoadCache();
// load and cache permissions for the current user on the given class
//
$iUser = $oUser->GetKey();
if (isset($this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode])) {
$aTest = $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode];
if (is_array($aTest)) {
return $aTest;
if (count($oUser->ListChanges()) === 0) {
// load and cache permissions for the current user on the given class
if (isset($this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode])) {
$aTest = $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode];
if (is_array($aTest)) {
return $aTest;
}
}
}
$sAction = self::$m_aActionCodes[$iActionCode];
$bStatus = null;
// Cache user's profiles
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
$aProfileList = $this->GetProfileList($oUser);
// Call the API of UserRights because it caches the list for us
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
foreach ($aProfileList as $iProfile => $oProfile) {
$bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction);
if (!is_null($bGrant)) {
if ($bGrant) {
@@ -742,7 +749,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
$aRes = [
'permission' => $iPermission,
];
$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes;
if (count($oUser->ListChanges()) === 0) {
$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode] = $aRes;
}
return $aRes;
}
@@ -824,18 +833,14 @@ class UserRightsProfile extends UserRightsAddOnAPI
{
$this->LoadCache();
// Note: this code is VERY close to the code of IsActionAllowed()
$iUser = $oUser->GetKey();
// Cache user's profiles
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
$aProfileList = $this->GetProfileList($oUser);
// Note: The object set is ignored because it was interesting to optimize for huge data sets
// and acceptable to consider only the root class of the object set
$bStatus = null;
// Call the API of UserRights because it caches the list for us
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
foreach ($aProfileList as $iProfile => $oProfile) {
$bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode);
if (!is_null($bGrant)) {
if ($bGrant) {
@@ -893,6 +898,25 @@ class UserRightsProfile extends UserRightsAddOnAPI
}
return $bHasSharing;
}
/**
* @param \User $oUser
*
* @return array
* @throws \Exception
*/
public function GetProfileList(User $oUser): array
{
if (count($oUser->ListChanges()) === 0) { // if user is already in db and not changed
$iUser = $oUser->GetKey();
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$aProfiles = UserRights::ListProfiles($oUser);
$this->aUsersProfilesList[$iUser] = $aProfiles;
}
return $this->aUsersProfilesList[$iUser];
}
return UserRights::ListProfiles($oUser);
}
}
UserRights::SelectModule('UserRightsProfile');

View File

@@ -1935,9 +1935,8 @@ class DBObjectSearch extends DBSearch
/**
* @inheritDoc
* @return DBObjectSearch
*/
protected function ApplyDataFilters(): DBObjectSearch
protected function ApplyDataFilters(): DBSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;

View File

@@ -676,9 +676,8 @@ class DBUnionSearch extends DBSearch
/**
* @inheritDoc
* @return DBUnionSearch
*/
protected function ApplyDataFilters(): DBUnionSearch
protected function ApplyDataFilters(): DBSearch
{
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
return $this;

View File

@@ -614,6 +614,13 @@ class LogChannels
* @since 3.2.0
*/
public const SECURITY = 'Security';
/**
* For Session parameters
*
* @Since 3.3.0
*/
public const SESSION_PARAMETERS = 'SessionParameters';
}
abstract class LogAPI

View File

@@ -5761,36 +5761,37 @@ abstract class MetaModel
self::$m_sEnvironment = $sEnvironment;
try {
if (!defined('MODULESROOT')) {
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
if (!defined('MODULESROOT')) {
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
// $config can be either a filename, or a Configuration object (volatile!)
if ($config instanceof Config) {
self::LoadConfig($config, $bAllowCache);
} else {
self::LoadConfig(new Config($config), $bAllowCache);
}
if ($bModelOnly) {
return;
}
// $config can be either a filename, or a Configuration object (volatile!)
if ($config instanceof Config) {
self::LoadConfig($config, $bAllowCache);
} else {
self::LoadConfig(new Config($config), $bAllowCache);
}
CMDBSource::SelectDB(self::$m_sDBName);
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
$oPHPClass::OnMetaModelStarted();
if ($bModelOnly) {
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
return;
}
ExpressionCache::Warmup();
} finally {
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
}
CMDBSource::SelectDB(self::$m_sDBName);
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
$oPHPClass::OnMetaModelStarted();
}
ExpressionCache::Warmup();
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
}
/**

View File

@@ -1530,6 +1530,37 @@ class UserRights
}
}
/**
* @param User $oUser
* @param array $aExcludedProfilesId Administrator by default, but can also be other proofiles depending on needs (e.g. power portal user or REST profile)
* @return bool
* @throws ArchivedObjectException
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws MySQLException
*/
public static function IsUserReadOnly(User $oUser, array $aExcludedProfilesId = [1]): bool
{
$oUserProfiles = $oUser->Get('profile_list');
$oUserRights = UserRights::GetModuleInstance();
while ($oUserProfile = $oUserProfiles->Fetch()) {
$iProfileId = $oUserProfile->Get('profileid');
if (in_array($iProfileId, $aExcludedProfilesId)) {
return false;
}
foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) {
foreach (['w', 'bw', 'd', 'bd'] as $sWriteActionCode) {
$bIsGranted = $oUserRights->GetProfileActionGrant($iProfileId, $sClass, $sWriteActionCode);
if ($bIsGranted === true) {
return false;
}
}
}
}
return true;
}
/**
* @param string $sClass
* @param int $iActionCode see UR_ACTION_* constants

View File

@@ -19,6 +19,7 @@ use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
use Combodo\iTop\Service\Session\SessionParameters;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use ContextTag;
@@ -47,6 +48,9 @@ class DataFeatureRemovalController extends Controller
{
$aParams = [];
SetupUtils::EraseSetupToken();
(new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME))->Erase();
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['iColumnCount'] = $this->iColumnCount;
@@ -180,7 +184,7 @@ class DataFeatureRemovalController extends Controller
];
foreach ($aHiddenInputs as $sInputName => $sInputValue) {
$aParams['aSetupParams']["_params[$sInputName]"] = $sInputValue;
$aParams['aSetupParams'][$sInputName] = $sInputValue;
}
[$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses);
@@ -189,6 +193,7 @@ class DataFeatureRemovalController extends Controller
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
if (!$aParams['bDeletionNeeded']) {
// Erase session setup parameters
SetupUtils::CreateSetupToken();
}
@@ -206,12 +211,13 @@ class DataFeatureRemovalController extends Controller
}
/**
* @param array $aAddedExtensions
* @param array $aRemovedExtensions
* @param bool $bForceCompilation
* @return void
* @throws \ConfigException
* @throws \CoreException
* @param array $aAddedExtensions
* @param array $aRemovedExtensions
* @param bool $bForceCompilation
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
private function Compile(array $aAddedExtensions, array $aRemovedExtensions, bool $bForceCompilation = true): void
{

View File

@@ -7,31 +7,22 @@
namespace Combodo\iTop\DataFeatureRemoval\Entity;
use MetaModel;
class DeletionPlanEntity
{
public readonly DeletionPlanItem $oDelete;
public readonly DeletionPlanItem $oUpdate;
public readonly DeletionPlanItem $oIssue;
public array $aQueriesForTempTable;
public array $aDependsOnTempTable;
public string $sClass;
/**
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oDelete
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oUpdate
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oIssue
*/
public function __construct(string $sClass)
public function __construct()
{
$this->sClass = $sClass;
$this->oDelete = $oDelete ?? new DeletionPlanItem();
$this->oUpdate = $oUpdate ?? new DeletionPlanItem();
$this->oIssue = $oIssue ?? new DeletionPlanItem();
$this->aQueriesForTempTable = [];
$this->aDependsOnTempTable = [];
}
public function TotalCount(): int
@@ -43,20 +34,4 @@ class DeletionPlanEntity
{
$this->oUpdate->FilterBy($this->oDelete);
}
public function AddQueryForTempTable(string $sQuery, ?string $sTempTableName = null): void
{
if (!in_array($sQuery, $this->aQueriesForTempTable)) {
$this->aQueriesForTempTable[] = $sQuery;
if (!is_null($sTempTableName) && !in_array($sTempTableName, $this->aDependsOnTempTable)) {
$this->aDependsOnTempTable[] = $sTempTableName;
}
}
}
public static function GetTempTableName(string $sClass): string
{
$sRootClass = MetaModel::GetRootClass($sClass);
return 'temp_'.MetaModel::DBGetTable($sRootClass);
}
}

View File

@@ -7,7 +7,6 @@
namespace Combodo\iTop\DataFeatureRemoval\Service;
use AttributeExternalKey;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanEntity;
@@ -49,70 +48,6 @@ class StaticDeletionPlan
return $aSummary;
}
public function GetTempTableDefinitions(array $aClasses): array
{
$aTempTables = [];
$aDeletionPlan = $this->GetStaticDeletionPlan($aClasses ?? []);
foreach ($aDeletionPlan as $sClass => $oDeletionPlanEntity) {
$sTempTableName = DeletionPlanEntity::GetTempTableName($sClass);
$aTempTables[$sTempTableName]['name'] = $sTempTableName;
$aTempTables[$sTempTableName]['queries'] = array_unique(array_merge($aTempTables[$sTempTableName]['queries'] ?? [], $oDeletionPlanEntity->aQueriesForTempTable));
$aTempTables[$sTempTableName]['depends_on'] = array_unique(array_merge($aTempTables[$sTempTableName]['depends_on'] ?? [], $oDeletionPlanEntity->aDependsOnTempTable));
}
usort($aTempTables, function ($a, $b) use ($aTempTables) {
if (empty($a['depends_on']) && empty($b['depends_on'])) {
// Both initial classes
return 0;
}
return $this->CompareEntries($a, $b, $aTempTables);
});
$aTableDefinitions = [];
foreach ($aTempTables as $aTempTable) {
if (count($aTempTable['queries']) > 0) {
$TempTableSelect = implode("\nUNION\n", $aTempTable['queries']);
$sTempTableName = $aTempTable['name'];
$aTableDefinitions[$sTempTableName] = [
"DROP TEMPORARY TABLE IF EXISTS `$sTempTableName`",
"CREATE TEMPORARY TABLE `$sTempTableName` ($TempTableSelect)",
];
}
}
return $aTableDefinitions;
}
/**
* tells if name1 depends on name2
* @param string $sName1
* @param string $sName2
* @param array $aAllTables
*
* @return bool
*/
private function DependsOn(string $sName1, string $sName2, array $aAllTables): bool
{
$aDependsOn = $aAllTables[$sName1]['depends_on'] ?? [];
if (in_array($sName2, $aDependsOn)) {
return true;
}
// Search on step further
foreach ($aAllTables[$sName1]['depends_on'] as $sParent) {
if ($this->DependsOn($sParent, $sName2, $aAllTables)) {
return true;
}
}
return false;
}
public function CompareEntries(array $aTable1, array $aTable2, array $aAllTables): int
{
return $this->DependsOn($aTable1['name'], $aTable2['name'], $aAllTables) ? 1 : -1;
}
/**
* @param array $aClasses Classes to clean entirely
*
@@ -124,9 +59,8 @@ class StaticDeletionPlan
{
foreach ($aClasses as $sClass) {
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
$oDeletionPlanEntity = new DeletionPlanEntity($sClass);
$oDeletionPlanEntity = new DeletionPlanEntity();
$oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem);
$oDeletionPlanEntity->AddQueryForTempTable($this->GetQueryForInitialClass($sClass));
$this->aDeletionPlan[$sClass] = $oDeletionPlanEntity;
$this->DeletionPlanForReferencingClasses($sClass);
@@ -141,7 +75,7 @@ class StaticDeletionPlan
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
if (!isset($this->aDeletionPlan[$sRemoteClass])) {
$this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity($sRemoteClass);
$this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity();
}
$oDeletionPlanEntity = $this->aDeletionPlan[$sRemoteClass];
/** @var \AttributeExternalKey $oExtKeyAttDef */
@@ -162,7 +96,7 @@ class StaticDeletionPlan
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
// Issue, do not recurse
$oDeletionPlanItem = new DeletionPlanItem($aRemoteIdsToRemove);
$oDeletionPlanItem = new DeletionPlanItem(aIds: $aRemoteIdsToRemove);
$oDeletionPlanEntity->oIssue->Merge($oDeletionPlanItem);
continue;
}
@@ -172,16 +106,14 @@ class StaticDeletionPlan
$sTargetIdsToRemove = implode(',', $this->aDeletionPlan[$sRemoteClass]->oDelete->aIds);
$oUpdateItem = $this->UpdateHierarchicalExtKey($sRemoteClass, $sExtKeyAttCode, $sTargetIdsToRemove);
$oDeletionPlanEntity->oUpdate->Merge($oUpdateItem);
// Delete current entry an recurse !
}
// Delete entries in Remote Class
[$sQueryForTempTable, $sDependsOnTempTable] = $this->GetQueryForExtKey($sRemoteClass, $oExtKeyAttDef);
$oDeletionPlanEntity->AddQueryForTempTable($sQueryForTempTable, $sDependsOnTempTable);
$oDeletionPlanEntity->oDelete->Merge(new DeletionPlanItem($aRemoteIdsToRemove));
// Infinite loops do not occurs due to the datamodel structure
$this->DeletionPlanForReferencingClasses($sRemoteClass);
if (count($aRemoteIdsToRemove) !== 0) {
$oDeletionPlanEntity->oDelete->Merge(new DeletionPlanItem($aRemoteIdsToRemove));
// Infinite loops do not occurs due to the datamodel structure
$this->DeletionPlanForReferencingClasses($sRemoteClass);
}
}
}
}
@@ -213,12 +145,12 @@ class StaticDeletionPlan
*/
public function UpdateHierarchicalExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem
{
[$sDBTable, $sDBKey, $sDBField] = $this->GetDBInfo($sRemoteClass, $sExtKeyAttCode);
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
$sSQL = <<<SQL
SELECT `$sDBKey`
FROM `$sDBTable` AS `updated`
INNER JOIN `$sDBTable` AS `removed` ON `updated`.`$sDBField` = `removed`.`$sDBField`
INNER JOIN `$sDBTable` AS `removed` ON `updated`.`$sDBField` = `removed`.`$sDBKey`
WHERE `removed`.`$sDBKey` IN ($sIdsToRemoveInTargetClass)
SQL;
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
@@ -240,10 +172,10 @@ SQL;
if (\utils::IsNullOrEmptyString($sIdsToRemoveInTargetClass)) {
return [];
}
[$sDBTable, $sDBKey, $sDBField] = $this->GetDBInfo($sRemoteClass, $sExtKeyAttCode);
$sSQL = "SELECT `$sDBKey` AS id FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)";
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
$sSQL = "SELECT `$sDBKey` FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)";
return CMDBSource::QueryToCol($sSQL, 'id');
return CMDBSource::QueryToCol($sSQL, $sDBKey);
}
/**
@@ -255,9 +187,9 @@ SQL;
*/
public function GetInitialClassDeletionPlan(string $sClass): DeletionPlanItem
{
[$sDBTable, $sDBKey] = $this->GetDBInfo($sClass);
$sSQL = "SELECT `$sDBKey` FROM `$sDBTable`";
$sTable = MetaModel::DBGetTable($sClass);
$sDBKey = MetaModel::DBGetKey($sClass);
$sSQL = "SELECT `$sDBKey` FROM `$sTable`";
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
return new DeletionPlanItem($aIds);
@@ -273,47 +205,15 @@ SQL;
* @throws \CoreException
* @throws \Exception
*/
public function GetDBInfo(string $sClass, ?string $sExtKeyAttCode = null): array
public function GetDBInfoForAttcode(string $sClass, string $sExtKeyAttCode): array
{
if (!is_null($sExtKeyAttCode)) {
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sExtKeyAttCode);
$sDBTable = MetaModel::DBGetTable($sOriginClass);
$sDBKey = MetaModel::DBGetKey($sOriginClass);
$oAttDef = MetaModel::GetAttributeDef($sOriginClass, $sExtKeyAttCode);
// External key is on a single DB column
$sDBField = array_keys($oAttDef->GetSQLColumns())[0];
return [$sDBTable, $sDBKey, $sDBField];
}
$sDBTable = MetaModel::DBGetTable($sClass);
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sExtKeyAttCode);
$sDBTable = MetaModel::DBGetTable($sOriginClass);
$oAttDef = MetaModel::GetAttributeDef($sOriginClass, $sExtKeyAttCode);
// External key is on a single DB column
$sDBField = array_keys($oAttDef->GetSQLColumns())[0];
$sDBKey = MetaModel::DBGetKey($sClass);
return [$sDBTable, $sDBKey];
return [$sDBTable, $sDBField, $sDBKey];
}
private function GetQueryForInitialClass(mixed $sClass): string
{
[$sDBTable, $sDBKey] = $this->GetDBInfo($sClass);
return "SELECT `$sDBKey` AS id FROM `$sDBTable`";
}
private function GetQueryForExtKey(string $sClass, AttributeExternalKey $oExtKeyAttDef)
{
$sExtKeyAttCode = $oExtKeyAttDef->GetCode();
[$sDBTable, $sDBKey, $sDBField] = $this->GetDBInfo($sClass, $sExtKeyAttCode);
$sTargetClass = $oExtKeyAttDef->GetTargetClass();
$sTempTable = DeletionPlanEntity::GetTempTableName($sTargetClass);
$sQuery = <<<SQL
SELECT `$sDBTable`.`$sDBKey` AS id FROM `$sDBTable`
INNER JOIN `$sTempTable` ON `$sTempTable`.`id` = `$sDBTable`.`$sDBField`
SQL;
return [$sQuery, $sTempTable];
}
}

View File

@@ -28,7 +28,7 @@ SetupWebPage::AddModule(
'category' => 'Portal',
// Setup
'dependencies' => [
'itop-attachments/3.2.1', //CMDBChangeOpAttachmentRemoved
'itop-attachments/3.2.1||true', //CMDBChangeOpAttachmentRemoved
],
'mandatory' => true,
'visible' => false,

View File

@@ -85,6 +85,13 @@
<class id="Attachment"/>
</classes>
</group>
<group id="Ticket" _delta="define">
<classes>
<class id="Ticket"/>
<class id="WorkOrder"/>
<class id="Attachment"/>
</classes>
</group>
<group id="Portal" _delta="define">
<classes>
<class id="lnkFunctionalCIToTicket"/>
@@ -205,6 +212,60 @@
</group>
</groups>
<profiles>
<profile id="5500" _delta="define">
<name>Configuration ReadOnly</name>
<description>This read-only profile allows to see CIs objects.</description>
<groups>
<group id="Configuration">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="5501" _delta="define">
<name>Ticket ReadOnly</name>
<description>This read-only profile allows to see Ticket objects.</description>
<groups>
<group id="Ticket">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="5502" _delta="define">
<name>Service Catalog ReadOnly</name>
<description>This read-only profile allows to see Service Catalog objects.</description>
<groups>
<group id="Service">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="117" _delta="define">
<name>SuperUser</name>
<description>This profile allows all actions which are not Administrator restricted.</description>

View File

@@ -4,80 +4,64 @@
<contactid>9</contactid>
<email>jules.vernes@it.com</email>
<org_id_friendlyname>IT Department</org_id_friendlyname>
<login>Agent</login>
<login>SupportAgent</login>
<language>EN US</language>
<status>disabled</status>
<profile_list>
<Set>
<URP_UserProfile id="5">
<profileid>5</profileid>
<profile>Support Agent</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
</Set>
</allowed_org_list>
<password><![CDATA[R6rQ;p]JT*FA$aaP^4]]></password>
<password><![CDATA[SupportAgent]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-06-04</password_renewed_date>
</UserLocal>
<URP_UserProfile id="5">
<userid>4</userid>
<userlogin>SupportAgent</userlogin>
<profileid>SELECT URP_Profiles WHERE id='5'</profileid>
<profile>Support Agent</profile>
<reason></reason>
</URP_UserProfile>
<UserLocal alias="UserLocal" id="5">
<contactid>26</contactid>
<contactid_friendlyname>Jean Ferrat</contactid_friendlyname>
<org_id_friendlyname>IT Department</org_id_friendlyname>
<login>config</login>
<login>ConfigManager</login>
<language>EN US</language>
<status>disabled</status>
<profile_list>
<Set>
<URP_UserProfile id="6">
<profileid>3</profileid>
<profile>Configuration Manager</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
</Set>
</allowed_org_list>
<password><![CDATA[$W[:"7+Gf"Y\sd8#E~]]></password>
<password><![CDATA[$W[ConfigManager]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-06-04</password_renewed_date>
</UserLocal>
<URP_UserProfile id="6">
<userid>5</userid>
<userlogin>ConfigManager</userlogin>
<profileid>SELECT URP_Profiles WHERE id='3'</profileid>
<profile>Configuration Manager</profile>
<reason></reason>
</URP_UserProfile>
<UserLocal alias="UserLocal" id="2">
<contactid>15</contactid>
<email>agatha.christie@demo.com</email>
<org_id_friendlyname>Sales Department</org_id_friendlyname>
<login>Portal</login>
<login>PortalUser</login>
<language>EN US</language>
<status>disabled</status>
<profile_list>
<Set>
<URP_UserProfile id="2">
<profileid>2</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
<URP_UserOrg id="2">
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
</Set>
</allowed_org_list>
<password><![CDATA[-Why]KDdMJvkuB8#e]]></password>
<password><![CDATA[PortalUser]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-04-17</password_renewed_date>
</UserLocal>
<URP_UserProfile id="2">
<userid>2</userid>
<userlogin>PortalUser</userlogin>
<profileid>SELECT URP_Profiles WHERE id='2'</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserOrg id="2">
<userid>2</userid>
<userlogin>PortalUser</userlogin>
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
<UserLocal alias="UserLocal" id="3">
<contactid>6</contactid>
<email>claude.monet@demo.com</email>
@@ -85,32 +69,48 @@
<login>SalesManager</login>
<language>EN US</language>
<status>disabled</status>
<profile_list>
<Set>
<URP_UserProfile id="4">
<profileid>12</profileid>
<profile>Portal power user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserProfile id="3">
<profileid>2</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
<URP_UserOrg id="1">
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
</Set>
</allowed_org_list>
<password><![CDATA[D%r7hoZ})5*hvq5`{Q]]></password>
<password><![CDATA[SalesManager]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-06-04</password_renewed_date>
</UserLocal>
<URP_UserProfile id="4">
<userid>3</userid>
<userlogin>SalesManager</userlogin>
<profileid>SELECT URP_Profiles WHERE id='12'</profileid>
<profile>Portal power user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserProfile id="3">
<userid>3</userid>
<userlogin>SalesManager</userlogin>
<profileid>SELECT URP_Profiles WHERE id='2'</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserOrg id="1">
<userid>3</userid>
<userlogin>SalesManager</userlogin>
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
<UserLocal alias="UserLocal" id="6">
<contactid>18</contactid>
<email>rene.descartes@demo.com</email>
<org_id_friendlyname>Demo</org_id_friendlyname>
<login>SuperUser</login>
<language>EN US</language>
<status>disabled</status>
<password><![CDATA[SuperUser]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-04-17</password_renewed_date>
</UserLocal>
<URP_UserProfile id="7">
<userid>6</userid>
<userlogin>SuperUser</userlogin>
<profileid>SELECT URP_Profiles WHERE id='117'</profileid>
<profile>Super User</profile>
<reason></reason>
</URP_UserProfile>
</Set>

View File

@@ -476,8 +476,7 @@ $(function()
let sStimulusCode = (undefined !== oData.stimulus_code) ? oData.stimulus_code : null
// If several entry forms filled, show a confirmation message
if ((GetUserPreference('activity_panel.show_multiple_entries_submit_confirmation',true) === true
|| GetUserPreference('activity_panel.show_multiple_entries_submit_confirmation', true) === "true")
if (GetUserPreferenceAsBoolean('activity_panel.show_multiple_entries_submit_confirmation',true) === true
&& (Object.keys(await this._GetEntriesFromAllForms()).length > 1)) {
this._ShowEntriesSubmitConfirmation(sStimulusCode);
}

View File

@@ -135,14 +135,10 @@ function SetUserPreference(sPreferenceCode, sPrefValue, bPersistent) {
} catch (err) {
sPreviousValue = undefined;
}
oUserPreferences[sPreferenceCode] = sPrefValue;
if (bPersistent && (sPrefValue != sPreviousValue)) {
return $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
{operation: 'set_pref', code: sPreferenceCode, value: sPrefValue}, function (data) {
}).done(function() {
oUserPreferences[sPreferenceCode] = sPrefValue;
}); // Make it persistent
} else {
oUserPreferences[sPreferenceCode] = sPrefValue;
ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
{operation: 'set_pref', code: sPreferenceCode, value: sPrefValue}); // Make it persistent
}
}

View File

@@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@@ -137,7 +137,6 @@ return array(
'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php',
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php',
'Combodo\\iTop\\Application\\Helper\\SearchHelper' => $baseDir . '/sources/Application/Helper/SearchHelper.php',
'Combodo\\iTop\\Application\\Helper\\Session' => $baseDir . '/sources/Application/Helper/Session.php',
'Combodo\\iTop\\Application\\Helper\\SynchroReplicaHelper' => $baseDir . '/sources/Application/Helper/SynchroReplicaHelper.php',
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => $baseDir . '/sources/Application/Helper/WebResourcesHelper.php',
'Combodo\\iTop\\Application\\Newsroom\\iTopNewsroomProvider' => $baseDir . '/sources/Application/Newsroom/iTopNewsroomProvider.php',
@@ -640,6 +639,8 @@ return array(
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php',
'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php',
'Combodo\\iTop\\Service\\Session\\Session' => $baseDir . '/sources/Service/Session/Session.php',
'Combodo\\iTop\\Service\\Session\\SessionParameters' => $baseDir . '/sources/Service/Session/SessionParameters.php',
'Combodo\\iTop\\Service\\Startup\\StartupService' => $baseDir . '/sources/Service/Startup/StartupService.php',
'Combodo\\iTop\\Service\\SummaryCard\\SummaryCardService' => $baseDir . '/sources/Service/SummaryCard/SummaryCardService.php',
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectConfig' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectConfig.php',
@@ -1423,6 +1424,7 @@ return array(
'ReportValue' => $baseDir . '/core/bulkchange.class.inc.php',
'RestDelete' => $baseDir . '/core/restservices.class.inc.php',
'RestResult' => $baseDir . '/application/applicationextension/rest/RestResult.php',
'RestResultWithObjectSets' => $baseDir . '/core/restservices.class.inc.php',
'RestResultWithObjects' => $baseDir . '/core/restservices.class.inc.php',
'RestResultWithRelations' => $baseDir . '/core/restservices.class.inc.php',
'RestUtils' => $baseDir . '/application/applicationextension/rest/RestUtils.php',

View File

@@ -538,7 +538,6 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php',
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php',
'Combodo\\iTop\\Application\\Helper\\SearchHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/SearchHelper.php',
'Combodo\\iTop\\Application\\Helper\\Session' => __DIR__ . '/../..' . '/sources/Application/Helper/Session.php',
'Combodo\\iTop\\Application\\Helper\\SynchroReplicaHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/SynchroReplicaHelper.php',
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/WebResourcesHelper.php',
'Combodo\\iTop\\Application\\Newsroom\\iTopNewsroomProvider' => __DIR__ . '/../..' . '/sources/Application/Newsroom/iTopNewsroomProvider.php',
@@ -1041,6 +1040,8 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php',
'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php',
'Combodo\\iTop\\Service\\Session\\Session' => __DIR__ . '/../..' . '/sources/Service/Session/Session.php',
'Combodo\\iTop\\Service\\Session\\SessionParameters' => __DIR__ . '/../..' . '/sources/Service/Session/SessionParameters.php',
'Combodo\\iTop\\Service\\Startup\\StartupService' => __DIR__ . '/../..' . '/sources/Service/Startup/StartupService.php',
'Combodo\\iTop\\Service\\SummaryCard\\SummaryCardService' => __DIR__ . '/../..' . '/sources/Service/SummaryCard/SummaryCardService.php',
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectConfig' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectConfig.php',
@@ -1824,6 +1825,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
'ReportValue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RestDelete' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestResult' => __DIR__ . '/../..' . '/application/applicationextension/rest/RestResult.php',
'RestResultWithObjectSets' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestResultWithObjects' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestResultWithRelations' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestUtils' => __DIR__ . '/../..' . '/application/applicationextension/rest/RestUtils.php',

View File

@@ -36,8 +36,7 @@ if ($issues) {
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;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}

View File

@@ -27,6 +27,7 @@ use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
use Combodo\iTop\Controller\Base\Layout\ObjectController;
use Combodo\iTop\Controller\WelcomePopupController;
use Combodo\iTop\Service\Router\Router;
use Combodo\iTop\Service\Session\SessionParameters;
/**
* Displays a popup welcome message, once per session at maximum
@@ -1306,6 +1307,10 @@ try {
///////////////////////////////////////////////////////////////////////////////////////////
default: // Menu node rendering (templates)
// Erase setup parameters
SetupUtils::EraseSetupToken();
(new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME))->Erase();
ApplicationMenu::LoadAdditionalMenus();
$oMenuNode = ApplicationMenu::GetMenuNode(ApplicationMenu::GetMenuIndexById(ApplicationMenu::GetActiveNodeId()));
if (is_object($oMenuNode)) {

View File

@@ -444,7 +444,7 @@ EOF;
$sPwd = $oConfig->Get('db_pwd');
$sSource = $oConfig->Get('db_name');
$sTlsEnabled = $oConfig->Get('db_tls.enabled');
$sTlsCA = $oConfig->Get('db_tls.ca');
$sTlsCA = $oConfig->Get('db_tls.ca') ?? '';
try {
$oMysqli = CMDBSource::GetMysqliInstance(

View File

@@ -2,11 +2,10 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use CoreException;
use Exception;
use IssueLog;
use SetupLog;
use SetupUtils;
use utils;
class ModelReflectionSerializer
@@ -15,6 +14,7 @@ class ModelReflectionSerializer
protected function __construct()
{
SetupLog::Enable(APPROOT.'log/setup.log');
}
final public static function GetInstance(): ModelReflectionSerializer
@@ -31,42 +31,53 @@ class ModelReflectionSerializer
self::$oInstance = $oInstance;
}
public const ERROR_LABEL = "Data consistency check failed: %s";
public function GetModelFromEnvironment(string $sEnv): array
{
IssueLog::Debug(__METHOD__, null, ['env' => $sEnv]);
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
$sOutput = "";
$iRes = 0;
SetupUtils::CheckCliPhpVersionIsOk(self::ERROR_LABEL);
$sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv));
exec($sCommandLine, $sOutput, $iRes);
if ($iRes != 0) {
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput, 'cmd' => $sCommandLine]);
throw new CoreException("Cannot get classes from env ".$sEnv);
//preliminary check
$sEnvDir = APPROOT."env-$sEnv";
if (! is_dir($sEnvDir)) {
$sMsg = sprintf(self::ERROR_LABEL, "Missing environment ($sEnvDir)");
SetupLog::Error($sMsg);
throw new CoreException($sMsg);
}
$aClasses = json_decode($sOutput[0] ?? null, true);
$sConfigFile = APPROOT."conf/$sEnv/config-itop.php";
if (! is_file($sConfigFile)) {
$sMsg = sprintf(self::ERROR_LABEL, "Missing configuration ($sConfigFile)");
SetupLog::Error($sMsg);
throw new CoreException($sMsg);
}
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
$aOutput = null;
$iRes = 0;
$sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv));
exec($sCommandLine, $aOutput, $iRes);
if ($iRes != 0) {
$sError = $aOutput[0] ?? 'Invalid output when serializing model';
SetupLog::Error(sprintf(self::ERROR_LABEL, '(cli error) '.$sError), null, ['env' => $sEnv, 'code' => $iRes, "output" => $aOutput, 'cmd' => $sCommandLine]);
throw new CoreException(sprintf(self::ERROR_LABEL, $sError));
}
$aClasses = json_decode($aOutput[0] ?? null, true);
if (false === $aClasses) {
$this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]);
throw new Exception("cannot get classes");
$sMsg = sprintf(self::ERROR_LABEL, 'Invalid JSON');
SetupLog::Error($sMsg, null, ['env' => $sEnv, "output" => $aOutput]);
throw new CoreException($sMsg);
}
if (!is_array($aClasses)) {
$this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]);
throw new Exception("cannot get classes from $sEnv");
$sError = $aOutput[0] ?? 'Invalid json array when serializing model';
SetupLog::Error(sprintf(self::ERROR_LABEL, '(JSON output not an array) '.$sError), null, ['env' => $sEnv, "classes" => $aClasses, "output" => $aOutput]);
throw new CoreException(sprintf(self::ERROR_LABEL, $sError));
}
return $aClasses;
}
//could be shared with others in log APIs ?
private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Error($sMessage, $sChannel, $aContext);
} else {
IssueLog::Error($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -21,8 +21,7 @@ $sConfFile = utils::GetConfigFilePath($sEnv);
try {
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
} catch (\Throwable $e) {
echo $e->getMessage();
echo $e->getTraceAsString();
SetupLog::Enable(APPROOT.'log/setup.log');
\SetupLog::Error(
"Cannot read model from provided environment",
null,
@@ -32,7 +31,9 @@ try {
'stack' => $e->getTraceAsString(),
]
);
echo "Cannot read model from provided environment";
//keep first echo to have proper setup feedbacks
echo $e->getMessage();
exit(1);
}

View File

@@ -70,6 +70,7 @@ class DataAuditSequencer extends StepSequencer
$aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
$bUseSymbolicLinks = $this->oParams->Get('use_symbolic_links', null) === 'on';
MetaModel::ResetAllCaches($this->oRunTimeEnvironment->GetBuildEnv());
$this->oRunTimeEnvironment->DoCompile(
$aSelectedExtensionCodes,
$aRemovedExtensionCodes,
@@ -79,6 +80,7 @@ class DataAuditSequencer extends StepSequencer
return $this->ComputeNextStep($sStep);
case 'setup-audit':
$this->oRunTimeEnvironment->ExitMaintenanceMode();
$this->oRunTimeEnvironment->DataToCleanupAudit();
return $this->ComputeNextStep($sStep);

View File

@@ -103,6 +103,9 @@ class CheckResult
*/
class SetupUtils
{
// Name of the parameter array in session for setup
public const SESSION_PARAMETERS_NAME = 'setup_params';
// -- Minimum versions (requirements : forbids installation if not met)
public const PHP_MIN_VERSION = '8.2.0';
public const MYSQL_MIN_VERSION = '5.7.0'; // 5.6 is no longer supported
@@ -229,6 +232,13 @@ class SetupUtils
$aResult[] = new CheckResult(CheckResult::WARNING, "Missing optional PHP extension: $sExtension. ".$sMessage);
}
}
try {
SetupUtils::CheckCliPhpVersionIsOk();
} catch (CoreException $e) {
$aResult[] = new CheckResult(CheckResult::WARNING, $e->getMessage());
}
// Check some ini settings here
if (function_exists('php_ini_loaded_file')) { // PHP >= 5.2.4
$sPhpIniFile = php_ini_loaded_file();
@@ -1427,7 +1437,7 @@ EOF
// Unsupported Password, warn the user
$oPage->add_ready_script(
<<<JS
$("#db_info").html('<div class="message message-error"><span class="message-title">Error:</span>On Windows, the backup won\'t work because database password contains %, ! or &quot; character</div>');
$("#db_info").html('<div class="message message-error ibo-is-html-content"><span class="message-title">Error:</span>On Windows, the backup won\'t work because database password contains %, ! or &quot; character</div>');
JS
);
} else {
@@ -2189,6 +2199,41 @@ JS
return [$sButtonLabel, $sButtonUrl];
}
/**
* @param string $sErrorLabel: error label with 1 placeholder inside for detailed error
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public static function CheckCliPhpVersionIsOk(string $sErrorLabel = '%s'): void
{
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
$aOutput = null;
$iRes = 0;
exec("$sPHPExec --version", $aOutput, $iRes);
if ($iRes != 0) {
$sError = sprintf($sErrorLabel, "Cannot check CLI/PHP version ($sPHPExec)");
SetupLog::Error($sError, null, ['code' => $iRes, "output" => $aOutput, 'php_path' => $sPHPExec]);
throw new CoreException($sError);
}
SetupUtils::CheckCliPhpVersionFromOutput($sErrorLabel, PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, $sPHPExec, $aOutput);
}
private static function CheckCliPhpVersionFromOutput(string $sErrorLabel, string $sUIPhpVersion, string $sPHPExec, $aOutput): void
{
$sFoundVersion = trim($aOutput[0] ?? "");
if (false !== preg_match('/(\d+\.\d+)(?:\.\d+)?/', $sFoundVersion, $aMatches)) {
$sFoundVersion = $aMatches[1];
}
if ($sFoundVersion !== $sUIPhpVersion) {
$sError = sprintf($sErrorLabel, "Mismatch between PHP versions (CLI: $sFoundVersion/ UI: $sUIPhpVersion)");
SetupLog::Error($sError, null, ["output" => $aOutput, 'php_path' => $sPHPExec]);
throw new CoreException($sError);
}
}
}
/**

View File

@@ -132,7 +132,7 @@ if ($bUseItopConfig && file_exists($sConfigFile)) {
$aDBXmlSettings ['prefix'] = $oConfig->Get('db_subname');
$aDBXmlSettings ['db_tls_enabled'] = $oConfig->Get('db_tls.enabled');
//cannot be null or infinite loop triggered!
$aDBXmlSettings ['db_tls_ca'] = $oConfig->Get('db_tls.ca') ?? "";
$aDBXmlSettings ['db_tls_ca'] = $oConfig->Get('db_tls.ca') ?? '';
$oParams->Set('database', $aDBXmlSettings);
$aFields = [

View File

@@ -41,5 +41,5 @@
</selected_modules>
<selected_extensions type="array">
</selected_extensions>
<use_symbolic_links>on</use_symbolic_links>
<use_symbolic_links>off</use_symbolic_links>
</installation>

View File

@@ -1,21 +1,11 @@
<?php
// Copyright (C) 2010-2024 Combodo SAS
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Service\Session\SessionParameters;
require_once(APPROOT.'setup/setuputils.class.inc.php');
require_once(APPROOT.'setup/parameters.class.inc.php');
@@ -26,14 +16,12 @@ require_once(APPROOT.'setup/extensionsmap.class.inc.php');
/**
* Engine for displaying the various pages of a "wizard"
* Each "step" of the wizard must be implemented as
* separate class derived from WizardStep. each 'step' can also have its own
* separate classes derived from WizardStep. Each 'step' can also have its own
* internal 'state' for developing complex wizards.
* The WizardController provides the "<< Back" feature by storing a stack
* of the previous screens. The WizardController also maintains from page
* to page a list of "parameters" to be dispayed/edited by each of the steps.
*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
class WizardController
@@ -41,37 +29,42 @@ class WizardController
protected $aWizardSteps;
protected $sInitialStepClass;
protected $sInitialState;
protected $aParameters;
protected SessionParameters $oSessionParameters;
/**
* Initiailization of the wizard controller
* Initialization of the wizard controller
* @param string $sInitialStepClass Class of the initial step/page of the wizard
* @param string $sInitialState Initial state of the initial page (if this class manages states)
*/
public function __construct($sInitialStepClass, $sInitialState = '')
public function __construct(string $sInitialStepClass, string $sInitialState = '')
{
$this->sInitialStepClass = $sInitialStepClass;
$this->sInitialState = $sInitialState;
$this->aParameters = [];
$this->aWizardSteps = [];
$this->oSessionParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
$this->oSessionParameters->LogParameters();
$this->aWizardSteps = $this->GetParameter('_steps', []);
}
/**
* Pushes information about the current step onto the stack
* @param array $aStepInfo Array('class' => , 'state' => )
*/
protected function PushStep($aStepInfo)
protected function PushStep(array $aStepInfo): void
{
array_push($this->aWizardSteps, $aStepInfo);
$this->aWizardSteps[] = $aStepInfo;
$this->SetParameter('_steps', $this->aWizardSteps);
}
/**
* Removes information about the previous step from the stack
* @return array{'class': string, 'state': string}
*/
protected function PopStep()
protected function PopStep(): array
{
return array_pop($this->aWizardSteps);
$aStep = array_pop($this->aWizardSteps);
$this->SetParameter('_steps', $this->aWizardSteps);
return $aStep;
}
/**
@@ -79,13 +72,9 @@ class WizardController
* @param string $sParamCode The code identifying this parameter
* @param mixed $defaultValue The default value of the parameter in case it was not set
*/
public function GetParameter($sParamCode, $defaultValue = '')
public function GetParameter(string $sParamCode, mixed $defaultValue = ''): mixed
{
if (array_key_exists($sParamCode, $this->aParameters)) {
return $this->aParameters[$sParamCode];
}
return $defaultValue;
return $this->oSessionParameters->GetParameter($sParamCode, $defaultValue);
}
/**
@@ -115,9 +104,9 @@ class WizardController
* @param string $sParamCode The code identifying this parameter
* @param mixed $value The value to store
*/
public function SetParameter($sParamCode, $value)
public function SetParameter(string $sParamCode, mixed $value): void
{
$this->aParameters[$sParamCode] = $value;
$this->oSessionParameters->SetParameter($sParamCode, $value);
}
/**
@@ -126,30 +115,46 @@ class WizardController
* @param mixed $defaultValue The default value for the parameter
* @param string $sSanitizationFilter A 'sanitization' fitler. Default is 'raw_data', which means no filtering
*/
public function SaveParameter($sParamCode, $defaultValue, $sSanitizationFilter = 'raw_data')
public function SaveParameter(string $sParamCode, mixed $defaultValue, string $sSanitizationFilter = 'raw_data'): void
{
$value = utils::ReadParam($sParamCode, $defaultValue, false, $sSanitizationFilter);
$this->aParameters[$sParamCode] = $value;
$this->oSessionParameters->SetParameterFromParams($sParamCode, $defaultValue, $sSanitizationFilter);
}
/**
* Stores the value of the page's parameter in a "persistent" parameter in the wizard's context
* @param string $sParamCode The code identifying this parameter
* @param mixed $defaultValue The default value for the parameter
* @param string $sSanitizationFilter A 'sanitization' fitler. Default is 'raw_data', which means no filtering
*/
public function SavePostedParameter(string $sParamCode, mixed $defaultValue = '', string $sSanitizationFilter = 'raw_data'): void
{
$this->oSessionParameters->SetParameterFromPostedParams($sParamCode, $defaultValue, $sSanitizationFilter);
}
/**
* Starts the wizard by displaying it in its initial state
*
* @throws \Exception
*/
public function Start()
public function Start(): void
{
if ($this->GetParameter('return_application', '') === '') {
// Fresh restart of the wizard
$this->EraseParameters();
}
$sCurrentStepClass = $this->sInitialStepClass;
$oStep = $this->GetWizardStep($sCurrentStepClass, $this->sInitialState);
$oStep = $this->InstantiateWizardStep($sCurrentStepClass, $this->sInitialState);
$this->DisplayStep($oStep);
}
/**
* Progress towards the next step of the wizard
* @throws Exception
*/
protected function Next()
protected function Next(): void
{
$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
$oStep = $this->GetWizardStep($sCurrentStepClass, $sCurrentState);
$oStep = $this->InstantiateWizardStep($sCurrentStepClass, $sCurrentState);
if ($oStep->ValidateParams()) {
$aPossibleSteps = $oStep->GetPossibleSteps();
if ($oStep->CanMoveBackward()) {
@@ -157,7 +162,7 @@ class WizardController
}
$oWizardState = $oStep->UpdateWizardStateAndGetNextStep(true); // true => moving forward
if (in_array($oWizardState->GetNextStep(), $aPossibleSteps)) {
$oNextStep = $this->GetWizardStep($oWizardState->GetNextStep(), $oWizardState->GetState());
$oNextStep = $this->InstantiateWizardStep($oWizardState->GetNextStep(), $oWizardState->GetState());
$this->DisplayStep($oNextStep);
} else {
throw new Exception("Internal error: Unexpected next step '{$oWizardState->GetNextStep()}'. The possible next steps are: ".implode(', ', $aPossibleSteps));
@@ -169,18 +174,20 @@ class WizardController
/**
* Move one step back
*
* @throws \Exception
*/
protected function Back()
protected function Back(): void
{
// let the current step save its parameters
$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
$oStep = $this->GetWizardStep($sCurrentStepClass, $sCurrentState);
$oStep = $this->InstantiateWizardStep($sCurrentStepClass, $sCurrentState);
$oStep->UpdateWizardStateAndGetNextStep(false); // false => Moving backwards
// Display the previous step
$aCurrentStepInfo = $this->PopStep();
$oStep = $this->GetWizardStep($aCurrentStepInfo['class'], $aCurrentStepInfo['state']);
$oStep = $this->InstantiateWizardStep($aCurrentStepInfo['class'], $aCurrentStepInfo['state']);
$this->DisplayStep($oStep);
}
@@ -195,6 +202,24 @@ class WizardController
{
SetupLog::Info("=== Setup screen: ".$oStep->GetTitle().' ('.get_class($oStep).')');
$oPage = new SetupPage($oStep->GetTitle());
if (!$oStep->CanAccessToWizardStep()) {
[$sButtonLabel, $sButtonUrl] = SetupUtils::GetBackButtonInfo($this->oSessionParameters->GetParameter('return_application', ''));
SetupUtils::ExitReadOnlyMode(false); // Reset readonly mode in case of problem
SetupUtils::EraseSetupToken();
$this->oSessionParameters->Erase();
$oP = new SetupPage('Installation Cannot Continue');
$oP->add("<h2>Fatal error</h2>\n");
$oP->error("<b>Error:</b> This setup step is not accessible with your access rights.");
$sButtonsHtml = <<<HTML
<button type="button" class="ibo-button ibo-is-regular ibo-is-primary" onclick="window.location.href='$sButtonUrl'">$sButtonLabel</button>
HTML;
$oP->p($sButtonsHtml);
$oP->output();
// Prevent token creation
exit;
}
$oPage->LinkScriptFromAppRoot('setup/setup.js');
$oPage->add('<form id="wiz_form" class="ibo-setup--wizard" method="post">');
@@ -208,11 +233,6 @@ class WizardController
// to store the parameters
$oPage->add('<input type="hidden" id="_class" name="_class" value="'.get_class($oStep).'"/>');
$oPage->add('<input type="hidden" id="_state" name="_state" value="'.$oStep->GetState().'"/>');
foreach ($this->aParameters as $sCode => $value) {
$oPage->add('<input type="hidden" name="_params['.$sCode.']" value="'.utils::EscapeHtml($value).'"/>');
}
$oPage->add('<input type="hidden" name="_steps" value="'.utils::EscapeHtml(json_encode($this->aWizardSteps)).'"/>');
$oPage->add('<table style="width:100%;" class="ibo-setup--wizard--buttons-container"><tr>');
if (count($this->aWizardSteps) > 0) {
if ($oStep->CanMoveBackward()) {
@@ -267,7 +287,7 @@ EOF
* Make the wizard run: 'Start', 'Next' or 'Back' depending WizardUpdateButtons();
* on the page's parameters
*/
public function Run()
public function Run(): void
{
/**
* @since 3.2.0 Add the ContextTag init
@@ -276,8 +296,6 @@ EOF
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$sOperation = utils::ReadParam('operation');
$this->aParameters = utils::ReadParam('_params', [], false, 'raw_data');
$this->SetWizardSteps(json_decode(utils::ReadParam('_steps', '[]', false, 'raw_data'), true));
switch ($sOperation) {
case 'next':
@@ -293,65 +311,6 @@ EOF
}
}
/**
* Provides information about the structure/workflow of the wizard by listing
* the possible list of 'steps' and their dependencies
* @param string $sStep Name of the class to start from (used for recursion)
* @param array $aAllSteps List of steps (used for recursion)
*/
public function DumpStructure($sStep = '', $aAllSteps = null)
{
if ($aAllSteps == null) {
$aAllSteps = [];
}
if ($sStep == '') {
$sStep = $this->sInitialStepClass;
}
$oStep = $this->GetWizardStep($sStep);
$aAllSteps[$sStep] = $oStep->GetPossibleSteps();
foreach ($aAllSteps[$sStep] as $sNextStep) {
if (!array_key_exists($sNextStep, $aAllSteps)) {
$aAllSteps = $this->DumpStructure($sNextStep, $aAllSteps);
}
}
return $aAllSteps;
}
/**
* Dump the wizard's structure as a string suitable to produce a chart
* using graphviz's "dot" program
* @return string The 'dot' formatted output
*/
public function DumpStructureAsDot()
{
$aAllSteps = $this->DumpStructure();
$sOutput = "digraph finite_state_machine {\n";
//$sOutput .= "\trankdir=LR;";
$sOutput .= "\tsize=\"10,12\"\n";
$aDeadEnds = [$this->sInitialStepClass];
foreach ($aAllSteps as $sStep => $aNextSteps) {
if (count($aNextSteps) == 0) {
$aDeadEnds[] = $sStep;
}
}
$sOutput .= "\tnode [shape = doublecircle]; ".implode(' ', $aDeadEnds).";\n";
$sOutput .= "\tnode [shape = box];\n";
foreach ($aAllSteps as $sStep => $aNextSteps) {
$oStep = $this->GetWizardStep($sStep);
$sOutput .= "\t$sStep [ label = \"".$oStep->GetTitle()."\"];\n";
if (count($aNextSteps) > 0) {
foreach ($aNextSteps as $sNextStep) {
$sOutput .= "\t$sStep -> $sNextStep;\n";
}
}
}
$sOutput .= "}\n";
return $sOutput;
}
public function SetWizardSteps(array $aWizardSteps): void
{
$this->aWizardSteps = $aWizardSteps;
@@ -364,11 +323,16 @@ EOF
* @return \WizardStep
* @throws \Exception
*/
private function GetWizardStep(string $sCurrentStepClass, string $sCurrentState = ''): WizardStep
private function InstantiateWizardStep(string $sCurrentStepClass, string $sCurrentState = ''): WizardStep
{
if (!is_subclass_of($sCurrentStepClass, WizardStep::class)) {
throw new Exception('Unknown step '.$sCurrentStepClass);
}
return new $sCurrentStepClass($this, $sCurrentState);
}
public function EraseParameters()
{
$this->oSessionParameters->Erase();
}
}

View File

@@ -55,9 +55,6 @@ abstract class AbstractWizStepInstall extends WizardStep
$sSourceDir = $this->oWizard->GetParameter('source_dir');
if (($sMode == 'upgrade') && ($this->oWizard->GetParameter('upgrade_type') == 'keep-previous')) {
//$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir');
//$aCopies[] = ['source' => $sSourceDir, 'destination' => 'modules']; // Source is an absolute path, destination is relative to APPROOT
//$aCopies[] = ['source' => $sPreviousVersionDir.'/portal', 'destination' => 'portal']; // Source is an absolute path, destination is relative to APPROOT
$sSourceDir = APPROOT.'modules';
}

View File

@@ -43,6 +43,11 @@ class WizStepDataAudit extends WizStepInstall
return 'Next';
}
public function CanAccessToWizardStep()
{
return true;
}
public function CanMoveForward()
{
if ($this->CheckDependencies()) {
@@ -135,7 +140,7 @@ HTML
$sButtonUrl = utils::HtmlEntities($sButtonUrl);
$oPage->add_ready_script(
<<<JS
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').after('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').before('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
JS
);
}

View File

@@ -17,6 +17,7 @@
*
* You should have received a copy of the GNU Affero General Public License
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
@@ -205,7 +206,7 @@ EOF
$sUpgradeDMVersionToDisplay = utils::HtmlEntities($sUpgradeDMVersion);
$oPage->add(
<<<HTML
<div class="message message-valid">The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion.</div>
<div class="message message-valid ibo-is-html-content">The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion.</div>
<input type="hidden" name="upgrade_type" value="use-compatible">
<input type="hidden" name="datamodel_path" value="$sCompatibleDMDirToDisplay">
<input type="hidden" name="datamodel_version" value="$sUpgradeDMVersionToDisplay">
@@ -214,15 +215,59 @@ HTML
}
$oPage->add('<div id="db_info"></div>');
$sDBServer = json_encode($this->oWizard->GetParameter('db_server', ''));
$sDBUser = json_encode($this->oWizard->GetParameter('db_user', ''));
$sDBPwd = json_encode($this->oWizard->GetParameter('db_pwd', ''));
$sDBName = json_encode($this->oWizard->GetParameter('db_name', ''));
$sTlsCA = json_encode($this->oWizard->GetParameter('db_tls_ca', ''));
$sTlsEnabled = $this->oWizard->GetParameter('db_tls_enabled', false) ? 1 : 0;
$oPage->add_ready_script(
<<<EOF
$("#changes_summary .title").on('click', function() { $(this).parent().toggleClass('closed'); } );
$('input[name=upgrade_type]').on('click change', function() { WizardUpdateButtons(); });
var iCheckDBTimer = null;
var oXHRCheckDB = null;
function CheckDBConnection()
{
// Don't call the server too often...
if (iCheckDBTimer !== null)
{
clearTimeout(iCheckDBTimer);
iCheckDBTimer = null;
}
iCheckDBTimer = setTimeout(DoCheckDBConnection, 500);
}
function DoCheckDBConnection()
{
iCheckDBTimer = null;
var oParams = {
'db_server': $sDBServer,
'db_user': $sDBUser,
'db_pwd': $sDBPwd,
'db_name': $sDBName,
'db_tls_enabled': $sTlsEnabled,
'db_tls_ca': $sTlsCA,
}
if ((oXHRCheckDB != null) && (oXHRCheckDB != undefined))
{
oXHRCheckDB.abort();
oXHRCheckDB = null;
}
oXHRCheckDB = WizardAsyncAction("check_db", oParams);
}
DoCheckDBConnection(); // Validate the initial values immediately
EOF
);
$oMutex = new iTopMutex(
'cron'.$this->oWizard->GetParameter('db_name', '').$this->oWizard->GetParameter('db_prefix', ''),
'cron'.$sDBName.$this->oWizard->GetParameter('db_prefix', ''),
$this->oWizard->GetParameter('db_server', ''),
$this->oWizard->GetParameter('db_user', ''),
$this->oWizard->GetParameter('db_pwd', ''),
@@ -235,6 +280,15 @@ EOF
}
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
switch ($sCode) {
case 'check_db':
SetupUtils::AsyncCheckDB($oPage, $aParameters);
break;
}
}
public function CanMoveForward()
{
return $this->bCanMoveForward;

View File

@@ -132,9 +132,15 @@ class WizStepDone extends WizardStep
if (false === $bHasBackup) {
SetupUtils::EraseSetupToken();
$this->oWizard->EraseParameters();
}
}
public function CanAccessToWizardStep()
{
return true;
}
public function CanMoveForward()
{
return false;
@@ -152,6 +158,7 @@ class WizStepDone extends WizardStep
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
SetupUtils::EraseSetupToken();
$this->oWizard->EraseParameters();
// For security reasons: add the extension now so that this action can be used to read *only* .tar.gz files from the disk...
$sBackupFile = $aParameters['backup'].'.tar.gz';
if (file_exists($sBackupFile)) {

View File

@@ -45,6 +45,11 @@ class WizStepInstall extends AbstractWizStepInstall
return 'Continue';
}
public function CanAccessToWizardStep()
{
return true;
}
public function CanMoveForward()
{
if ($this->CheckDependencies()) {
@@ -120,7 +125,7 @@ JS);
$sButtonUrl = utils::HtmlEntities($sButtonUrl);
$oPage->add_ready_script(
<<<JS
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').after('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').before('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
JS
);
}

View File

@@ -1,216 +0,0 @@
<?php
/**
* Copyright (C) 2013-2026 Combodo SAS
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
use Combodo\iTop\Application\WebPage\WebPage;
/**
* Second step of the iTop Installation Wizard: Install or Upgrade
*/
class WizStepInstallOrUpgrade extends WizardStep
{
public function GetTitle()
{
return 'Install or Upgrade choice';
}
public function GetPossibleSteps()
{
return [WizStepDetectedInfo::class, WizStepLicense::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$sNextStep = '';
$sInstallMode = utils::ReadParam('install_mode');
$this->oWizard->SaveParameter('previous_version_dir', '');
$this->oWizard->SaveParameter('db_server', '');
$this->oWizard->SaveParameter('db_user', '');
$this->oWizard->SaveParameter('db_pwd', '');
$this->oWizard->SaveParameter('db_name', '');
$this->oWizard->SaveParameter('db_prefix', '');
$this->oWizard->SaveParameter('db_tls_enabled', false);
$this->oWizard->SaveParameter('db_tls_ca', '');
if ($sInstallMode == 'install') {
$this->oWizard->SetParameter('install_mode', 'install');
$sFullSourceDir = SetupUtils::GetLatestDataModelDir();
$this->oWizard->SetParameter('source_dir', $sFullSourceDir);
$this->oWizard->SetParameter('datamodel_version', SetupUtils::GetDataModelVersion($sFullSourceDir));
$sNextStep = WizStepLicense::class;
} else {
$this->oWizard->SetParameter('install_mode', 'upgrade');
$sNextStep = WizStepDetectedInfo::class;
}
return new WizardState($sNextStep);
}
public function Display(SetupPage $oPage): void
{
$sInstallMode = $this->oWizard->GetParameter('install_mode', '');
$sDBServer = $this->oWizard->GetParameter('db_server', '');
$sDBUser = $this->oWizard->GetParameter('db_user', '');
$sDBPwd = $this->oWizard->GetParameter('db_pwd', '');
$sDBName = $this->oWizard->GetParameter('db_name', '');
$sDBPrefix = $this->oWizard->GetParameter('db_prefix', '');
$sTlsEnabled = $this->oWizard->GetParameter('db_tls_enabled', false);
$sTlsCA = $this->oWizard->GetParameter('db_tls_ca', '');
$sPreviousVersionDir = '';
if ($sInstallMode == '') {
$aPreviousInstance = SetupUtils::GetPreviousInstance(APPROOT);
if ($aPreviousInstance['found']) {
$sInstallMode = 'upgrade';
$sDBServer = $aPreviousInstance['db_server'];
$sDBUser = $aPreviousInstance['db_user'];
$sDBPwd = $aPreviousInstance['db_pwd'];
$sDBName = $aPreviousInstance['db_name'];
$sDBPrefix = $aPreviousInstance['db_prefix'];
$sTlsEnabled = $aPreviousInstance['db_tls_enabled'];
$sTlsCA = $aPreviousInstance['db_tls_ca'];
$this->oWizard->SaveParameter('graphviz_path', $aPreviousInstance['graphviz_path']);
$sPreviousVersionDir = APPROOT;
} else {
$sInstallMode = 'install';
}
}
$sPreviousVersionDir = $this->oWizard->GetParameter('previous_version_dir', $sPreviousVersionDir);
$sUpgradeInfoStyle = '';
if ($sInstallMode == 'install') {
$sUpgradeInfoStyle = ' style="display: none;" ';
}
$oPage->add('<div class="setup-content-title">What do you want to do?</div>');
$sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
$oPage->p('<input id="radio_install" type="radio" name="install_mode" value="install" '.$sChecked.'/><label for="radio_install">&nbsp;Install a new '.ITOP_APPLICATION.'</label>');
$sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
$sDisabled = (($sInstallMode == 'install') && (empty($sPreviousVersionDir))) ? ' disabled' : '';
$oPage->p('<input id="radio_update" type="radio" name="install_mode" value="upgrade" '.$sChecked.$sDisabled.'/><label for="radio_update">&nbsp;Upgrade an existing '.ITOP_APPLICATION.' instance</label>');
$sUpgradeDir = utils::HtmlEntities($sPreviousVersionDir);
$oPage->add(
<<<HTML
<div id="upgrade_info"'.$sUpgradeInfoStyle.'>
<div class="setup-disk-location--input--container">Location on the disk:<input id="previous_version_dir_display" type="text" value="$sUpgradeDir" class="ibo-input" disabled>
<input type="hidden" name="previous_version_dir" value="$sUpgradeDir"></div>
HTML
);
SetupUtils::DisplayDBParameters(
$oPage,
false,
$sDBServer,
$sDBUser,
$sDBPwd,
$sDBName,
$sDBPrefix,
$sTlsEnabled,
$sTlsCA
);
$oPage->add('</div>');
//$oPage->add('</fieldset>');
$oPage->add_ready_script(
<<<JS
$("#radio_update").on('change', function() { if (this.checked ) { $('#upgrade_info').show(); WizardUpdateButtons(); } else { $('#upgrade_info').hide(); } });
$("#radio_install").on('change', function() { if (this.checked ) { $('#upgrade_info').hide(); WizardUpdateButtons(); } else { $('#upgrade_info').show(); } });
JS
);
}
public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
{
switch ($sCode) {
case 'check_path':
$sPreviousVersionDir = $aParameters['previous_version_dir'];
$aPreviousInstance = SetupUtils::GetPreviousInstance($sPreviousVersionDir);
if ($aPreviousInstance['found']) {
$sDBServer = utils::EscapeHtml($aPreviousInstance['db_server']);
$sDBUser = utils::EscapeHtml($aPreviousInstance['db_user']);
$sDBPwd = utils::EscapeHtml($aPreviousInstance['db_pwd']);
$sDBName = utils::EscapeHtml($aPreviousInstance['db_name']);
$sDBPrefix = utils::EscapeHtml($aPreviousInstance['db_prefix']);
$oPage->add_ready_script(
<<<EOF
$("#db_server").val('$sDBServer');
$("#db_user").val('$sDBUser');
$("#db_pwd").val('$sDBPwd');
$("#db_name").val('$sDBName');
$("#db_prefix").val('$sDBPrefix');
$("#db_pwd").trigger('change'); // Forces check of the DB connection
EOF
);
}
break;
case 'check_db':
SetupUtils::AsyncCheckDB($oPage, $aParameters);
break;
case 'check_backup':
$sDBBackupPath = $aParameters['db_backup_path'];
$fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath);
if ($fFreeSpace !== false) {
$sMessage = utils::EscapeHtml(SetupUtils::HumanReadableSize($fFreeSpace).' free in '.dirname($sDBBackupPath));
$oPage->add_ready_script(
<<<EOF
$("#backup_info").html('$sMessage');
EOF
);
} else {
$oPage->add_ready_script(
<<<EOF
$("#backup_info").html('');
EOF
);
}
break;
}
}
/**
* Tells whether the "Next" button should be enabled interactively
* @return string A piece of javascript code returning either true or false
*/
public function JSCanMoveForward()
{
return
<<<EOF
if ($("#radio_install").prop("checked"))
{
ValidateField("db_name", false);
ValidateField("db_new_name", false);
ValidateField("db_prefix", false);
return true;
}
else
{
var bRet = ($("#wiz_form").data("db_connection") !== "error");
bRet = ValidateField("db_name", true) && bRet;
bRet = ValidateField("db_new_name", true) && bRet;
bRet = ValidateField("db_prefix", true) && bRet;
return bRet;
}
EOF
;
}
}

View File

@@ -34,7 +34,8 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
$oWizard->SetParameter('extensions_not_uninstallable', '[]');
$oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks());
$oWizard->SaveParameter('force-uninstall', '');
$oWizard->SaveParameter('force-uninstall', false);
$oWizard->SaveParameter('skip_wizard', false);
// should be done at the end
parent::__construct($oWizard, $sCurrentState, false);
@@ -52,7 +53,8 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
*/
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
if ($this->oWizard->GetParameter('skip_wizard', false)) {
$bSkipWizard = $this->oWizard->GetParameter('skip_wizard', false);
if ($bSkipWizard) {
$oRuntimeEnv = new RunTimeEnvironment();
$sBuildConfigFile = APPCONF.$oRuntimeEnv->GetBuildEnv().'/'.ITOP_CONFIG_FILE;
$oConfig = new Config($sBuildConfigFile);
@@ -61,24 +63,45 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
$this->oWizard->SetParameter('selected_extensions', json_encode($aExtensionsFromDatabase));
$adModulesFromDatabase = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
$this->oWizard->SetParameter('selected_modules', json_encode(array_keys($adModulesFromDatabase)));
} else {
$this->oWizard->SavePostedParameter('selected_modules');
$this->oWizard->SavePostedParameter('selected_extensions');
$this->oWizard->SavePostedParameter('added_extensions');
$this->oWizard->SavePostedParameter('removed_extensions');
$this->oWizard->SavePostedParameter('extensions_not_uninstallable');
$this->oWizard->SavePostedParameter('copy_setup_files', '1');
$this->oWizard->SavePostedParameter('force-uninstall');
$this->oWizard->SavePostedParameter('use_symbolic_links');
$this->oWizard->SavePostedParameter('return_application');
$this->oWizard->SavePostedParameter('target_env');
}
$aWizardSteps = $this->GetWizardSteps();
$this->oWizard->SetWizardSteps($aWizardSteps);
$this->sCurrentState = count($aWizardSteps) - 1;
$aWizardSteps = $this->oWizard->GetParameter('_steps', null);
if (is_null($aWizardSteps)) {
$aWizardSteps = $this->GetWizardSteps();
if ($bSkipWizard) {
$this->oWizard->SetParameter('_steps', $aWizardSteps);
}
$aSelectedComponents = $this->GetSelectedComponents($this->aSteps, $this->oWizard->GetParameter('selected_extensions'));
$this->oWizard->SetParameter('selected_components', json_encode($aSelectedComponents));
// Component selection in previous screens
if ($this->oWizard->GetParameter('selected_components', '[]') === '[]') {
// Save the choices for the summary step
$sDisplayChoices = '<ul>';
$i = 0;
foreach ($this->aSteps as $aStepInfo) {
$sDisplayChoices .= $this->GetSelectedModules($aStepInfo, $aSelectedComponents[$i], $aModules, '', '', $aExtensions);
$i++;
$aSelectedComponents = $this->GetSelectedComponents($this->aSteps, $this->oWizard->GetParameter('selected_extensions', '[]'));
$this->oWizard->SetParameter('selected_components', json_encode($aSelectedComponents));
} else {
$aSelectedComponents = json_decode($this->oWizard->GetParameter('selected_components'), true);
}
// Save the choices for the summary step
$sDisplayChoices = '<ul>';
$i = 0;
foreach ($this->aSteps as $aStepInfo) {
$sDisplayChoices .= $this->GetSelectedModules($aStepInfo, $aSelectedComponents[$i], $aModules, '', '', $aExtensions);
$i++;
}
$sDisplayChoices .= '</ul>';
$this->oWizard->SetParameter('display_choices', $sDisplayChoices);
}
$sDisplayChoices .= '</ul>';
$this->oWizard->SetParameter('display_choices', $sDisplayChoices);
return new WizardState(WizStepDataAudit::class);
}

View File

@@ -171,13 +171,13 @@ class WizStepModulesChoice extends AbstractWizStepInstall
{
$aSteps = [
["class" => "WizStepWelcome","state" => ""],
["class" => "WizStepInstallOrUpgrade","state" => ""],
["class" => "WizStepDetectedInfo","state" => ""],
["class" => "WizStepUpgradeMiscParams","state" => ""],
];
$i = 0;
$this->aSteps = null;
while (null != $this->GetStepInfo($i)) {
// Allow looping for all existing steps
$this->aSteps = null;
$aSteps [] = ["class" => "WizStepModulesChoice","state" => "$i"];
$i++;

View File

@@ -39,6 +39,11 @@ class WizStepSummary extends AbstractWizStepInstall
return [WizStepInstall::class];
}
public function CanAccessToWizardStep()
{
return true;
}
/**
* Returns the label for the " Next >> " button
* @return string The label for the button
@@ -123,21 +128,23 @@ class WizStepSummary extends AbstractWizStepInstall
$oPage->add($sExtensionsRemoved);
$oPage->add('</div>');
$oPage->add('<div class="closed"><a class="title ibo-setup-summary-title" href="#" aria-label="Database Parameters">Database Parameters</a><ul>');
$oPage->add('<li>Server Name: '.$aInstallParams['database']['server'].'</li>');
$oPage->add('<li>DB User Name: '.$aInstallParams['database']['user'].'</li>');
$oPage->add('<li>DB user password: ***</li>');
if (($sMode == 'install') && ($this->oWizard->GetParameter('create_db') == 'yes')) {
$oPage->add('<li>Database Name: '.$aInstallParams['database']['name'].' (will be created)</li>');
} else {
$oPage->add('<li>Database Name: '.$aInstallParams['database']['name'].'</li>');
if ($this->oWizard->GetParameter('return_application') === '') {
$oPage->add('<div class="closed"><a class="title ibo-setup-summary-title" href="#" aria-label="Database Parameters">Database Parameters</a><ul>');
$oPage->add('<li>Server Name: '.$aInstallParams['database']['server'].'</li>');
$oPage->add('<li>DB User Name: '.$aInstallParams['database']['user'].'</li>');
$oPage->add('<li>DB user password: ***</li>');
if (($sMode == 'install') && ($this->oWizard->GetParameter('create_db') == 'yes')) {
$oPage->add('<li>Database Name: '.$aInstallParams['database']['name'].' (will be created)</li>');
} else {
$oPage->add('<li>Database Name: '.$aInstallParams['database']['name'].'</li>');
}
if ($aInstallParams['database']['prefix'] != '') {
$oPage->add('<li>Prefix for the '.ITOP_APPLICATION.' tables: '.$aInstallParams['database']['prefix'].'</li>');
} else {
$oPage->add('<li>Prefix for the '.ITOP_APPLICATION.' tables: none</li>');
}
$oPage->add('</ul></div>');
}
if ($aInstallParams['database']['prefix'] != '') {
$oPage->add('<li>Prefix for the '.ITOP_APPLICATION.' tables: '.$aInstallParams['database']['prefix'].'</li>');
} else {
$oPage->add('<li>Prefix for the '.ITOP_APPLICATION.' tables: none</li>');
}
$oPage->add('</ul></div>');
$oPage->add('<div class="closed"><a class="title ibo-setup-summary-title" href="#" aria-label="Data Model Configuration">Data Model Configuration</a>');
$oPage->add($this->oWizard->GetParameter('display_choices'));
@@ -253,7 +260,7 @@ JS
$sButtonUrl = utils::HtmlEntities($sButtonUrl);
$oPage->add_ready_script(
<<<JS
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').after('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').before('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
JS
);
}

View File

@@ -40,7 +40,7 @@ class WizStepWelcome extends WizardStep
}
/**
* Returns the label for the " Next >> " button
* Returns the label for the "Next >>" button
* @return string The label for the button
*/
public function GetNextButtonLabel()
@@ -50,16 +50,43 @@ class WizStepWelcome extends WizardStep
public function GetPossibleSteps()
{
return [WizStepInstallOrUpgrade::class];
return [WizStepDetectedInfo::class, WizStepLicense::class];
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
return new WizardState(WizStepInstallOrUpgrade::class);
if ($this->oWizard->GetParameter('install_mode', 'install') === 'install') {
return new WizardState(WizStepLicense::class);
}
return new WizardState(WizStepDetectedInfo::class);
}
public function Display(SetupPage $oPage): void
{
$this->oWizard->EraseParameters();
$this->oWizard->SetWizardSteps([]);
$aPreviousInstance = SetupUtils::GetPreviousInstance(APPROOT);
if ($aPreviousInstance['found']) {
$this->oWizard->SetParameter('install_mode', 'upgrade');
$this->oWizard->SetParameter('db_server', $aPreviousInstance['db_server']);
$this->oWizard->SetParameter('db_user', $aPreviousInstance['db_user']);
$this->oWizard->SetParameter('db_pwd', $aPreviousInstance['db_pwd']);
$this->oWizard->SetParameter('db_name', $aPreviousInstance['db_name']);
$this->oWizard->SetParameter('db_prefix', $aPreviousInstance['db_prefix']);
$this->oWizard->SetParameter('db_tls_enabled', $aPreviousInstance['db_tls_enabled']);
$this->oWizard->SetParameter('db_tls_ca', $aPreviousInstance['db_tls_ca'] ?? '');
$this->oWizard->SetParameter('graphviz_path', $aPreviousInstance['graphviz_path']);
} else {
$this->oWizard->SetParameter('install_mode', 'install');
$sFullSourceDir = SetupUtils::GetLatestDataModelDir();
$this->oWizard->SetParameter('source_dir', $sFullSourceDir);
$this->oWizard->SetParameter('datamodel_version', SetupUtils::GetDataModelVersion($sFullSourceDir));
}
$this->oWizard->SetParameter('previous_version_dir', APPROOT);
// Store the misc_options for the future...
$aMiscOptions = utils::ReadParam('option', [], false, 'raw_data');
$sMiscOptions = $this->oWizard->GetParameter('misc_options', json_encode($aMiscOptions));
@@ -127,7 +154,7 @@ HTML
<form id="fast_setup" method="post">
<input type="hidden" name="_class" value="WizStepLandingBeforeAudit"/>
<input type="hidden" name="operation" value="next"/>
<input type="hidden" name="_params[skip_wizard]" value="1"/>
<input type="hidden" name="skip_wizard" value="1"/>
</form>
HTML
);

View File

@@ -24,7 +24,6 @@
* Steps order (can be retrieved using \WizardController::DumpStructure) :
*
* WizStepWelcome
* WizStepInstallOrUpgrade
* + +
* | |
* v +----->
@@ -76,6 +75,11 @@ abstract class WizardStep
{
}
public function CanAccessToWizardStep()
{
return ($this->oWizard->GetParameter('return_application', '') === '');
}
protected function CheckDependencies()
{
if (is_null($this->bDependencyCheck)) {
@@ -187,15 +191,6 @@ abstract class WizardStep
return 'return true;';
}
/**
* Tells whether this step of the wizard requires that the configuration file be writable
* @return bool True if the wizard will possibly need to modify the configuration at some point
*/
public function RequiresWritableConfig()
{
return true;
}
/**
* Overload this function to implement asynchronous action(s) (AJAX)
* @param string $sCode The code of the action (if several actions need to be distinguished)

View File

@@ -4,7 +4,6 @@ require_once(APPROOT.'setup/wizardsteps/WizardState.php');
require_once(APPROOT.'setup/wizardsteps/WizardStep.php');
require_once(APPROOT.'setup/wizardsteps/AbstractWizStepInstall.php');
require_once(APPROOT.'setup/wizardsteps/WizStepWelcome.php');
require_once(APPROOT.'setup/wizardsteps/WizStepInstallOrUpgrade.php');
require_once(APPROOT.'setup/wizardsteps/WizStepDetectedInfo.php');
require_once(APPROOT.'setup/wizardsteps/WizStepLicense.php');
require_once(APPROOT.'setup/wizardsteps/WizStepLicense2.php');

View File

@@ -166,8 +166,10 @@ class AttributeImage extends AttributeBlob
return null;
}
$bExistingImageModified = ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges())));
if ($oHostObject->IsNew() || ($bExistingImageModified)) {
if (is_null($oHostObject)
|| $oHostObject->IsNew()
|| ($oHostObject->IsModified() && array_key_exists($this->GetCode(), $oHostObject->ListChanges())) //$bExistingImageModified
) {
// If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline
// otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it
return 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData());

View File

@@ -1,11 +1,11 @@
<?php
/**
* @copyright Copyright (C) 2010-2024 Combodo SAS
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Application\Helper;
namespace Combodo\iTop\Service\Session;
use Combodo\iTop\SessionTracker\SessionHandler;
@@ -21,9 +21,9 @@ class Session
/** @var int|null */
public static $iSessionId = null;
/** @var bool */
public static $bAllowCLI = false;
public static bool $bAllowCLI = false;
public static function Start()
public static function Start(): void
{
if (session_status() === PHP_SESSION_DISABLED) {
return;
@@ -47,7 +47,7 @@ class Session
self::$iSessionId = session_id();
}
public static function RegenerateId($bDeleteOldSession = false)
public static function RegenerateId($bDeleteOldSession = false): void
{
if (session_status() === PHP_SESSION_DISABLED || headers_sent()) {
return;
@@ -61,7 +61,7 @@ class Session
self::$iSessionId = session_id();
}
public static function WriteClose()
public static function WriteClose(): void
{
if (session_status() === PHP_SESSION_DISABLED) {
return;
@@ -76,7 +76,7 @@ class Session
* @param string|array $key key to access to the session variable. To access to $_SESSION['a']['b'] $key must be ['a', 'b']
* @param $value
*/
public static function Set($key, $value)
public static function Set($key, $value): void
{
if (!isset($_SESSION) || self::Get($key) == $value) {
return;
@@ -103,7 +103,7 @@ class Session
/**
* @param string|array $key key to access to the session variable. To access to $_SESSION['a']['b'] $key must be ['a', 'b']
*/
public static function Unset($key)
public static function Unset($key): void
{
if (self::IsSet($key)) {
$aSession = $_SESSION;
@@ -137,7 +137,7 @@ class Session
*
* @return mixed
*/
public static function Get($key, $default = null)
public static function Get($key, $default = null): mixed
{
if (isset($_SESSION)) {
$aSession = $_SESSION;
@@ -189,7 +189,7 @@ class Session
/**
* @return bool|string
*/
public static function GetLog()
public static function GetLog(): string
{
return print_r($_SESSION, true);
}

View File

@@ -0,0 +1,96 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Service\Session;
use IssueLog;
use LogChannels;
use utils;
class SessionParameters
{
private string $sSessionArrayName;
private array $aParameters;
public function __construct(string $sParamsName)
{
$this->sSessionArrayName = $sParamsName;
$this->aParameters = Session::Get($this->sSessionArrayName, []);
IssueLog::Enable(APPROOT.'log/error.log');
}
/**
* Reads a "persistent" parameter from the wizard's context
* @param string $sParamCode The code identifying this parameter
* @param mixed $defaultValue The default value of the parameter in case it was not set
*/
public function GetParameter(string $sParamCode, mixed $defaultValue = '')
{
if (array_key_exists($sParamCode, $this->aParameters)) {
return $this->aParameters[$sParamCode];
}
return $defaultValue;
}
/**
* Stores a "persistent" parameter in the wizard's context
*
* @param string $sParamCode The code identifying this parameter
* @param mixed $value The value to store
*/
public function SetParameter($sParamCode, $value): void
{
$this->aParameters[$sParamCode] = $value;
$this->Save();
}
/**
* Stores the value of the page's parameter in a "persistent" parameter in the wizard's context
* @param string $sParamCode The code identifying this parameter
* @param mixed $defaultValue The default value for the parameter
* @param string $sSanitizationFilter A 'sanitization' fitler. Default is 'raw_data', which means no filtering
*/
public function SetParameterFromPostedParams(string $sParamCode, mixed $defaultValue, string $sSanitizationFilter = 'raw_data'): void
{
$value = utils::ReadPostedParam($sParamCode, $defaultValue, $sSanitizationFilter);
$this->aParameters[$sParamCode] = $value;
$this->Save();
}
/**
* Stores the value of the page's parameter in a "persistent" parameter in the wizard's context
* @param string $sParamCode The code identifying this parameter
* @param mixed $defaultValue The default value for the parameter
* @param string $sSanitizationFilter A 'sanitization' fitler. Default is 'raw_data', which means no filtering
*/
public function SetParameterFromParams(string $sParamCode, mixed $defaultValue, string $sSanitizationFilter = 'raw_data'): void
{
$value = utils::ReadParam($sParamCode, $defaultValue, false, $sSanitizationFilter);
$this->aParameters[$sParamCode] = $value;
$this->Save();
}
private function Save(): void
{
Session::Set($this->sSessionArrayName, $this->aParameters);
}
public function Erase(): void
{
$this->aParameters = [];
$this->Save();
$this->LogParameters();
}
public function LogParameters(): void
{
IssueLog::Debug('---------------------------------', LogChannels::SESSION_PARAMETERS);
IssueLog::Debug(json_encode(Session::Get($this->sSessionArrayName, []), JSON_PRETTY_PRINT), LogChannels::SESSION_PARAMETERS);
IssueLog::Debug('---------------------------------', LogChannels::SESSION_PARAMETERS);
}
}

View File

@@ -121,3 +121,6 @@ class_alias(\Combodo\iTop\PropertyType\ValueType\Leaf\ValueTypeText::class, 'Com
class_alias(\Combodo\iTop\PropertyType\Serializer\XMLFormat\XMLFormatCSV::class, 'Combodo-XMLFormat-CSV');
class_alias(\Combodo\iTop\PropertyType\Serializer\XMLFormat\XMLFormatValueAsId::class, 'Combodo-XMLFormat-ValueAsId');
class_alias(\Combodo\iTop\PropertyType\Serializer\XMLFormat\XMLFormatFlatArray::class, 'Combodo-XMLFormat-FlatArray');
// Moved classes
class_alias(\Combodo\iTop\Service\Session\Session::class, '\Combodo\iTop\Application\Helper\Session');

View File

@@ -87,8 +87,6 @@ abstract class ItopDataTestCase extends ItopTestCase
*/
public const DEFAULT_TEST_ENVIRONMENT = 'production';
public const USE_TRANSACTION = true;
public const CREATE_TEST_ORG = false;
protected static $aURP_Profiles = [
'Administrator' => 1,
'Portal user' => 2,
@@ -102,9 +100,15 @@ abstract class ItopDataTestCase extends ItopTestCase
'Service Manager' => 10,
'Document author' => 11,
'Portal power user' => 12,
'Business partner user' => 40,
'REST Services User' => 1024,
'Configuration ReadOnly' => 5500,
'Ticket ReadOnly' => 5501,
'Service Catalog ReadOnly' => 5502,
];
public const CREATE_TEST_ORG = false;
/**
* This method is called before the first test of this test class is run (in the current process).
*/
@@ -1463,16 +1467,42 @@ abstract class ItopDataTestCase extends ItopTestCase
]);
}
/**
* @description To avoid adding finalclasses parameters to GivenUserInDB
* @param string $sPassword
* @param array $aProfiles Profile names Example: ['Administrator']
* @param bool $bReturnLogin
*
* @return string|int The unique login
* @throws \Exception
*/
protected function GivenTokenUserInDB(array $aProfiles, bool $bReturnLogin = true): string|int
{
$sLogin = 'demo_test_'.uniqid(__CLASS__, true);
$aProfileList = array_map(function ($sProfileId) {
return 'profileid:'.self::$aURP_Profiles[$sProfileId];
}, $aProfiles);
$iUser = $this->GivenObjectInDB('UserToken', [
'login' => $sLogin,
'language' => 'EN US',
'profile_list' => $aProfileList,
]);
return $bReturnLogin ? $sLogin : $iUser;
}
/**
* @param string $sPassword
* @param array $aProfiles Profile names Example: ['Administrator']
* @param string|null $sLogin
* @param string|null $sUserId
* @param bool $bReturnLogin
*
* @return string The unique login
* @throws \Exception
*/
protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null): string
protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null, bool $bReturnLogin = true): string
{
if (is_null($sLogin)) {
$sLogin = 'demo_test_'.uniqid(__CLASS__, true);
@@ -1489,7 +1519,7 @@ abstract class ItopDataTestCase extends ItopTestCase
'profile_list' => $aProfileList,
]);
return $sLogin;
return $bReturnLogin ? $sLogin : $sUserId;
}
/**

View File

@@ -2,7 +2,7 @@
namespace Combodo\iTop\Test\UnitTest\Application;
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Service\Session\Session;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**

View File

@@ -835,8 +835,16 @@ class utilsTest extends ItopTestCase
public function testFileGetContentsAndMIMETypeOnLocalURL()
{
$sURL = utils::GetAbsoluteUrlAppRoot().'env-production/itop-request-mgmt/images/user-request.svg';
$sPath = APPROOT.'env-production/itop-request-mgmt/images/user-request.svg';
// For the test to work on most dev / CI environments, we need to use a module with an SVG image that is installed with the current setup choices
if (file_exists(APPROOT.'env-production/itop-request-mgmt/images/user-request.svg')) {
$sImgRelPath = 'itop-request-mgmt/images/user-request.svg';
} elseif (file_exists(APPROOT.'env-production/itop-request-mgmt-itil/images/user-request.svg')) {
$sImgRelPath = 'itop-request-mgmt-itil/images/user-request.svg';
}
$sURL = utils::GetAbsoluteUrlModulesRoot().$sImgRelPath;
$sPath = APPROOT.'env-production/'.$sImgRelPath;
$oExpectedDocument = new ormDocument(file_get_contents($sPath), 'image/svg+xml; charset=us-ascii', 'user-request.svg');
$this->assertEquals($oExpectedDocument, utils::FileGetContentsAndMIMEType($sURL));
// Read local URL directly on disk

View File

@@ -0,0 +1,67 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DBObjectSearch;
use DBUnionSearch;
use UserRights;
class DBSearchApplyDataFiltersTest extends ItopDataTestCase
{
public const CREATE_TEST_ORG = true;
protected string $sOriginalUserRightsSelectModuleClass = '';
/**
* @throws \Exception
*/
protected function setUp(): void
{
parent::setUp();
// Backup original UserRights select module as it will changed in some tests
$this->sOriginalUserRightsSelectModuleClass = get_class(UserRights::GetModuleInstance());
$this->RequireOnceUnitTestFile('Fixtures/N9687_CustomGetSelectFilterClass.php');
}
/**
* @inheritDoc
*/
public function tearDown(): void
{
// Restore original UserRights select module to not interfere with next tests
UserRights::SelectModule($this->sOriginalUserRightsSelectModuleClass);
parent::tearDown();
}
public function testApplyDataFiltersOnDBObjectSearchShouldAcceptGetSelectFilterClassReturningDBUnionSearch()
{
// Use custom select filter that returns a DBUnionSearch
$sPreviousSelectModuleClass = get_class(UserRights::GetModuleInstance());
UserRights::SelectModule('\\Combodo\\iTop\\Test\\UnitTest\\Core\\Fixtures\\N9687_CustomGetSelectFilterClass');
// Create a user and login, otherwise the select filter won't apply
self::CreateUser('test_dbsearch_applydatafilters', 3);
UserRights::Login('test_dbsearch_applydatafilters');
// Create a person
$oCreatedPerson = $this->CreatePerson(microtime());
// Try to retrieve it using the select filter
$oSearch = DBObjectSearch::FromOQL("SELECT Person WHERE id = {$oCreatedPerson->GetKey()}");
$oFilteredSearch = $this->InvokeNonPublicMethod(DBObjectSearch::class, 'ApplyDataFilters', $oSearch);
// Restore original select module to not interfere with next tests
UserRights::SelectModule($sPreviousSelectModuleClass);
$this->assertEquals(DBUnionSearch::class, get_class($oFilteredSearch), "DBObjectSearch::ApplyDataFilters() should be able to return a \DBUnionSearch");
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core\Fixtures;
use DBObjectSearch;
use DBUnionSearch;
use UserRightsProfile;
class N9687_CustomGetSelectFilterClass extends UserRightsProfile
{
public function GetSelectFilter($oUser, $sClass, $aSettings = [])
{
// We just need the method to return an union search
return new DBUnionSearch([
DBObjectSearch::FromOQL("SELECT $sClass WHERE 1!=2"),
DBObjectSearch::FromOQL("SELECT $sClass WHERE 1=1"),
]);
}
}

View File

@@ -29,11 +29,11 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreCannotSaveObjectException;
use CoreException;
use DBObject;
use DBObjectSearch;
use DBObjectSet;
use DeleteException;
use Dict;
use MetaModel;
use UserLocal;
use UserRights;
@@ -81,6 +81,65 @@ class UserRightsTest extends ItopDataTestCase
return $oUser;
}
public function testIsActionAllowedWithNonInstantiatedUserObject()
{
$oUser = $this->GivenUserWithProfiles('test1', [self::$aURP_Profiles['Configuration Manager']]); // not a readonly profile
$oAdminUser = $this->GivenUserWithProfiles('test2', [self::$aURP_Profiles['Administrator']]);
$oAdminUser->DBInsert();
$_SESSION = [];
UserRights::Login($oAdminUser->Get('login'));
self::assertTrue(UserRights::IsActionAllowed('Server', UR_ACTION_MODIFY, null, $oUser) === UR_ALLOWED_YES);
}
/**
* @param array $aProfileIds
* @param array $aShouldBeAllowedToSeeClass
* @param array $aShouldBeAllowedToEditClass
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \DictExceptionUnknownLanguage
* @throws \MySQLException
* @throws \OQLException
* @dataProvider ReadOnlyProvider
*/
public function testReadOnlyUser(array $aProfileIds, array $aShouldBeAllowedToSeeClass, array $aShouldBeAllowedToEditClass): void
{
$oUser = $this->GivenUserWithProfiles('test1', $aProfileIds);
$oUser->DBInsert();
$_SESSION = [];
UserRights::Login($oUser->Get('login'));
$aClassesToTest = ['FunctionalCI', 'Ticket', 'ServiceFamily'];
foreach ($aClassesToTest as $sClass) {
$bShouldBeAllowedToSee = in_array($sClass, $aShouldBeAllowedToSeeClass);
$bIsAllowedReading = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_READ);
$this->assertSame(
$bShouldBeAllowedToSee,
$bIsAllowedReading,
"User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToSee ? "" : "NOT ")."be allowed to see class $sClass"
);
$bShouldBeAllowedToEdit = in_array($sClass, $aShouldBeAllowedToEditClass);
$bIsAllowedEditing = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY);
$this->assertSame(
$bIsAllowedEditing,
$bShouldBeAllowedToEdit,
"User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToEdit ? "" : "NOT ")."be allowed to edit class $sClass"
);
}
}
protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DBObject
{
$oProfiles = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class));
@@ -433,7 +492,7 @@ class UserRightsTest extends ItopDataTestCase
$oUser = $this->GivenUserWithProfiles('test1', [$iProfileId, 2]);
$this->expectException(CoreCannotSaveObjectException::class);
$this->expectExceptionMessage('Profile "Portal user" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)');
$this->expectExceptionMessage(Dict::Format('Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice', PORTAL_PROFILE_NAME));
$oUser->DBInsert();
}
@@ -572,4 +631,82 @@ class UserRightsTest extends ItopDataTestCase
$oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]);
static::assertNull($oUser, 'FindUser should return null when the login is unknown');
}
protected function ReadOnlyProvider(): array
{
return [
'CI' => [
'ProfilesId' => [
5500,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets' => [
'ProfilesId' => [
5501,
],
'ShouldBeAllowedToSeeClasses' => [
'Ticket',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Catalog' => [
'ProfilesId' => [
5502,
],
'ShouldBeAllowedToSeeClasses' => [
'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'CI and Tickets' => [
'ProfilesId' => [
5500, 5501,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket',
],
'ShouldBeAllowedToEditClasses' => [],
],
'CI and Catalog' => [
'ProfilesId' => [
5500, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets and Catalog' => [
'ProfilesId' => [
5501, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets and Catalog + profile Ccnfiguration Manager' => [
'ProfilesId' => [
5501, 5502, 3,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => ['FunctionalCI'],
],
'CI, Tickets and Catalog' => [
'ProfilesId' => [
5500, 5501, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
];
}
}

View File

@@ -72,13 +72,12 @@ class StaticDeletionPlanTest extends \AbstractCleanup
public function testGetStaticDeletionPlan_DeleteObjRecursively()
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
create DFRToUpdate (name = DFRToUpdate_1, extkey_id = DFRToRemoveLeaf_1)
create DFRRemovedCollateral (name = DFRRemovedCollateral_1, extkey_id = DFRToRemoveLeaf_1)
create DFRRemovedCollateralCascade (name = DFRRemovedCollateralCascade_1, extkey_id = DFRRemovedCollateral_1)
create DFRRemovedCollateralCascade (name = DFRRemovedCollateralCascade_2, extkey_id = DFRRemovedCollateral_1)
EOF);
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_2
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oService = new StaticDeletionPlan();
@@ -86,6 +85,7 @@ EOF);
self::assertArrayHasKey('DFRRemovedCollateralCascade', $aRes, 'The cleanup should descend to the cascaded classes');
// echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
// echo json_encode($this->aIdByClass, JSON_PRETTY_PRINT);
}
@@ -174,95 +174,37 @@ EOF);
public function testMultipleExtKeys()
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC1 (name = DFRC1_1)
create DFRC1 (name = DFRC1_2)
create DFRC1 (name = DFRC1_3)
create DFRC1 (name = DFRC1_4)
create DFRC1 (name = DFRC1_5)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC1_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC1_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC1_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC2 (name = DFRC2_1, extkey1_id = DFRC1_4, extkey2_id = DFRC1_5, extkey3_id = DFRC3_3)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC2_1)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
EOF);
$aClasses = [ 'DFRC01' ];
$aClasses = [ 'DFRC1' ];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetStaticDeletionPlan($aClasses);
echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
'DFRC01' => ['iDeleteCount' => 5],
'DFRC21' => ['iDeleteCount' => 1],
'DFRC1' => ['iDeleteCount' => 5],
'DFRC2' => ['iDeleteCount' => 1],
'DFRC3' => ['iDeleteCount' => 3],
'DFRC4' => ['iDeleteCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testMultipleInitialSubClassesFromSameRoot()
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC02 (name = DFRC02_1)
create DFRC02 (name = DFRC02_2)
create DFRC02 (name = DFRC02_3)
create DFRC03 (name = DFRC03_1)
create DFRC03 (name = DFRC03_2)
create DFRC03 (name = DFRC03_3)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC3 (name = DFRC3_4, extkey1_id = DFRC02_1)
create DFRC3 (name = DFRC3_5, extkey1_id = DFRC02_2)
create DFRC3 (name = DFRC3_6, extkey1_id = DFRC02_3)
create DFRC3 (name = DFRC3_7, extkey1_id = DFRC03_1)
create DFRC3 (name = DFRC3_8, extkey1_id = DFRC03_2)
create DFRC3 (name = DFRC3_9, extkey1_id = DFRC03_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
EOF);
$aClasses = [ 'DFRC01', 'DFRC03' ];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetStaticDeletionPlan($aClasses);
echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
$aRes = $oService->GetCleanupSummary($aClasses);
echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
$aExpected = [
'DFRC01' => ['iDeleteCount' => 5],
'DFRC2' => ['iDeleteCount' => 1],
'DFRC21' => ['iDeleteCount' => 1],
'DFRC3' => ['iDeleteCount' => 6],
'DFRC4' => ['iDeleteCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
private function AssertSummaryEquals(array $aExpected, array $aActual, string $sMessage = '')

View File

@@ -1,124 +0,0 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval;
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
class TempTablesTest extends \AbstractCleanup
{
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/data_cleanup_delta.xml';
}
public function testMultipleExtKeys()
{
echo "--------------------------------------------\n".__METHOD__."\n";
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
EOF);
$aClasses = [ 'DFRC01' ];
$oService = new StaticDeletionPlan();
$aTempTables = $oService->GetTempTableDefinitions($aClasses);
echo json_encode($aTempTables, JSON_PRETTY_PRINT)."\n";
self::assertTrue(true);
}
public function testMultipleInitialSubClassesFromSameRoot()
{
echo "--------------------------------------------\n".__METHOD__."\n";
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC02 (name = DFRC02_1)
create DFRC02 (name = DFRC02_2)
create DFRC02 (name = DFRC02_3)
create DFRC03 (name = DFRC03_1)
create DFRC03 (name = DFRC03_2)
create DFRC03 (name = DFRC03_3)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC3 (name = DFRC3_4, extkey1_id = DFRC02_1)
create DFRC3 (name = DFRC3_5, extkey1_id = DFRC02_2)
create DFRC3 (name = DFRC3_6, extkey1_id = DFRC02_3)
create DFRC3 (name = DFRC3_7, extkey1_id = DFRC03_1)
create DFRC3 (name = DFRC3_8, extkey1_id = DFRC03_2)
create DFRC3 (name = DFRC3_9, extkey1_id = DFRC03_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC22 (name = DFRC22_1, extkey1_id = DFRC03_1)
create DFRC22 (name = DFRC22_2, extkey1_id = DFRC03_1)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
create DFRC4 (name = DFRC4_2, extkey1_id = DFRC22_1)
create DFRC4 (name = DFRC4_3, extkey1_id = DFRC22_2)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
update DFRC3 (name = DFRC3_5, extkey2_id = DFRC4_2)
update DFRC3 (name = DFRC3_6, extkey2_id = DFRC4_3)
EOF);
$aClasses = [ 'DFRC01', 'DFRC03' ];
$oService = new StaticDeletionPlan();
$aTempTables = $oService->GetTempTableDefinitions($aClasses);
echo json_encode($aTempTables, JSON_PRETTY_PRINT)."\n";
$this->GetIds($aTempTables);
$aRes = $oService->GetStaticDeletionPlan($aClasses);
echo "\nDeletionPlan: ".json_encode($aRes, JSON_PRETTY_PRINT)."\n";
self::assertTrue(true);
}
private function GetIds(array $aTempTables): void
{
foreach ($aTempTables as $sDBName => $aQueries) {
foreach ($aQueries as $sQuery) {
CMDBSource::Query($sQuery);
}
$aIds = CMDBSource::QueryToCol("SELECT id FROM `$sDBName`", 'id');
echo "\n$sDBName: ".implode(', ', $aIds)."\n";
}
}
}

View File

@@ -446,11 +446,11 @@
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRC00" _created_in="itop-structure" _delta="define">
<class id="DFRC1" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>true</abstract>
<db_table>dfrc00</db_table>
<abstract>false</abstract>
<db_table>dfrc1</db_table>
<naming>
<attributes/>
</naming>
@@ -459,6 +459,14 @@
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
@@ -478,130 +486,10 @@
</presentation>
<parent>DFRC0</parent>
</class>
<class id="DFRC01" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc01</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC00</parent>
</class>
<class id="DFRC02" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc02</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC00</parent>
</class>
<class id="DFRC03" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc03</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC00</parent>
</class>
<class id="DFRC2" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>true</abstract>
<abstract>false</abstract>
<db_table>dfrc2</db_table>
<naming>
<attributes/>
@@ -628,41 +516,6 @@
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRC21" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc21</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="extkey2_id" xsi:type="AttributeExternalKey">
<sql>extkey2_id</sql>
<filter/>
@@ -701,42 +554,7 @@
</items>
</details>
</presentation>
<parent>DFRC2</parent>
</class>
<class id="DFRC22" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc22</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC2</parent>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRC3" _created_in="itop-structure" _delta="define">
<properties>
@@ -825,7 +643,7 @@
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRC21</target_class>
<target_class>DFRC2</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>

View File

@@ -2,6 +2,7 @@
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use SetupUtils;
@@ -146,4 +147,42 @@ class SetupUtilsTest extends ItopTestCase
);
}
}
public function testCheckCliPhpVersionIsOk()
{
$this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php');
SetupUtils::CheckCliPhpVersionIsOk(ModelReflectionSerializer::ERROR_LABEL);
$this->assertTrue(true);
}
public function testCheckCliPhpVersionFromOutputFail()
{
$this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php');
$sOuput = <<<OUTPUT
PHP 7.4.33 (cli) (built: Aug 2 2024 16:22:28) ( NTS )
OUTPUT;
$this->expectException(\CoreException::class);
$this->expectExceptionMessage("Data consistency check failed: Mismatch between PHP versions (CLI: 7.4/ UI: 6.6)");
$this->InvokeNonPublicStaticMethod(SetupUtils::class, 'CheckCliPhpVersionFromOutput', [ModelReflectionSerializer::ERROR_LABEL, '6.6', 'sPHPExec', [$sOuput]]);
}
public function CheckOKProvider()
{
return [
["7.4 7.2 7.3.33"],
["PHP 7.4.33 (cli) (built: Aug 2 2024 16:22:28) ( NTS )"],
["PHP 7.4.33 PHP 7.33.22"],
["version: 7.4.27 stable"],
];
}
/**
* @dataProvider CheckOKProvider
*/
public function testCheckCliPhpVersionFromOutputOK($sOuput)
{
$this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php');
$this->InvokeNonPublicStaticMethod(SetupUtils::class, 'CheckCliPhpVersionFromOutput', [ModelReflectionSerializer::ERROR_LABEL, '7.4', 'sPHPExec', [$sOuput]]);
$this->assertTrue(true);
}
}

View File

@@ -1420,7 +1420,6 @@ HTML,
$expected = [
["class" => "WizStepWelcome","state" => ""],
["class" => "WizStepInstallOrUpgrade","state" => ""],
["class" => "WizStepDetectedInfo","state" => ""],
["class" => "WizStepUpgradeMiscParams","state" => ""],
];
@@ -1441,7 +1440,6 @@ HTML,
$expected = [
["class" => "WizStepWelcome","state" => ""],
["class" => "WizStepInstallOrUpgrade","state" => ""],
["class" => "WizStepDetectedInfo","state" => ""],
["class" => "WizStepUpgradeMiscParams","state" => ""],
];

View File

@@ -20,10 +20,58 @@ class ModelSerializationTest extends ItopDataTestCase
$this->assertEqualsCanonicalizing(MetaModel::GetClasses(), $aModel);
}
public function testGetModelFromEnvironmentFailure()
public function testGetModelFromEnvironmentFailure_NoEnvt()
{
$this->expectException(\CoreException::class);
$this->expectExceptionMessage("Cannot get classes");
$sEnvDir = APPROOT."env-gabuzomeu";
$this->expectExceptionMessage("Data consistency check failed: Missing environment ($sEnvDir)");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
public function testGetModelFromEnvironmentFailure_NoConfiguration()
{
$sEnvDir = APPROOT."env-gabuzomeu";
$this->aFileToClean [] = $sEnvDir;
mkdir($sEnvDir);
$this->expectException(\CoreException::class);
$sConfigFile = APPROOT."conf/gabuzomeu/config-itop.php";
$this->expectExceptionMessage("Data consistency check failed: Missing configuration ($sConfigFile)");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
public function testGetModelFromEnvironmentFailure_BrokenConfiguration()
{
$sEnvDir = APPROOT."env-gabuzomeu";
mkdir($sEnvDir);
$this->aFileToClean [] = $sEnvDir;
mkdir(APPROOT."conf/gabuzomeu");
$this->aFileToClean [] = APPROOT."conf/gabuzomeu";
$sConfigFile = APPROOT."conf/gabuzomeu/config-itop.php";
touch($sConfigFile);
file_put_contents($sConfigFile, 'invalid php content...');
$this->expectException(\CoreException::class);
$sError = <<<ERROR
Syntax error in configuration file: file = $sConfigFile, error = <tt>invalid php content...</tt>
ERROR;
$this->expectExceptionMessage("Data consistency check failed: $sError");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
public function testGetModelFromEnvironmentFailure_ItopInMaintenanceMode()
{
touch(MAINTENANCE_MODE_FILE);
$this->aFileToClean [] = MAINTENANCE_MODE_FILE;
$this->expectException(\CoreException::class);
$sError = <<<ERROR
This application is currently under maintenance.
ERROR;
$this->expectExceptionMessage("Data consistency check failed: $sError");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->GetTestEnvironment());
}
}

View File

@@ -201,6 +201,7 @@ class DataAuditSequencerTest extends ItopTestCase
$oRunTimeEnvironment = $this->createMock(\RunTimeEnvironment::class);
$oRunTimeEnvironment->expects($this->once())->method('GetApplicationVersion')
->willReturn(['product_version' => ITOP_VERSION_FULL]);
$oRunTimeEnvironment->expects($this->once())->method('ExitMaintenanceMode');
$oRunTimeEnvironment->expects($this->once())->method('DataToCleanupAudit');
$oRunTimeEnvironment->expects($this->any())->method('GetFinalEnv')
->willReturn('production');