* @package Combodo\iTop\SessionTracker * @since 3.1.1 3.2.0 N°6901 */ class SessionHandler extends \SessionHandler { /** * @param string $session_id * * @return bool */ #[ReturnTypeWillChange] public function destroy($session_id): bool { IssueLog::Debug("Destroy PHP session", \LogChannels::SESSIONTRACKER, [ 'session_id' => $session_id, ]); $bRes = parent::destroy($session_id); if ($bRes) { $this->unlink_session_file($session_id); } return $bRes; } /** * @param int $max_lifetime */ #[ReturnTypeWillChange] public function gc($max_lifetime): bool { IssueLog::Debug("Run PHP sessions garbage collector", \LogChannels::SESSIONTRACKER, [ 'max_lifetime' => $max_lifetime, ]); $iRes = parent::gc($max_lifetime); $this->gc_with_time_limit($max_lifetime); return $iRes; } /** * @param string $save_path * @param string $session_name */ #[ReturnTypeWillChange] public function open($save_path, $session_name): bool { $bRes = parent::open($save_path, $session_name); $session_id = session_id(); IssueLog::Debug("Open PHP session", \LogChannels::SESSIONTRACKER, [ 'session_id' => $session_id, ]); if ($bRes) { $this->touch_session_file($session_id); } return $bRes; } /** * @param string $session_id * @param string $data * * @return bool */ #[ReturnTypeWillChange] public function write($session_id, $data): bool { $bRes = parent::write($session_id, $data); IssueLog::Debug("Write PHP session", \LogChannels::SESSIONTRACKER, [ 'session_id' => $session_id, 'data' => $data, ]); if ($bRes) { $this->touch_session_file($session_id); } return $bRes; } public static function session_set_save_handler(): void { if (false === utils::GetConfig()->Get('sessions_tracking.enabled')) { //feature disabled return; } $sessionhandler = new SessionHandler(); session_set_save_handler($sessionhandler, true); $iThreshold = utils::GetConfig()->Get('sessions_tracking.gc_threshold'); $iThreshold = min(100, $iThreshold); $iThreshold = max(1, $iThreshold); if ((100 != $iThreshold) && (rand(1, 100) > $iThreshold)) { return; } $iMaxLifetime = ini_get('session.gc_maxlifetime') ?? 60; $iMaxDurationInSeconds = utils::GetConfig()->Get('sessions_tracking.gc_duration_in_seconds'); $sessionhandler->gc_with_time_limit($iMaxLifetime, time() + $iMaxDurationInSeconds); } private function generate_session_content(?string $sPreviousFileVersionContent): ?string { try { $sUserId = UserRights::GetUserId(); if (null === $sUserId) { return null; } // Default value in case of // - First time file creation // - Data corruption (not a json / not an array / no previous creation_time key) $iCreationTime = time(); $aJson = []; if (! is_null($sPreviousFileVersionContent)) { $aJson = json_decode($sPreviousFileVersionContent, true); if (is_array($aJson)) { if (array_key_exists('creation_time', $aJson)) { $iCreationTime = $aJson['creation_time']; } } else { $aJson = []; } } $aData = [ 'login_mode' => Session::Get('login_mode'), 'user_id' => $sUserId, 'creation_time' => $iCreationTime, 'context' => implode('|', ContextTag::GetStack()), ]; $oiSessionHandlerExtension = $this->GetSessionHandlerExtension(); if (! is_null($oiSessionHandlerExtension)) { $oiSessionHandlerExtension->CompleteSessionData($aJson, $aData); } return json_encode( $aData ); } catch (Exception $e) { IssueLog::Error(__METHOD__, null, [ 'error' => $e->getMessage() ]); } return null; } private function GetSessionHandlerExtension(): ?iSessionHandlerExtension { $sSessionHandlerExtensionClass = utils::GetConfig()->Get('sessions_tracking.session_handler_extension'); if (strlen($sSessionHandlerExtensionClass) != 0) { try { if (! class_exists($sSessionHandlerExtensionClass)) { throw new \Exception("Cannot find class"); } /** @var iSessionHandlerExtension $oSessionHandlerExtension */ $oSessionHandlerExtension = new $sSessionHandlerExtensionClass(); if ($oSessionHandlerExtension instanceof iSessionHandlerExtension) { return $oSessionHandlerExtension; } throw new \Exception("Not an instance of iSessionHandlerExtension"); } catch (\Exception $e) { IssueLog::Error(__METHOD__.': cannot instanciate iSessionHandlerExtension', null, ['sessions_tracking.session_handler_extension' => $sSessionHandlerExtensionClass]); } } return null; } private function get_file_path($session_id): string { return utils::GetDataPath()."sessions/session_$session_id"; } private function touch_session_file($session_id): ?string { if (strlen($session_id) == 0) { return null; } clearstatcache(); if (! is_dir(utils::GetDataPath()."sessions")) { @mkdir(utils::GetDataPath()."sessions"); } $sFilePath = $this->get_file_path($session_id); $sPreviousFileVersionContent = null; if (is_file($sFilePath)) { $sPreviousFileVersionContent = file_get_contents($sFilePath); } $sNewContent = $this->generate_session_content($sPreviousFileVersionContent); if (is_null($sNewContent) || ($sPreviousFileVersionContent === $sNewContent)) { @touch($sFilePath); } else { file_put_contents($sFilePath, $sNewContent); } return $sFilePath; } private function unlink_session_file($session_id) { $sFilePath = $this->get_file_path($session_id); if (is_file($sFilePath)) { @unlink($sFilePath); } } /** * @param int $max_lifetime * @param int $iTimeLimit Unix timestamp of time limit not to exceed. -1 for no limit. * * @return int */ public function gc_with_time_limit(int $max_lifetime, int $iTimeLimit = -1): int { $aFiles = $this->list_session_files(); $iProcessed = 0; $now = time(); foreach ($aFiles as $sFile) { if ($now - filemtime($sFile) > $max_lifetime) { @unlink($sFile); $iProcessed++; } if (-1 !== $iTimeLimit && time() > $iTimeLimit) { //cleanup processing has to stop after $iTimeLimit break; } } return $iProcessed; } public function list_session_files(): array { clearstatcache(); if (! is_dir(utils::GetDataPath()."sessions")) { @mkdir(utils::GetDataPath()."sessions"); } return glob(utils::GetDataPath()."sessions/session_*"); } }