mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-22 01:58:47 +02:00
Merge remote-tracking branch 'origin/support/3.1' into develop
This commit is contained in:
@@ -4544,7 +4544,7 @@ HTML;
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function PostInsertActions(): void
|
||||
protected function PostInsertActions(): void
|
||||
{
|
||||
parent::PostInsertActions();
|
||||
|
||||
@@ -4610,7 +4610,7 @@ HTML;
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function PostUpdateActions(array $aChanges): void
|
||||
protected function PostUpdateActions(array $aChanges): void
|
||||
{
|
||||
parent::PostUpdateActions($aChanges);
|
||||
|
||||
@@ -5354,7 +5354,7 @@ EOF
|
||||
$aErrors = $oObj->UpdateObjectFromPostedForm('');
|
||||
$bResult = (count($aErrors) == 0);
|
||||
if ($bResult) {
|
||||
list($bResult, $aErrors) = $oObj->CheckToWrite();
|
||||
[$bResult, $aErrors] = $oObj->CheckToWrite();
|
||||
}
|
||||
if ($bPreview) {
|
||||
$sStatus = $bResult ? Dict::S('UI:BulkModifyStatusOk') : Dict::S('UI:BulkModifyStatusError');
|
||||
@@ -5958,42 +5958,56 @@ JS
|
||||
}
|
||||
|
||||
/**
|
||||
* If the passed object is an instance of a link class, then will register each remote object for modification using {@see static::RegisterObjectAwaitingEventDbLinksChanged()}
|
||||
* Possibility for linked classes to be notified of current class modification
|
||||
*
|
||||
* If an external key was modified, register also the previous object that was linked previously.
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \Exception
|
||||
* @uses static::RegisterObjectAwaitingEventDbLinksChanged()
|
||||
*
|
||||
* @since 3.1.0 N°5906
|
||||
* @throws ArchivedObjectException
|
||||
* @throws CoreException
|
||||
* @throws Exception
|
||||
*
|
||||
* @since 3.1.0 N°5906 method creation
|
||||
* @since 3.1.1 3.2.0 N°6228 now just notify attributes having `with_php_computation`
|
||||
*/
|
||||
final protected function NotifyAttachedObjectsOnLinkClassModification(): void
|
||||
{
|
||||
$sClass = get_class($this);
|
||||
if (false === MetaModel::IsLinkClass($sClass)) {
|
||||
return;
|
||||
}
|
||||
// previous values in case of link change
|
||||
$aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
|
||||
$sClass = get_class($this);
|
||||
$aClassExtKeyAttCodes = MetaModel::GetAttributesList($sClass, [AttributeExternalKey::class]);
|
||||
foreach ($aClassExtKeyAttCodes as $sExternalKeyAttCode) {
|
||||
/** @var AttributeExternalKey $oAttDef */
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sExternalKeyAttCode);
|
||||
|
||||
$aLnkClassExternalKeys = MetaModel::GetAttributesList($sClass, [AttributeExternalKey::class]);
|
||||
foreach ($aLnkClassExternalKeys as $sExternalKeyAttCode) {
|
||||
/** @var \AttributeExternalKey $oExternalKeyAttDef */
|
||||
$oExternalKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExternalKeyAttCode);
|
||||
$sRemoteClassName = $oExternalKeyAttDef->GetTargetClass();
|
||||
|
||||
$sRemoteObjectId = $this->Get($sExternalKeyAttCode);
|
||||
if ($sRemoteObjectId > 0) {
|
||||
self::RegisterObjectAwaitingEventDbLinksChanged($sRemoteClassName, $sRemoteObjectId);
|
||||
if (false === $this->DoesTargetObjectHavePhpComputation($oAttDef)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sPreviousRemoteObjectId = $aPreviousValues[$sExternalKeyAttCode] ?? 0;
|
||||
if ($sPreviousRemoteObjectId > 0) {
|
||||
self::RegisterObjectAwaitingEventDbLinksChanged($sRemoteClassName, $sPreviousRemoteObjectId);
|
||||
$sTargetObjectId = $this->Get($sExternalKeyAttCode);
|
||||
if ($sTargetObjectId > 0) {
|
||||
self::RegisterObjectAwaitingEventDbLinksChanged($oAttDef->GetTargetClass(), $sTargetObjectId);
|
||||
}
|
||||
|
||||
$sPreviousTargetObjectId = $aPreviousValues[$sExternalKeyAttCode] ?? 0;
|
||||
if ($sPreviousTargetObjectId > 0) {
|
||||
self::RegisterObjectAwaitingEventDbLinksChanged($oAttDef->GetTargetClass(), $sPreviousTargetObjectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function DoesTargetObjectHavePhpComputation(AttributeExternalKey $oAttDef): bool
|
||||
{
|
||||
/** @var AttributeLinkedSet $oAttDefMirrorLink */
|
||||
$oAttDefMirrorLink = $oAttDef->GetMirrorLinkAttribute();
|
||||
if (is_null($oAttDefMirrorLink) || false === $oAttDefMirrorLink->HasPHPComputation()){
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register one object for later EVENT_DB_LINKS_CHANGED event.
|
||||
*
|
||||
|
||||
@@ -103,7 +103,7 @@ class ApplicationMenu
|
||||
{
|
||||
self::$sFavoriteSiloQuery = $sOQL;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the query used to limit the list of displayed organizations in the drop-down menu
|
||||
* @return string The OQL query returning a list of Organization objects
|
||||
@@ -536,7 +536,7 @@ EOF
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the currently active menu (if any, otherwise the first menu is the default)
|
||||
* @return string The Id of the currently active menu
|
||||
@@ -544,7 +544,7 @@ EOF
|
||||
public static function GetActiveNodeId()
|
||||
{
|
||||
$oAppContext = new ApplicationContext();
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
if ($sMenuId === null)
|
||||
{
|
||||
$sMenuId = self::GetDefaultMenuId();
|
||||
@@ -654,7 +654,7 @@ abstract class MenuNode
|
||||
|
||||
/**
|
||||
* Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu
|
||||
*/
|
||||
*/
|
||||
protected $m_aEnableStimuli;
|
||||
|
||||
/**
|
||||
@@ -814,7 +814,7 @@ abstract class MenuNode
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a limiting display condition for the same menu node. The conditions will be combined with a AND
|
||||
* @param $oMenuNode MenuNode Another definition of the same menu node, with potentially different access restriction
|
||||
@@ -987,7 +987,7 @@ class TemplateMenuNode extends MenuNode
|
||||
* @var string
|
||||
*/
|
||||
protected $sTemplateFile;
|
||||
|
||||
|
||||
/**
|
||||
* Create a menu item based on a custom template and inserts it into the application's main menu
|
||||
* @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary)
|
||||
@@ -1058,7 +1058,7 @@ class OQLMenuNode extends MenuNode
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $bSearchFormOpen;
|
||||
|
||||
|
||||
/**
|
||||
* Extra parameters to be passed to the display block to fine tune its appearence
|
||||
*/
|
||||
@@ -1091,7 +1091,7 @@ class OQLMenuNode extends MenuNode
|
||||
// Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects
|
||||
// of the class specified by the OQL...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set some extra parameters to be passed to the display block to fine tune its appearence
|
||||
* @param array $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters
|
||||
@@ -1111,7 +1111,7 @@ class OQLMenuNode extends MenuNode
|
||||
*/
|
||||
public function RenderContent(WebPage $oPage, $aExtraParams = array())
|
||||
{
|
||||
ContextTag::AddContext(ContextTag::TAG_OBJECT_SEARCH);
|
||||
$oTag = new ContextTag(ContextTag::TAG_OBJECT_SEARCH);
|
||||
ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId());
|
||||
OQLMenuNode::RenderOQLSearch
|
||||
(
|
||||
@@ -1120,7 +1120,7 @@ class OQLMenuNode extends MenuNode
|
||||
'Menu_'.$this->GetMenuId(),
|
||||
$this->bSearch, // Search pane
|
||||
$this->bSearchFormOpen, // Search open
|
||||
$oPage,
|
||||
$oPage,
|
||||
array_merge($this->m_aParams, $aExtraParams),
|
||||
true
|
||||
);
|
||||
@@ -1354,10 +1354,10 @@ class NewObjectMenuNode extends MenuNode
|
||||
{
|
||||
// Enable this menu, only if the current user has enough rights to create such an object, or an object of
|
||||
// any child class
|
||||
|
||||
|
||||
$aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
|
||||
$bActionIsAllowed = false;
|
||||
|
||||
|
||||
foreach($aSubClasses as $sCandidateClass)
|
||||
{
|
||||
if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
|
||||
@@ -1366,7 +1366,7 @@ class NewObjectMenuNode extends MenuNode
|
||||
break; // Enough for now
|
||||
}
|
||||
}
|
||||
return $bActionIsAllowed;
|
||||
return $bActionIsAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1508,7 +1508,7 @@ class DashboardMenuNode extends MenuNode
|
||||
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1549,7 +1549,7 @@ class ShortcutContainerMenuNode extends MenuNode
|
||||
$sName = $this->GetMenuId().'_'.$oShortcut->GetKey();
|
||||
new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++);
|
||||
}
|
||||
|
||||
|
||||
// Complete the tree
|
||||
//
|
||||
parent::PopulateChildMenus();
|
||||
|
||||
@@ -1739,11 +1739,20 @@ class AttributeLinkedSet extends AttributeDefinition
|
||||
* @return bool true if Attribute has constraints
|
||||
* @since 3.1.0 N°6228
|
||||
*/
|
||||
public function GetHasConstraint()
|
||||
public function HasPHPConstraint(): bool
|
||||
{
|
||||
return $this->GetOptional('with_php_constraint', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if Attribute has computation (DB_LINKS_CHANGED event propagation, `with_php_computation` attribute xml property), false otherwise
|
||||
* @since 3.1.1 3.2.0 N°6228
|
||||
*/
|
||||
public function HasPHPComputation(): bool
|
||||
{
|
||||
return $this->GetOptional('with_php_computation', false);
|
||||
}
|
||||
|
||||
public function GetLinkedClass()
|
||||
{
|
||||
return $this->Get('linked_class');
|
||||
|
||||
@@ -474,7 +474,7 @@ abstract class CMDBObject extends DBObject
|
||||
public function DBDelete(&$oDeletionPlan = null)
|
||||
{
|
||||
$this->LogCRUDEnter(__METHOD__);
|
||||
$oDeletionPlan = $this->DBDeleteTracked_Internal($oDeletionPlan);
|
||||
$oDeletionPlan = parent::DBDelete($oDeletionPlan);
|
||||
$this->LogCRUDExit(__METHOD__);
|
||||
return $oDeletionPlan;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// 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.
|
||||
@@ -380,7 +380,7 @@ class CMDBSource
|
||||
public static function GetDBVendor()
|
||||
{
|
||||
$sDBVendor = static::ENUM_DB_VENDOR_MYSQL;
|
||||
|
||||
|
||||
$sVersionComment = static::GetServerVariable('version') . ' - ' . static::GetServerVariable('version_comment');
|
||||
if(preg_match('/mariadb/i', $sVersionComment) === 1)
|
||||
{
|
||||
@@ -390,7 +390,7 @@ class CMDBSource
|
||||
{
|
||||
$sDBVendor = static::ENUM_DB_VENDOR_PERCONA;
|
||||
}
|
||||
|
||||
|
||||
return $sDBVendor;
|
||||
}
|
||||
|
||||
@@ -934,7 +934,7 @@ class CMDBSource
|
||||
{
|
||||
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
|
||||
}
|
||||
|
||||
|
||||
while ($aRow = $oResult->fetch_array($iMode))
|
||||
{
|
||||
$aData[] = $aRow;
|
||||
@@ -1088,7 +1088,7 @@ class CMDBSource
|
||||
if (!array_key_exists($iKey, $aTableInfo["Fields"])) return false;
|
||||
$aFieldData = $aTableInfo["Fields"][$iKey];
|
||||
if (!array_key_exists("Key", $aFieldData)) return false;
|
||||
return ($aFieldData["Key"] == "PRI");
|
||||
return ($aFieldData["Key"] == "PRI");
|
||||
}
|
||||
|
||||
public static function IsAutoIncrement($sTable, $sField)
|
||||
@@ -1099,7 +1099,7 @@ class CMDBSource
|
||||
$aFieldData = $aTableInfo["Fields"][$sField];
|
||||
if (!array_key_exists("Extra", $aFieldData)) return false;
|
||||
//MyHelpers::debug_breakpoint($aFieldData);
|
||||
return (strstr($aFieldData["Extra"], "auto_increment"));
|
||||
return (strstr($aFieldData["Extra"], "auto_increment"));
|
||||
}
|
||||
|
||||
public static function IsField($sTable, $sField)
|
||||
@@ -1366,13 +1366,13 @@ class CMDBSource
|
||||
public static function GetTableFieldsList($sTable)
|
||||
{
|
||||
assert(!empty($sTable));
|
||||
|
||||
|
||||
$aTableInfo = self::GetTableInfo($sTable);
|
||||
if (empty($aTableInfo)) return array(); // #@# or an error ?
|
||||
|
||||
return array_keys($aTableInfo["Fields"]);
|
||||
}
|
||||
|
||||
|
||||
// Cache the information about existing tables, and their fields
|
||||
private static $m_aTablesInfo = array();
|
||||
private static function _TablesInfoCacheReset($sTableName = null)
|
||||
@@ -1505,7 +1505,7 @@ class CMDBSource
|
||||
{
|
||||
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql));
|
||||
}
|
||||
|
||||
|
||||
$aRows = array();
|
||||
while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC))
|
||||
{
|
||||
@@ -1514,7 +1514,7 @@ class CMDBSource
|
||||
$oResult->free();
|
||||
return $aRows;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value of the specified server variable
|
||||
* @param string $sVarName Name of the server variable
|
||||
@@ -1530,7 +1530,7 @@ class CMDBSource
|
||||
/**
|
||||
* Returns the privileges of the current user
|
||||
* @return string privileges in a raw format
|
||||
*/
|
||||
*/
|
||||
public static function GetRawPrivileges()
|
||||
{
|
||||
try
|
||||
@@ -1556,8 +1556,8 @@ class CMDBSource
|
||||
|
||||
/**
|
||||
* Determine the slave status of the server
|
||||
* @return bool true if the server is slave
|
||||
*/
|
||||
* @return bool true if the server is slave
|
||||
*/
|
||||
public static function IsSlaveServer()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1193,6 +1193,30 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'sessions_tracking.enabled' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Whether or not the whole mechanism to track active sessions is enabled. See PHP session.gc_maxlifetime setting to configure session expiration.',
|
||||
'default' => false,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'sessions_tracking.gc_threshold' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'fallback in case cron is not active: probability in percent that session files are cleanup during any itop request (100 means always)',
|
||||
'default' => 1,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'sessions_tracking.gc_duration_in_seconds' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'fallback in case cron is not active: when a cleanup is triggered cleanup duration will not exceed this duration (in seconds).',
|
||||
'default' => 1,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'transaction_storage' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The type of mechanism to use for storing the unique identifiers for transactions (Session|File).',
|
||||
|
||||
@@ -507,6 +507,12 @@
|
||||
<type>boolean</type>
|
||||
<default>false</default>
|
||||
</property>
|
||||
<property id="with_php_computation">
|
||||
<php_param>with_php_computation</php_param>
|
||||
<mandatory>false</mandatory>
|
||||
<type>boolean</type>
|
||||
<default>false</default>
|
||||
</property>
|
||||
<property id="create_temporary_object">
|
||||
<php_param>create_temporary_object</php_param>
|
||||
<mandatory>false</mandatory>
|
||||
|
||||
@@ -187,12 +187,13 @@ abstract class DBObject implements iDisplay
|
||||
protected $m_oLinkHostObject = null;
|
||||
|
||||
/**
|
||||
* @var array List all the CRUD stack in progress
|
||||
*
|
||||
* The array contains instances of
|
||||
* ['type' => 'type of CRUD operation (INSERT, UPDATE, DELETE)',
|
||||
* 'class' => 'class of the object in the CRUD process',
|
||||
* 'id' => 'id of the object in the CRUD process']
|
||||
* @var array{array{
|
||||
* type: string,
|
||||
* class: string,
|
||||
* id: string,
|
||||
* }} List all the CRUD stack in progress, with :
|
||||
* - type: CRUD operation (INSERT, UPDATE, DELETE)',
|
||||
* - class: class of the object in the CRUD process, leaf (object finalclass) if we have a hierarchy
|
||||
*
|
||||
* @since 3.1.0 N°5906
|
||||
*/
|
||||
@@ -2461,6 +2462,130 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger onObjectUpdate on the target object when an object pointed by a LinkSet is modified, added or removed
|
||||
*
|
||||
* @since 3.1.1 3.2.0 N°6531 method creation
|
||||
*/
|
||||
final protected function ActivateOnObjectUpdateTriggersForTargetObjects(): void
|
||||
{
|
||||
$aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
|
||||
|
||||
$aClassExtKeyAttCodes = MetaModel::GetAttributesList(get_class($this), [AttributeExternalKey::class]);
|
||||
foreach ($aClassExtKeyAttCodes as $sExtKeyWithMirrorLinkAttCode) {
|
||||
/** @var AttributeExternalKey $oExtKeyWithMirrorLinkAttDef */
|
||||
$oExtKeyWithMirrorLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyWithMirrorLinkAttCode);
|
||||
|
||||
/** @var AttributeLinkedSet $oAttDefMirrorLink */
|
||||
$oAttDefMirrorLink = $oExtKeyWithMirrorLinkAttDef->GetMirrorLinkAttribute();
|
||||
if (is_null($oAttDefMirrorLink)) {
|
||||
// No LinkSet pointing to me
|
||||
continue;
|
||||
}
|
||||
$sAttCodeMirrorLink = $oAttDefMirrorLink->GetCode();
|
||||
$sTargetObjectClass = $oExtKeyWithMirrorLinkAttDef->GetTargetClass();
|
||||
|
||||
if (array_key_exists($sExtKeyWithMirrorLinkAttCode, $aPreviousValues)) {
|
||||
// need to update old target also
|
||||
$sPreviousTargetObjectKey = $aPreviousValues[$sExtKeyWithMirrorLinkAttCode];
|
||||
$oPreviousTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $sPreviousTargetObjectKey);
|
||||
$this->ActivateOnObjectUpdateTriggers($oPreviousTargetObject, [$sAttCodeMirrorLink]);
|
||||
}
|
||||
|
||||
// we need to update remote with current lnk instance
|
||||
$oTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $this->Get($sExtKeyWithMirrorLinkAttCode));
|
||||
$this->ActivateOnObjectUpdateTriggers($oTargetObject, [$sAttCodeMirrorLink]);
|
||||
}
|
||||
}
|
||||
|
||||
final static protected function GetObjectIfNotInCRUDStack($sClass, $sKey)
|
||||
{
|
||||
if (DBObject::IsObjectCurrentlyInCrud($sClass, $sKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MetaModel::GetObject($sClass, $sKey, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cascade CheckToWrite to Target Objects With LinkSet Pointing To Me
|
||||
* @since 3.1.1 3.2.0 N°6228 method creation
|
||||
*/
|
||||
final protected function CheckToWriteForTargetObjects(bool $bIsCheckToDelete = false): void
|
||||
{
|
||||
$aChanges = $this->ListChanges();
|
||||
|
||||
$aClassExtKeyAttCodes = MetaModel::GetAttributesList(get_class($this), [AttributeExternalKey::class]);
|
||||
foreach ($aClassExtKeyAttCodes as $sExtKeyWithMirrorLinkAttCode) {
|
||||
/** @var AttributeExternalKey $oExtKeyWithMirrorLinkAttDef */
|
||||
$oExtKeyWithMirrorLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyWithMirrorLinkAttCode);
|
||||
|
||||
/** @var AttributeLinkedSet $oAttDefMirrorLink */
|
||||
$oAttDefMirrorLink = $oExtKeyWithMirrorLinkAttDef->GetMirrorLinkAttribute();
|
||||
if (is_null($oAttDefMirrorLink) || (false === $oAttDefMirrorLink->HasPHPConstraint())) {
|
||||
continue;
|
||||
}
|
||||
$sAttCodeMirrorLink = $oAttDefMirrorLink->GetCode();
|
||||
$sTargetObjectClass = $oExtKeyWithMirrorLinkAttDef->GetTargetClass();
|
||||
|
||||
$oTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $this->Get($sExtKeyWithMirrorLinkAttCode));
|
||||
|
||||
if ($this->IsNew()) {
|
||||
$this->CheckToWriteForSingleTargetObject_Internal('add', $oTargetObject, $sAttCodeMirrorLink, false);
|
||||
} else if ($bIsCheckToDelete) {
|
||||
$this->CheckToWriteForSingleTargetObject_Internal('remove', $oTargetObject, $sAttCodeMirrorLink, true);
|
||||
} else {
|
||||
if (array_key_exists($sExtKeyWithMirrorLinkAttCode, $aChanges)) {
|
||||
// need to update remote old + new
|
||||
$aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
|
||||
$sPreviousTargetObjectKey = $aPreviousValues[$sExtKeyWithMirrorLinkAttCode];
|
||||
$oPreviousTargetObject = static::GetObjectIfNotInCRUDStack($sTargetObjectClass, $sPreviousTargetObjectKey);
|
||||
$this->CheckToWriteForSingleTargetObject_Internal('remove', $oPreviousTargetObject, $sAttCodeMirrorLink, false);
|
||||
$this->CheckToWriteForSingleTargetObject_Internal('add', $oTargetObject, $sAttCodeMirrorLink, false);
|
||||
} else {
|
||||
$this->CheckToWriteForSingleTargetObject_Internal('modify', $oTargetObject, $sAttCodeMirrorLink, false); // we need to update remote with current lnk instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function CheckToWriteForSingleTargetObject_Internal(string $sAction, ?DBObject $oTargetObject, string $sAttCodeMirrorLink, bool $bIsCheckToDelete): void
|
||||
{
|
||||
if (is_null($oTargetObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->LogCRUDDebug(__METHOD__, "action: $sAction ".get_class($oTargetObject).'::'.$oTargetObject->GetKey()." ($sAttCodeMirrorLink)");
|
||||
|
||||
/** @var \ormLinkSet $oTargetValue */
|
||||
$oTargetValue = $oTargetObject->Get($sAttCodeMirrorLink);
|
||||
switch ($sAction) {
|
||||
case 'add':
|
||||
$oTargetValue->AddItem($this);
|
||||
break;
|
||||
case 'remove':
|
||||
$oTargetValue->RemoveItem($this->GetKey());
|
||||
break;
|
||||
case 'modify':
|
||||
$oTargetValue->ModifyItem($this);
|
||||
break;
|
||||
}
|
||||
$oTargetObject->Set($sAttCodeMirrorLink, $oTargetValue);
|
||||
[$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oTargetObject->CheckToWrite();
|
||||
if (false === $bCheckStatus) {
|
||||
if ($bIsCheckToDelete) {
|
||||
$this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues ?? [], $aCheckIssues);
|
||||
} else {
|
||||
$this->m_aCheckIssues = array_merge($this->m_aCheckIssues ?? [], $aCheckIssues);
|
||||
}
|
||||
$this->m_bSecurityIssue = $this->m_bSecurityIssue || $bSecurityIssue;
|
||||
}
|
||||
$aTargetCheckWarnings = $oTargetObject->GetCheckWarnings();
|
||||
if (is_array($aTargetCheckWarnings)) {
|
||||
$this->m_aCheckWarnings = array_merge($this->m_aCheckWarnings ?? [], $aTargetCheckWarnings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @api-advanced
|
||||
@@ -2484,6 +2609,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
return array(true, array());
|
||||
}
|
||||
|
||||
if (is_null($this->m_bCheckStatus))
|
||||
{
|
||||
$this->m_aCheckIssues = array();
|
||||
@@ -2500,6 +2626,9 @@ abstract class DBObject implements iDisplay
|
||||
$oKPI = new ExecutionKPI();
|
||||
$this->DoCheckToWrite();
|
||||
$oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite');
|
||||
|
||||
$this->CheckToWriteForTargetObjects();
|
||||
|
||||
if (count($this->m_aCheckIssues) == 0)
|
||||
{
|
||||
$this->m_bCheckStatus = true;
|
||||
@@ -2509,6 +2638,7 @@ abstract class DBObject implements iDisplay
|
||||
$this->m_bCheckStatus = false;
|
||||
}
|
||||
}
|
||||
|
||||
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
|
||||
}
|
||||
|
||||
@@ -2594,7 +2724,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* an array of displayable error is added in {@see DBObject::$m_aDeleteIssues}
|
||||
*
|
||||
* @internal
|
||||
* @internal
|
||||
*
|
||||
* @param \DeletionPlan $oDeletionPlan
|
||||
*
|
||||
@@ -2659,8 +2789,15 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
public function CheckToDelete(&$oDeletionPlan)
|
||||
{
|
||||
$this->MakeDeletionPlan($oDeletionPlan);
|
||||
$oDeletionPlan->ComputeResults();
|
||||
$this->AddCurrentObjectInCrudStack('DELETE');
|
||||
try {
|
||||
$this->MakeDeletionPlan($oDeletionPlan);
|
||||
$oDeletionPlan->ComputeResults();
|
||||
}
|
||||
finally {
|
||||
$this->RemoveCurrentObjectInCrudStack();
|
||||
}
|
||||
|
||||
return (!$oDeletionPlan->FoundStopper());
|
||||
}
|
||||
|
||||
@@ -2711,7 +2848,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
// The value is a scalar, the comparison must be 100% strict
|
||||
if($this->m_aOrigValues[$sAtt] !== $proposedValue)
|
||||
{
|
||||
{
|
||||
//echo "$sAtt:<pre>\n";
|
||||
//var_dump($this->m_aOrigValues[$sAtt]);
|
||||
//var_dump($proposedValue);
|
||||
@@ -2833,7 +2970,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Used only by insert, Meant to be overloaded
|
||||
*
|
||||
*
|
||||
* @overwritable-hook You can extend this method in order to provide your own logic.
|
||||
*/
|
||||
protected function OnObjectKeyReady()
|
||||
@@ -2941,7 +3078,7 @@ abstract class DBObject implements iDisplay
|
||||
// fields in first array, values in the second
|
||||
$aFieldsToWrite = array();
|
||||
$aValuesToWrite = array();
|
||||
|
||||
|
||||
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
|
||||
{
|
||||
// Add it to the list of fields to write
|
||||
@@ -2950,7 +3087,7 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
|
||||
$aHierarchicalKeys = array();
|
||||
|
||||
|
||||
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) {
|
||||
// Skip this attribute if not defined in this table
|
||||
if ((!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode) && !$oAttDef->CopyOnAllTables())
|
||||
@@ -2960,7 +3097,7 @@ abstract class DBObject implements iDisplay
|
||||
$aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
|
||||
foreach($aAttColumns as $sColumn => $sValue)
|
||||
{
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aValuesToWrite[] = CMDBSource::Quote($sValue);
|
||||
}
|
||||
if ($oAttDef->IsHierarchicalKey())
|
||||
@@ -2984,7 +3121,7 @@ abstract class DBObject implements iDisplay
|
||||
self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite);
|
||||
}
|
||||
self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')';
|
||||
|
||||
|
||||
$iNewKey = 999999; // TODO - compute next id....
|
||||
}
|
||||
else
|
||||
@@ -3069,7 +3206,7 @@ abstract class DBObject implements iDisplay
|
||||
// fields in first array, values in the second
|
||||
$aFieldsToWrite = array();
|
||||
$aValuesToWrite = array();
|
||||
|
||||
|
||||
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
|
||||
{
|
||||
// Add it to the list of fields to write
|
||||
@@ -3104,7 +3241,7 @@ abstract class DBObject implements iDisplay
|
||||
$aAttColumns = $oAttDef->GetSQLValues($value);
|
||||
foreach($aAttColumns as $sColumn => $sValue)
|
||||
{
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aFieldsToWrite[] = "`$sColumn`";
|
||||
$aValuesToWrite[] = CMDBSource::Quote($sValue);
|
||||
}
|
||||
if ($oAttDef->IsHierarchicalKey())
|
||||
@@ -3214,7 +3351,7 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
|
||||
list($bRes, $aIssues) = $this->CheckToWrite(false);
|
||||
[$bRes, $aIssues] = $this->CheckToWrite(false);
|
||||
if (!$bRes) {
|
||||
throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
|
||||
}
|
||||
@@ -3335,7 +3472,7 @@ abstract class DBObject implements iDisplay
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function PostInsertActions(): void
|
||||
protected function PostInsertActions(): void
|
||||
{
|
||||
$this->FireEventAfterWrite([], true);
|
||||
$oKPI = new ExecutionKPI();
|
||||
@@ -3359,6 +3496,9 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
// - TriggerOnObjectMention
|
||||
$this->ActivateOnMentionTriggers(true);
|
||||
|
||||
// - Trigger for object pointing to the current object
|
||||
$this->ActivateOnObjectUpdateTriggersForTargetObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3386,7 +3526,7 @@ abstract class DBObject implements iDisplay
|
||||
$this->RecordObjCreation();
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is automatically called after cloning an object with the "clone" PHP language construct
|
||||
* The purpose of this method is to reset the appropriate attributes of the object in
|
||||
@@ -3454,7 +3594,7 @@ abstract class DBObject implements iDisplay
|
||||
return $this->m_iKey;
|
||||
}
|
||||
|
||||
list($bRes, $aIssues) = $this->CheckToWrite(false);
|
||||
[$bRes, $aIssues] = $this->CheckToWrite(false);
|
||||
if (!$bRes) {
|
||||
throw new CoreCannotSaveObjectException(['issues' => $aIssues, 'class' => $sClass, 'id' => $this->GetKey()]);
|
||||
}
|
||||
@@ -3651,7 +3791,7 @@ abstract class DBObject implements iDisplay
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function PostUpdateActions(array $aChanges): void
|
||||
protected function PostUpdateActions(array $aChanges): void
|
||||
{
|
||||
$this->FireEventAfterWrite($aChanges, false);
|
||||
$oKPI = new ExecutionKPI();
|
||||
@@ -3659,20 +3799,10 @@ abstract class DBObject implements iDisplay
|
||||
$oKPI->ComputeStatsForExtension($this, 'AfterUpdate');
|
||||
|
||||
// - TriggerOnObjectUpdate
|
||||
$aClassList = MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL);
|
||||
$aParams = array('class_list' => $aClassList);
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)'),
|
||||
array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
/** @var \TriggerOnObjectUpdate $oTrigger */
|
||||
try {
|
||||
$oTrigger->DoActivate($this->ToArgs());
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$oTrigger->LogException($e, $this);
|
||||
utils::EnrichRaisedException($oTrigger, $e);
|
||||
}
|
||||
}
|
||||
$this->ActivateOnObjectUpdateTriggers($this);
|
||||
|
||||
// - Trigger for object pointing to the current object
|
||||
$this->ActivateOnObjectUpdateTriggersForTargetObjects();
|
||||
|
||||
$sClass = get_class($this);
|
||||
if (MetaModel::HasLifecycle($sClass))
|
||||
@@ -3718,6 +3848,37 @@ abstract class DBObject implements iDisplay
|
||||
$this->ActivateOnMentionTriggers(false, $aChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DBObject $oObject
|
||||
* @param array|null $aAttributes
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
private function ActivateOnObjectUpdateTriggers(?DBObject $oObject, array $aAttributes = null): void
|
||||
{
|
||||
if (is_null($oObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// - TriggerOnObjectUpdate
|
||||
$aClassList = MetaModel::EnumParentClasses(get_class($oObject), ENUM_PARENT_CLASSES_ALL);
|
||||
$aParams = array('class_list' => $aClassList);
|
||||
$oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN (:class_list)'),
|
||||
array(), $aParams);
|
||||
while ($oTrigger = $oSet->Fetch()) {
|
||||
/** @var \TriggerOnObjectUpdate $oTrigger */
|
||||
try {
|
||||
$oTrigger->DoActivateForSpecificAttributes($oObject->ToArgs(), $aAttributes);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$oTrigger->LogException($e, $oObject);
|
||||
utils::EnrichRaisedException($oTrigger, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment attribute with specified value.
|
||||
@@ -3921,6 +4082,8 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
protected function DBDeleteSingleObject()
|
||||
{
|
||||
$this->LogCRUDEnter(__METHOD__);
|
||||
|
||||
if (MetaModel::DBIsReadOnly())
|
||||
{
|
||||
$this->LogCRUDExit(__METHOD__, 'DB is read-only');
|
||||
@@ -4041,8 +4204,11 @@ abstract class DBObject implements iDisplay
|
||||
$this->AfterDelete();
|
||||
$oKPI->ComputeStatsForExtension($this, 'AfterDelete');
|
||||
|
||||
// - Trigger for object pointing to the current object
|
||||
$this->ActivateOnObjectUpdateTriggersForTargetObjects();
|
||||
|
||||
$this->m_bIsInDB = false;
|
||||
$this->LogCRUDExit(__METHOD__);
|
||||
// Fix for N°926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete
|
||||
// objects, reported as bug N°926)
|
||||
// Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected
|
||||
@@ -4054,7 +4220,7 @@ abstract class DBObject implements iDisplay
|
||||
* First, checks if the object can be deleted regarding database integrity.
|
||||
* If the answer is yes, it performs any required cleanup (delete other objects or reset external keys) in addition to the object
|
||||
* deletion.
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param \DeletionPlan $oDeletionPlan Do not use: aims at dealing with recursion
|
||||
@@ -4073,74 +4239,65 @@ abstract class DBObject implements iDisplay
|
||||
public function DBDelete(&$oDeletionPlan = null)
|
||||
{
|
||||
$this->LogCRUDEnter(__METHOD__);
|
||||
try {
|
||||
static $iLoopTimeLimit = null;
|
||||
if ($iLoopTimeLimit == null) {
|
||||
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
|
||||
}
|
||||
if (is_null($oDeletionPlan)) {
|
||||
$oDeletionPlan = new DeletionPlan();
|
||||
}
|
||||
|
||||
static $iLoopTimeLimit = null;
|
||||
if ($iLoopTimeLimit == null)
|
||||
{
|
||||
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
|
||||
}
|
||||
if (is_null($oDeletionPlan))
|
||||
{
|
||||
$oDeletionPlan = new DeletionPlan();
|
||||
}
|
||||
$this->MakeDeletionPlan($oDeletionPlan);
|
||||
$oDeletionPlan->ComputeResults();
|
||||
|
||||
if ($oDeletionPlan->FoundStopper())
|
||||
{
|
||||
$aIssues = $oDeletionPlan->GetIssues();
|
||||
$this->LogCRUDError(__METHOD__, ' Errors: '.implode(', ', $aIssues));
|
||||
throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues)));
|
||||
}
|
||||
if (false === $this->CheckToDelete($oDeletionPlan)) {
|
||||
$aIssues = $oDeletionPlan->GetIssues();
|
||||
$this->LogCRUDError(__METHOD__, ' Errors: '.implode(', ', $aIssues));
|
||||
throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues)));
|
||||
}
|
||||
|
||||
|
||||
// Getting and setting time limit are not symetric:
|
||||
// www.php.net/manual/fr/function.set-time-limit.php#72305
|
||||
$iPreviousTimeLimit = ini_get('max_execution_time');
|
||||
// Getting and setting time limit are not symmetric:
|
||||
// www.php.net/manual/fr/function.set-time-limit.php#72305
|
||||
$iPreviousTimeLimit = ini_get('max_execution_time');
|
||||
|
||||
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete)
|
||||
{
|
||||
foreach ($aToDelete as $iId => $aData)
|
||||
{
|
||||
/** @var \DBObject $oToDelete */
|
||||
$oToDelete = $aData['to_delete'];
|
||||
// The deletion based on a deletion plan should not be done for each object if the deletion plan is common (Trac #457)
|
||||
// because for each object we would try to update all the preceding ones... that are already deleted
|
||||
// A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once
|
||||
// As a temporary fix: delete only the objects that are still to be deleted...
|
||||
if ($oToDelete->m_bIsInDB)
|
||||
{
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete) {
|
||||
foreach ($aToDelete as $iId => $aData) {
|
||||
/** @var \DBObject $oToDelete */
|
||||
$oToDelete = $aData['to_delete'];
|
||||
// The deletion based on a deletion plan should not be done for each object if the deletion plan is common (Trac #457)
|
||||
// because for each object we would try to update all the preceding ones... that are already deleted
|
||||
// A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once
|
||||
// As a temporary fix: delete only the objects that are still to be deleted...
|
||||
if ($oToDelete->m_bIsInDB) {
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
|
||||
$oToDelete->AddCurrentObjectInCrudStack('DELETE');
|
||||
try {
|
||||
$oToDelete->DBDeleteSingleObject();
|
||||
}
|
||||
finally {
|
||||
$oToDelete->RemoveCurrentObjectInCrudStack();
|
||||
$oToDelete->AddCurrentObjectInCrudStack('DELETE');
|
||||
try {
|
||||
$oToDelete->DBDeleteSingleObject();
|
||||
}
|
||||
finally {
|
||||
$oToDelete->RemoveCurrentObjectInCrudStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate)
|
||||
{
|
||||
foreach ($aToUpdate as $aData)
|
||||
{
|
||||
$oToUpdate = $aData['to_reset'];
|
||||
/** @var \DBObject $oToUpdate */
|
||||
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef)
|
||||
{
|
||||
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
$oToUpdate->DBUpdate();
|
||||
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
|
||||
foreach ($aToUpdate as $aData) {
|
||||
$oToUpdate = $aData['to_reset'];
|
||||
/** @var \DBObject $oToUpdate */
|
||||
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
|
||||
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
|
||||
set_time_limit(intval($iLoopTimeLimit));
|
||||
$oToUpdate->DBUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_time_limit(intval($iPreviousTimeLimit));
|
||||
} finally {
|
||||
$this->LogCRUDExit(__METHOD__);
|
||||
}
|
||||
|
||||
set_time_limit(intval($iPreviousTimeLimit));
|
||||
|
||||
$this->LogCRUDExit(__METHOD__);
|
||||
return $oDeletionPlan;
|
||||
}
|
||||
|
||||
@@ -4447,7 +4604,7 @@ abstract class DBObject implements iDisplay
|
||||
*
|
||||
* @api
|
||||
*
|
||||
*/
|
||||
*/
|
||||
public function Reset($sAttCode)
|
||||
{
|
||||
$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
|
||||
@@ -4459,7 +4616,7 @@ abstract class DBObject implements iDisplay
|
||||
* Suitable for use as a lifecycle action
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
*/
|
||||
public function Copy($sDestAttCode, $sSourceAttCode)
|
||||
{
|
||||
$oTypeValueToCopy = MetaModel::GetAttributeDef(get_class($this), $sSourceAttCode);
|
||||
@@ -4789,7 +4946,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this));
|
||||
}
|
||||
|
||||
|
||||
$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
|
||||
if (!$oKeyAttDef instanceof AttributeExternalKey)
|
||||
{
|
||||
@@ -4807,14 +4964,14 @@ abstract class DBObject implements iDisplay
|
||||
$ret = $oRemoteObj->GetForTemplate($sRemoteAttCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
switch($sPlaceholderAttCode)
|
||||
{
|
||||
case 'id':
|
||||
$ret = $this->GetKey();
|
||||
break;
|
||||
|
||||
|
||||
case 'name()':
|
||||
$ret = $this->GetName();
|
||||
break;
|
||||
@@ -5001,7 +5158,7 @@ abstract class DBObject implements iDisplay
|
||||
if ($oOwner)
|
||||
{
|
||||
$sLinkSetOwnerClass = get_class($oOwner);
|
||||
|
||||
|
||||
$oMyChangeOp = MetaModel::NewObject($sChangeOpClass);
|
||||
$oMyChangeOp->Set("objclass", $sLinkSetOwnerClass);
|
||||
$oMyChangeOp->Set("objkey", $iLinkSetOwnerId);
|
||||
@@ -5028,7 +5185,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
/** @var \AttributeLinkedSet $oLinkSet */
|
||||
if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue;
|
||||
|
||||
|
||||
$iLinkSetOwnerId = $this->Get($sExtKeyAttCode);
|
||||
$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove');
|
||||
if ($oMyChangeOp)
|
||||
@@ -5246,8 +5403,9 @@ abstract class DBObject implements iDisplay
|
||||
$this->m_aDeleteIssues = array(); // Ok
|
||||
$this->FireEventCheckToDelete($oDeletionPlan);
|
||||
$this->DoCheckToDelete($oDeletionPlan);
|
||||
$this->CheckToWriteForTargetObjects(true);
|
||||
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
|
||||
|
||||
|
||||
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
|
||||
|
||||
// Getting and setting time limit are not symmetric:
|
||||
@@ -5429,7 +5587,7 @@ abstract class DBObject implements iDisplay
|
||||
$aSynchroClasses[] = $sTarget;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach($aSynchroClasses as $sClass)
|
||||
{
|
||||
if ($this instanceof $sClass)
|
||||
@@ -6236,6 +6394,18 @@ abstract class DBObject implements iDisplay
|
||||
$this->m_aCheckWarnings[] = $sWarning;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @return string[]|null
|
||||
* @since 3.1.1 3.2.0
|
||||
*/
|
||||
public function GetCheckWarnings(): ?array
|
||||
{
|
||||
return $this->m_aCheckWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
@@ -6483,9 +6653,10 @@ abstract class DBObject implements iDisplay
|
||||
// during insert key is reset from -1 to null
|
||||
// so we need to handle null values (will give empty string after conversion)
|
||||
$sConvertedId = (string)$sId;
|
||||
$oRootClass = MetaModel::GetRootClass($sClass);
|
||||
|
||||
foreach (self::$m_aCrudStack as $aCrudStackEntry) {
|
||||
if (($sClass === $aCrudStackEntry['class'])
|
||||
if (($oRootClass === $aCrudStackEntry['class'])
|
||||
&& ($sConvertedId === $aCrudStackEntry['id'])) {
|
||||
return true;
|
||||
}
|
||||
@@ -6500,12 +6671,14 @@ abstract class DBObject implements iDisplay
|
||||
* @param string $sClass
|
||||
*
|
||||
* @return bool
|
||||
* @throws \CoreException
|
||||
* @since 3.1.0 N°5609
|
||||
*/
|
||||
final public static function IsClassCurrentlyInCrud(string $sClass): bool
|
||||
{
|
||||
$sRootClass = MetaModel::GetRootClass($sClass);
|
||||
foreach (self::$m_aCrudStack as $aCrudStackEntry) {
|
||||
if ($sClass === $aCrudStackEntry['class']) {
|
||||
if ($sRootClass === $aCrudStackEntry['class']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6523,9 +6696,11 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
private function AddCurrentObjectInCrudStack(string $sCrudType): void
|
||||
{
|
||||
$this->LogCRUDDebug(__METHOD__);
|
||||
$sRootClass = MetaModel::GetRootClass(get_class($this));
|
||||
self::$m_aCrudStack[] = [
|
||||
'type' => $sCrudType,
|
||||
'class' => get_class($this),
|
||||
'class' => $sRootClass,
|
||||
'id' => (string)$this->GetKey(), // GetKey() doesn't have type hinting, so forcing type to avoid getting an int
|
||||
];
|
||||
}
|
||||
@@ -6539,6 +6714,7 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
private function UpdateCurrentObjectInCrudStack(): void
|
||||
{
|
||||
$this->LogCRUDDebug(__METHOD__);
|
||||
$aCurrentCrudStack = array_pop(self::$m_aCrudStack);
|
||||
$aCurrentCrudStack['id'] = (string)$this->GetKey();
|
||||
self::$m_aCrudStack[] = $aCurrentCrudStack;
|
||||
@@ -6552,7 +6728,8 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
private function RemoveCurrentObjectInCrudStack(): void
|
||||
{
|
||||
array_pop(self::$m_aCrudStack);
|
||||
$aRemoved = array_pop(self::$m_aCrudStack);
|
||||
$this->LogCRUDDebug(__METHOD__, $aRemoved['class'].':'.$aRemoved['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// 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.
|
||||
@@ -576,6 +576,11 @@ class LogChannels
|
||||
public const DATATABLE = 'Datatable';
|
||||
|
||||
public const DEADLOCK = 'DeadLock';
|
||||
/**
|
||||
* @var string Everything related to PHP sessions tracking
|
||||
* @since 3.1.1 3.2.0 N°6901
|
||||
*/
|
||||
public const SESSIONTRACKER = 'SessionTracker';
|
||||
|
||||
/**
|
||||
* @var string Everything related to the datamodel CRUD
|
||||
|
||||
@@ -5155,7 +5155,7 @@ abstract class MetaModel
|
||||
*/
|
||||
protected static function DBCreateTables($aCallback = null)
|
||||
{
|
||||
list($aErrors, $aSugFix, $aCondensedQueries) = self::DBCheckFormat();
|
||||
[$aErrors, $aSugFix, $aCondensedQueries] = self::DBCheckFormat();
|
||||
|
||||
//$sSQL = implode('; ', $aCondensedQueries); Does not work - multiple queries not allowed
|
||||
foreach($aCondensedQueries as $sQuery)
|
||||
@@ -5177,7 +5177,7 @@ abstract class MetaModel
|
||||
*/
|
||||
protected static function DBCreateViews()
|
||||
{
|
||||
list($aErrors, $aSugFix) = self::DBCheckViews();
|
||||
[$aErrors, $aSugFix] = self::DBCheckViews();
|
||||
|
||||
foreach($aSugFix as $sClass => $aTarget)
|
||||
{
|
||||
@@ -6926,6 +6926,22 @@ abstract class MetaModel
|
||||
return $iCount === 1;
|
||||
}
|
||||
|
||||
public static function GetFinalClassName(string $sClass, int $iKey): string
|
||||
{
|
||||
if (MetaModel::IsStandaloneClass($sClass)) {
|
||||
return $sClass;
|
||||
}
|
||||
|
||||
$sRootClass = MetaModel::GetRootClass($sClass);
|
||||
$sTable = MetaModel::DBGetTable($sRootClass);
|
||||
$sKeyCol = MetaModel::DBGetKey($sRootClass);
|
||||
$sEscapedKey = CMDBSource::Quote($iKey);
|
||||
$sFinalClassField = Metamodel::DBGetClassField($sRootClass);
|
||||
|
||||
$sQuery = "SELECT `{$sFinalClassField}` FROM `{$sTable}` WHERE `{$sKeyCol}` = {$sEscapedKey}";
|
||||
return CMDBSource::QueryToScalar($sQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the specified class and id. If the object is archived it will be returned anyway (this is for pre-2.4
|
||||
* module compatibility, see N.1108)
|
||||
|
||||
@@ -546,6 +546,38 @@ class TriggerOnObjectUpdate extends TriggerOnObject
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate trigger based on attribute list given instead of changed attributes
|
||||
*
|
||||
* @param array $aContextArgs
|
||||
* @param array|null $aAttributes if null default to changed attributes
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \OQLException
|
||||
* @since 3.1.1 3.2.0 N°6228
|
||||
*/
|
||||
public function DoActivateForSpecificAttributes(array $aContextArgs, ?array $aAttributes)
|
||||
{
|
||||
if (isset($aContextArgs['this->object()']))
|
||||
{
|
||||
/** @var \DBObject $oObject */
|
||||
$oObject = $aContextArgs['this->object()'];
|
||||
if (is_null($aAttributes)) {
|
||||
$aChanges = $oObject->ListPreviousValuesForUpdatedAttributes();
|
||||
} else {
|
||||
$aChanges = array_fill_keys($aAttributes, true);
|
||||
}
|
||||
if (false === $this->IsTargetObject($oObject->GetKey(), $aChanges)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
parent::DoActivate($aContextArgs);
|
||||
}
|
||||
|
||||
public function IsTargetObject($iObjectId, $aChanges = array())
|
||||
{
|
||||
if (!parent::IsTargetObject($iObjectId, $aChanges))
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
<count_min>0</count_min>
|
||||
<count_max>0</count_max>
|
||||
<ext_key_to_remote>contact_id</ext_key_to_remote>
|
||||
<with_php_computation>true</with_php_computation>
|
||||
<duplicates/>
|
||||
</field>
|
||||
<field id="functionalcis_list" xsi:type="AttributeLinkedSetIndirect">
|
||||
@@ -210,6 +211,7 @@
|
||||
<count_min>0</count_min>
|
||||
<count_max>0</count_max>
|
||||
<ext_key_to_remote>functionalci_id</ext_key_to_remote>
|
||||
<with_php_computation>true</with_php_computation>
|
||||
<duplicates/>
|
||||
</field>
|
||||
<field id="workorders_list" xsi:type="AttributeLinkedSet">
|
||||
|
||||
@@ -182,6 +182,7 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Changing status is not allowed for your own User',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Allowed organizations must contain User organization',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'The current list of profiles does not give sufficient access rights (Users are not modifiable anymore)',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'The Portal power user profile does not give sufficient access rights (another profile must be added)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'At least one organization must be assigned to this user.',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organization not allowed.',
|
||||
'Class:User/Error:UserOrganizationNotAllowed' => 'The user account does not belong to your allowed organizations.',
|
||||
|
||||
@@ -166,6 +166,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
|
||||
'Class:User/Error:StatusChangeIsNotAllowed' => 'Impossible de changer l\'état de son propre utilisateur',
|
||||
'Class:User/Error:AllowedOrgsMustContainUserOrg' => 'Les organisations permises doivent contenir l\'organisation de l\'utilisateur',
|
||||
'Class:User/Error:CurrentProfilesHaveInsufficientRights' => 'Les profils existants ne permettent pas de modifier les utilisateurs',
|
||||
'Class:User/Error:PortalPowerUserHasInsufficientRights' => 'Le profil Portal power user ne donne pas suffisamment de droits à l\'utilisateur (un autre profil doit être ajouté)',
|
||||
'Class:User/Error:AtLeastOneOrganizationIsNeeded' => 'L\'utilisateur doit avoir au moins une organisation.',
|
||||
'Class:User/Error:OrganizationNotAllowed' => 'Organisation non autorisée.',
|
||||
'Class:User/Error:UserOrganizationNotAllowed' => 'L\'utilisateur n\'appartient pas à vos organisations.',
|
||||
|
||||
@@ -473,6 +473,8 @@ return array(
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectManager' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectManager.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectRepository' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectRepository.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectsEvents' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionGC' => $baseDir . '/sources/SessionTracker/SessionGC.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionHandler' => $baseDir . '/sources/SessionTracker/SessionHandler.php',
|
||||
'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Config' => $baseDir . '/core/config.class.inc.php',
|
||||
|
||||
@@ -837,6 +837,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectManager' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectManager.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectRepository' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectRepository.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectsEvents' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectsEvents.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionGC' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionGC.php',
|
||||
'Combodo\\iTop\\SessionTracker\\SessionHandler' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionHandler.php',
|
||||
'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php',
|
||||
|
||||
@@ -68,7 +68,7 @@ try
|
||||
break;
|
||||
|
||||
default:
|
||||
ContextTag::AddContext(ContextTag::TAG_CONSOLE);
|
||||
$oTag = new ContextTag(ContextTag::TAG_CONSOLE);
|
||||
}
|
||||
|
||||
// First check if we can redirect the route to a dedicated controller
|
||||
|
||||
@@ -2080,6 +2080,7 @@ EOF
|
||||
$this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
|
||||
$this->CompileCommonProperty('with_php_computation', $oField, $aParameters, $sModuleRelativeDir, false);
|
||||
$aParameters['depends_on'] = $sDependencies;
|
||||
} elseif ($sAttType == 'AttributeLinkedSet') {
|
||||
$this->CompileCommonProperty('linked_class', $oField, $aParameters, $sModuleRelativeDir);
|
||||
@@ -2091,6 +2092,7 @@ EOF
|
||||
$this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir);
|
||||
$this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false);
|
||||
$this->CompileCommonProperty('with_php_computation', $oField, $aParameters, $sModuleRelativeDir, false);
|
||||
$aParameters['depends_on'] = $sDependencies;
|
||||
} elseif ($sAttType == 'AttributeExternalKey') {
|
||||
$this->CompileCommonProperty('target_class', $oField, $aParameters, $sModuleRelativeDir);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace Combodo\iTop\Application\Helper;
|
||||
|
||||
use Combodo\iTop\SessionTracker\SessionHandler;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,7 @@ class Session
|
||||
}
|
||||
|
||||
if (!self::$bIsInitialized) {
|
||||
SessionHandler::session_set_save_handler();
|
||||
session_name('itop-'.md5(APPROOT));
|
||||
}
|
||||
|
||||
@@ -214,4 +216,4 @@ class Session
|
||||
|
||||
return utils::IsModeCLI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
sources/SessionTracker/SessionGC.php
Normal file
32
sources/SessionTracker/SessionGC.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\SessionTracker;
|
||||
|
||||
/**
|
||||
* Class SessionGC
|
||||
*
|
||||
* @author Olivier Dain <olivier.dain@combodo.com>
|
||||
* @package Combodo\iTop\SessionTracker
|
||||
* @since 3.1.1 3.2.0 N°6901
|
||||
*/
|
||||
class SessionGC implements \iBackgroundProcess
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetPeriodicity()
|
||||
{
|
||||
return 60 * 1; // seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function Process($iTimeLimit)
|
||||
{
|
||||
$iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 1440;
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$iProcessed = $oSessionHandler->gc_with_time_limit($iMaxLifetime, $iTimeLimit);
|
||||
return "processed $iProcessed tasks";
|
||||
}
|
||||
}
|
||||
240
sources/SessionTracker/SessionHandler.php
Normal file
240
sources/SessionTracker/SessionHandler.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\SessionTracker;
|
||||
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use ContextTag;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use ReturnTypeWillChange;
|
||||
use UserRights;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class SessionHandler:
|
||||
* defaut PHP SessionHandler already relies on files that are accessible by iTop.
|
||||
* this new iTop SessionHandler creates additional session files that are located under iTop folders.
|
||||
* these new session files are meant to monitor the application and contain additional data:
|
||||
* - current user id
|
||||
* - context
|
||||
* - login_mode
|
||||
* - session creation timestamp
|
||||
*
|
||||
* @author Olivier Dain <olivier.dain@combodo.com>
|
||||
* @package Combodo\iTop\SessionTracker
|
||||
* @since 3.1.1 3.2.0 N°6901
|
||||
*/
|
||||
class SessionHandler extends \SessionHandler
|
||||
{
|
||||
/**
|
||||
* @param string $session_id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function destroy($session_id) : bool
|
||||
{
|
||||
IssueLog::Debug("Destroy PHP session", \LogChannels::SESSIONTRACKER, [
|
||||
'session_id' => $session_id,
|
||||
]);
|
||||
$bRes = parent::destroy($session_id);
|
||||
|
||||
if ($bRes) {
|
||||
$this->unlink_session_file($session_id);
|
||||
}
|
||||
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $max_lifetime
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function gc($max_lifetime) : bool
|
||||
{
|
||||
IssueLog::Debug("Run PHP sessions garbage collector", \LogChannels::SESSIONTRACKER, [
|
||||
'max_lifetime' => $max_lifetime,
|
||||
]);
|
||||
$iRes = parent::gc($max_lifetime);
|
||||
$this->gc_with_time_limit($max_lifetime);
|
||||
return $iRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $save_path
|
||||
* @param string $session_name
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function open($save_path, $session_name) : bool
|
||||
{
|
||||
$bRes = parent::open($save_path, $session_name);
|
||||
|
||||
$session_id = session_id();
|
||||
IssueLog::Debug("Open PHP session", \LogChannels::SESSIONTRACKER, [
|
||||
'session_id' => $session_id,
|
||||
]);
|
||||
|
||||
if ($bRes) {
|
||||
$this->touch_session_file($session_id);
|
||||
}
|
||||
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $session_id
|
||||
* @param string $data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function write($session_id, $data) : bool
|
||||
{
|
||||
$bRes = parent::write($session_id, $data);
|
||||
|
||||
IssueLog::Debug("Write PHP session", \LogChannels::SESSIONTRACKER, [
|
||||
'session_id' => $session_id,
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
if ($bRes) {
|
||||
$this->touch_session_file($session_id);
|
||||
}
|
||||
|
||||
return $bRes;
|
||||
}
|
||||
|
||||
public static function session_set_save_handler() : void
|
||||
{
|
||||
if (false === utils::GetConfig()->Get('sessions_tracking.enabled')){
|
||||
//feature disabled
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionhandler = new SessionHandler();
|
||||
session_set_save_handler($sessionhandler, true);
|
||||
|
||||
$iThreshold = utils::GetConfig()->Get('sessions_tracking.gc_threshold');
|
||||
$iThreshold = min(100, $iThreshold);
|
||||
$iThreshold = max(1, $iThreshold);
|
||||
if ((100 != $iThreshold) && (rand(1, 100) > $iThreshold)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 60;
|
||||
$iMaxDurationInSeconds = utils::GetConfig()->Get('sessions_tracking.gc_duration_in_seconds');
|
||||
$sessionhandler->gc_with_time_limit($iMaxLifetime, time() + $iMaxDurationInSeconds);
|
||||
}
|
||||
|
||||
private function generate_session_content(?string $sPreviousFileVersionContent) : ?string
|
||||
{
|
||||
try {
|
||||
$sUserId = UserRights::GetUserId();
|
||||
if (null === $sUserId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Default value in case of
|
||||
// - First time file creation
|
||||
// - Data corruption (not a json / not an array / no previous creation_time key)
|
||||
$iCreationTime = time();
|
||||
|
||||
if (! is_null($sPreviousFileVersionContent)) {
|
||||
$aJson = json_decode($sPreviousFileVersionContent, true);
|
||||
if (is_array($aJson) && array_key_exists('creation_time', $aJson)) {
|
||||
$iCreationTime = $aJson['creation_time'];
|
||||
}
|
||||
}
|
||||
|
||||
return json_encode (
|
||||
[
|
||||
'login_mode' => Session::Get('login_mode'),
|
||||
'user_id' => $sUserId,
|
||||
'creation_time' => $iCreationTime,
|
||||
'context' => implode('|', ContextTag::GetStack())
|
||||
]
|
||||
);
|
||||
} catch(Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function get_file_path($session_id) : string
|
||||
{
|
||||
return utils::GetDataPath() . "sessions/session_$session_id";
|
||||
}
|
||||
|
||||
private function touch_session_file($session_id) : ?string
|
||||
{
|
||||
if (strlen($session_id) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
clearstatcache();
|
||||
if (! is_dir(utils::GetDataPath() . "sessions")) {
|
||||
@mkdir(utils::GetDataPath() . "sessions");
|
||||
}
|
||||
|
||||
$sFilePath = $this->get_file_path($session_id);
|
||||
|
||||
$sPreviousFileVersionContent = null;
|
||||
if (is_file($sFilePath)) {
|
||||
$sPreviousFileVersionContent = file_get_contents($sFilePath);
|
||||
}
|
||||
$sNewContent = $this->generate_session_content($sPreviousFileVersionContent);
|
||||
if (is_null($sNewContent) || ($sPreviousFileVersionContent === $sNewContent)) {
|
||||
@touch($sFilePath);
|
||||
} else {
|
||||
file_put_contents($sFilePath, $sNewContent);
|
||||
}
|
||||
|
||||
return $sFilePath;
|
||||
}
|
||||
|
||||
private function unlink_session_file($session_id)
|
||||
{
|
||||
$sFilePath = $this->get_file_path($session_id);
|
||||
if (is_file($sFilePath)) {
|
||||
@unlink($sFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $max_lifetime
|
||||
* @param int $iTimeLimit Unix timestamp of time limit not to exceed. -1 for no limit.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function gc_with_time_limit(int $max_lifetime, int $iTimeLimit = -1) : int
|
||||
{
|
||||
$aFiles = $this->list_session_files();
|
||||
$iProcessed = 0;
|
||||
$now = time();
|
||||
|
||||
foreach ($aFiles as $sFile) {
|
||||
if ($now - filemtime($sFile) > $max_lifetime) {
|
||||
@unlink($sFile);
|
||||
$iProcessed++;
|
||||
}
|
||||
|
||||
if (-1 !== $iTimeLimit && time() > $iTimeLimit) {
|
||||
//cleanup processing has to stop after $iTimeLimit
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $iProcessed;
|
||||
}
|
||||
|
||||
public function list_session_files() : array
|
||||
{
|
||||
clearstatcache();
|
||||
if (! is_dir(utils::GetDataPath() . "sessions")) {
|
||||
@mkdir(utils::GetDataPath() . "sessions");
|
||||
}
|
||||
|
||||
return glob(utils::GetDataPath() . "sessions/session_*");
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,22 @@ abstract class ItopDataTestCase extends ItopTestCase
|
||||
const USE_TRANSACTION = true;
|
||||
const CREATE_TEST_ORG = false;
|
||||
|
||||
protected static $aURP_Profiles = [
|
||||
'Administrator' => 1,
|
||||
'Portal user' => 2,
|
||||
'Configuration Manager' => 3,
|
||||
'Service Desk Agent' => 4,
|
||||
'Support Agent' => 5,
|
||||
'Problem Manager' => 6,
|
||||
'Change Implementor' => 7,
|
||||
'Change Supervisor' => 8,
|
||||
'Change Approver' => 9,
|
||||
'Service Manager' => 10,
|
||||
'Document author' => 11,
|
||||
'Portal power user' => 12,
|
||||
'REST Services User' => 1024,
|
||||
];
|
||||
|
||||
/**
|
||||
* This method is called before the first test of this test class is run (in the current process).
|
||||
*/
|
||||
|
||||
@@ -1,82 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
|
||||
class cmdbAbstractObjectTest extends ItopDataTestCase {
|
||||
const USE_TRANSACTION = true;
|
||||
const CREATE_TEST_ORG = true;
|
||||
// Count the events by name
|
||||
private static array $aEventCalls = [];
|
||||
private static int $iEventCalls = 0;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
static::$aEventCalls = [];
|
||||
static::$iEventCalls = 0;
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public static function IncrementCallCount(string $sEvent)
|
||||
{
|
||||
self::$aEventCalls[$sEvent] = (self::$aEventCalls[$sEvent] ?? 0) + 1;
|
||||
self::$iEventCalls++;
|
||||
}
|
||||
|
||||
public function testCheckLinkModifications() {
|
||||
$aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
|
||||
$this->assertSame([], $aLinkModificationsStack);
|
||||
|
||||
// retain events
|
||||
cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
|
||||
|
||||
// Create the person
|
||||
$oPerson = $this->CreatePerson(1);
|
||||
$this->assertIsObject($oPerson);
|
||||
// Create the team
|
||||
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
|
||||
$oTeam->DBInsert();
|
||||
// contact types
|
||||
$oContactType1 = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.rand(10000, 99999)]);
|
||||
$oContactType1->DBInsert();
|
||||
$oContactType2 = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.rand(10000, 99999)]);
|
||||
$oContactType2->DBInsert();
|
||||
|
||||
// Prepare the link for the insertion with the team
|
||||
|
||||
$aValues = [
|
||||
'person_id' => $oPerson->GetKey(),
|
||||
'role_id' => $oContactType1->GetKey(),
|
||||
'team_id' => $oTeam->GetKey(),
|
||||
];
|
||||
$oLinkPersonToTeam1 = MetaModel::NewObject(lnkPersonToTeam::class, $aValues);
|
||||
$oLinkPersonToTeam1->DBInsert();
|
||||
|
||||
$aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
|
||||
self::assertCount(3, $aLinkModificationsStack);
|
||||
$aExpectedLinkStack = [
|
||||
'Team' => [$oTeam->GetKey() => 1],
|
||||
'Person' => [$oPerson->GetKey() => 1],
|
||||
'ContactType' => [$oContactType1->GetKey() => 1],
|
||||
];
|
||||
self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
|
||||
|
||||
$oLinkPersonToTeam1->Set('role_id', $oContactType2->GetKey());
|
||||
$oLinkPersonToTeam1->DBWrite();
|
||||
$aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
|
||||
self::assertCount(3, $aLinkModificationsStack);
|
||||
$aExpectedLinkStack = [
|
||||
'Team' => [$oTeam->GetKey() => 2],
|
||||
'Person' => [$oPerson->GetKey() => 2],
|
||||
'ContactType' => [
|
||||
$oContactType1->GetKey() => 2,
|
||||
$oContactType2->GetKey() => 1,
|
||||
],
|
||||
];
|
||||
self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
|
||||
}
|
||||
|
||||
public function testProcessClassIdDeferredUpdate()
|
||||
{
|
||||
// Create the team
|
||||
@@ -152,288 +86,4 @@ class cmdbAbstractObjectTest extends ItopDataTestCase {
|
||||
{
|
||||
$this->SetNonPublicStaticProperty(cmdbAbstractObject::class, 'aObjectsAwaitingEventDbLinksChanged', $aObjects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that EVENT_DB_LINKS_CHANGED events are not sent to the current updated/created object (Team)
|
||||
* the events are sent to the other side (Person)
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function testDBInsertTeam()
|
||||
{
|
||||
// Prepare the link set
|
||||
$sLinkedClass = lnkPersonToTeam::class;
|
||||
$aLinkedObjectsArray = [];
|
||||
$oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
|
||||
$oLinkSet = new ormLinkSet(Team::class, 'persons_list', $oSet);
|
||||
|
||||
// Create the 3 persons
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$oPerson = $this->CreatePerson($i);
|
||||
$this->assertIsObject($oPerson);
|
||||
// Add the person to the link
|
||||
$oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey()]);
|
||||
$oLinkSet->AddItem($oLink);
|
||||
}
|
||||
|
||||
$this->debug("\n-------------> Test Starts HERE\n");
|
||||
|
||||
$oEventReceiver = new LinksEventReceiver($this);
|
||||
$oEventReceiver->RegisterCRUDListeners();
|
||||
|
||||
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'persons_list' => $oLinkSet, 'org_id' => $this->getTestOrgId()]);
|
||||
$oTeam->DBInsert();
|
||||
$this->assertIsObject($oTeam);
|
||||
|
||||
// 3 links added to person + 1 for the Team
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when creating a new lnk object
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function testAddLinkToTeam()
|
||||
{
|
||||
// Create a person
|
||||
$oPerson = $this->CreatePerson(1);
|
||||
$this->assertIsObject($oPerson);
|
||||
|
||||
// Create a Team
|
||||
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
|
||||
$oTeam->DBInsert();
|
||||
$this->assertIsObject($oTeam);
|
||||
|
||||
$this->debug("\n-------------> Test Starts HERE\n");
|
||||
$oEventReceiver = new LinksEventReceiver($this);
|
||||
$oEventReceiver->RegisterCRUDListeners();
|
||||
|
||||
// The link creation will signal both the Person an the Team
|
||||
$oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
|
||||
$oLink->DBInsert();
|
||||
|
||||
// 2 events one for Person and One for Team
|
||||
$this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when updating an existing lnk object
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function testUpdateLinkRole()
|
||||
{
|
||||
// Create a person
|
||||
$oPerson = $this->CreatePerson(1);
|
||||
$this->assertIsObject($oPerson);
|
||||
|
||||
// Create a Team
|
||||
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
|
||||
$oTeam->DBInsert();
|
||||
$this->assertIsObject($oTeam);
|
||||
|
||||
// Create the link
|
||||
$oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
|
||||
$oLink->DBInsert();
|
||||
|
||||
$this->debug("\n-------------> Test Starts HERE\n");
|
||||
$oEventReceiver = new LinksEventReceiver($this);
|
||||
$oEventReceiver->RegisterCRUDListeners();
|
||||
|
||||
// The link update will signal both the Person, the Team and the ContactType
|
||||
// Change the role
|
||||
$oContactType = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.$oLink->GetKey()]);
|
||||
$oContactType->DBInsert();
|
||||
$oLink->Set('role_id', $oContactType->GetKey());
|
||||
$oLink->DBUpdate();
|
||||
|
||||
// 3 events one for Person, one for Team and one for ContactType
|
||||
$this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when a link changes from an object to another, then both objects are notified
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function testUpdateLinkPerson()
|
||||
{
|
||||
// Create 2 person
|
||||
$oPerson1 = $this->CreatePerson(1);
|
||||
$this->assertIsObject($oPerson1);
|
||||
|
||||
$oPerson2 = $this->CreatePerson(2);
|
||||
$this->assertIsObject($oPerson2);
|
||||
|
||||
// Create a Team
|
||||
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
|
||||
$oTeam->DBInsert();
|
||||
$this->assertIsObject($oTeam);
|
||||
|
||||
// Create the link between Person1 and Team
|
||||
$oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson1->GetKey(), 'team_id' => $oTeam->GetKey()]);
|
||||
$oLink->DBInsert();
|
||||
|
||||
$this->debug("\n-------------> Test Starts HERE\n");
|
||||
$oEventReceiver = new LinksEventReceiver($this);
|
||||
$oEventReceiver->RegisterCRUDListeners();
|
||||
|
||||
// The link update will signal both the Persons and the Team
|
||||
// Change the person
|
||||
$oLink->Set('person_id', $oPerson2->GetKey());
|
||||
$oLink->DBUpdate();
|
||||
|
||||
// 3 events 2 for Person, one for Team
|
||||
$this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when deleting an existing lnk object
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function testDeleteLink()
|
||||
{
|
||||
// Create a person
|
||||
$oPerson = $this->CreatePerson(1);
|
||||
$this->assertIsObject($oPerson);
|
||||
|
||||
// Create a Team
|
||||
$oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
|
||||
$oTeam->DBInsert();
|
||||
$this->assertIsObject($oTeam);
|
||||
|
||||
// Create the link
|
||||
$oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
|
||||
$oLink->DBInsert();
|
||||
|
||||
$this->debug("\n-------------> Test Starts HERE\n");
|
||||
$oEventReceiver = new LinksEventReceiver($this);
|
||||
$oEventReceiver->RegisterCRUDListeners();
|
||||
|
||||
// The link delete will signal both the Person an the Team
|
||||
$oLink->DBDelete();
|
||||
|
||||
// 3 events one for Person, one for Team
|
||||
$this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug called by event receivers
|
||||
*
|
||||
* @param $sMsg
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function DebugStatic($sMsg)
|
||||
{
|
||||
if (static::$DEBUG_UNIT_TEST) {
|
||||
if (is_string($sMsg)) {
|
||||
echo "$sMsg\n";
|
||||
} else {
|
||||
print_r($sMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Count events received
|
||||
* And allow callbacks on events
|
||||
*/
|
||||
class LinksEventReceiver
|
||||
{
|
||||
private $oTestCase;
|
||||
private $aCallbacks = [];
|
||||
|
||||
public static $bIsObjectInCrudStack;
|
||||
|
||||
public function __construct(ItopDataTestCase $oTestCase)
|
||||
{
|
||||
$this->oTestCase = $oTestCase;
|
||||
}
|
||||
|
||||
public function AddCallback(string $sEvent, string $sClass, string $sFct, int $iCount = 1): void
|
||||
{
|
||||
$this->aCallbacks[$sEvent][$sClass] = [
|
||||
'callback' => [$this, $sFct],
|
||||
'count' => $iCount,
|
||||
];
|
||||
}
|
||||
|
||||
public function CleanCallbacks()
|
||||
{
|
||||
$this->aCallbacks = [];
|
||||
}
|
||||
|
||||
// Event callbacks
|
||||
public function OnEvent(EventData $oData)
|
||||
{
|
||||
$sEvent = $oData->GetEvent();
|
||||
$oObject = $oData->Get('object');
|
||||
$sClass = get_class($oObject);
|
||||
$iKey = $oObject->GetKey();
|
||||
$this->Debug(__METHOD__.": received event '$sEvent' for $sClass::$iKey");
|
||||
cmdbAbstractObjectTest::IncrementCallCount($sEvent);
|
||||
|
||||
if (isset($this->aCallbacks[$sEvent][$sClass])) {
|
||||
$aCallBack = $this->aCallbacks[$sEvent][$sClass];
|
||||
if ($aCallBack['count'] > 0) {
|
||||
$this->aCallbacks[$sEvent][$sClass]['count']--;
|
||||
call_user_func($this->aCallbacks[$sEvent][$sClass]['callback'], $oObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function RegisterCRUDListeners(string $sEvent = null, $mEventSource = null)
|
||||
{
|
||||
$this->Debug('Registering Test event listeners');
|
||||
if (is_null($sEvent)) {
|
||||
$this->oTestCase->EventService_RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
|
||||
return;
|
||||
}
|
||||
$this->oTestCase->EventService_RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
|
||||
}
|
||||
|
||||
private function Debug($msg)
|
||||
{
|
||||
cmdbAbstractObjectTest::DebugStatic($msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class QueryTest extends ItopDataTestCase
|
||||
}
|
||||
|
||||
$oQuery->DBInsert();
|
||||
$this->assertFalse($oQuery->IsNew());
|
||||
|
||||
return $oQuery;
|
||||
}
|
||||
|
||||
@@ -208,4 +208,34 @@ PHP
|
||||
$oFormFieldNoTouchedAtt = $oAttDef->MakeFormField($oPerson);
|
||||
$this->assertTrue($oFormFieldNoTouchedAtt->IsValidationDisabled(), 'email wasn\'t modified, we must not validate the corresponding field');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider WithConstraintParameterProvider
|
||||
*
|
||||
* @param string $sClass
|
||||
* @param string $sAttCode
|
||||
* @param bool $bConstraintExpected
|
||||
* @param bool $bComputationExpected
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testWithConstraintAndComputationParameters(string $sClass, string $sAttCode, bool $bConstraintExpected, bool $bComputationExpected)
|
||||
{
|
||||
$oAttDef = \MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
$sConstraintExpected = $bConstraintExpected ? 'true' : 'false';
|
||||
$sComputationExpected = $bComputationExpected ? 'true' : 'false';
|
||||
$this->assertEquals($bConstraintExpected, $oAttDef->HasPHPConstraint(), "Standard DataModel should be configured with property 'has_php_constraint'=$sConstraintExpected for $sClass:$sAttCode");
|
||||
$this->assertEquals($bComputationExpected, $oAttDef->HasPHPComputation(), "Standard DataModel should be configured with property 'has_php_computation'=$sComputationExpected for $sClass:$sAttCode");
|
||||
}
|
||||
|
||||
public function WithConstraintParameterProvider()
|
||||
{
|
||||
return [
|
||||
['User', 'profile_list', true, false],
|
||||
['User', 'allowed_org_list', true, false],
|
||||
['Person', 'team_list', false, false],
|
||||
['Ticket', 'functionalcis_list', false, true],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
namespace Combodo\iTop\Test\UnitTest\Core\CRUD;
|
||||
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use ContactType;
|
||||
use CoreException;
|
||||
@@ -21,6 +20,7 @@ use ormLinkSet;
|
||||
use Person;
|
||||
use Team;
|
||||
use utils;
|
||||
use const EVENT_DB_LINKS_CHANGED;
|
||||
|
||||
class CRUDEventTest extends ItopDataTestCase
|
||||
{
|
||||
@@ -339,8 +339,8 @@ class CRUDEventTest extends ItopDataTestCase
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_CHECK_TO_WRITE]);
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_BEFORE_WRITE]);
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_AFTER_WRITE]);
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
$this->assertEquals(20, self::$iEventCalls);
|
||||
$this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
|
||||
$this->assertEquals(16, self::$iEventCalls);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,8 +388,8 @@ class CRUDEventTest extends ItopDataTestCase
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_CHECK_TO_WRITE]);
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_BEFORE_WRITE]);
|
||||
$this->assertEquals(4, self::$aEventCalls[EVENT_DB_AFTER_WRITE]);
|
||||
$this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
$this->assertEquals(19, self::$iEventCalls);
|
||||
$this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
|
||||
$this->assertEquals(16, self::$iEventCalls);
|
||||
|
||||
// Read the object explicitly from the DB to check that the role has been set
|
||||
$oSet = new DBObjectSet(DBSearch::FromOQL('SELECT Team WHERE id=:id'), [], ['id' => $oTeam->GetKey()]);
|
||||
@@ -495,7 +495,7 @@ class CRUDEventTest extends ItopDataTestCase
|
||||
$oLnk = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
|
||||
$oLnk->DBInsert();
|
||||
|
||||
$this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
$this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
|
||||
}
|
||||
|
||||
public function testLinksDeleted()
|
||||
@@ -517,7 +517,7 @@ class CRUDEventTest extends ItopDataTestCase
|
||||
|
||||
$oLnk->DBDelete();
|
||||
|
||||
$this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
$this->assertArrayNotHasKey(EVENT_DB_LINKS_CHANGED, self::$aEventCalls, 'no relation with the with_php_compute attribute !');
|
||||
}
|
||||
|
||||
// Tests with MockDBObject
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core\DBObject;
|
||||
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use URP_UserProfile;
|
||||
use UserLocal;
|
||||
|
||||
class CheckToWritePropagationTest extends ItopDataTestCase
|
||||
{
|
||||
private static array $aEventCalls;
|
||||
|
||||
public function testCascadeCheckToWrite()
|
||||
{
|
||||
$sLogin = 'testCascadeCheckToWrite-'.uniqid('', true);
|
||||
$oUser1 = $this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'], 'ABCD1234@gabuzomeu');
|
||||
$sUserId1 = $oUser1->GetKey();
|
||||
$sLogin = 'testCascadeCheckToWrite-'.uniqid('', true);
|
||||
$oUser2 = $this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'], 'ABCD1234@gabuzomeu');
|
||||
$sUserId2 = $oUser2->GetKey();
|
||||
|
||||
$this->EventService_RegisterListener(EVENT_DB_CHECK_TO_WRITE, [$this, 'CheckToWriteEventListener'], 'User');
|
||||
$sEventKeyUser1 = $this->GetEventKey(EVENT_DB_CHECK_TO_WRITE, UserLocal::class, $sUserId1);
|
||||
$sEventKeyUser2 = $this->GetEventKey(EVENT_DB_CHECK_TO_WRITE, UserLocal::class, $sUserId2);
|
||||
|
||||
// Add URP_UserProfile
|
||||
self::$aEventCalls = [];
|
||||
$oURPUserProfile = new URP_UserProfile();
|
||||
$oURPUserProfile->Set('profileid', self::$aURP_Profiles['Support Agent']);
|
||||
$oURPUserProfile->Set('userid', $sUserId1);
|
||||
$oURPUserProfile->Set('reason', 'UNIT Tests');
|
||||
$oURPUserProfile->DBInsert();
|
||||
$this->assertArrayHasKey($sEventKeyUser1, self::$aEventCalls, 'User checkToWrite should be called when a URP_UserProfile is created');
|
||||
|
||||
// Update URP_UserProfile (change profile)
|
||||
self::$aEventCalls = [];
|
||||
$oURPUserProfile->Set('profileid', self::$aURP_Profiles['Problem Manager']);
|
||||
$oURPUserProfile->DBUpdate();
|
||||
$this->assertArrayHasKey($sEventKeyUser1, self::$aEventCalls, 'User checkToWrite should be called when a URP_UserProfile is updated');
|
||||
|
||||
// Update URP_UserProfile (move from User1 to User2)
|
||||
self::$aEventCalls = [];
|
||||
$oURPUserProfile->Set('userid', $sUserId2);
|
||||
$oURPUserProfile->DBUpdate();
|
||||
$this->assertCount(2, self::$aEventCalls, 'Previous User and new User checkToWrite should be called when a URP_UserProfile is moved from a User to another');
|
||||
$this->assertArrayHasKey($sEventKeyUser1, self::$aEventCalls, 'Previous User checkToWrite should be called when a URP_UserProfile is moved from a User to another');
|
||||
$this->assertArrayHasKey($sEventKeyUser2, self::$aEventCalls, 'New User checkToWrite should be called when a URP_UserProfile is moved from a User to another');
|
||||
|
||||
// Delete URP_UserProfile from User2
|
||||
self::$aEventCalls = [];
|
||||
$oURPUserProfile->DBDelete();
|
||||
$this->assertArrayHasKey($sEventKeyUser2, self::$aEventCalls, 'User checkToWrite should be called when a URP_UserProfile is deleted');
|
||||
|
||||
$oUser1->DBDelete();
|
||||
$oUser2->DBDelete();
|
||||
}
|
||||
|
||||
public function CheckToWriteEventListener(EventData $oEventData)
|
||||
{
|
||||
$oObject = $oEventData->GetEventData()['object'];
|
||||
$sEvent = $oEventData->GetEvent();
|
||||
$sClass = get_class($oObject);
|
||||
$sId = $oObject->GetKey();
|
||||
self::$aEventCalls[$this->GetEventKey($sEvent, $sClass, $sId)] = true;
|
||||
}
|
||||
|
||||
private function GetEventKey($sEvent, $sClass, $sId)
|
||||
{
|
||||
return "event: $sEvent, class: $sClass, id: $sId";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core\DBObject;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use URP_UserProfile;
|
||||
use User;
|
||||
use UserLocal;
|
||||
|
||||
class CustomCheckToWriteTest extends ItopDataTestCase
|
||||
{
|
||||
public function PortaPowerUserProvider()
|
||||
{
|
||||
return [
|
||||
'No profile' => [
|
||||
'aProfiles' => [],
|
||||
'bExpectedCheckStatus' => false,
|
||||
],
|
||||
'Portal power user' => [
|
||||
'aProfiles' => ['Portal power user',],
|
||||
'bExpectedCheckStatus' => true,
|
||||
],
|
||||
'Portal power user + Configuration Manager' => [
|
||||
'aProfiles' => ['Portal power user', 'Configuration Manager',],
|
||||
'bExpectedCheckStatus' => true,
|
||||
],
|
||||
'Portal power user + Configuration Manager + Admin' => [
|
||||
'aProfiles' => ['Portal power user', 'Configuration Manager', 'Administrator',],
|
||||
'bExpectedCheckStatus' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider PortaPowerUserProvider
|
||||
* @covers User::CheckPortalProfiles
|
||||
*/
|
||||
public function testUserLocalCheckPortalProfiles($aProfiles, $bExpectedCheckStatus)
|
||||
{
|
||||
$oUser = new UserLocal();
|
||||
$sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid('', true);
|
||||
$oUser->Set('login', $sLogin);
|
||||
$oUser->Set('password', 'ABCD1234@gabuzomeu');
|
||||
$oUser->Set('language', 'EN US');
|
||||
$oProfileList = $oUser->Get('profile_list');
|
||||
|
||||
foreach ($aProfiles as $sProfileName) {
|
||||
$oAdminUrpProfile = new URP_UserProfile();
|
||||
$oAdminUrpProfile->Set('profileid', self::$aURP_Profiles[$sProfileName]);
|
||||
$oAdminUrpProfile->Set('reason', 'UNIT Tests');
|
||||
$oProfileList->AddItem($oAdminUrpProfile);
|
||||
}
|
||||
|
||||
$oUser->Set('profile_list', $oProfileList);
|
||||
|
||||
[$bCheckStatus, $aCheckIssues, $bSecurityIssue] = $oUser->CheckToWrite();
|
||||
$this->assertEquals($bExpectedCheckStatus, $bCheckStatus);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,29 @@ class MetaModelTest extends ItopDataTestCase
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers MetaModel::GetObjectByName
|
||||
* @return void
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function testGetFinalClassName()
|
||||
{
|
||||
// Standalone classes
|
||||
$this->assertEquals('Organization', MetaModel::GetFinalClassName('Organization', 1), 'Should work with standalone classes');
|
||||
$this->assertEquals('SynchroDataSource', MetaModel::GetFinalClassName('SynchroDataSource', 1), 'Should work with standalone classes');
|
||||
|
||||
// 2 levels hierarchy
|
||||
$this->assertEquals('Person', MetaModel::GetFinalClassName('Contact', 1));
|
||||
$this->assertEquals('Person', MetaModel::GetFinalClassName('Person', 1));
|
||||
|
||||
// multi-level hierarchy
|
||||
$oServer1 = MetaModel::GetObjectByName('Server', 'Server1');
|
||||
$sServer1Id = $oServer1->GetKey();
|
||||
foreach (MetaModel::EnumParentClasses('Server',ENUM_PARENT_CLASSES_ALL) as $sClass) {
|
||||
$this->assertEquals('Server', MetaModel::GetFinalClassName($sClass, $sServer1Id), 'Should return Server for all the classes in the hierarchy');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @group itopRequestMgmt
|
||||
* @covers MetaModel::ApplyParams()
|
||||
|
||||
@@ -488,29 +488,4 @@ class UserRightsTest extends ItopDataTestCase
|
||||
'with Admins hidden' => [true, 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider WithConstraintParameterProvider
|
||||
* @param string $sClass
|
||||
* @param string $sAttCode
|
||||
* @param bool $bExpected
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testWithConstraintParameter(string $sClass, string $sAttCode, bool $bExpected)
|
||||
{
|
||||
$oAttDef = \MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
$this->assertTrue(method_exists($oAttDef, "GetHasConstraint"));
|
||||
$this->assertEquals($bExpected, $oAttDef->GetHasConstraint());
|
||||
}
|
||||
|
||||
public function WithConstraintParameterProvider()
|
||||
{
|
||||
return [
|
||||
['User', 'profile_list', true],
|
||||
['User', 'allowed_org_list', true],
|
||||
['Person', 'team_list', false],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\SessionTracker;
|
||||
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\SessionTracker\SessionHandler;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use ContextTag;
|
||||
|
||||
class SessionHandlerTest extends ItopDataTestCase
|
||||
{
|
||||
private $aFiles ;
|
||||
private $oTag ;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->aFiles=[];
|
||||
$this->oTag = new ContextTag(ContextTag::TAG_REST);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
$this->oTag = null;
|
||||
|
||||
foreach ($this->aFiles as $sFile){
|
||||
if (is_file($sFile)){
|
||||
@unlink($sFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function CreateUserAndLogIn() : ? string {
|
||||
$_SESSION = [];
|
||||
$oUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg");
|
||||
|
||||
\UserRights::Login($oUser->Get('login'));
|
||||
return $oUser->GetKey();
|
||||
}
|
||||
|
||||
private function GenerateSessionContent(SessionHandler $oSessionHandler, ?string $sPreviousFileVersionContent) : ?string {
|
||||
return $this->InvokeNonPublicMethod(SessionHandler::class, "generate_session_content", $oSessionHandler, $aArgs = [$sPreviousFileVersionContent]);
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::generate_session_content
|
||||
*/
|
||||
public function testGenerateSessionContentNoUserLoggedIn(){
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$sContent = $this->GenerateSessionContent($oSessionHandler, null);
|
||||
$this->assertNull($sContent, "Session content should be null when there is no user logged in");
|
||||
}
|
||||
|
||||
public function GenerateSessionContentCorruptedPreviousFileContentProvider() {
|
||||
return [
|
||||
'not a json' => [ "not a json" ],
|
||||
'not an array' => [ json_encode("not an array") ],
|
||||
'array without creation_time key' => [ json_encode([]) ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers SessionHandler::generate_session_content
|
||||
* @dataProvider GenerateSessionContentCorruptedPreviousFileContentProvider
|
||||
*/
|
||||
public function testGenerateSessionContent_SessionFileRepairment(?string $sFileContent){
|
||||
$sUserId = $this->CreateUserAndLogIn();
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
Session::Set('login_mode', 'foo_login_mode');
|
||||
|
||||
$sContent = $this->GenerateSessionContent($oSessionHandler, $sFileContent);
|
||||
|
||||
$this->assertNotNull($sContent, 'Should not return null');
|
||||
$aJson = json_decode($sContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sContent);
|
||||
$this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sContent");
|
||||
$this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sContent");
|
||||
$this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sContent");
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::generate_session_content
|
||||
*/
|
||||
public function testGenerateSessionContent(){
|
||||
$sUserId = $this->CreateUserAndLogIn();
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
Session::Set('login_mode', 'foo_login_mode');
|
||||
|
||||
//first time
|
||||
$sFirstContent = $this->GenerateSessionContent($oSessionHandler, null);
|
||||
|
||||
$this->assertNotNull($sFirstContent, 'Should not return null');
|
||||
$aJson = json_decode($sFirstContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sFirstContent);
|
||||
$this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sFirstContent");
|
||||
$this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sFirstContent");
|
||||
$this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sFirstContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sFirstContent");
|
||||
|
||||
$iFirstSessionCreationTime = $aJson['creation_time'];
|
||||
|
||||
// Switch context + change user id via impersonation
|
||||
// check it is still tracked in session files
|
||||
$oOtherUser = $this->CreateContactlessUser("admin" . uniqid(), 1, "1234@Abcdefg");
|
||||
$this->assertTrue(\UserRights::Impersonate($oOtherUser->Get('login')), "Failed to execute impersonate on: ".$oOtherUser->Get('login'));
|
||||
$oTag2 = new ContextTag(ContextTag::TAG_SYNCHRO);
|
||||
$sNewContent = $this->GenerateSessionContent($oSessionHandler, $sFirstContent);
|
||||
$this->assertNotNull($sNewContent, 'Should not return null');
|
||||
$aJson = json_decode($sNewContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sNewContent);
|
||||
$this->assertEquals(ContextTag::TAG_REST . '|' . ContextTag::TAG_SYNCHRO, $aJson['context'] ?? '', "After impersonation, should report the new context tags in [context]: $sNewContent");
|
||||
$this->assertEquals($iFirstSessionCreationTime, $aJson['creation_time'] ?? '', "After impersonation, should still report the the session start timestamp in [creation_time]: $sNewContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "After impersonation, should still report the login mode in [login_mode]: $sNewContent");
|
||||
$this->assertEquals($oOtherUser->GetKey(), $aJson['user_id'] ?? '', "Should report the impersonate user in [user_id]: $sNewContent");
|
||||
}
|
||||
|
||||
private function touchSessionFile(SessionHandler $oSessionHandler, $session_id) : ?string {
|
||||
$sRes = $this->InvokeNonPublicMethod(SessionHandler::class, "touch_session_file", $oSessionHandler, $aArgs = [$session_id]);
|
||||
if (!is_null($sRes) && is_file($sRes)) {
|
||||
// Record the file for cleanup on tearDown
|
||||
$this->aFiles[] = $sRes;
|
||||
}
|
||||
clearstatcache();
|
||||
return $sRes;
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::touch_session_file
|
||||
*/
|
||||
public function testTouchSessionFile_NoUserLoggedIn(){
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$session_id = uniqid();
|
||||
$sFile = $this->touchSessionFile($oSessionHandler, $session_id);
|
||||
$this->assertEquals(true, is_file($sFile), "Should return a file name: '$sFile' is not a valid file name");
|
||||
$sContent = file_get_contents($sFile);
|
||||
$this->assertEquals(null, $sContent, 'Should create an empty file, found: '.$sContent);
|
||||
}
|
||||
|
||||
/*
|
||||
* @covers SessionHandler::touch_session_file
|
||||
*/
|
||||
public function testTouchSessionFile_UserLoggedIn(){
|
||||
$sUserId = $this->CreateUserAndLogIn();
|
||||
Session::Set('login_mode', 'foo_login_mode');
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$session_id = uniqid();
|
||||
$sFile = $this->touchSessionFile($oSessionHandler, $session_id);
|
||||
$this->assertEquals(true, is_file($sFile), "Should return a file name: '$sFile' is not a valid file name");
|
||||
$sFirstContent = file_get_contents($sFile);
|
||||
|
||||
$iFirstCTime = filectime($sFile) - 1;
|
||||
// Set it in the past to check that it will be further updated (without the need to sleep...)
|
||||
touch($sFile, $iFirstCTime);
|
||||
|
||||
$this->assertNotNull($sFirstContent, 'Should not return null');
|
||||
$aJson = json_decode($sFirstContent, true);
|
||||
$this->assertNotEquals(false, $aJson, 'Should return a valid json string, found: '.$sFirstContent);
|
||||
$this->assertEquals($sUserId, $aJson['user_id'] ?? '', "Should report the login of the logged in user in [user_id]: $sFirstContent");
|
||||
$this->assertEquals(ContextTag::TAG_REST, $aJson['context'] ?? '', "Should report the context tag(s) in [context]: $sFirstContent");
|
||||
$this->assertIsInt($aJson['creation_time'] ?? '', "Should report the session start timestamp in [creation_time]: $sFirstContent");
|
||||
$this->assertEquals('foo_login_mode', $aJson['login_mode'] ?? '', "Should report the current login mode in [login_mode]: $sFirstContent");
|
||||
|
||||
$this->touchSessionFile($oSessionHandler, $session_id);
|
||||
$sNewContent = file_get_contents($sFile);
|
||||
$this->assertEquals($sFirstContent, $sNewContent, 'On successive calls, should not modify an existing session file');
|
||||
$this->assertGreaterThan($iFirstCTime, filectime($sFile), 'On successive calls, should have changed the file ctime');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers SessionHandler::touch_session_file
|
||||
*/
|
||||
public function testTouchSessionFileWithEmptySessionId() {
|
||||
$this->CreateUserAndLogIn();
|
||||
Session::Set('login_mode', 'toto');
|
||||
|
||||
$oSessionHandler = new SessionHandler();
|
||||
$this->assertNull($this->touchSessionFile($oSessionHandler, ''), 'Should return null when session id is an empty string');
|
||||
$this->assertNull($this->touchSessionFile($oSessionHandler, false), 'Should return null when session id (boolean) false');
|
||||
}
|
||||
|
||||
private function GetFilePath(SessionHandler $oSessionHandler, $session_id) : string {
|
||||
$sFile = $this->InvokeNonPublicMethod(SessionHandler::class, "get_file_path", $oSessionHandler, $aArgs = [$session_id]);
|
||||
// Record file for cleanup on tearDown
|
||||
$this->aFiles[] = $sFile;
|
||||
return $sFile;
|
||||
}
|
||||
|
||||
public function GgcWithTimeLimitProvider(){
|
||||
return [
|
||||
'no cleanup time limit' => [
|
||||
'iTimeLimit' => -1,
|
||||
'iExpectedProcessed' => 2
|
||||
],
|
||||
'cleanup time limit in the pass => first file removed only' => [
|
||||
'iTimeLimit' => time() - 1,
|
||||
'iExpectedProcessed' => 1
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers SessionHandler::gc_with_time_limit
|
||||
* @covers SessionHandler::list_session_files
|
||||
* @dataProvider GgcWithTimeLimitProvider
|
||||
*/
|
||||
public function testGgcWithTimeLimit($iTimeLimit, $iExpectedProcessed) {
|
||||
$oSessionHandler = new SessionHandler();
|
||||
//remove all first
|
||||
$oSessionHandler->gc_with_time_limit(-1);
|
||||
$this->assertEquals([], $oSessionHandler->list_session_files(), 'list_session_files should report no file at startup');
|
||||
|
||||
$max_lifetime = 1440;
|
||||
$iNbExpiredFiles = 2;
|
||||
$iNbFiles = 5;
|
||||
$iExpiredTimeStamp = time() - $max_lifetime - 1;
|
||||
for($i=0; $i<$iNbFiles; $i++) {
|
||||
$sFile = $this->GetFilePath($oSessionHandler, uniqid());
|
||||
file_put_contents($sFile, "fakedata");
|
||||
|
||||
if ($iNbExpiredFiles > 0){
|
||||
$iNbExpiredFiles--;
|
||||
touch($sFile, $iExpiredTimeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
$aFoundSessionFiles = $oSessionHandler->list_session_files();
|
||||
$this->assertEquals($iNbFiles, sizeof($aFoundSessionFiles), 'list_session_files should reports all files');
|
||||
foreach ($aFoundSessionFiles as $sFile){
|
||||
$this->assertTrue(is_file($sFile), 'list_session_files should return a valid file paths, found: '.$sFile);
|
||||
}
|
||||
|
||||
$iProcessed = $oSessionHandler->gc_with_time_limit($max_lifetime, $iTimeLimit);
|
||||
$this->assertEquals($iExpectedProcessed, $iProcessed, 'gc_with_time_limit should report the count of expired files');
|
||||
$this->assertEquals($iNbFiles - $iExpectedProcessed, sizeof($oSessionHandler->list_session_files()), 'gc_with_time_limit should actually remove all processed files');
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ function RunTask(BackgroundTask $oTask, $iTimeLimit)
|
||||
// Time in seconds allowed to the task
|
||||
$iCurrTimeLimit = $iTimeLimit;
|
||||
// Compute allowed time
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess') === false)
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess') === false)
|
||||
{
|
||||
// Periodic task, allow only X times ($iMaxTaskExecutionTime) its periodicity (GetPeriodicity())
|
||||
$iMaxTaskExecutionTime = MetaModel::GetConfig()->Get('cron_task_max_execution_time');
|
||||
@@ -148,7 +148,7 @@ function RunTask(BackgroundTask $oTask, $iTimeLimit)
|
||||
$oTask->Set('first_run_date', $oDateStarted->format('Y-m-d H:i:s'));
|
||||
}
|
||||
$oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics
|
||||
|
||||
|
||||
// Update the timestamp since we want to be able to re-order the tasks based on the time they finished
|
||||
$oDateEnded = new DateTime();
|
||||
$oTask->Set('latest_run_date', $oDateEnded->format('Y-m-d H:i:s'));
|
||||
|
||||
Reference in New Issue
Block a user