. * --------------------------------------------------------------------- */ /** * @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 = "" .sprintf(__('Force unlock %1s #%2s'), $this->itemtypename, $this->itemid).""; return $msg; } /** * Summary of setLockedByYouMessage * Shows 'Locked by You!' message and proposes to unlock it **/ private function setLockedByYouMessage() { echo $this->getScriptToUnlock(); $msg = ""; $msg .= __("Locked by you!"); $msg .= $this->getForceUnlockButton(); $msg .= ""; $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 = ""; $msg .= sprintf(__('Locked by %s'), "" . $userdata['name'] . ""); $msg .= " " . Html::showToolTip($userdata["comment"], ['link' => $userdata['link'], 'display' => false]); $msg .= " -> " . Html::convDateTime($this->fields['date_mod']); $msg .= ""; if ($showAskUnlock) { $msg .= "".__('Ask for unlock').""; } $msg .= ""; $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 .= ""; $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 = ""; $msg .= __('Warning: read-only!').""; $msg .= "". __('Request write on ').$this->itemtypename." #".$this->itemid.""; $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 "
"; 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; } }