CRUD reentrance protection

This commit is contained in:
Eric Espie
2022-04-05 10:28:12 +02:00
parent c788c93542
commit f6d92a189b
11 changed files with 555 additions and 166 deletions

View File

@@ -3075,4 +3075,9 @@ HTML;
return $sUrl;
}
public static function GetUniqId()
{
return hash('sha256', uniqid(sprintf('%x', rand()), true).sprintf('%x', rand()));
}
}

View File

@@ -608,15 +608,7 @@ abstract class CMDBObject extends DBObject
public function DBUpdate()
{
// Copy the changes list before the update (the list should be reset afterwards)
$aChanges = $this->ListChanges();
if (count($aChanges) == 0)
{
return;
}
$ret = parent::DBUpdate();
return $ret;
parent::DBUpdate();
}

View File

@@ -136,32 +136,6 @@
</argument>
</arguments>
</event>
<event id="DBObjectLoaded" _delta="define">
<description>An object has been loaded from the database</description>
<arguments>
<argument id="object">
<description>The object loaded</description>
<type>DBObject</type>
</argument>
<argument id="debug_info">
<description>Debug string</description>
<type>string</type>
</argument>
</arguments>
</event>
<event id="DBObjectNew" _delta="define">
<description>An object has been created in memory</description>
<arguments>
<argument id="object">
<description>The object created</description>
<type>DBObject</type>
</argument>
<argument id="debug_info">
<description>Debug string</description>
<type>string</type>
</argument>
</arguments>
</event>
<event id="DBObjectReload" _delta="define">
<description>An object has been re-loaded from the database</description>
<arguments>
@@ -193,7 +167,7 @@
</argument>
</arguments>
</event>
<event id="DoCheckToDelete" _delta="define">
<event id="OnCheckToDelete" _delta="define">
<description>Check an object before it is deleted from the database</description>
<replaces>cmdbAbstractObject::DoCheckToDelete</replaces>
<arguments>

View File

@@ -149,7 +149,6 @@ abstract class DBObject implements iDisplay
* @var string local events suffix
*/
protected $m_sEventUniqId = '';
protected static $m_iEventUniqCounter = 0;
/**
@@ -176,9 +175,8 @@ abstract class DBObject implements iDisplay
$this->m_bFullyLoaded = $this->IsFullyLoaded();
$this->m_aTouchedAtt = array();
$this->m_aModifiedAtt = array();
$this->m_sEventUniqId = 'DataModel_'.get_class($this).'_'.self::$m_iEventUniqCounter++;
$this->m_sEventUniqId = get_class($this).'::'.$this->GetKey().'_'.utils::GetUniqId();
$this->RegisterEvents();
$this->FireEvent(EVENT_SERVICE_DB_OBJECT_LOADED);
return;
}
// Creation of a brand new object
@@ -206,9 +204,8 @@ abstract class DBObject implements iDisplay
$this->UpdateMetaAttributes();
$this->m_sEventUniqId = 'DataModel_'.self::$m_iEventUniqCounter++;
$this->m_sEventUniqId = get_class($this).'::0'.'_'.utils::GetUniqId();
$this->RegisterEvents();
$this->FireEvent(EVENT_SERVICE_DB_OBJECT_NEW);
}
protected function RegisterEvents()
@@ -504,7 +501,6 @@ abstract class DBObject implements iDisplay
// Load extended data
if ($aExtendedDataSpec != null)
{
$aExtendedDataSpec['table'];
foreach($aExtendedDataSpec['fields'] as $sColumn)
{
$sColRef = $sClassAlias.'_extdata_'.$sColumn;
@@ -1633,10 +1629,10 @@ abstract class DBObject implements iDisplay
{
if ($sType == FriendlyNameType::SHORT) {
return $this->Get('friendlyname');
} else {
$oExpression = MetaModel::GetNameExpression(get_class($this), $sType);
$this->EvaluateExpression($oExpression);
}
$oExpression = MetaModel::GetNameExpression(get_class($this), $sType);
return $this->EvaluateExpression($oExpression);
}
/**
@@ -2809,6 +2805,10 @@ abstract class DBObject implements iDisplay
}
$sClass = get_class($this);
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: DBInsert $sClass::0 Starting", LogChannels::DM_CRUD);
}
$sRootClass = MetaModel::GetRootClass($sClass);
// Ensure the update of the values (we are accessing the data directly)
@@ -2893,7 +2893,6 @@ abstract class DBObject implements iDisplay
}
$this->OnObjectKeyReady();
$this->FireEvent(EVENT_SERVICE_DB_OBJECT_KEY_READY);
$this->DBWriteLinks();
$this->WriteExternalAttributes();
@@ -3105,6 +3104,11 @@ abstract class DBObject implements iDisplay
}
$this->Reload();
$sClass = get_class($this);
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: DBInsert $sClass::{$this->m_iKey} Created", LogChannels::DM_CRUD);
}
return $this->m_iKey;
}
@@ -3168,15 +3172,22 @@ abstract class DBObject implements iDisplay
}
// Protect against reentrance (e.g. cascading the update of ticket logs)
static $aUpdateReentrance = array();
$sKey = get_class($this).'::'.$this->GetKey();
$sClass = get_class($this);
$sKey = $sClass.'::'.$this->GetKey();
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: DBUpdate $sKey Starting", LogChannels::DM_CRUD);
}
if (array_key_exists($sKey, $aUpdateReentrance))
{
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: DBUpdate $sKey Rejected (reentrance)", LogChannels::DM_CRUD);
}
return false;
}
$aUpdateReentrance[$sKey] = true;
$this->InitPreviousValuesForUpdatedAttributes();
try
{
$this->DoComputeValues();
@@ -3184,7 +3195,7 @@ abstract class DBObject implements iDisplay
$sState = $this->GetState();
if ($sState != '')
{
foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
if ($oAttDef instanceof AttributeStopWatch)
{
@@ -3202,12 +3213,16 @@ abstract class DBObject implements iDisplay
$this->OnUpdate();
$this->FireEvent(EVENT_SERVICE_BEFORE_UPDATE);
$this->InitPreviousValuesForUpdatedAttributes();
$aChanges = $this->ListChanges();
if (count($aChanges) == 0)
{
// Attempting to update an unchanged object
unset($aUpdateReentrance[$sKey]);
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: DBUpdate $sKey Aborted (no change)", LogChannels::DM_CRUD);
}
return $this->m_iKey;
}
@@ -3218,7 +3233,7 @@ abstract class DBObject implements iDisplay
{
throw new CoreCannotSaveObjectException(array(
'issues' => $aIssues,
'class' => get_class($this),
'class' => $sClass,
'id' => $this->GetKey()
));
}
@@ -3227,7 +3242,6 @@ abstract class DBObject implements iDisplay
$aOriginalValues = $this->m_aOrigValues;
// Activate any existing trigger
$sClass = get_class($this);
// - TriggerOnObjectMention
$this->ActivateOnMentionTriggers(false);
@@ -3236,7 +3250,7 @@ abstract class DBObject implements iDisplay
$aDBChanges = array();
foreach ($aChanges as $sAttCode => $valuecurr)
{
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
if ($oAttDef->IsExternalKey())
{
$bHasANewExternalKeyValue = true;
@@ -3275,7 +3289,7 @@ abstract class DBObject implements iDisplay
// Update the left & right indexes for each hierarchical key
foreach ($aHierarchicalKeys as $sAttCode => $oAttDef)
{
$sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
$sTable = MetaModel::DBGetTable($sClass, $sAttCode);
$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey();
$aRes = CMDBSource::QueryToArray($sSQL);
$iMyLeft = $aRes[0]['left'];
@@ -3315,7 +3329,7 @@ abstract class DBObject implements iDisplay
// Update scalar attributes
if (count($aDBChanges) != 0)
{
$oFilter = new DBObjectSearch(get_class($this));
$oFilter = new DBObjectSearch($sClass);
$oFilter->AddCondition('id', $this->m_iKey, '=');
$oFilter->AllowAllData();
@@ -3360,7 +3374,7 @@ abstract class DBObject implements iDisplay
$aErrors = array($e->getMessage());
throw new CoreCannotSaveObjectException(array(
'id' => $this->GetKey(),
'class' => get_class($this),
'class' => $sClass,
'issues' => $aErrors
), $e);
}
@@ -3383,7 +3397,7 @@ abstract class DBObject implements iDisplay
$aErrors = array($e->getMessage());
throw new CoreCannotSaveObjectException(array(
'id' => $this->GetKey(),
'class' => get_class($this),
'class' => $sClass,
'issues' => $aErrors,
));
}
@@ -3412,7 +3426,6 @@ abstract class DBObject implements iDisplay
}
$this->AfterUpdate();
$this->FireEvent(EVENT_SERVICE_AFTER_UPDATE);
// Reload to get the external attributes
if ($bHasANewExternalKeyValue) {
@@ -3432,13 +3445,18 @@ abstract class DBObject implements iDisplay
catch (Exception $e)
{
$aErrors = array($e->getMessage());
throw new CoreCannotSaveObjectException(array('id' => $this->GetKey(), 'class' => get_class($this), 'issues' => $aErrors));
throw new CoreCannotSaveObjectException(array('id' => $this->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
}
}
finally
{
unset($aUpdateReentrance[$sKey]);
}
$this->FireEvent(EVENT_SERVICE_AFTER_UPDATE, ['changes' => $aChanges]);
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: DBUpdate $sKey Updated", LogChannels::DM_CRUD);
}
return $this->m_iKey;
}
@@ -3931,7 +3949,9 @@ abstract class DBObject implements iDisplay
IssueLog::Error("$sClass: Transition $sStimulusCode is not allowed in ".$this->Get($sStateAttCode));
return false;
}
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: ApplyStimulus $sStimulusCode $sClass::{$this->m_iKey} Starting", LogChannels::DM_CRUD);
}
// save current object values in case of an action failure (in memory rollback)
$aBackupValues = array();
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
@@ -3947,7 +3967,7 @@ abstract class DBObject implements iDisplay
$aTransitionDef = $aStateTransitions[$sStimulusCode];
$this->FireEvent(EVENT_SERVICE_BEFORE_APPLY_STIMULUS);
$this->FireEvent(EVENT_SERVICE_BEFORE_APPLY_STIMULUS, ['stimulus' => $sStimulusCode]);
// Change the state before proceeding to the actions, this is necessary because an action might
// trigger another stimuli (alternative: push the stimuli into a queue)
@@ -4067,7 +4087,7 @@ abstract class DBObject implements iDisplay
}
}
$this->FireEvent(EVENT_SERVICE_AFTER_APPLY_STIMULUS);
$this->FireEvent(EVENT_SERVICE_AFTER_APPLY_STIMULUS, ['stimulus' => $sStimulusCode]);
}
else
{
@@ -4077,7 +4097,9 @@ abstract class DBObject implements iDisplay
$this->m_aCurrValues[$sAttCode] = $aBackupValues[$sAttCode];
}
}
if ($sClass == 'UserRequest') {
IssueLog::Debug("CRUD: ApplyStimulus $sStimulusCode $sClass::{$this->m_iKey} Ending", LogChannels::DM_CRUD);
}
return $bSuccess;
}

View File

@@ -15,8 +15,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Service\EventData;
use Combodo\iTop\Service\EventService;
/**
@@ -39,8 +37,6 @@ class ExecutionKPI
* @var array[ExecutionKPI]
*/
static protected $m_aExecutionStack = []; // embedded execution stats
// Event listener
static protected $oKPIEventListener;
protected $m_fStarted = null;
protected $m_fChildrenDuration = 0; // Count embedded
@@ -292,9 +288,6 @@ class ExecutionKPI
public function __construct()
{
if (is_null(static::$oKPIEventListener)) {
static::$oKPIEventListener = new KPIEventListener();
}
$this->ResetCounters();
self::Push($this);
}
@@ -306,7 +299,7 @@ class ExecutionKPI
*/
private static function Push(ExecutionKPI $oExecutionKPI)
{
array_push(self::$m_aExecutionStack, $oExecutionKPI);
self::$m_aExecutionStack[] = $oExecutionKPI;
}
/**
@@ -456,21 +449,3 @@ class ExecutionKPI
return 0;
}
}
class KPIEventListener
{
// Data model is not yet loaded, so EVENT_SERVICE_DB_OBJECT_LOADED is not yet defined
const EVENT_SERVICE_DB_OBJECT_LOADED = 'DBObjectLoaded';
public function __construct()
{
EventService::RegisterListener(self::EVENT_SERVICE_DB_OBJECT_LOADED, [get_class($this), 'OnObjectLoaded']);
}
public static function OnObjectLoaded(EventData $oEventData)
{
$oObject = $oEventData->GetEventData()['object'];
$oKPI = new ExecutionKPI();
$oKPI->ComputeStats('Objects Loaded (no duration)', get_class($oObject));
}
}

View File

@@ -568,6 +568,14 @@ class LogChannels
public const INLINE_IMAGE = 'InlineImage';
public const PORTAL = 'portal';
/**
* @var string
* @since 3.1.0 specific channel for event service
*/
public const EVENT_SERVICE = 'EventService';
public const DM_CRUD = 'DMCRUD';
}

View File

@@ -357,6 +357,7 @@ return array(
'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php',
'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php',
'Combodo\\iTop\\Service\\EventData' => $baseDir . '/sources/application/Service/EventData.php',
'Combodo\\iTop\\Service\\EventHelper' => $baseDir . '/sources/application/Service/EventHelper.php',
'Combodo\\iTop\\Service\\EventService' => $baseDir . '/sources/application/Service/EventService.php',
'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php',
'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php',
@@ -499,7 +500,6 @@ return array(
'JSButtonItem' => $baseDir . '/application/applicationextension.inc.php',
'JSPopupMenuItem' => $baseDir . '/application/applicationextension.inc.php',
'JsonPage' => $baseDir . '/sources/application/WebPage/JsonPage.php',
'KPIEventListener' => $baseDir . '/core/kpi.class.inc.php',
'KeyValueStore' => $baseDir . '/core/counter.class.inc.php',
'ListExpression' => $baseDir . '/core/oql/expression.class.inc.php',
'ListOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',

View File

@@ -587,6 +587,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php',
'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php',
'Combodo\\iTop\\Service\\EventData' => __DIR__ . '/../..' . '/sources/application/Service/EventData.php',
'Combodo\\iTop\\Service\\EventHelper' => __DIR__ . '/../..' . '/sources/application/Service/EventHelper.php',
'Combodo\\iTop\\Service\\EventService' => __DIR__ . '/../..' . '/sources/application/Service/EventService.php',
'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php',
'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php',
@@ -729,7 +730,6 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'JSButtonItem' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'JSPopupMenuItem' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'JsonPage' => __DIR__ . '/../..' . '/sources/application/WebPage/JsonPage.php',
'KPIEventListener' => __DIR__ . '/../..' . '/core/kpi.class.inc.php',
'KeyValueStore' => __DIR__ . '/../..' . '/core/counter.class.inc.php',
'ListExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
'ListOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',

View File

@@ -0,0 +1,42 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Service;
use SetupUtils;
use utils;
class EventHelper
{
public static function GetCachedClasses($sModuleName, callable $ListBuilder)
{
$aClasses = [];
$sCacheFileName = '';
if (!utils::IsDevelopmentEnvironment()) {
// Try to read from cache
$sCacheFileName = utils::GetCachePath()."EventsClassList/$sModuleName.php";
if (is_file($sCacheFileName)) {
$aClasses = include $sCacheFileName;
}
}
if (empty($aClasses)) {
$aClasses = call_user_func($ListBuilder);
if (!utils::IsDevelopmentEnvironment() && !empty($aClasses)) {
// Save to cache
$sCacheContent = "<?php\n\nreturn ".var_export($aClasses, true).";";
SetupUtils::builddir(dirname($sCacheFileName));
file_put_contents($sCacheFileName, $sCacheContent);
}
}
return $aClasses;
}
}

View File

@@ -11,14 +11,14 @@ use ContextTag;
use Exception;
use ExecutionKPI;
use IssueLog;
define('LOG_EVENT_SERVICE_CHANNEL', 'EventService');
use LogChannels;
class EventService
{
public static $aEventListeners = [];
private static $iEventIdCounter = 0;
private static $aEventDescription = [];
Private static $aReentranceProtection = [];
/**
* Register a callback for a specific event
@@ -32,7 +32,6 @@ class EventService
*
* @return string Id of the registration (used for unregistering)
*
* @throws \Exception
*/
public static function RegisterListener(string $sEvent, callable $callback, $sEventSource = null, $aCallbackData = [], $context = null, float $fPriority = 0.0): string
{
@@ -64,8 +63,8 @@ class EventService
foreach (self::$aEventListeners as $aEvent) {
$iTotalRegistrations += count($aEvent);
}
$sEventName = "$sEvent:".self::GetSourcesAsString($sEventSource);
IssueLog::Trace("Registering event '$sEventName' for '$sName' with id '$sId' (total $iTotalRegistrations)", LOG_EVENT_SERVICE_CHANNEL);
$sLogEventName = "$sEvent:".self::GetSourcesAsString($sEventSource);
IssueLog::Trace("Registering event '$sLogEventName' for '$sName' with id '$sId' (total $iTotalRegistrations)", LogChannels::EVENT_SERVICE);
return $sId;
}
@@ -88,10 +87,10 @@ class EventService
$eventSource = $oEventData->GetEventSource();
$oKPI = new ExecutionKPI();
$sSource = isset($aEventData['debug_info']) ? " {$aEventData['debug_info']}" : '';
$sEventName = "$sEvent:".self::GetSourcesAsString($eventSource);
IssueLog::Trace("Fire event '$sEventName'$sSource", LOG_EVENT_SERVICE_CHANNEL);
$sLogEventName = "$sEvent - ".self::GetSourcesAsString($eventSource);
IssueLog::Trace("Fire event '$sLogEventName'$sSource", LogChannels::EVENT_SERVICE);
if (!isset(self::$aEventListeners[$sEvent])) {
IssueLog::Trace("No registration found for event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Trace("No registration found for event '$sEvent'", LogChannels::EVENT_SERVICE);
$oKPI->ComputeStats('FireEvent', $sEvent);
return;
@@ -105,18 +104,26 @@ class EventService
continue;
}
$sName = $aEventCallback['name'];
IssueLog::Debug("Fire event '$sEventName'$sSource calling '$sName'", LOG_EVENT_SERVICE_CHANNEL);
// Reentrance protection
if (isset(self::$aReentranceProtection[$aEventCallback['id']])) {
if (!self::$aReentranceProtection[$aEventCallback['id']]) {
unset(self::$aReentranceProtection[$aEventCallback['id']]);
}
IssueLog::Debug("Reentrance protection for '$sLogEventName'$sSource for '$sName'", LogChannels::EVENT_SERVICE);
continue;
}
IssueLog::Debug("Fire event '$sLogEventName'$sSource calling '$sName'", LogChannels::EVENT_SERVICE);
try {
if (is_callable($aEventCallback['callback'])) {
$oEventData->SetCallbackData($aEventCallback['data']);
call_user_func($aEventCallback['callback'], $oEventData);
} else {
IssueLog::Debug("Callback '$sName' not a callable anymore, unregister", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Debug("Callback '$sName' not a callable anymore, unregister", LogChannels::EVENT_SERVICE);
self::UnRegisterCallback($aEventCallback['id']);
}
}
catch (Exception $e) {
IssueLog::Error("Event '$sEventName' for '$sName' id {$aEventCallback['id']} failed with error: ".$e->getMessage());
IssueLog::Error("Event '$sLogEventName' for '$sName' id {$aEventCallback['id']} failed with error: ".$e->getMessage());
throw $e;
}
}
@@ -134,19 +141,19 @@ class EventService
return false;
}
if (is_string($srcRegistered)) {
$aSrcRegistered = array($srcRegistered);
$aSrcRegistered = [$srcRegistered];
} elseif (is_array($srcRegistered)) {
$aSrcRegistered = $srcRegistered;
} else {
$aSrcRegistered = array();
$aSrcRegistered = [];
}
if (is_string($srcEvent)) {
$aSrcEvent = array($srcEvent);
$aSrcEvent = [$srcEvent];
} elseif (is_array($srcEvent)) {
$aSrcEvent = $srcEvent;
} else {
$aSrcEvent = array();
$aSrcEvent = [];
}
foreach ($aSrcEvent as $sSrcEvent) {
@@ -187,10 +194,10 @@ class EventService
return '';
}
if (is_string($srcRegistered)) {
return $srcRegistered;
return substr($srcRegistered, 0, 30);
}
if (is_array($srcRegistered)) {
return implode(',', $srcRegistered);
return substr(implode(',', $srcRegistered), 0, 30);
}
return '';
@@ -207,7 +214,7 @@ class EventService
if ($aEventCallback['id'] == $sId) {
$sName = self::$aEventListeners[$sEvent][$idx]['name'];
unset (self::$aEventListeners[$sEvent][$idx]);
IssueLog::Trace("Unregistered callback '$sName' id $sId' on event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Trace("Unregistered callback '$sName' id $sId' on event '$sEvent'", LogChannels::EVENT_SERVICE);
return false;
}
@@ -216,7 +223,7 @@ class EventService
});
if (!$bRemoved) {
IssueLog::Trace("No registration found for callback '$sId'", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Trace("No registration found for callback '$sId'", LogChannels::EVENT_SERVICE);
}
}
@@ -228,13 +235,13 @@ class EventService
public static function UnRegisterEvent(string $sEvent)
{
if (!isset(self::$aEventListeners[$sEvent])) {
IssueLog::Trace("No registration found for event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Trace("No registration found for event '$sEvent'", LogChannels::EVENT_SERVICE);
return;
}
unset(self::$aEventListeners[$sEvent]);
IssueLog::Trace("Unregistered all the callbacks on event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Trace("Unregistered all the callbacks on event '$sEvent'", LogChannels::EVENT_SERVICE);
}
/**
@@ -243,7 +250,7 @@ class EventService
public static function UnRegisterAll()
{
self::$aEventListeners = array();
IssueLog::Trace("Unregistered all events", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Trace("Unregistered all events", LogChannels::EVENT_SERVICE);
}
/**
@@ -271,7 +278,7 @@ class EventService
{
if (isset(self::$aEventDescription[$sEvent])) {
$sPrevious = self::$aEventDescription[$sEvent]['module'];
IssueLog::Error("The Event $sEvent defined by $sModule has already been defined in $sPrevious, check your delta", LOG_EVENT_SERVICE_CHANNEL);
IssueLog::Error("The Event $sEvent defined by $sModule has already been defined in $sPrevious, check your delta", LogChannels::EVENT_SERVICE);
}
self::$aEventDescription[$sEvent] = [
@@ -295,9 +302,31 @@ class EventService
return implode('_', $aRet);
}
public static function GetDefinedEventsAsJSON()
{
return json_encode(self::$aEventDescription, JSON_PRETTY_PRINT);
}
/**
* @param string $sListenerId The id given by the RegisterListener() method
* @param bool $bPermanentProtection Be very careful when setting false, infinite loops can occur
*
* @return void
*/
public static function EnableReentranceProtection($sListenerId, $bPermanentProtection = true)
{
self::$aReentranceProtection[$sListenerId] = $bPermanentProtection;
}
/**
* @param string $sListenerId The id given by the RegisterListener() method
*
* @return void
*/
public static function DisableReentranceProtection($sListenerId)
{
if (isset(self::$aReentranceProtection[$sListenerId])) {
unset(self::$aReentranceProtection[$sListenerId]);
}
}
}

View File

@@ -6,6 +6,7 @@ use Combodo\iTop\Service\EventData;
use Combodo\iTop\Service\EventService;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ContextTag;
use Exception;
use TypeError;
/**
@@ -55,7 +56,10 @@ class EventTest extends ItopTestCase
public function testNoParameterCallbackFunction()
{
$sId = EventService::RegisterListener('event', function () { $this->debug("Closure: event received !!!"); self::IncrementCallCount(); });
$sId = EventService::RegisterListener('event', function () {
$this->debug("Closure: event received !!!");
self::IncrementCallCount();
});
$this->debug("Registered $sId");
EventService::FireEvent(new EventData('event'));
$this->assertEquals(1, self::$iEventCalls);
@@ -81,9 +85,10 @@ class EventTest extends ItopTestCase
public function GoodCallbackProvider()
{
$oReceiver = new TestEventReceiver();
return array(
'method' => array(array($oReceiver, 'OnEvent1')),
'static' => array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1'),
'method' => array(array($oReceiver, 'OnEvent1')),
'static' => array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1'),
'static2' => array(array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver', 'OnStaticEvent1')),
);
}
@@ -291,6 +296,157 @@ class EventTest extends ItopTestCase
self::$iEventCalls++;
}
/**
* @dataProvider ReentranceProvider
*/
public function testReentrance($aClasses, $iEventCount)
{
foreach ($aClasses as $sName => $aClass) {
new TestReentrance($sName, $aClass['prio'], $aClass['events'], $aClass['permanent_protection']);
}
EventService::FireEvent(new EventData('event1', 'main'));
$this->assertEquals($iEventCount, self::$iEventCalls);
}
public function ReentranceProvider()
{
return [
'1 class' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
],
'iEventCount' => 1,
],
'2 classes - 1' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
],
'iEventCount' => 3,
],
'2 classes - 2' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
],
'iEventCount' => 4,
],
'2 classes - 3' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => true]]],
],
'iEventCount' => 3,
],
'3 classes - 1' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => true]]],
],
'iEventCount' => 11,
],
'3 classes - non permanent' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => true]]],
],
'iEventCount' => 12,
],
'2 classes - loop' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
],
'iEventCount' => 4,
],
'2 classes - loop2' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]],
],
'iEventCount' => 5,
],
];
}
/**
* @dataProvider ReentranceCRUDProvider
*/
public function testReentranceCRUD($aClasses, $iEventCount)
{
foreach ($aClasses as $sName => $aClass) {
new TestReentranceCRUD($sName, $aClass['prio'], $aClass['events'], $aClass['permanent_protection']);
}
$oObject = new TestCRUDObject();
$oObject->DBInsert('main');
$this->assertEquals($iEventCount, self::$iEventCalls);
}
public function ReentranceCRUDProvider()
{
return [
'1 class' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
],
'iEventCount' => 1,
],
'2 classes - 1' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
],
'iEventCount' => 2,
],
'2 classes - 2'=> [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
],
'iEventCount' => 4,
],
'2 classes - 3' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => true]]],
],
'iEventCount' => 3,
],
'3 classes - 1' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]],
'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => true]]],
],
'iEventCount' => 11,
],
'3 classes - non permanent' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]],
'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => true]]],
],
'iEventCount' => 12,
],
'2 classes - loop' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
],
'iEventCount' => 4,
],
'2 classes - loop2' => [
'aClasses' => [
'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]],
],
'iEventCount' => 5,
],
];
}
/**
* static version of the debug to be accessible from other objects
*
@@ -298,21 +454,50 @@ class EventTest extends ItopTestCase
*/
public static function DebugStatic($sMsg)
{
if (DEBUG_UNIT_TEST)
{
if (is_string($sMsg))
{
if (DEBUG_UNIT_TEST) {
if (is_string($sMsg)) {
echo "$sMsg\n";
}
else
{
} else {
print_r($sMsg);
}
}
}
}
class TestEventReceiver
class TestClassesWithDebug
{
/**
* static version of the debug to be accessible from other objects
*
* @param $sMsg
*/
public static function DebugStatic($sMsg)
{
if (DEBUG_UNIT_TEST) {
if (is_string($sMsg)) {
echo "$sMsg\n";
} else {
print_r($sMsg);
}
}
}
/**
* @param $sMsg
*/
public function Debug($sMsg)
{
if (DEBUG_UNIT_TEST) {
if (is_string($sMsg)) {
echo "$sMsg\n";
} else {
print_r($sMsg);
}
}
}
}
class TestEventReceiver extends TestClassesWithDebug
{
// Event callbacks
@@ -363,44 +548,201 @@ class TestEventReceiver
self::DebugStatic(__METHOD__.": received event '{$sEvent}'");
EventTest::IncrementCallCount();
}
/**
* static version of the debug to be accessible from other objects
*
* @param $sMsg
*/
public static function DebugStatic($sMsg)
{
if (DEBUG_UNIT_TEST)
{
if (is_string($sMsg))
{
echo "$sMsg\n";
}
else
{
print_r($sMsg);
}
}
}
/**
* static version of the debug to be accessible from other objects
*
* @param $sMsg
*/
public function Debug($sMsg)
{
if (DEBUG_UNIT_TEST)
{
if (is_string($sMsg))
{
echo "$sMsg\n";
}
else
{
print_r($sMsg);
}
}
}
}
class TestCRUDObject extends TestClassesWithDebug
{
private static $aEventList = [];
public function DBInsert($sName)
{
$this->sName = $sName;
$this->FireEvent('insert');
}
public function DBUpdate($sName)
{
$this->sName = $sName;
$this->FireEvent('update');
}
public function FireEvent($sEvent, $aEventData = [])
{
$aEventData['name'] = $this->sName;
$aEventData['object'] = $this;
$oEventData = new EventData($sEvent, $this->sName, $aEventData);
$bFireEventNow = empty(self::$aEventList['event']);
self::$aEventList['event'][] = $oEventData;
$iCount = 0;
while ($bFireEventNow) {
$sEvent = $oEventData->GetEvent();
$sName = $oEventData->Get('name');
$this->Debug("Object from $sName: Fire event '$sEvent'");
EventService::FireEvent($oEventData);
$this->Debug("Object from $sName: End of event '$sEvent'");
array_shift(self::$aEventList['event']);
$oEventData = reset(self::$aEventList['event']);
if ($oEventData === false) {
$bFireEventNow = false;
}
if ($iCount++ > 10) {
throw new Exception('Infinite loop');
}
}
}
}
class TestReentranceCRUD extends TestClassesWithDebug
{
public $sName;
public $sEvent1;
public $sEvent2;
public $bPermanentProtection;
private static $iIndent1 = 0;
private static $iIndent2 = 0;
public $aSendEvent = [];
/**
* @param $sName
* @param $fPriority
* @param array $aSendEvent
* @param bool $bPermanentProtection
*/
public function __construct($sName, $fPriority, array $aSendEvent, $bPermanentProtection)
{
$this->sName = $sName;
$this->sEvent1 = EventService::RegisterListener('insert', [$this, 'OnInsert'], null, [], null, $fPriority);
$this->sEvent2 = EventService::RegisterListener('update', [$this, 'OnUpdate'], null, [], null, $fPriority);
$this->aSendEvent = $aSendEvent;
$this->bPermanentProtection = $bPermanentProtection;
}
public function OnInsert(EventData $oData)
{
$sIndent = str_repeat(' ', self::$iIndent1);
$oObject = $oData->Get('object');
$sEvent = $oData->GetEvent();
$sSource = $oData->GetEventSource();
$sName = $sIndent.$this->sName;
$this->Debug("$sName: received event '$sEvent' from '$sSource'");
EventTest::IncrementCallCount();
if ($this->aSendEvent['listener1']['update']) {
self::$iIndent2 += 1;
$this->Debug("$sName: Update");
EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection);
$oObject->DBUpdate($sName);
EventService::DisableReentranceProtection($this->sEvent2);
self::$iIndent2 -= 1;
}
}
public function OnUpdate(EventData $oData)
{
$sIndent = str_repeat(' ', self::$iIndent2);
$oObject = $oData->Get('object');
$sEvent = $oData->GetEvent();
$sSource = $oData->GetEventSource();
$sName = $sIndent.$this->sName;
$this->Debug("$sName: received event '$sEvent' from '$sSource'");
EventTest::IncrementCallCount();
if ($this->aSendEvent['listener2']['update']) {
self::$iIndent2 += 1;
$this->Debug("$sName: Update");
EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection);
$oObject->DBUpdate($sName);
EventService::DisableReentranceProtection($this->sEvent2);
self::$iIndent2 -= 1;
}
}
}
class TestReentrance extends TestClassesWithDebug
{
public $sName;
public $sEvent1;
public $sEvent2;
public $bPermanentProtection;
private static $iIndent1 = 0;
private static $iIndent2 = 0;
public $aSendEvent = [];
/**
* @param $sName
* @param $fPriority
* @param array $aSendEvent
* @param bool $bPermanentProtection
*/
public function __construct($sName, $fPriority, array $aSendEvent, $bPermanentProtection)
{
$this->sName = $sName;
$this->sEvent1 = EventService::RegisterListener('event1', [$this, 'ListenerEvent1'], null, [], null, $fPriority);
$this->sEvent2 = EventService::RegisterListener('event2', [$this, 'ListenerEvent2'], null, [], null, $fPriority);
$this->aSendEvent = $aSendEvent;
$this->bPermanentProtection = $bPermanentProtection;
}
public function ListenerEvent1(EventData $oData)
{
$sIndent = str_repeat(' ', self::$iIndent1);
$sEvent = $oData->GetEvent();
$sSource = $oData->GetEventSource();
$sName = $sIndent.$this->sName;
$this->Debug("$sName: received event '$sEvent' from '$sSource'");
EventTest::IncrementCallCount();
if ($this->aSendEvent['listener1']['event1']) {
$this->Debug("$sName: Fire event 'event1'");
self::$iIndent1 += 1;
EventService::EnableReentranceProtection($this->sEvent1, $this->bPermanentProtection);
EventService::FireEvent(new EventData('event1', $sName));
EventService::DisableReentranceProtection($this->sEvent1);
self::$iIndent1 -= 1;
$this->Debug("$sName: End of event 'event1'");
}
if ($this->aSendEvent['listener1']['event2']) {
$this->Debug("$sName: Fire event 'event2'");
self::$iIndent2 += 1;
EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection);
EventService::FireEvent(new EventData('event2', $sName));
EventService::DisableReentranceProtection($this->sEvent2);
self::$iIndent2 -= 1;
$this->Debug("$sName: End of event 'event2'");
}
}
public function ListenerEvent2(EventData $oData)
{
$sIndent = str_repeat(' ', self::$iIndent2);
$sEvent = $oData->GetEvent();
$sSource = $oData->GetEventSource();
$sName = $sIndent.$this->sName;
$this->Debug("$sName: received event '$sEvent' from '$sSource'");
EventTest::IncrementCallCount();
if ($this->aSendEvent['listener2']['event1']) {
$this->Debug("$sName: Fire event 'event1'");
self::$iIndent1 += 1;
EventService::EnableReentranceProtection($this->sEvent1, $this->bPermanentProtection);
EventService::FireEvent(new EventData('event1', $sName));
EventService::DisableReentranceProtection($this->sEvent1);
self::$iIndent1 -= 1;
$this->Debug("$sName: End of event 'event1'");
}
if ($this->aSendEvent['listener2']['event2']) {
$this->Debug("$sName: Fire event 'event2'");
self::$iIndent2 += 1;
EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection);
EventService::FireEvent(new EventData('event2', $sName));
EventService::DisableReentranceProtection($this->sEvent2);
self::$iIndent2 -= 1;
$this->Debug("$sName: End of event 'event2'");
}
}
}