mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 18:48:51 +02:00
#765: prevent two persons to edit the same object at the same time.
SVN:trunk[3617]
This commit is contained in:
@@ -32,6 +32,8 @@ define('HILIGHT_CLASS_WARNING', 'orange');
|
||||
define('HILIGHT_CLASS_OK', 'green');
|
||||
define('HILIGHT_CLASS_NONE', '');
|
||||
|
||||
define('MIN_WATCHDOG_INTERVAL', 15); // Minimum interval for the watchdog: 15s
|
||||
|
||||
require_once(APPROOT.'/core/cmdbobject.class.inc.php');
|
||||
require_once(APPROOT.'/application/applicationextension.inc.php');
|
||||
require_once(APPROOT.'/application/utils.inc.php');
|
||||
@@ -60,6 +62,39 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
{
|
||||
return 'UI.php';
|
||||
}
|
||||
|
||||
function ReloadAndDisplay($oPage, $oObj, $aParams)
|
||||
{
|
||||
$oAppContext = new ApplicationContext();
|
||||
// Reload the page to let the "calling" page execute its 'onunload' method.
|
||||
// Note 1: The redirection MUST NOT be made via an HTTP "header" since onunload is only called when the actual content of the DOM
|
||||
// is replaced by some other content. So the "bouncing" page must provide some content (in our case a script making the redirection).
|
||||
// Note 2: make sure that the URL below is different from the one of the "Modify" button, otherwise the button will have no effect. This is why we add "&a=1" at the end !!!
|
||||
// Note 3: we use the toggle of a flag in the sessionStorage object to prevent an infinite loop of reloads in case the object is actually locked by another window
|
||||
$sSessionStorageKey = get_class($oObj).'_'.$oObj->GetKey();
|
||||
$sParams = '';
|
||||
foreach($aParams as $sName => $value)
|
||||
{
|
||||
$sParams .= $sName.'='.urlencode($value).'&'; // Always add a trailing &
|
||||
}
|
||||
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/'.$oObj->GetUIPage().'?'.$sParams.'class='.get_class($oObj).'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink().'&a=1';
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
if (!sessionStorage.getItem('$sSessionStorageKey'))
|
||||
{
|
||||
sessionStorage.setItem('$sSessionStorageKey', 1);
|
||||
window.location.href= "$sUrl";
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionStorage.removeItem('$sSessionStorageKey');
|
||||
}
|
||||
EOF
|
||||
);
|
||||
|
||||
$oObj->Reload();
|
||||
$oObj->DisplayDetails($oPage, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a message diplayed to the end-user next time this object will be displayed
|
||||
@@ -90,7 +125,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
'message' => $sMessage
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DisplayBareHeader(WebPage $oPage, $bEditMode = false)
|
||||
{
|
||||
@@ -98,25 +133,39 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
//
|
||||
|
||||
// Is there a message for this object ??
|
||||
$aMessages = array();
|
||||
$aRanks = array();
|
||||
if (MetaModel::GetConfig()->Get('concurrent_lock_enabled'))
|
||||
{
|
||||
$aLockInfo = iTopOwnershipLock::IsLocked(get_class($this), $this->GetKey());
|
||||
if ($aLockInfo['locked'])
|
||||
{
|
||||
$aRanks[] = 0;
|
||||
$sName = $aLockInfo['owner']->GetName();
|
||||
if ($aLockInfo['owner']->Get('contactid') != 0)
|
||||
{
|
||||
$sName .= ' ('.$aLockInfo['owner']->Get('contactid_friendlyname').')';
|
||||
}
|
||||
$aResult['message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName); $aMessages[] = "<div class=\"header_message message_error\">".Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName)."</div>";
|
||||
}
|
||||
}
|
||||
$sMessageKey = get_class($this).'::'.$this->GetKey();
|
||||
if (array_key_exists('obj_messages', $_SESSION) && array_key_exists($sMessageKey, $_SESSION['obj_messages']))
|
||||
{
|
||||
$aMessages = array();
|
||||
$aRanks = array();
|
||||
foreach ($_SESSION['obj_messages'][$sMessageKey] as $sMessageId => $aMessageData)
|
||||
{
|
||||
$sMsgClass = 'message_'.$aMessageData['severity'];
|
||||
$aMessages[] = "<div class=\"header_message $sMsgClass\">".$aMessageData['message']."</div>";
|
||||
$aRanks[] = $aMessageData['rank'];
|
||||
}
|
||||
array_multisort($aRanks, $aMessages);
|
||||
foreach ($aMessages as $sMessage)
|
||||
{
|
||||
$oPage->add($sMessage);
|
||||
}
|
||||
unset($_SESSION['obj_messages'][$sMessageKey]);
|
||||
}
|
||||
|
||||
array_multisort($aRanks, $aMessages);
|
||||
foreach ($aMessages as $sMessage)
|
||||
{
|
||||
$oPage->add($sMessage);
|
||||
}
|
||||
|
||||
// action menu
|
||||
$oSingletonFilter = new DBObjectSearch(get_class($this));
|
||||
$oSingletonFilter->AddCondition('id', $this->GetKey(), '=');
|
||||
@@ -1938,6 +1987,53 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
|
||||
public function DisplayModifyForm(WebPage $oPage, $aExtraParams = array())
|
||||
{
|
||||
$sOwnershipToken = null;
|
||||
$iKey = $this->GetKey();
|
||||
$sClass = get_class($this);
|
||||
if ($iKey > 0)
|
||||
{
|
||||
// The concurrent access lock makes sense only for already existing objects
|
||||
$LockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
|
||||
if ($LockEnabled)
|
||||
{
|
||||
$sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data');
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
// We're probably inside something like "apply_modify" where the validation failed and we must prompt the user again to edit the object
|
||||
// let's extend our lock
|
||||
$aLockInfo = iTopOwnershipLock::ExtendLock($sClass, $iKey, $sOwnershipToken);
|
||||
$sOwnershipDate = $aLockInfo['acquired'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$aLockInfo = iTopOwnershipLock::AcquireLock($sClass, $iKey);
|
||||
if ($aLockInfo['success'])
|
||||
{
|
||||
$sOwnershipToken = $aLockInfo['token'];
|
||||
$sOwnershipDate = $aLockInfo['acquired'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$oOwner = $aLockInfo['lock']->GetOwner();
|
||||
// If the object is locked by the current user, it's worth trying again, since
|
||||
// the lock may be released by 'onunload' which is called AFTER loading the current page.
|
||||
//$bTryAgain = $oOwner->GetKey() == UserRights::GetUserId();
|
||||
self::ReloadAndDisplay($oPage, $this, array('operation' => 'modify'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($aExtraParams['wizard_container']) && $aExtraParams['wizard_container'])
|
||||
{
|
||||
$sClassLabel = MetaModel::GetName($sClass);
|
||||
$oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $this->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
|
||||
$oPage->add("<div class=\"page_header\">\n");
|
||||
$oPage->add("<h1>".$this->GetIcon()." ".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $this->GetName())."</h1>\n");
|
||||
$oPage->add("</div>\n");
|
||||
$oPage->add("<div class=\"wizContainer\">\n");
|
||||
}
|
||||
self::$iGlobalFormId++;
|
||||
$this->aFieldsMap = array();
|
||||
$sPrefix = '';
|
||||
@@ -1948,10 +2044,8 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
$aFieldsComments = (isset($aExtraParams['fieldsComments'])) ? $aExtraParams['fieldsComments'] : array();
|
||||
|
||||
$this->m_iFormId = $sPrefix.self::$iGlobalFormId;
|
||||
$sClass = get_class($this);
|
||||
$oAppContext = new ApplicationContext();
|
||||
$sStateAttCode = MetaModel::GetStateAttributeCode($sClass);
|
||||
$iKey = $this->GetKey();
|
||||
$aDetails = array();
|
||||
$aFieldsMap = array();
|
||||
if (!isset($aExtraParams['action']))
|
||||
@@ -2052,9 +2146,10 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
}
|
||||
|
||||
$sConfirmationMessage = addslashes(Dict::S('UI:NavigateAwayConfirmationMessage'));
|
||||
$sJSToken = json_encode($sOwnershipToken);
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$(window).unload(function() { return OnUnload('$iTransactionId') } );
|
||||
$(window).unload(function() { return OnUnload('$iTransactionId', '$sClass', $iKey, $sJSToken) } );
|
||||
window.onbeforeunload = function() {
|
||||
if (!window.bInSubmit && !window.bInCancel)
|
||||
{
|
||||
@@ -2098,6 +2193,10 @@ EOF
|
||||
$oPage->add("<input type=\"hidden\" name=\"$sName\" value=\"$value\">\n");
|
||||
}
|
||||
}
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
$oPage->add("<input type=\"hidden\" name=\"ownership_token\" value=\"".htmlentities($sOwnershipToken, ENT_QUOTES, 'UTF-8')."\">\n");
|
||||
}
|
||||
$oPage->add($oAppContext->GetForForm());
|
||||
if ($sButtonsPosition != 'top')
|
||||
{
|
||||
@@ -2111,18 +2210,26 @@ EOF
|
||||
$oPage->add_ready_script("$('#form_{$this->m_iFormId} button.cancel').click( function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl')} );");
|
||||
$oPage->add("</form>\n");
|
||||
|
||||
if (isset($aExtraParams['wizard_container']) && $aExtraParams['wizard_container'])
|
||||
{
|
||||
$oPage->add("</div>\n");
|
||||
}
|
||||
|
||||
$iFieldsCount = count($aFieldsMap);
|
||||
$sJsonFieldsMap = json_encode($aFieldsMap);
|
||||
$sState = $this->GetState();
|
||||
|
||||
$sSessionStorageKey = $sClass.'_'.$iKey;
|
||||
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
sessionStorage.removeItem('$sSessionStorageKey');
|
||||
|
||||
// Create the object once at the beginning of the page...
|
||||
var oWizardHelper$sPrefix = new WizardHelper('$sClass', '$sPrefix', '$sState');
|
||||
oWizardHelper$sPrefix.SetFieldsMap($sJsonFieldsMap);
|
||||
oWizardHelper$sPrefix.SetFieldsCount($iFieldsCount);
|
||||
EOF
|
||||
);
|
||||
);
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
oWizardHelper$sPrefix.UpdateWizard();
|
||||
@@ -2130,7 +2237,27 @@ EOF
|
||||
CheckFields('form_{$this->m_iFormId}', false);
|
||||
|
||||
EOF
|
||||
);
|
||||
);
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
$this->GetOwnershipJSHandler($oPage, $sOwnershipToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Probably a new object (or no concurrent lock), let's add a watchdog so that the session is kept open while editing
|
||||
$iInterval = MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay') * 1000 / 2;
|
||||
if ($iInterval > 0)
|
||||
{
|
||||
$iInterval = max(MIN_WATCHDOG_INTERVAL*1000, $iInterval); // Minimum interval for the watchdog is MIN_WATCHDOG_INTERVAL
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
window.setInterval(function() {
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'watchdog'});
|
||||
}, $iInterval);
|
||||
EOF
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function DisplayCreationForm(WebPage $oPage, $sClass, $oObjectToClone = null, $aArgs = array(), $aExtraParams = array())
|
||||
@@ -2209,6 +2336,7 @@ EOF
|
||||
public function DisplayStimulusForm(WebPage $oPage, $sStimulus)
|
||||
{
|
||||
$sClass = get_class($this);
|
||||
$iKey = $this->GetKey();
|
||||
$aTransitions = $this->EnumTransitions();
|
||||
$aStimuli = MetaModel::EnumStimuli($sClass);
|
||||
if (!isset($aTransitions[$sStimulus]))
|
||||
@@ -2216,6 +2344,28 @@ EOF
|
||||
// Invalid stimulus
|
||||
throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus, $this->GetName(), $this->GetStateLabel()));
|
||||
}
|
||||
// Check for concurrent access lock
|
||||
$LockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
|
||||
$sOwnershipToken = null;
|
||||
if ($LockEnabled)
|
||||
{
|
||||
$sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data');
|
||||
$aLockInfo = iTopOwnershipLock::AcquireLock($sClass, $iKey);
|
||||
if ($aLockInfo['success'])
|
||||
{
|
||||
$sOwnershipToken = $aLockInfo['token'];
|
||||
$sOwnershipDate = $aLockInfo['acquired'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$oOwner = $aLockInfo['lock']->GetOwner();
|
||||
// If the object is locked by the current user, it's worth trying again, since
|
||||
// the lock may be released by 'onunload' which is called AFTER loading the current page.
|
||||
//$bTryAgain = $oOwner->GetKey() == UserRights::GetUserId();
|
||||
self::ReloadAndDisplay($oPage, $this, array('operation' => 'stimulus', 'stimulus' => $sStimulus));
|
||||
return;
|
||||
}
|
||||
}
|
||||
$sActionLabel = $aStimuli[$sStimulus]->GetLabel();
|
||||
$sActionDetails = $aStimuli[$sStimulus]->GetDescription();
|
||||
$aTransition = $aTransitions[$sStimulus];
|
||||
@@ -2304,10 +2454,15 @@ EOF
|
||||
$oPage->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">\n");
|
||||
$oPage->add("<input type=\"hidden\" name=\"operation\" value=\"apply_stimulus\">\n");
|
||||
$oPage->add("<input type=\"hidden\" name=\"stimulus\" value=\"$sStimulus\">\n");
|
||||
$oPage->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::GetNewTransactionId()."\">\n");
|
||||
$iTransactionId = utils::GetNewTransactionId();
|
||||
$oPage->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".$iTransactionId."\">\n");
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
$oPage->add("<input type=\"hidden\" name=\"ownership_token\" value=\"".htmlentities($sOwnershipToken, ENT_QUOTES, 'UTF-8')."\">\n");
|
||||
}
|
||||
$oAppContext = new ApplicationContext();
|
||||
$oPage->add($oAppContext->GetForForm());
|
||||
$oPage->add("<button type=\"button\" class=\"action\" onClick=\"BackToDetails('$sClass', ".$this->GetKey().")\"><span>".Dict::S('UI:Button:Cancel')."</span></button> \n");
|
||||
$oPage->add("<button type=\"button\" class=\"action cancel\" onClick=\"BackToDetails('$sClass', ".$this->GetKey().")\"><span>".Dict::S('UI:Button:Cancel')."</span></button> \n");
|
||||
$oPage->add("<button type=\"submit\" class=\"action\"><span>$sActionLabel</span></button>\n");
|
||||
$oPage->add("</form>\n");
|
||||
$oPage->add("</div>\n");
|
||||
@@ -2330,12 +2485,19 @@ EOF
|
||||
oWizardHelper.SetFieldsCount($iFieldsCount);
|
||||
EOF
|
||||
);
|
||||
$sJSToken = json_encode($sOwnershipToken);
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
// Starts the validation when the page is ready
|
||||
CheckFields('apply_stimulus', false);
|
||||
$(window).unload(function() { return OnUnload('$iTransactionId', '$sClass', $iKey, $sJSToken) } );
|
||||
EOF
|
||||
);
|
||||
);
|
||||
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
$this->GetOwnershipJSHandler($oPage, $sOwnershipToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static function ProcessZlist($aList, $aDetails, $sCurrentTab, $sCurrentCol, $sCurrentSet)
|
||||
@@ -3868,4 +4030,45 @@ EOF
|
||||
}
|
||||
return $aRet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the javascript code handle the "watchdog" associated with the concurrent access locking mechanism
|
||||
* @param Webpage $oPage
|
||||
* @param string $sOwnershipToken
|
||||
*/
|
||||
protected function GetOwnershipJSHandler($oPage, $sOwnershipToken)
|
||||
{
|
||||
$iInterval = max(MIN_WATCHDOG_INTERVAL, MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')) * 1000 / 2; // Minimum interval for the watchdog is MIN_WATCHDOG_INTERVAL
|
||||
$sJSClass = json_encode(get_class($this));
|
||||
$iKey = (int) $this->GetKey();
|
||||
$sJSToken = json_encode($sOwnershipToken);
|
||||
$sJSTitle = json_encode(Dict::S('UI:DisconnectedDlgTitle'));
|
||||
$sJSOk = json_encode(Dict::S('UI:Button:Ok'));
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
window.setInterval(function() {
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'extend_lock', obj_class: $sJSClass, obj_key: $iKey, token: $sJSToken }, function(data) {
|
||||
if (!data.status)
|
||||
{
|
||||
if ($('.lock_owned').length == 0)
|
||||
{
|
||||
$('.ui-layout-content').prepend('<div class="header_message message_error lock_owned">'+data.message+'</div>');
|
||||
$('<div>'+data.popup_message+'</div>').dialog({title: $sJSTitle, modal: true, autoOpen: true, buttons:[ {text: $sJSOk, click: function() { $(this).dialog('close'); } }], close: function() { $(this).remove(); }});
|
||||
}
|
||||
$('.wizContainer form button.action:not(.cancel)').attr('disabled', 'disabled');
|
||||
}
|
||||
else if ((data.operation == 'lost') || (data.operation == 'expired'))
|
||||
{
|
||||
if ($('.lock_owned').length == 0)
|
||||
{
|
||||
$('.ui-layout-content').prepend('<div class="header_message message_error lock_owned">'+data.message+'</div>');
|
||||
$('<div>'+data.popup_message+'</div>').dialog({title: $sJSTitle, modal: true, autoOpen: true, buttons:[ {text: $sJSOk, click: function() { $(this).dialog('close'); } }], close: function() { $(this).remove(); }});
|
||||
}
|
||||
$('.wizContainer form button.action:not(.cancel)').attr('disabled', 'disabled');
|
||||
}
|
||||
}, 'json');
|
||||
}, $iInterval);
|
||||
EOF
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,8 +1384,20 @@ class MenuBlock extends DisplayBlock
|
||||
case 1:
|
||||
$oObj = $oSet->Fetch();
|
||||
$id = $oObj->GetKey();
|
||||
$bIsModifyAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
|
||||
$bIsDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, $oSet);
|
||||
$bLocked = false;
|
||||
if (MetaModel::GetConfig()->Get('concurrent_lock_enabled'))
|
||||
{
|
||||
$aLockInfo = iTopOwnershipLock::IsLocked(get_class($oObj), $id);
|
||||
if ($aLockInfo['locked'])
|
||||
{
|
||||
$bLocked = true;
|
||||
//$this->AddMenuSeparator($aActions);
|
||||
//$aActions['concurrent_lock_unlock'] = array ('label' => Dict::S('UI:Menu:ReleaseConcurrentLock'), 'url' => "{$sRootUrl}pages/$sUIPage?operation=kill_lock&class=$sClass&id=$id{$sContext}");
|
||||
}
|
||||
}
|
||||
$bRawModifiedAllowed = (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
|
||||
$bIsModifyAllowed = !$bLocked && $bRawModifiedAllowed;
|
||||
$bIsDeleteAllowed = !$bLocked && UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, $oSet);
|
||||
// Just one object in the set, possible actions are "new / clone / modify and delete"
|
||||
if (!isset($aExtraParams['link_attr']))
|
||||
{
|
||||
@@ -1393,22 +1405,25 @@ class MenuBlock extends DisplayBlock
|
||||
if ($bIsCreationAllowed) { $aActions['UI:Menu:New'] = array ('label' => Dict::S('UI:Menu:New'), 'url' => "{$sRootUrl}pages/$sUIPage?operation=new&class=$sClass{$sContext}{$sDefault}"); }
|
||||
if ($bIsDeleteAllowed) { $aActions['UI:Menu:Delete'] = array ('label' => Dict::S('UI:Menu:Delete'), 'url' => "{$sRootUrl}pages/$sUIPage?operation=delete&class=$sClass&id=$id{$sContext}"); }
|
||||
// Transitions / Stimuli
|
||||
$aTransitions = $oObj->EnumTransitions();
|
||||
if (count($aTransitions))
|
||||
if (!$bLocked)
|
||||
{
|
||||
$this->AddMenuSeparator($aActions);
|
||||
$aStimuli = Metamodel::EnumStimuli(get_class($oObj));
|
||||
foreach($aTransitions as $sStimulusCode => $aTransitionDef)
|
||||
$aTransitions = $oObj->EnumTransitions();
|
||||
if (count($aTransitions))
|
||||
{
|
||||
$iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass, $sStimulusCode, $oSet) : UR_ALLOWED_NO;
|
||||
switch($iActionAllowed)
|
||||
$this->AddMenuSeparator($aActions);
|
||||
$aStimuli = Metamodel::EnumStimuli(get_class($oObj));
|
||||
foreach($aTransitions as $sStimulusCode => $aTransitionDef)
|
||||
{
|
||||
case UR_ALLOWED_YES:
|
||||
$aActions[$sStimulusCode] = array('label' => $aStimuli[$sStimulusCode]->GetLabel(), 'url' => "{$sRootUrl}pages/UI.php?operation=stimulus&stimulus=$sStimulusCode&class=$sClass&id=$id{$sContext}");
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing
|
||||
$iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass, $sStimulusCode, $oSet) : UR_ALLOWED_NO;
|
||||
switch($iActionAllowed)
|
||||
{
|
||||
case UR_ALLOWED_YES:
|
||||
$aActions[$sStimulusCode] = array('label' => $aStimuli[$sStimulusCode]->GetLabel(), 'url' => "{$sRootUrl}pages/UI.php?operation=stimulus&stimulus=$sStimulusCode&class=$sClass&id=$id{$sContext}");
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1429,6 +1444,38 @@ class MenuBlock extends DisplayBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($bLocked && $bRawModifiedAllowed)
|
||||
{
|
||||
// Add a special menu to kill the lock, but only to allowed users who can also modify this object
|
||||
$aAllowedProfiles = MetaModel::GetConfig()->Get('concurrent_lock_override_profiles');
|
||||
$bCanKill = false;
|
||||
|
||||
$oUser = UserRights::GetUserObject();
|
||||
$aUserProfiles = array();
|
||||
if (!is_null($oUser))
|
||||
{
|
||||
$oProfileSet = $oUser->Get('profile_list');
|
||||
while ($oProfile = $oProfileSet->Fetch())
|
||||
{
|
||||
$aUserProfiles[$oProfile->Get('profile')] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach($aAllowedProfiles as $sProfile)
|
||||
{
|
||||
if (array_key_exists($sProfile, $aUserProfiles))
|
||||
{
|
||||
$bCanKill = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bCanKill)
|
||||
{
|
||||
$this->AddMenuSeparator($aActions);
|
||||
$aActions['concurrent_lock_unlock'] = array ('label' => Dict::S('UI:Menu:KillConcurrentLock'), 'url' => "{$sRootUrl}pages/$sUIPage?operation=kill_lock&class=$sClass&id=$id{$sContext}");
|
||||
}
|
||||
}
|
||||
/*
|
||||
$this->AddMenuSeparator($aActions);
|
||||
// Static menus: Email this page & CSV Export
|
||||
|
||||
@@ -444,7 +444,7 @@ EOF
|
||||
window.bInCancel = true;
|
||||
if (id > 0)
|
||||
{
|
||||
window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=details&class='+sClass+'&id='+id);
|
||||
window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=release_lock_and_details&class='+sClass+'&id='+id);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -452,7 +452,6 @@ EOF
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function BackToList(sClass)
|
||||
{
|
||||
window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=search_oql&oql_class='+sClass+'&oql_clause=WHERE id=0');
|
||||
@@ -758,7 +757,7 @@ EOF
|
||||
|
||||
if (utils::CanLogOff())
|
||||
{
|
||||
$oLogOff = new URLPopupMenuItem('UI:LogOffMenu', Dict::S('UI:LogOffMenu'), utils::GetAbsoluteUrlAppRoot().'pages/logoff.php');
|
||||
$oLogOff = new URLPopupMenuItem('UI:LogOffMenu', Dict::S('UI:LogOffMenu'), utils::GetAbsoluteUrlAppRoot().'pages/logoff.php?operation=do_logoff');
|
||||
$aActions[$oLogOff->GetUID()] = $oLogOff->GetMenuItem();
|
||||
}
|
||||
if (UserRights::CanChangePassword())
|
||||
|
||||
@@ -337,7 +337,7 @@ EOF
|
||||
$sMenu = '';
|
||||
if ($this->m_bEnableDisconnectButton)
|
||||
{
|
||||
$this->AddMenuButton('logoff', 'Portal:Disconnect', utils::GetAbsoluteUrlAppRoot().'pages/logoff.php'); // This menu is always present and is the last one
|
||||
$this->AddMenuButton('logoff', 'Portal:Disconnect', utils::GetAbsoluteUrlAppRoot().'pages/logoff.php?operation=do_logoff'); // This menu is always present and is the last one
|
||||
}
|
||||
foreach($this->m_aMenuButtons as $aMenuItem)
|
||||
{
|
||||
@@ -796,6 +796,17 @@ EOF
|
||||
|
||||
$this->p("<h1>".Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())."</h1>\n");
|
||||
}
|
||||
$bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
|
||||
if ($bLockEnabled)
|
||||
{
|
||||
// Release the concurrent lock, if any
|
||||
$sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data');
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
// We're done, let's release the lock
|
||||
iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -777,14 +777,6 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'xlsx_exporter_cleanup_old_files_delay' => array(
|
||||
'type' => 'int',
|
||||
'description' => 'Delay (in seconds) for which to let the exported XLSX files on the server so that the user who initiated the export can download the result',
|
||||
'default' => 86400,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'xlsx_exporter_memory_limit' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Memory limit to use when (interactively) exporting data to Excel',
|
||||
@@ -817,6 +809,30 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'concurrent_lock_enabled' => array(
|
||||
'type' => 'bool',
|
||||
'description' => 'Whether or not to activate the locking mechanism in order to prevent concurrent edition of the same object.',
|
||||
'default' => true,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => true,
|
||||
),
|
||||
'concurrent_lock_expiration_delay' => array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Delay (in seconds) for a concurrent lock to expire',
|
||||
'default' => 120,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'concurrent_lock_override_profiles' => array(
|
||||
'type' => 'array',
|
||||
'description' => 'The list of profiles allowed to "kill" a lock',
|
||||
'default' => array('Administrator'),
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
);
|
||||
|
||||
public function IsProperty($sPropCode)
|
||||
@@ -958,6 +974,7 @@ class Config
|
||||
'core/action.class.inc.php',
|
||||
'core/trigger.class.inc.php',
|
||||
'core/bulkexport.class.inc.php',
|
||||
'core/ownershiplock.class.inc.php',
|
||||
'synchro/synchrodatasource.class.inc.php',
|
||||
'core/backgroundtask.class.inc.php',
|
||||
);
|
||||
|
||||
352
core/ownershiplock.class.inc.php
Normal file
352
core/ownershiplock.class.inc.php
Normal file
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
// Copyright (C) 2015 Combodo SARL
|
||||
//
|
||||
// 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' => 'application',
|
||||
'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"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
|
||||
MetaModel::Init_AddAttribute(new AttributeDateTime("last_seen", array("allowed_values"=>null, "sql"=>'last_seen', "default_value"=>'', "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);
|
||||
}
|
||||
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('Y-m-d H:i:s'));
|
||||
$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('Y-m-d H:i:s'));
|
||||
$this->oToken->Set('user_id', UserRights::GetUserId());
|
||||
$this->oToken->Set('last_seen', date('Y-m-d H:i:s'));
|
||||
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('Y-m-d H:i:s', time() - MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')))));
|
||||
while($oToken = $oSet->Fetch())
|
||||
{
|
||||
$oToken->DBDelete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -991,5 +991,11 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
|
||||
'UI:DisconnectedDlgTitle' => 'Warning!~~',
|
||||
'UI:LoginAgain' => 'Login again~~',
|
||||
'UI:StayOnThePage' => 'Stay on this page~~',
|
||||
'UI:CurrentObjectIsLockedBy_User' => 'The object is locked since it is currently being modified by %1$s.~~',
|
||||
'UI:CurrentObjectIsLockedBy_User_Explanation' => 'The object is currently being modified by %1$s. Your modifications cannot be submitted since they would be overwritten.~~',
|
||||
'UI:CurrentObjectLockExpired' => 'The lock to prevent concurrent modifications of the object has expired.~~',
|
||||
'UI:CurrentObjectLockExpired_Explanation' => 'The lock to prevent concurrent modifications of the object has expired. You can no longer submit your modification since other users are now allowed to modify this object.~~',
|
||||
'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.~~',
|
||||
'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !~~',
|
||||
));
|
||||
?>
|
||||
|
||||
@@ -1257,5 +1257,12 @@ When associated with a trigger, each action is given an "order" number, specifyi
|
||||
'ExcelExport:Statistics' => 'Statistics',
|
||||
'portal:legacy_portal' => 'End-User Portal',
|
||||
'portal:backoffice' => 'iTop Back-Office User Interface',
|
||||
|
||||
'UI:CurrentObjectIsLockedBy_User' => 'The object is locked since it is currently being modified by %1$s.',
|
||||
'UI:CurrentObjectIsLockedBy_User_Explanation' => 'The object is currently being modified by %1$s. Your modifications cannot be submitted since they would be overwritten.',
|
||||
'UI:CurrentObjectLockExpired' => 'The lock to prevent concurrent modifications of the object has expired.',
|
||||
'UI:CurrentObjectLockExpired_Explanation' => 'The lock to prevent concurrent modifications of the object has expired. You can no longer submit your modification since other users are now allowed to modify this object.',
|
||||
'UI:ConcurrentLockKilled' => 'The lock preventing modifications on the current object has been deleted.',
|
||||
'UI:Menu:KillConcurrentLock' => 'Kill the Concurrent Modification Lock !',
|
||||
));
|
||||
?>
|
||||
|
||||
@@ -1099,5 +1099,12 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
|
||||
'ExcelExport:Statistics' => 'Statistiques',
|
||||
'portal:legacy_portal' => 'Portail Utilisateurs',
|
||||
'portal:backoffice' => 'Console iTop',
|
||||
|
||||
'UI:CurrentObjectIsLockedBy_User' => 'L\'objet est verrouillé car il est en train d\'être modifié par %1$s.',
|
||||
'UI:CurrentObjectIsLockedBy_User_Explanation' => 'L\'objet est en train d\'être modifié par %1$s. Vos modifications ne peuvent pas être acceptées car elles risquent d\'être écrasées.',
|
||||
'UI:CurrentObjectLockExpired' => 'Le verrouillage interdisant les modifications concurrentes à expiré.',
|
||||
'UI:CurrentObjectLockExpired_Explanation' => 'Le verrouillage interdisant les modifications concurrentes à expiré. Vos modifications ne peuvent pas être acceptées car d\'autres utilisateurs peuvent modifier cet objet.',
|
||||
'UI:ConcurrentLockKilled' => 'Le verrouillage en édition de l\'objet courant a été supprimé.',
|
||||
'UI:Menu:KillConcurrentLock' => 'Supprimer le verrouillage !',
|
||||
));
|
||||
?>
|
||||
?>
|
||||
@@ -90,14 +90,17 @@ function ActivateStep(iTargetStep)
|
||||
//$('#wizStep'+(iTargetStep)).block({ message: null });
|
||||
}
|
||||
|
||||
function OnUnload(sTransactionId)
|
||||
function OnUnload(sTransactionId, sObjClass, iObjKey, sToken)
|
||||
{
|
||||
if (!window.bInSubmit)
|
||||
{
|
||||
// If it's not a submit, then it's a "cancel" (Pressing the Cancel button, closing the window, using the back button...)
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'on_form_cancel', transaction_id: sTransactionId }, function()
|
||||
{
|
||||
// Do nothing for now...
|
||||
// IMPORTANT: the ajax request MUST BE synchronous to be executed in this context
|
||||
$.ajax({
|
||||
url: GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
|
||||
async: false,
|
||||
method: 'POST',
|
||||
data: {operation: 'on_form_cancel', transaction_id: sTransactionId, obj_class: sObjClass, obj_key: iObjKey, token: sToken }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
49
pages/UI.php
49
pages/UI.php
@@ -357,6 +357,13 @@ try
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'release_lock_and_details':
|
||||
$sClass = utils::ReadParam('class', '');
|
||||
$id = utils::ReadParam('id', '');
|
||||
$oObj = MetaModel::GetObject($sClass, $id);
|
||||
cmdbAbstractObject::ReloadAndDisplay($oP, $oObj, array('operation' => 'details'));
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -540,7 +547,6 @@ EOF
|
||||
|
||||
case 'modify': // Form to modify an object
|
||||
$sClass = utils::ReadParam('class', '', false, 'class');
|
||||
$sClassLabel = MetaModel::GetName($sClass);
|
||||
$id = utils::ReadParam('id', '');
|
||||
if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid !
|
||||
{
|
||||
@@ -562,14 +568,7 @@ EOF
|
||||
throw new SecurityException('User not allowed to modify this object', array('class' => $sClass, 'id' => $id));
|
||||
}
|
||||
// Note: code duplicated to the case 'apply_modify' when a data integrity issue has been found
|
||||
$oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
|
||||
$oP->add("<div class=\"page_header\">\n");
|
||||
$oP->add("<h1>".$oObj->GetIcon()." ".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $oObj->GetName())."</h1>\n");
|
||||
$oP->add("</div>\n");
|
||||
|
||||
$oP->add("<div class=\"wizContainer\">\n");
|
||||
$oObj->DisplayModifyForm($oP);
|
||||
$oP->add("</div>\n");
|
||||
$oObj->DisplayModifyForm($oP, array('wizard_container' => 1)); // wizard_container: Display the blue borders and the title above the form
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -792,20 +791,14 @@ EOF
|
||||
$bDisplayDetails = false;
|
||||
// Found issues, explain and give the user a second chance
|
||||
//
|
||||
$oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
|
||||
$oP->add("<div class=\"page_header\">\n");
|
||||
$oP->add("<h1>".$oObj->GetIcon()." ".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $oObj->GetName())."</h1>\n");
|
||||
$oP->add("</div>\n");
|
||||
$oP->add("<div class=\"wizContainer\">\n");
|
||||
$oObj->DisplayModifyForm($oP);
|
||||
$oP->add("</div>\n");
|
||||
$oObj->DisplayModifyForm($oP, array('wizard_container' => true), $sToken); // wizard_container: display the wizard border and the title
|
||||
$sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten', implode(', ', $aIssues));
|
||||
$oP->add_ready_script("alert('".addslashes($sIssueDesc)."');");
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($bDisplayDetails)
|
||||
{
|
||||
{
|
||||
$oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); //Workaround: reload the object so that the linkedset are displayed properly
|
||||
$sNextAction = utils::ReadPostedParam('next_action', '');
|
||||
if (!empty($sNextAction))
|
||||
@@ -817,6 +810,18 @@ EOF
|
||||
// Nothing more to do
|
||||
ReloadAndDisplay($oP, $oObj, 'update', $sMessage, $sSeverity);
|
||||
}
|
||||
|
||||
$bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
|
||||
if ($bLockEnabled)
|
||||
{
|
||||
// Release the concurrent lock, if any
|
||||
$sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data');
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
// We're done, let's release the lock
|
||||
iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1516,6 +1521,16 @@ EOF
|
||||
$oP->SetCurrentTab('');
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
case 'kill_lock':
|
||||
$sClass = utils::ReadParam('class', '');
|
||||
$id = utils::ReadParam('id', '');
|
||||
iTopOwnershipLock::KillLock($sClass, $id);
|
||||
$oObj = MetaModel::GetObject($sClass, $id);
|
||||
ReloadAndDisplay($oP, $oObj, 'concurrent_lock_killed', Dict::S('UI:ConcurrentLockKilled'), 'info');
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
case 'cancel': // An action was cancelled
|
||||
|
||||
@@ -825,6 +825,13 @@ try
|
||||
{
|
||||
$oExtensionInstance->OnFormCancel($sTempId);
|
||||
}
|
||||
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
|
||||
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
|
||||
$sToken = utils::ReadParam('token', 0, false, 'raw_data');
|
||||
if (($sObjClass != '') && ($iObjKey != 0) && ($sToken != ''))
|
||||
{
|
||||
$bReleaseLock = iTopOwnershipLock::ReleaseLock($sObjClass, $iObjKey, $sToken);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'reload_dashboard':
|
||||
@@ -2143,6 +2150,36 @@ EOF
|
||||
$aResult = array('code' => 'error', 'percentage' => 100, 'message' => Dict::S('Core:BulkExport:ExportCancelledByUser'));
|
||||
$oPage->add(json_encode($aResult));
|
||||
break;
|
||||
|
||||
case 'extend_lock':
|
||||
$sObjClass = utils::ReadParam('obj_class', '', false, 'class');
|
||||
$iObjKey = (int)utils::ReadParam('obj_key', 0, false, 'integer');
|
||||
$sToken = utils::ReadParam('token', 0, false, 'raw_data');
|
||||
$aResult = iTopOwnershipLock::ExtendLock($sObjClass, $iObjKey, $sToken);
|
||||
if (!$aResult['status'])
|
||||
{
|
||||
if ($aResult['operation'] == 'lost')
|
||||
{
|
||||
$sName = $aResult['owner']->GetName();
|
||||
if ($aResult['owner']->Get('contactid') != 0)
|
||||
{
|
||||
$sName .= ' ('.$aResult['owner']->Get('contactid_friendlyname').')';
|
||||
}
|
||||
$aResult['message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User', $sName);
|
||||
$aResult['popup_message'] = Dict::Format('UI:CurrentObjectIsLockedBy_User_Explanation', $sName);
|
||||
}
|
||||
else if ($aResult['operation'] == 'expired')
|
||||
{
|
||||
$aResult['message'] = Dict::S('UI:CurrentObjectLockExpired');
|
||||
$aResult['popup_message'] = Dict::S('UI:CurrentObjectLockExpired_Explanation');
|
||||
}
|
||||
}
|
||||
$oPage->add(json_encode($aResult));
|
||||
break;
|
||||
|
||||
case 'watchdog':
|
||||
$oPage->add('ok'); // Better for debugging...
|
||||
break;
|
||||
|
||||
default:
|
||||
$oPage->p("Invalid query.");
|
||||
|
||||
@@ -25,10 +25,22 @@ require_once(APPROOT.'/application/startup.inc.php');
|
||||
$oAppContext = new ApplicationContext();
|
||||
$currentOrganization = utils::ReadParam('org_id', '');
|
||||
$operation = utils::ReadParam('operation', '');
|
||||
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
|
||||
$bPortal = utils::ReadParam('portal', false);
|
||||
$sUrl = utils::GetAbsoluteUrlAppRoot();
|
||||
|
||||
if ($operation == 'do_logoff')
|
||||
{
|
||||
// Reload the same dummy page to let the "calling" page execute its 'onunload' method before performing the actual logoff.
|
||||
// Note the redirection MUST NOT be made via an HTTP "header" since onunload is called only when the actual content of the DOM
|
||||
// is replaced by some other content. So the "bouncing" page must provide some content (in our case a script making the redirection).
|
||||
$oPage = new ajax_page('');
|
||||
$oPage->add_script("window.location.href='{$sUrl}pages/logoff.php?portal=$bPortal'");
|
||||
$oPage->output();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($bPortal)
|
||||
{
|
||||
$sUrl .= 'portal/';
|
||||
@@ -37,7 +49,6 @@ else
|
||||
{
|
||||
$sUrl .= 'pages/UI.php';
|
||||
}
|
||||
|
||||
if (isset($_SESSION['auth_user']))
|
||||
{
|
||||
$sAuthUser = $_SESSION['auth_user'];
|
||||
|
||||
Reference in New Issue
Block a user