Files
MYSOPHAL/inc/objectlock.class.php
2025-08-07 13:15:31 +01:00

652 lines
21 KiB
PHP

<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2019 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
/**
* @since 9.1
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* Itemlock is dedicated to manage real-time lock of items in GLPI.
*
* Item locks are used to lock items like Ticket, Computer, Reminder, ..., see list in $CFG_GLPI['lock_lockable_objects']
*
* @author Olivier Moron
* @since 9.1
*
**/
class ObjectLock extends CommonDBTM {
private $itemtype = "";
private $itemtypename = "";
private $itemid = 0;
private static $shutdownregistered = false;
/**
* @see CommonGLPI::getTypeName()
*/
static function getTypeName($nb = 0) {
return _n('Object Lock', 'Object Locks', $nb);
}
/**
* Summary of __construct
*
* @param $locitemtype (default ObjectLoc
* @param $locitemid (default 0)
**/
function __construct($locitemtype = 'ObjectLock', $locitemid = 0) {
$this->itemtype = $locitemtype;
$this->itemid = $locitemid;
$this->itemtypename = $locitemtype::getTypeName(1);
}
/**
* Summary of getEntityID
* @return 0
**/
function getEntityID() {
return 0;
}
/**
* Summary of getLockableObjects
*
* @return array of lockable objects 'itemtype' => 'plural itemtype'
**/
static function getLockableObjects() {
global $CFG_GLPI;
$ret = [];
foreach ($CFG_GLPI['lock_lockable_objects'] as $lo) {
$ret[$lo] = $lo::getTypeName(Session::getPluralNumber());
}
asort($ret, SORT_STRING);
return $ret;
}
/**
* Summary of autoLockMode
* Manages autolock mode
*
* @return bool: true if read-only profile lock has been set
**/
private function autoLockMode() {
// if !autolock mode then we are going to view the item with read-only profile
// if isset($_REQUEST['lockwrite']) then will behave like if automode was true but for this object only and for the lifetime of the session
// look for lockwrite request
if (isset($_REQUEST['lockwrite'])) {
$_SESSION['glpilock_autolock_items'][ $this->itemtype ][$this->itemid] = 1;
}
$ret = isset($_SESSION['glpilock_autolock_items'][ $this->itemtype ][ $this->itemid ])
|| $_SESSION['glpilock_autolock_mode'] == 1;
$locked = $this->getLockedObjectInfo();
if (!$ret && !$locked) {
// open the object using read-only profile
self::setReadonlyProfile();
$this->setReadOnlyMessage();
}
return $ret || $locked;
}
/**
* Summary of getScriptToUnlock
*/
private function getScriptToUnlock() {
global $CFG_GLPI;
$ret = Html::scriptBlock("
function unlockIt(obj) {
function callUnlock( ) {
$.ajax({
url: '".$CFG_GLPI['root_doc']."/ajax/unlockobject.php',
cache: false,
data: 'unlock=1&force=1&id=".$this->fields['id']."',
success: function( data, textStatus, jqXHR ) { ".
Html::jsConfirmCallback(__('Reload page?'), __('Item unlocked!'), "function() {
window.location.reload(true);
}") ."
},
error: function() { ".
Html::jsAlertCallback(__('Contact your GLPI admin!'), __('Item NOT unlocked!'))."
}
});
}".
Html::jsConfirmCallback(__('Force unlock this item?'), $this->itemtypename." #".$this->itemid, "callUnlock")."
}");
return $ret;
}
/**
* Summary of getForceUnlockMessage
* @return string '' if no rights to unlock type,
* else html @see getForceUnlockButton
*/
private function getForceUnlockMessage() {
if (isset($_SESSION['glpilocksavedprofile']) && ($_SESSION['glpilocksavedprofile'][strtolower($this->itemtype)] & UNLOCK)) {
echo $this->getScriptToUnlock();
return $this->getForceUnlockButton();
}
return '';
}
private function getForceUnlockButton() {
$msg = "<a class='vsubmit floatright' onclick='javascript:unlockIt(this);'>"
.sprintf(__('Force unlock %1s #%2s'), $this->itemtypename, $this->itemid)."</a>";
return $msg;
}
/**
* Summary of setLockedByYouMessage
* Shows 'Locked by You!' message and proposes to unlock it
**/
private function setLockedByYouMessage() {
echo $this->getScriptToUnlock();
$msg = "<strong class='nowrap'>";
$msg .= __("Locked by you!");
$msg .= $this->getForceUnlockButton();
$msg .= "</strong>";
$this->displayLockMessage($msg);
}
/**
* Summary of setLockedByMessage
* Shows 'Locked by ' message and proposes to request unlock from locker
**/
private function setLockedByMessage() {
global $CFG_GLPI;
// should get locking user info
$user = new User();
$user->getFromDB($this->fields['users_id']);
$useremail = new UserEmail();
$showAskUnlock = $useremail->getFromDBByCrit([
'users_id' => $this->fields['users_id'],
'is_default' => 1
]) && ($CFG_GLPI['notifications_mailing'] == 1);
$userdata = getUserName($this->fields['users_id'], 2);
if ($showAskUnlock) {
$ret = Html::scriptBlock("
function askUnlock() {
". Html::jsConfirmCallback( __('Ask for unlock this item?'), $this->itemtypename." #".$this->itemid, "function() {
$.ajax({
url: '".$CFG_GLPI['root_doc']."/ajax/unlockobject.php',
cache: false,
data: 'requestunlock=1&id=".$this->fields['id']."',
success: function( data, textStatus, jqXHR ) {
".Html::jsAlertCallback($userdata['name'], __('Request sent to') )."
}
});
}"
) ."
}");
echo $ret;
}
$ret = Html::scriptBlock("
$(function(){
var lockStatusTimer;
$('#alertMe').change(function( eventObject ){
if( this.checked ) {
lockStatusTimer = setInterval( function() {
$.ajax({
url: '".$CFG_GLPI['root_doc']."/ajax/unlockobject.php',
cache: false,
data: 'lockstatus=1&id=".$this->fields['id']."',
success: function( data, textStatus, jqXHR ) {
if( data == 0 ) {
clearInterval(lockStatusTimer);".
Html::jsConfirmCallback(__('Reload page?'), __('Item unlocked!'), "function() {
window.location.reload(true);
}") ."
}
}
});
},15000)
} else {
clearInterval(lockStatusTimer);
}
});
});
");
echo $ret;
$msg = "<strong class='nowrap'>";
$msg .= sprintf(__('Locked by %s'), "<a href='" . $user->getLinkURL() . "'>" . $userdata['name'] . "</a>");
$msg .= "&nbsp;" . Html::showToolTip($userdata["comment"], ['link' => $userdata['link'], 'display' => false]);
$msg .= " -> " . Html::convDateTime($this->fields['date_mod']);
$msg .= "</strong>";
if ($showAskUnlock) {
$msg .= "<a class='vsubmit' onclick='javascript:askUnlock();'>".__('Ask for unlock')."</a>";
}
$msg .= "<label for='alertMe'>" . __('Alert me when unlocked') . "</label>";
$msg .= Html::getCheckbox(['id' => 'alertMe']);
$msg .= $this->getForceUnlockMessage(); // will get a button to force unlock if UNLOCK rights are in the user's profile
$msg .= "</span>";
$this->displayLockMessage($msg);
}
/**
* Summary of setReadOnlyMessage
* Shows 'Read-only!' message and propose to request a lock on the item
* This function is used by autoLockMode function
**/
private function setReadOnlyMessage() {
echo Html::scriptBlock("
function requestLock() {
window.location.assign( window.location.href + '&lockwrite=1');
}
");
$msg = "<span class=red style='padding-left:5px;'>";
$msg .= __('Warning: read-only!')."</span>";
$msg .= "<a class='vsubmit' onclick='javascript:requestLock();'>".
__('Request write on ').$this->itemtypename." #".$this->itemid."</a>";
$this->displayLockMessage($msg);
}
/**
* Summary of lockObject
* Tries to lock object and if yes output code to auto unlock it when leaving browser page.
* If lock can't be set (i.e.: someone has already locked it), LockedBy message is shown accordingly,
* and read-only profile is set
* @return bool: true if locked
**/
private function lockObject() {
global $CFG_GLPI;
$ret = false;
if (!($gotIt = $this->getFromDBByCrit(['itemtype' => $this->itemtype,
'items_id' => $this->itemid]))
&& $id = $this->add(['itemtype' => $this->itemtype,
'items_id' => $this->itemid,
'users_id' => Session::getLoginUserID()])) {
// add a script to unlock the Object
echo Html::scriptBlock( "$(function() {
$(window).on('beforeunload', function() {
var fallback_request = function() {
$.ajax({
url: '".$CFG_GLPI['root_doc']."/ajax/unlockobject.php',
async: false,
cache: false,
data: 'unlock=1&id=$id'
});
};
if (typeof window.fetch !== 'undefined') {
fetch('".$CFG_GLPI['root_doc']."/ajax/unlockobject.php?unlock=1&id=$id', {
cache: 'no-cache',
}).catch(function(error) {
//fallback if fetch fails
fallback_request();
});
} else {
//fallback for browsers with no fetch support
fallback_request();
}
});
})" );
$ret = true;
} else { // can't add a lock as another one is already existing
if (!$gotIt) {
$this->getFromDBByCrit([
'itemtype' => $this->itemtype,
'items_id' => $this->itemid
]);
}
// open the object as read-only as it is already locked by someone
self::setReadonlyProfile();
if ($this->fields['users_id'] != Session::getLoginUserID()) {
$this->setLockedByMessage();
} else {
$this->setLockedByYouMessage();
}
// and if autolock was set for this item then unset it
unset($_SESSION['glpilock_autolock_items'][ $this->itemtype ][ $this->itemid ]);
}
return $ret;
}
/**
* Summary of getLockedObjectInfo
*
* @return bool: true if object is locked, and $this is filled with record from DB
**/
private function getLockedObjectInfo() {
global $CFG_GLPI;
$ret = false;
if ($CFG_GLPI["lock_use_lock_item"]
&& ($CFG_GLPI["lock_lockprofile_id"] > 0)
&& Session::getCurrentInterface() == 'central'
&& in_array($this->itemtype, $CFG_GLPI['lock_item_list'])
&& $this->getFromDBByCrit(['itemtype' => $this->itemtype,
'items_id' => $this->itemid])) {
$ret = true;
}
return $ret;
}
/**
* Summary of isLocked
*
* @param $itemtype
* @param $items_id
*
* @return bool|ObjectLock: returns ObjectLock if locked, else false
**/
static function isLocked($itemtype, $items_id) {
$ol = new self($itemtype, $items_id);
return ($ol->getLockedObjectInfo( )?$ol:false);
}
/**
* Summary of setReadOnlyProfile
* Switches current profile with read-only profile
* Registers a shutdown function to be sure that even in case of die() calls,
* the switch back will be done: to ensure correct reset of normal profile
**/
static function setReadOnlyProfile() {
global $CFG_GLPI, $_SESSION;
// to prevent double set ReadOnlyProfile
if (!isset($_SESSION['glpilocksavedprofile'])) {
if (isset($CFG_GLPI['lock_lockprofile'])) {
if (!self::$shutdownregistered) {
// this is a security in case of a die that can prevent correct revert of profile
register_shutdown_function([__CLASS__, 'revertProfile']);
self::$shutdownregistered = true;
}
$_SESSION['glpilocksavedprofile'] = $_SESSION['glpiactiveprofile'];
$_SESSION['glpiactiveprofile'] = $CFG_GLPI['lock_lockprofile'];
// this mask is mandatory to prevent read of information
// that are not permitted to view by active profile
$rights = ProfileRight::getAllPossibleRights();
foreach ($rights as $key => $val) {
if (isset($_SESSION['glpilocksavedprofile'][$key])) {
$_SESSION['glpiactiveprofile'][$key]
= intval($_SESSION['glpilocksavedprofile'][$key])
& (isset($CFG_GLPI['lock_lockprofile'][$key])
?intval($CFG_GLPI['lock_lockprofile'][$key]) :0);
}
}
// don't forget entities
$_SESSION['glpiactiveprofile']['entities'] = $_SESSION['glpilocksavedprofile']['entities'];
}
}
}
/**
* Summary of revertProfile
* Will revert normal user profile
**/
static function revertProfile() {
global $_SESSION;
if (isset($_SESSION['glpilocksavedprofile'])) {
$_SESSION['glpiactiveprofile'] = $_SESSION['glpilocksavedprofile'];
unset($_SESSION['glpilocksavedprofile']);
}
}
/**
* Summary of manageObjectLock
* Is the main function to be called in order to lock an item
*
* @param $itemtype
* @param $options
**/
static function manageObjectLock($itemtype, &$options) {
global $CFG_GLPI;
if (isset($options['id']) && ($options['id'] > 0)) {
$ol = new self($itemtype, $options['id']);
$template = (isset($options['withtemplate'])
&& ($options['withtemplate'] > 0) ? true : false);
if ((Session::getCurrentInterface() == "central")
&& isset($CFG_GLPI["lock_use_lock_item"]) && $CFG_GLPI["lock_use_lock_item"]
&& ($CFG_GLPI["lock_lockprofile_id"] > 0)
&& in_array($itemtype, $CFG_GLPI['lock_item_list'])
&& Session::haveRightsOr($itemtype::$rightname, [UPDATE, DELETE, PURGE, UPDATENOTE])
&& !$template) {
if (!$ol->autoLockMode()
|| !$ol->lockObject($options['id'])) {
$options['locked'] = 1;
}
}
}
}
/**
* Summary of displayLockMessage
* Shows a short message top-left of screen
* This message is permanent, and can't be closed
*
* @param $msg : message to be shown
* @param $title : if $title is '' then title bar it is not shown (default '')
**/
private function displayLockMessage($msg, $title = '') {
echo "<div id='message_after_lock' class='objectlockmessage' style='display:table;' >";
echo $msg;
echo "</div>";
echo Html::scriptBlock("$('#message_after_lock').hide();");
echo Html::scriptBlock("
$(function() {
$('#message_after_lock').insertAfter('.navigationheader');
$('#message_after_lock').show('slide', {direction: 'up'}, 1000);
});
");
}
/**
* @see CommonDBTM::processMassiveActionsForOneItemtype
**/
static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item,
array $ids) {
foreach ($ids as $items_id) {
$itemtype = get_class($item);
$lo = new self($itemtype, $items_id);
if ($lo->getLockedObjectInfo()) {
$lo->deleteFromDB();
Log::history($items_id, $itemtype, [0, '', ''], 0, Log::HISTORY_UNLOCK_ITEM);
$ma->itemDone($itemtype, $items_id, MassiveAction::ACTION_OK);
}
}
}
static public function rawSearchOptionsToAdd($itemtype) {
global $CFG_GLPI;
$tab = [];
if ((Session::getCurrentInterface() == "central")
&& isset($CFG_GLPI["lock_use_lock_item"]) && $CFG_GLPI["lock_use_lock_item"]
&& ($CFG_GLPI["lock_lockprofile_id"] > 0)
&& in_array($itemtype, $CFG_GLPI['lock_item_list'])) {
$tab[] = [
'id' => '205',
'table' => 'glpi_users',
'field' => 'name',
'datatype' => 'dropdown',
'right' => 'all',
'name' => __('Locked by'),
'forcegroupby' => true,
'massiveaction' => false,
'joinparams' => [
'jointype' => '',
'beforejoin' => [
'table' => getTableForItemType('ObjectLock'),
'joinparams' => ['jointype' => "itemtype_item"]
]
]
];
$tab[] = [
'id' => '206',
'table' => getTableForItemType('ObjectLock'),
'field' => 'date_mod',
'datatype' => 'datetime',
'name' => __('Locked date'),
'joinparams' => ['jointype' => 'itemtype_item'],
'massiveaction' => false,
'forcegroupby' => true
];
}
return $tab;
}
/**
* Summary of getRightsToAdd
*
* @param $itemtype
* @param $interface (default 'central')
*
* @return array: empty array if itemtype is not lockable; else returns UNLOCK right
**/
static function getRightsToAdd($itemtype, $interface = 'central') {
global $CFG_GLPI;
$ret = [];
if (($interface == "central")
&& isset($CFG_GLPI["lock_use_lock_item"]) && $CFG_GLPI["lock_use_lock_item"]
&& ($CFG_GLPI["lock_lockprofile_id"] > 0)
&& in_array($itemtype, $CFG_GLPI['lock_lockable_objects'])) {
$ret = [UNLOCK => __('Unlock')];
}
return $ret;
}
/**
* Give cron information
*
* @param $name : task's name
*
* @return array of information
**/
static function cronInfo($name) {
switch ($name) {
case 'unlockobject' :
return ['description' => __('Unlock forgotten locked objects'),
'parameter' => __('Timeout to force unlock (hours)')];
}
return [];
}
/**
* Cron for unlocking forgotten locks
*
* @param $task : crontask object
*
* @return integer
* >0 : done
* <0 : to be run again (not finished)
* 0 : nothing to do
**/
static function cronUnlockObject($task) {
// here we have to delete old locks
$actionCode = 0; // by default
$task->setVolume(0); // start with zero
$lockedItems = getAllDataFromTable(
getTableForItemType(__CLASS__), [
'date_mod' => ['<', date("Y-m-d H:i:s", time() - ($task->fields['param'] * HOUR_TIMESTAMP))]
]
);
foreach ($lockedItems as $row) {
$ol = new self;
if ($ol->delete($row)) {
$actionCode++;
$item = new $row['itemtype']();
$item->getFromDB($row['items_id']);
$task->log($row['itemtype']." #".$row['items_id'].": ".$item->getLink());
$task->addVolume(1);
Log::history($row['items_id'], $row['itemtype'], [0, '', ''], 0,
Log::HISTORY_UNLOCK_ITEM);
} else {
return -1; // error can't delete record, then exit with error
}
}
return $actionCode;
}
}