#765: prevent two persons to edit the same object at the same time.

SVN:trunk[3617]
This commit is contained in:
Denis Flaven
2015-07-02 15:40:39 +00:00
parent 7f65e9fd5e
commit 9917d6355c
13 changed files with 784 additions and 69 deletions

View File

@@ -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()."&nbsp;".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>&nbsp;&nbsp;&nbsp;&nbsp;\n");
$oPage->add("<button type=\"button\" class=\"action cancel\" onClick=\"BackToDetails('$sClass', ".$this->GetKey().")\"><span>".Dict::S('UI:Button:Cancel')."</span></button>&nbsp;&nbsp;&nbsp;&nbsp;\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
);
}
}

View File

@@ -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

View File

@@ -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())

View File

@@ -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);
}
}
}
/**

View File

@@ -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',
);

View 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();
}
}
}

View File

@@ -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 !~~',
));
?>

View File

@@ -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 !',
));
?>

View File

@@ -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 !',
));
?>
?>

View File

@@ -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 }
});
}
}

View File

@@ -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()."&nbsp;".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()."&nbsp;".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

View File

@@ -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.");

View File

@@ -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'];