mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-24 02:58:43 +02:00
Improved change tracking: user login replaced by the full name if available
Added a tab into the CSV import: browse the CSV imports history Finalized the read-only mode (distinguish between users and everybody, admin message displayed on top of the main screen) SVN:trunk[1007]
This commit is contained in:
@@ -789,6 +789,272 @@ class BulkChange
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the history of bulk imports
|
||||
*/
|
||||
static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false)
|
||||
{
|
||||
$sAjaxDivId = "CSVImportHistory";
|
||||
if (!$bFromAjax)
|
||||
{
|
||||
$oPage->add('<div id="'.$sAjaxDivId.'">');
|
||||
}
|
||||
|
||||
$oPage->p(Dict::S('UI:History:BulkImports+'));
|
||||
|
||||
$oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'");
|
||||
|
||||
$iQueryLimit = $bShowAll ? 0 : utils::GetConfig()->GetMaxDisplayLimit() + 1;
|
||||
$oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), $iQueryLimit);
|
||||
|
||||
$oAppContext = new ApplicationContext();
|
||||
|
||||
$bLimitExceeded = false;
|
||||
if ($oBulkChanges->Count() > utils::GetConfig()->GetMaxDisplayLimit())
|
||||
{
|
||||
$bLimitExceeded = true;
|
||||
if (!$bShowAll)
|
||||
{
|
||||
$iMaxObjects = utils::GetConfig()->GetMinDisplayLimit();
|
||||
$oBulkChanges->SetLimit($iMaxObjects);
|
||||
}
|
||||
}
|
||||
$oBulkChanges->Seek(0);
|
||||
|
||||
$aDetails = array();
|
||||
while ($oChange = $oBulkChanges->Fetch())
|
||||
{
|
||||
$sDate = '<a href="?step=10&changeid='.$oChange->GetKey().'&'.$oAppContext->GetForLink().'">'.$oChange->Get('date').'</a>';
|
||||
$sUser = $oChange->GetUserName();
|
||||
if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches))
|
||||
{
|
||||
$sUser = $aMatches[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
$sUser = $oChange->Get('userinfo');
|
||||
}
|
||||
|
||||
$oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id");
|
||||
$oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
|
||||
$iCreated = $oOpSet->Count();
|
||||
|
||||
//while ($oCreated = $oOpSet->Fetch())
|
||||
//{
|
||||
// $oPage->p("Created ".$oCreated->Get('objclass')."::".$oCreated->Get('objkey'));
|
||||
//}
|
||||
|
||||
$oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id");
|
||||
$oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
|
||||
|
||||
$aModified = array();
|
||||
$aAttList = array();
|
||||
while ($oModified = $oOpSet->Fetch())
|
||||
{
|
||||
$sClass = $oModified->Get('objclass');
|
||||
$iKey = $oModified->Get('objkey');
|
||||
$sAttCode = $oModified->Get('attcode');
|
||||
|
||||
$aAttList[$sClass][$sAttCode] = true;
|
||||
$aModified["$sClass::$iKey"] = true;
|
||||
}
|
||||
$iModified = count($aModified);
|
||||
|
||||
// Assumption: there is only one class of objects being loaded
|
||||
// Then the last class found gives us the class for every object
|
||||
|
||||
$aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
|
||||
|
||||
}
|
||||
|
||||
$aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
|
||||
'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')),
|
||||
'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')),
|
||||
'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')),
|
||||
'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')),
|
||||
);
|
||||
|
||||
if ($bLimitExceeded)
|
||||
{
|
||||
if ($bShowAll)
|
||||
{
|
||||
// Collapsible list
|
||||
$oPage->add('<p>'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).' <a class="truncated" onclick="OnTruncatedHistoryToggle(false);">'.Dict::S('UI:CollapseList').'</a></p>');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Truncated list
|
||||
$iMinDisplayLimit = utils::GetConfig()->GetMinDisplayLimit();
|
||||
$sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
|
||||
$sLinkLabel = Dict::S('UI:DisplayAll');
|
||||
$oPage->add('<p>'.$sCollapsedLabel.' <a class="truncated" onclick="OnTruncatedHistoryToggle(true);">'.$sLinkLabel.'</p>');
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$('#$sAjaxDivId table.listResults').addClass('truncated');
|
||||
$('#$sAjaxDivId table.listResults tr:last td').addClass('truncated');
|
||||
EOF
|
||||
);
|
||||
|
||||
|
||||
$sAppContext = $oAppContext->GetForLink();
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
function OnTruncatedHistoryToggle(bShowAll)
|
||||
{
|
||||
$.get('../pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
|
||||
{
|
||||
$('#$sAjaxDivId').html(data);
|
||||
var table = $('#$sAjaxDivId .listResults');
|
||||
table.tableHover(); // hover tables
|
||||
table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
|
||||
}
|
||||
);
|
||||
}
|
||||
EOF
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal display - full list without any decoration
|
||||
}
|
||||
|
||||
$oPage->table($aConfig, $aDetails);
|
||||
|
||||
if (!$bFromAjax)
|
||||
{
|
||||
$oPage->add('</div>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the details of an import
|
||||
*/
|
||||
static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange)
|
||||
{
|
||||
if ($iChange == 0)
|
||||
{
|
||||
throw new Exception("Missing parameter changeid");
|
||||
}
|
||||
$oChange = MetaModel::GetObject('CMDBChange', $iChange, false);
|
||||
if (is_null($oChange))
|
||||
{
|
||||
throw new Exception("Unknown change: $iChange");
|
||||
}
|
||||
$oPage->add("<div><p><h1>".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."</h1></p></div>\n");
|
||||
|
||||
// Assumption : change made one single class of objects
|
||||
$aObjects = array();
|
||||
$aAttributes = array(); // array of attcode => occurences
|
||||
|
||||
$oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id");
|
||||
$oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange));
|
||||
while ($oOperation = $oOpSet->Fetch())
|
||||
{
|
||||
$sClass = $oOperation->Get('objclass');
|
||||
$iKey = $oOperation->Get('objkey');
|
||||
$iObjId = "$sClass::$iKey";
|
||||
if (!isset($aObjects[$iObjId]))
|
||||
{
|
||||
$aObjects[$iObjId] = array();
|
||||
$aObjects[$iObjId]['__class__'] = $sClass;
|
||||
$aObjects[$iObjId]['__id__'] = $iKey;
|
||||
}
|
||||
if (get_class($oOperation) == 'CMDBChangeOpCreate')
|
||||
{
|
||||
$aObjects[$iObjId]['__created__'] = true;
|
||||
}
|
||||
elseif (is_subclass_of($oOperation, 'CMDBChangeOpSetAttribute'))
|
||||
{
|
||||
$sAttCode = $oOperation->Get('attcode');
|
||||
|
||||
if (get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar')
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
if ($oAttDef->IsExternalKey())
|
||||
{
|
||||
$oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
|
||||
$oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue'));
|
||||
$sOldValue = $oOldTarget->GetHyperlink();
|
||||
$sNewValue = $oNewTarget->GetHyperlink();
|
||||
}
|
||||
else
|
||||
{
|
||||
$sOldValue = $oOperation->GetAsHTML('oldvalue');
|
||||
$sNewValue = $oOperation->GetAsHTML('newvalue');
|
||||
}
|
||||
$aObjects[$iObjId][$sAttCode] = $sOldValue.' -> '.$sNewValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
$aObjects[$iObjId][$sAttCode] = 'n/a';
|
||||
}
|
||||
|
||||
if (isset($aAttributes[$sAttCode]))
|
||||
{
|
||||
$aAttributes[$sAttCode]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$aAttributes[$sAttCode] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$aDetails = array();
|
||||
foreach($aObjects as $iUId => $aObjData)
|
||||
{
|
||||
$aRow = array();
|
||||
$oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false);
|
||||
if (is_null($oObject))
|
||||
{
|
||||
$aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)';
|
||||
}
|
||||
else
|
||||
{
|
||||
$aRow['object'] = $oObject->GetHyperlink();
|
||||
}
|
||||
if (isset($aObjData['__created__']))
|
||||
{
|
||||
$aRow['operation'] = Dict::S('Change:ObjectCreated');
|
||||
}
|
||||
else
|
||||
{
|
||||
$aRow['operation'] = Dict::S('Change:ObjectModified');
|
||||
}
|
||||
foreach ($aAttributes as $sAttCode => $iOccurences)
|
||||
{
|
||||
if (isset($aObjData[$sAttCode]))
|
||||
{
|
||||
$aRow[$sAttCode] = $aObjData[$sAttCode];
|
||||
}
|
||||
elseif (!is_null($oObject))
|
||||
{
|
||||
// This is the current vaslue: $oObject->GetAsHtml($sAttCode)
|
||||
// whereas we are displaying the value that was set at the time
|
||||
// the object was created
|
||||
// This requires addtional coding...let's do that later
|
||||
$aRow[$sAttCode] = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
$aRow[$sAttCode] = '';
|
||||
}
|
||||
}
|
||||
$aDetails[] = $aRow;
|
||||
}
|
||||
|
||||
$aConfig = array();
|
||||
$aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass));
|
||||
$aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+'));
|
||||
foreach ($aAttributes as $sAttCode => $iOccurences)
|
||||
{
|
||||
$aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
|
||||
}
|
||||
$oPage->table($aConfig, $aDetails);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,34 @@ class CMDBChange extends DBObject
|
||||
MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
|
||||
MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
|
||||
}
|
||||
|
||||
// Helper to keep track of the author of a given change,
|
||||
// taking into account a variety of cases (contact attached or not, impersonation)
|
||||
static public function GetCurrentUserName()
|
||||
{
|
||||
if (UserRights::IsImpersonated())
|
||||
{
|
||||
$sUserString = Dict::Format('UI:Archive_User_OnBehalfOf_User', UserRights::GetRealUserFriendlyName(), UserRights::GetUserFriendlyName());
|
||||
}
|
||||
else
|
||||
{
|
||||
$sUserString = UserRights::GetUserFriendlyName();
|
||||
}
|
||||
return $sUserString;
|
||||
}
|
||||
|
||||
public function GetUserName()
|
||||
{
|
||||
if (preg_match('/^(.*)\\(CSV\\)$/i', $this->Get('userinfo'), $aMatches))
|
||||
{
|
||||
$sUser = $aMatches[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
$sUser = $this->Get('userinfo');
|
||||
}
|
||||
return $sUser;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -18,6 +18,11 @@ define('ITOP_VERSION', '$ITOP_VERSION$');
|
||||
define('ITOP_REVISION', '$WCREV$');
|
||||
define('ITOP_BUILD_DATE', '$WCNOW$');
|
||||
|
||||
define('ACCESS_USER_WRITE', 1);
|
||||
define('ACCESS_ADMIN_WRITE', 2);
|
||||
define('ACCESS_FULL', ACCESS_USER_WRITE | ACCESS_ADMIN_WRITE);
|
||||
define('ACCESS_READONLY', 0);
|
||||
|
||||
/**
|
||||
* Configuration read/write
|
||||
*
|
||||
@@ -143,14 +148,6 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'database_read_only' => array(
|
||||
'type' => 'bool',
|
||||
'description' => 'Freeze the data for administration purposes - administrators can still do anything... in appearance!',
|
||||
'default' => false,
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
// Levels that trigger a confirmation in the CSV import/synchro wizard
|
||||
'csv_import_min_object_confirmation' => array(
|
||||
'type' => 'integer',
|
||||
@@ -184,6 +181,22 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
),
|
||||
'access_mode' => array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Combination of flags (ACCESS_USER_WRITE | ACCESS_ADMIN_WRITE, or ACCESS_FULL)',
|
||||
'default' => ACCESS_FULL,
|
||||
'value' => ACCESS_FULL,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => true,
|
||||
),
|
||||
'access_message' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Message displayed to the users when there is any access restriction',
|
||||
'default' => 'iTop is temporarily frozen, please wait... (the admin team)',
|
||||
'value' => '',
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => true,
|
||||
),
|
||||
);
|
||||
|
||||
public function IsProperty($sPropCode)
|
||||
|
||||
@@ -2547,28 +2547,26 @@ abstract class MetaModel
|
||||
|
||||
/*
|
||||
* Determines wether the target DB is frozen or not
|
||||
* 1 - consider the DB property 'status'
|
||||
* 2 - check the setting 'database_read_only'
|
||||
*/
|
||||
public static function DBIsReadOnly()
|
||||
{
|
||||
$sStatus = DBProperty::GetProperty('status', null);
|
||||
if (!is_null($sStatus))
|
||||
// Improvement: check the mySQL variable -> Read-only
|
||||
|
||||
if (UserRights::IsAdministrator())
|
||||
{
|
||||
switch (strtolower(trim($sStatus)))
|
||||
{
|
||||
case 'fullaccess':
|
||||
$ret = false;
|
||||
break;
|
||||
default:
|
||||
$ret = true;
|
||||
}
|
||||
return (!self::DBHasAccess(ACCESS_ADMIN_WRITE));
|
||||
}
|
||||
else
|
||||
{
|
||||
$ret = self::$m_oConfig->Get('database_read_only');
|
||||
return (!self::DBHasAccess(ACCESS_USER_WRITE));
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public static function DBHasAccess($iRequested = ACCESS_FULL)
|
||||
{
|
||||
$iMode = self::$m_oConfig->Get('access_mode');
|
||||
if (($iMode & $iRequested) == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function MakeDictEntry($sKey, $sValueFromOldSystem, $sDefaultValue, &$bNotInDico)
|
||||
|
||||
@@ -110,6 +110,35 @@ abstract class User extends cmdbAbstractObject
|
||||
abstract public function CanChangePassword();
|
||||
abstract public function ChangePassword($sOldPassword, $sNewPassword);
|
||||
|
||||
/*
|
||||
* Compute a name in best effort mode
|
||||
*/
|
||||
public function GetFriendlyName()
|
||||
{
|
||||
if (!MetaModel::IsValidAttCode(get_class($this), 'contactid'))
|
||||
{
|
||||
return $this->Get('login');
|
||||
}
|
||||
if ($this->Get('contactid') != 0)
|
||||
{
|
||||
$sFirstName = $this->Get('first_name');
|
||||
$sLastName = $this->Get('last_name');
|
||||
$sEmail = $this->Get('email');
|
||||
if (strlen($sFirstName) > 0)
|
||||
{
|
||||
return "$sFirstName $sLastName";
|
||||
}
|
||||
elseif (strlen($sEmail) > 0)
|
||||
{
|
||||
return "$sLastName <$sEmail>";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $sLastName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Overload the standard behavior
|
||||
*/
|
||||
@@ -490,6 +519,24 @@ class UserRights
|
||||
return $oUser->Get('contactid');
|
||||
}
|
||||
|
||||
// Render the user name in best effort mode
|
||||
public static function GetUserFriendlyName($sName = '')
|
||||
{
|
||||
if (empty($sName))
|
||||
{
|
||||
$oUser = self::$m_oUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
$oUser = FindUser($sName);
|
||||
}
|
||||
if (is_null($oUser))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
return $oUser->GetFriendlyName();
|
||||
}
|
||||
|
||||
public static function IsImpersonated()
|
||||
{
|
||||
if (is_null(self::$m_oRealUser))
|
||||
@@ -517,6 +564,15 @@ class UserRights
|
||||
return self::$m_oRealUser->GetKey();
|
||||
}
|
||||
|
||||
public static function GetRealUserFriendlyName()
|
||||
{
|
||||
if (is_null(self::$m_oRealUser))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
return self::$m_oRealUser->GetFriendlyName();
|
||||
}
|
||||
|
||||
protected static function CheckLogin()
|
||||
{
|
||||
if (!self::IsLoggedIn())
|
||||
@@ -551,8 +607,6 @@ class UserRights
|
||||
// When initializing, we need to let everything pass trough
|
||||
if (!self::CheckLogin()) return true;
|
||||
|
||||
if (self::IsAdministrator($oUser)) return true;
|
||||
|
||||
if (MetaModel::DBIsReadOnly())
|
||||
{
|
||||
if ($iActionCode == UR_ACTION_MODIFY) return false;
|
||||
@@ -561,6 +615,8 @@ class UserRights
|
||||
if ($iActionCode == UR_ACTION_BULK_DELETE) return false;
|
||||
}
|
||||
|
||||
if (self::IsAdministrator($oUser)) return true;
|
||||
|
||||
if (MetaModel::HasCategory($sClass, 'bizmodel'))
|
||||
{
|
||||
// #@# Temporary?????
|
||||
@@ -589,8 +645,6 @@ class UserRights
|
||||
// When initializing, we need to let everything pass trough
|
||||
if (!self::CheckLogin()) return true;
|
||||
|
||||
if (self::IsAdministrator($oUser)) return true;
|
||||
|
||||
if (MetaModel::DBIsReadOnly())
|
||||
{
|
||||
if ($iActionCode == UR_ACTION_MODIFY) return false;
|
||||
@@ -599,6 +653,8 @@ class UserRights
|
||||
if ($iActionCode == UR_ACTION_BULK_DELETE) return false;
|
||||
}
|
||||
|
||||
if (self::IsAdministrator($oUser)) return true;
|
||||
|
||||
if (MetaModel::HasCategory($sClass, 'bizmodel'))
|
||||
{
|
||||
if (is_null($oUser))
|
||||
@@ -619,8 +675,6 @@ class UserRights
|
||||
// When initializing, we need to let everything pass trough
|
||||
if (!self::CheckLogin()) return true;
|
||||
|
||||
if (self::IsAdministrator($oUser)) return true;
|
||||
|
||||
if (MetaModel::DBIsReadOnly())
|
||||
{
|
||||
if ($iActionCode == UR_ACTION_MODIFY) return false;
|
||||
@@ -629,6 +683,8 @@ class UserRights
|
||||
if ($iActionCode == UR_ACTION_BULK_DELETE) return false;
|
||||
}
|
||||
|
||||
if (self::IsAdministrator($oUser)) return true;
|
||||
|
||||
// this module is forbidden for non admins
|
||||
if (MetaModel::HasCategory($sClass, 'addon/userrights')) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user