mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
353 lines
12 KiB
PHP
353 lines
12 KiB
PHP
<?php
|
|
// Copyright (C) 2024 Combodo SAS
|
|
//
|
|
// This file is part of iTop.
|
|
//
|
|
// iTop is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// iTop is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
/**
|
|
* Mechanism to obtain an exclusive lock while editing an object
|
|
*
|
|
* @package iTopORM
|
|
*/
|
|
|
|
/**
|
|
* Persistent storage (in the database) for remembering that an object is locked
|
|
*/
|
|
class iTopOwnershipToken extends DBObject
|
|
{
|
|
public static function Init()
|
|
{
|
|
$aParams = array
|
|
(
|
|
'category' => '',
|
|
'key_type' => 'autoincrement',
|
|
'name_attcode' => array('obj_class', 'obj_key'),
|
|
'state_attcode' => '',
|
|
'reconc_keys' => array(''),
|
|
'db_table' => 'priv_ownership_token',
|
|
'db_key_field' => 'id',
|
|
'db_finalclass_field' => '',
|
|
);
|
|
MetaModel::Init_Params($aParams);
|
|
MetaModel::Init_InheritAttributes();
|
|
MetaModel::Init_AddAttribute(new AttributeDateTime("acquired", array("allowed_values"=>null, "sql"=>'acquired', "default_value"=>'NOW()', "is_null_allowed"=>false, "depends_on"=>array())));
|
|
MetaModel::Init_AddAttribute(new AttributeDateTime("last_seen", array("allowed_values"=>null, "sql"=>'last_seen', "default_value"=>'NOW()', "is_null_allowed"=>false, "depends_on"=>array())));
|
|
MetaModel::Init_AddAttribute(new AttributeString("obj_class", array("allowed_values"=>null, "sql"=>'obj_class', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
|
|
MetaModel::Init_AddAttribute(new AttributeInteger("obj_key", array("allowed_values"=>null, "sql"=>'obj_key', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
|
|
MetaModel::Init_AddAttribute(new AttributeString("token", array("allowed_values"=>null, "sql"=>'token', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
|
|
MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=> '', "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array())));
|
|
|
|
MetaModel::Init_SetZListItems('details', array ('obj_class', 'obj_key', 'last_seen', 'token'));
|
|
MetaModel::Init_SetZListItems('standard_search', array ('obj_class', 'obj_key', 'last_seen', 'token'));
|
|
MetaModel::Init_SetZListItems('list', array ('obj_class', 'obj_key', 'last_seen', 'token'));
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility class to acquire/extend/release/kill an exclusive lock on a given persistent object,
|
|
* for example to prevent concurrent edition of the same object.
|
|
* Each lock has an expiration delay of 120 seconds (tunable via the configuration parameter 'concurrent_lock_expiration_delay')
|
|
* A watchdog (called twice during this delay) is in charge of keeping the lock "alive" while an object is being edited.
|
|
*/
|
|
class iTopOwnershipLock
|
|
{
|
|
protected $sObjClass;
|
|
protected $iObjKey;
|
|
protected $oToken;
|
|
|
|
/**
|
|
* Acquires an exclusive lock on the specified DBObject. Once acquired, the lock is identified
|
|
* by a unique "token" string.
|
|
* @param string $sObjClass The class of the object for which to acquire the lock
|
|
* @param integer $iObjKey The identifier of the object for which to acquire the lock
|
|
* @return multitype:boolean iTopOwnershipLock Ambigous <boolean, string, DBObjectSet>
|
|
*/
|
|
public static function AcquireLock($sObjClass, $iObjKey)
|
|
{
|
|
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
|
|
|
|
$oMutex->Lock();
|
|
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
|
|
$token = $oOwnershipLock->Acquire();
|
|
$oMutex->Unlock();
|
|
|
|
return array('success' => $token !== false, 'token' => $token, 'lock' => $oOwnershipLock, 'acquired' => $oOwnershipLock->oToken->Get('acquired'));
|
|
}
|
|
|
|
/**
|
|
* Extends the ownership lock or acquires it if none exists
|
|
* Returns a hash array with 3 elements:
|
|
* 'status': either true or false, tells if the lock is still owned
|
|
* 'owner': is status is false, the User object currently owning the lock
|
|
* 'operation': whether the lock was 'renewed' (i.e. the lock was valid, its duration has been extended) or 'acquired' (there was no valid lock for this object and a new one was created)
|
|
* @param string $sToken
|
|
* @return multitype:boolean string User
|
|
*/
|
|
public static function ExtendLock($sObjClass, $iObjKey, $sToken)
|
|
{
|
|
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
|
|
|
|
$oMutex->Lock();
|
|
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
|
|
$aResult = $oOwnershipLock->Extend($sToken);
|
|
$oMutex->Unlock();
|
|
|
|
return $aResult;
|
|
}
|
|
|
|
/**
|
|
* Releases the given lock for the specified object
|
|
*
|
|
* @param string $sObjClass The class of the object
|
|
* @param int $iObjKey The identifier of the object
|
|
* @param string $sToken The string identifying the lock
|
|
* @return boolean
|
|
*/
|
|
public static function ReleaseLock($sObjClass, $iObjKey, $sToken)
|
|
{
|
|
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
|
|
|
|
$oMutex->Lock();
|
|
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
|
|
$bResult = $oOwnershipLock->Release($sToken);
|
|
self::DeleteExpiredLocks(); // Cleanup orphan locks
|
|
$oMutex->Unlock();
|
|
|
|
return $bResult;
|
|
}
|
|
|
|
/**
|
|
* Kills the lock for the specified object
|
|
*
|
|
* @param string $sObjClass The class of the object
|
|
* @param int $iObjKey The identifier of the object
|
|
* @return boolean
|
|
*/
|
|
public static function KillLock($sObjClass, $iObjKey)
|
|
{
|
|
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
|
|
|
|
$oMutex->Lock();
|
|
$sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key";
|
|
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('obj_class' => $sObjClass, 'obj_key' => $iObjKey)));
|
|
while($oLock = $oSet->Fetch())
|
|
{
|
|
$oLock->DBDelete();
|
|
}
|
|
$oMutex->Unlock();
|
|
}
|
|
|
|
/**
|
|
* Checks if an exclusive lock exists on the specified DBObject.
|
|
* @param string $sObjClass The class of the object for which to acquire the lock
|
|
* @param integer $iObjKey The identifier of the object for which to acquire the lock
|
|
* @return multitype:boolean iTopOwnershipLock Ambigous <boolean, string, DBObjectSet>
|
|
*/
|
|
public static function IsLocked($sObjClass, $iObjKey)
|
|
{
|
|
$bLocked = false;
|
|
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
|
|
|
|
$oMutex->Lock();
|
|
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
|
|
if ($oOwnershipLock->IsOwned())
|
|
{
|
|
$bLocked = true;
|
|
}
|
|
$oMutex->Unlock();
|
|
|
|
return array('locked' =>$bLocked, 'owner' => $oOwnershipLock->GetOwner());
|
|
}
|
|
|
|
/**
|
|
* Get the current owner of the lock
|
|
* @return User
|
|
*/
|
|
public function GetOwner()
|
|
{
|
|
if ($this->IsTokenValid())
|
|
{
|
|
return MetaModel::GetObject('User', $this->oToken->Get('user_id'), false, true);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The constructor is protected. Use the static methods AcquireLock / ExtendLock / ReleaseLock / KillLock
|
|
* which are protected against concurrent access by a Mutex.
|
|
* @param string $sObjClass The class of the object for which to create a lock
|
|
* @param integer $iObjKey The identifier of the object for which to create a lock
|
|
*/
|
|
protected function __construct($sObjClass, $iObjKey)
|
|
{
|
|
$sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key";
|
|
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('obj_class' => $sObjClass, 'obj_key' => $iObjKey)));
|
|
$this->oToken = $oSet->Fetch();
|
|
$this->sObjClass = $sObjClass;
|
|
$this->iObjKey = $iObjKey;
|
|
// IssueLog::Info("iTopOwnershipLock::__construct($sObjClass, $iObjKey) oToken::".($this->oToken ? $this->oToken->GetKey() : 'null'));
|
|
}
|
|
|
|
protected function IsOwned()
|
|
{
|
|
return $this->IsTokenValid();
|
|
}
|
|
|
|
protected function Acquire($sToken = null)
|
|
{
|
|
if ($this->IsTokenValid())
|
|
{
|
|
// IssueLog::Info("Acquire($sToken) returns false");
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
$sToken = $this->TakeOwnership($sToken);
|
|
// IssueLog::Info("Acquire($sToken) returns $sToken");
|
|
return $sToken;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extends the ownership lock or acquires it if none exists
|
|
* Returns a hash array with 3 elements:
|
|
* 'status': either true or false, tells if the lock is still owned
|
|
* 'owner': is status is false, the User object currently owning the lock
|
|
* 'operation': whether the lock was 'renewed' (i.e. the lock was valid, its duration was extended) or 'expired' (there was no valid lock for this object) or 'lost' (someone else grabbed it)
|
|
* 'acquired': date at which the lock was initially acquired
|
|
* @param string $sToken
|
|
* @return multitype:boolean string User
|
|
*/
|
|
protected function Extend($sToken)
|
|
{
|
|
$aResult = array('status' => true, 'owner' => '', 'operation' => 'renewed');
|
|
|
|
if ($this->IsTokenValid())
|
|
{
|
|
if ($sToken === $this->oToken->Get('token'))
|
|
{
|
|
$this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat()));
|
|
$this->oToken->DBUpdate();
|
|
$aResult['acquired'] = $this->oToken->Get('acquired');
|
|
}
|
|
else
|
|
{
|
|
// IssueLog::Info("Extend($sToken) returns false");
|
|
$aResult['status'] = false;
|
|
$aResult['operation'] = 'lost';
|
|
$aResult['owner'] = $this->GetOwner();
|
|
$aResult['acquired'] = $this->oToken->Get('acquired');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$aResult['status'] = false;
|
|
$aResult['operation'] = 'expired';
|
|
}
|
|
// IssueLog::Info("Extend($sToken) returns true");
|
|
return $aResult;
|
|
}
|
|
|
|
protected function HasOwnership($sToken)
|
|
{
|
|
$bRet = false;
|
|
if ($this->IsTokenValid())
|
|
{
|
|
if ($sToken === $this->oToken->Get('token'))
|
|
{
|
|
$bRet = true;
|
|
}
|
|
}
|
|
// IssueLog::Info("HasOwnership($sToken) return $bRet");
|
|
return $bRet;
|
|
}
|
|
|
|
protected function Release($sToken)
|
|
{
|
|
$bRet = false;
|
|
// IssueLog::Info("Release... begin [$sToken]");
|
|
if (($this->oToken) && ($sToken === $this->oToken->Get('token')))
|
|
{
|
|
// IssueLog::Info("oToken::".$this->oToken->GetKey().' ('.$sToken.') to be deleted');
|
|
$this->oToken->DBDelete();
|
|
// IssueLog::Info("oToken deleted");
|
|
$this->oToken = null;
|
|
$bRet = true;
|
|
}
|
|
else if ($this->oToken == null)
|
|
{
|
|
// IssueLog::Info("Release FAILED oToken == null !!!");
|
|
}
|
|
else
|
|
{
|
|
// IssueLog::Info("Release FAILED inconsistent tokens: sToken=\"".$sToken.'", oToken->Get(\'token\')="'.$this->oToken->Get('token').'"');
|
|
}
|
|
// IssueLog::Info("Release... end");
|
|
return $bRet;
|
|
}
|
|
|
|
protected function IsTokenValid()
|
|
{
|
|
$bRet = false;
|
|
if ($this->oToken != null)
|
|
{
|
|
$sToken = $this->oToken->Get('token');
|
|
$sDate = $this->oToken->Get('last_seen');
|
|
if (($sDate != '') && ($sToken != ''))
|
|
{
|
|
$oLastSeenTime = new DateTime($sDate);
|
|
$iNow = date('U');
|
|
if (($iNow - $oLastSeenTime->format('U')) < MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay'))
|
|
{
|
|
$bRet = true;
|
|
}
|
|
}
|
|
}
|
|
return $bRet;
|
|
}
|
|
|
|
protected function TakeOwnership($sToken = null)
|
|
{
|
|
if ($this->oToken == null)
|
|
{
|
|
$this->oToken = new iTopOwnershipToken();
|
|
$this->oToken->Set('obj_class', $this->sObjClass);
|
|
$this->oToken->Set('obj_key', $this->iObjKey);
|
|
}
|
|
$this->oToken->Set('acquired', date(AttributeDateTime::GetSQLFormat()));
|
|
$this->oToken->Set('user_id', UserRights::GetUserId());
|
|
$this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat()));
|
|
if ($sToken === null)
|
|
{
|
|
$sToken = sprintf('%X', microtime(true));
|
|
}
|
|
$this->oToken->Set('token', $sToken);
|
|
$this->oToken->DBWrite();
|
|
return $this->oToken->Get('token');
|
|
}
|
|
|
|
protected static function DeleteExpiredLocks()
|
|
{
|
|
$sOQL = "SELECT iTopOwnershipToken WHERE last_seen < :last_seen_limit";
|
|
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('last_seen_limit' => date(AttributeDateTime::GetSQLFormat(), time() - MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')))));
|
|
while($oToken = $oSet->Fetch())
|
|
{
|
|
$oToken->DBDelete();
|
|
}
|
|
|
|
}
|
|
}
|