ソースを参照

+ session log in/out in real time

Piotr Labudda 7 年 前
コミット
f55c469fc7

+ 2 - 0
SE/se-lib/Route/Users.php

@@ -1026,6 +1026,8 @@ class Route_Users extends RouteBase {
 				if (global.history && global.history.pushState) history.replaceState({}, global.document.title, 'index.php');
 			})(window)
 		");
+		Lib::loadClass('S');
+		S::printLogoutJs();
 		UI::dol();
 	}
 	public function reloadPermsView($data = [], $fixUserPermsExecTime = 0) {

+ 22 - 93
SE/se-lib/S.php

@@ -6,10 +6,10 @@ Lib::loadClass('User');
 
 /**
  * SESSION timeout.
- * 
+ *
  * @use $_SESSION['USER_SESSION_EXPIRE'] = 1800;// TODO: read from DB - ADMIN_USERS in authorize user or default value 30 min
  * @use $_SESSION['USER_SESSION_LAST_ACTIVITY'] = ;
- * 
+ *
  */
 class S {
 
@@ -100,10 +100,6 @@ class S {
 		return false;
 	}
 
-	public static function timeout_get() {// TOOD: legacy
-		return self::timeoutGet();
-	}
-
 	public static function timeoutGet() {
 		if (self::timeoutCheck()) {
 			$ret = $_SESSION['USER_SESSION_EXPIRE'] - (time() - $_SESSION['USER_SESSION_LAST_ACTIVITY']);
@@ -126,96 +122,29 @@ class S {
 			return;
 		}
 
-		?>
-		<script type="text/javascript">
-var _sesTimerEl=null;
-var _sesExpireTimer='<?php echo $_SESSION['USER_SESSION_EXPIRE']; ?>';
-var _sesExpireCheck=false;
-
-function sesExpireTimeoutUpdate(){
-	//console.log('sesExpireTimeoutUpdate: ' + _sesExpireTimer);
-	if(!_sesTimerEl) return;
-	if(_sesExpireTimer>=0){
-		var min = Math.floor(_sesExpireTimer / 60);
-		var sek = _sesExpireTimer % 60;
-		if (sek < 10) { sek = '0' + sek; }
-		_sesTimerEl.innerHTML='' + min + ':' + sek + '';
-		window.setTimeout('sesExpireTimeoutUpdate()', 1000);
-	}else{
-		_sesTimerEl.innerHTML='expired!';
-		window.setTimeout('sesExpireTimeoutUpdate()', 1000);
-	}
-	_sesExpireTimer-=1;
-}
-
-
-var sessionTimeoutStop = false;
-var sessionTimeoutCheckFreq = 5;
-var sessionTimeoutCheck = sessionTimeoutCheckFreq;
-function sesAjaxTimerCheck(){
-	if (sessionTimeoutStop) return;
-	jQuery.get('session-expire.php'
-		, {task: 'getTimer'}
-		, function(data){
-			if (sessionTimeoutStop) return;
-			if (data==='expired' || data === '0' || data === '401: Unauthorized') {
-				_sesExpireTimer=-1;// logout
-
-				var exprModal=document.getElementById('session-timer-modal');
-				if (!exprModal) {
-					var modalHtml = '<div id="session-timer-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="session-timer-modal-label" aria-hidden="true">' +
-						'	<div class="modal-dialog">' +
-						'		<div class="modal-content">' +
-						'			<div class="modal-header">' +
-						'				<h3 id="session-timer-modal-label">Zostałeś wylogowany</h3>' +
-						'			</div>' +
-						'			<div class="modal-footer">' +
-						'				<a href="index.php" class="btn btn-primary">Zaloguj</a>' +
-						'			</div>' +
-						'		</div>' +
-						'	</div>' +
-						'</div>';
-					exprModal = jQuery(modalHtml).appendTo('body');
-				}
-				jQuery('#session-timer-modal').modal({show: true, keyboard: false, backdrop: 'static'});
-
-			} else {
-				//console.log('session time = '+data);
-				_sesExpireTimer=data;
-			}
-		}
-	);
-	if (sessionTimeoutStop) return;
-	if(_sesExpireTimer>=0){
-		window.setTimeout('sesAjaxTimerCheck()', sessionTimeoutCheckFreq * 1000);
-		_sesExpireCheck=false;
-	}else{
-		// last chance
-//		if (_sesExpireCheck) {
-//			alert('Your current Session is over.');
-//		} else {
-//			window.setTimeout('sesAjaxTimerCheck()', sessionTimeoutCheckFreq * 1000);
-//			_sesExpireCheck=true;
-//		}
+		UI::inlineJS( __FILE__ . ".sessionTimer.js", [
+			'BASE_URL' => Request::getPathUri(),
+			'USER_SESSION_EXPIRE' => $_SESSION['USER_SESSION_EXPIRE'],
+			'SESSION_TIMER_URL' => "session-expire.php?task=getTimer",
+			'SESSION_AUTH_STATUS_URL' => "session-expire.php?task=getAuthStatus",
+			'USER_LOGIN' => User::getLogin(),
+		] );
 	}
-}
-
-jQuery(document).ready(function(){
-	_sesTimerEl=document.getElementById('session-timer');
-	if(_sesTimerEl){
-		_sesTimerEl.innerHTML='...';
-		sesExpireTimeoutUpdate();
+	static function printLogoutJs() {
+		UI::inlineJS( __FILE__ . ".expiredSessionTimer.js", [
+			'BASE_URL' => Request::getPathUri(),
+		] );
 	}
-	sesAjaxTimerCheck();
-});
 
-function stopTimer(){
-	//console.log('stopTimer()...');
-	sessionTimeoutStop = true;
-	return true;
-}
-		</script>
-		<?php
+	static function getAuthStatus() {
+		if (!self::timeoutCheck()) return [ 'status' => "expired" ];
+		$currentTime = time();
+		return [
+			'type' => "logged_in",
+			'login' => User::getLogin(),
+			'expire' => $_SESSION['USER_SESSION_EXPIRE'] - ($currentTime - $_SESSION['USER_SESSION_LAST_ACTIVITY']),
+			'time' => $currentTime,
+		];
 	}
 
 }

+ 20 - 0
SE/se-lib/S.php.expiredSessionTimer.js

@@ -0,0 +1,20 @@
+var DBG = DBG || false;
+var DBG1 = 1;
+if (!BASE_URL) throw "Mising BASE_URL";
+
+if ('serviceWorker' in navigator) {
+	navigator.serviceWorker.register(BASE_URL + 'sw.js').then(function() {
+		return navigator.serviceWorker.ready;
+	}).then(function (reg) {
+		DBG && console.log("SW registration succeeded. Scope is "+reg.scope, { reg });
+		sendMsgToServiceWorker('session_expired');
+	}).catch(function (err) {
+		DBG && console.error("SW registration failed with error "+err);
+	});
+}
+
+function sendMsgToServiceWorker(data) {
+	if (navigator.serviceWorker && navigator.serviceWorker.controller) {
+		navigator.serviceWorker.controller.postMessage(data);
+	}
+}

+ 182 - 0
SE/se-lib/S.php.sessionTimer.js

@@ -0,0 +1,182 @@
+var DBG = DBG || false;
+var DBG1 = 1;
+var _sesTimerEl = null;
+var _sesExpireTimer = USER_SESSION_EXPIRE || 1200;
+var _sesExpireCheck = false;
+if (!BASE_URL) throw "Mising BASE_URL";
+if (!SESSION_TIMER_URL) throw "Mising SESSION_TIMER_URL";
+// if (!SESSION_AUTH_STATUS_URL) throw "Mising SESSION_AUTH_STATUS_URL";
+if (!USER_LOGIN) throw "Mising USER_LOGIN";
+
+/**
+	IAuthStatus {
+		type: string "expired" | "logged_in" | "logged_in_again"
+		login: string
+		expire: int
+		time: timestamp (seconds)
+	}
+ */
+
+function sesExpireTimeoutUpdate() {
+	//console.log('sesExpireTimeoutUpdate: ' + _sesExpireTimer);
+	if (!_sesTimerEl) return;
+	if (_sesExpireTimer>=0) {
+		var min = Math.floor(_sesExpireTimer / 60);
+		var sek = _sesExpireTimer % 60;
+		if (sek < 10) { sek = '0' + sek; }
+		_sesTimerEl.innerHTML='' + min + ':' + sek + '';
+		window.setTimeout(sesExpireTimeoutUpdate, 1000);
+	} else {
+		_sesTimerEl.innerHTML = 'expired!';
+		window.setTimeout(sesExpireTimeoutUpdate, 1000);
+	}
+	_sesExpireTimer -= 1;
+}
+
+
+var sessionTimeoutStop = false;
+var sessionTimeoutCheckFreq = 5;
+var sessionTimeoutCheck = sessionTimeoutCheckFreq;
+function sesAjaxTimerCheck() {
+	DBG && console.log("DBG:S:sesAjaxTimerCheck", { sessionTimeoutStop, _sesExpireTimer });
+	if (sessionTimeoutStop) return;
+
+	if (SESSION_AUTH_STATUS_URL) fetchAuthStatus(SESSION_AUTH_STATUS_URL);
+	else fetchSessionTimer(SESSION_TIMER_URL);
+
+	if (_sesExpireTimer >= 0) {
+		window.setTimeout(sesAjaxTimerCheck, sessionTimeoutCheckFreq * 1000);
+		_sesExpireCheck = false;
+	} else {
+		// if (_sesExpireCheck) {
+		// 	alert('Your current Session is over.');
+		// } else {
+		// 	window.setTimeout('sesAjaxTimerCheck', sessionTimeoutCheckFreq * 1000);
+		// 	_sesExpireCheck=true;
+		// }
+	}
+}
+function fetchAuthStatus(url) {
+	window.fetch(url, {
+		credentials: "same-origin",
+	}).then(function (response) {
+		return response.text();
+	}).then(function (responseText) {
+		DBG && console.log("DBG:S:fetchAuthStatus response", { responseText, sessionTimeoutStop, _sesExpireTimer });
+		try {
+			var json = JSON.parse(responseText);
+			return json;
+		} catch (e) {
+			return { type: responseText };
+		}
+	}).then(function (data) {
+		DBG && console.warn("DBG:S:response data", { data });
+		if (sessionTimeoutStop) return;
+		var newSesTimer = parseInt(data.expire);
+		if (!isNaN(newSesTimer) && newSesTimer > 0) {
+			_sesExpireTimer = newSesTimer;
+		} else {
+			_sesExpireTimer = -1; // logout
+			sendMsgToServiceWorker('session_expired')
+			showSesExpireModal()
+		}
+	}).catch(function (err) {
+		DBG && console.error(err)
+	})
+}
+function fetchSessionTimer(url) {
+	window.fetch(url, {
+		credentials: "same-origin",
+	}).then(function (response) {
+		return response.text();
+	}).then(function (data) {
+		DBG && console.warn("DBG:S:response data", { data });
+		if (sessionTimeoutStop) return;
+		var newSesTimer = parseInt(data);
+		if (!isNaN(newSesTimer) && newSesTimer > 0) {
+			_sesExpireTimer = newSesTimer;
+		} else {
+			_sesExpireTimer = -1;// logout
+			sendMsgToServiceWorker('session_expired')
+			showSesExpireModal()
+		}
+	}).catch(function (err) {
+		DBG && console.error(err)
+	})
+}
+function showSesExpireModal() {
+	var exprModal = document.getElementById('session-timer-modal');
+	if (!exprModal) {
+		var modalHtml = '<div id="session-timer-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="session-timer-modal-label" aria-hidden="true">' +
+			'	<div class="modal-dialog">' +
+			'		<div class="modal-content">' +
+			'			<div class="modal-header">' +
+			'				<h3 id="session-timer-modal-label">Zostałeś wylogowany</h3>' +
+			'			</div>' +
+			'			<div class="modal-footer">' +
+			'				<a href="index.php" class="btn btn-primary">Zaloguj</a>' +
+			'			</div>' +
+			'		</div>' +
+			'	</div>' +
+			'</div>';
+		exprModal = jQuery(modalHtml).appendTo('body');
+	}
+	jQuery('#session-timer-modal').modal({ show: true, keyboard: false, backdrop: 'static' });
+	jQuery('#session-timer-modal').show();
+}
+
+
+jQuery(document).ready(function() {
+	_sesTimerEl = document.getElementById('session-timer');
+	if (_sesTimerEl) {
+		_sesTimerEl.innerHTML = '...';
+		sesExpireTimeoutUpdate();
+	}
+	window.setTimeout(sesAjaxTimerCheck, sessionTimeoutCheckFreq * 1000);
+});
+
+function stopTimer() {
+	sessionTimeoutStop = true;
+	return true;
+}
+
+
+function sendMsgToServiceWorker(data) {
+	if (navigator.serviceWorker && navigator.serviceWorker.controller) {
+		navigator.serviceWorker.controller.postMessage(data);
+	}
+}
+
+if ('serviceWorker' in navigator) {
+	navigator.serviceWorker.register(BASE_URL + 'sw.js').then(function() {
+		return navigator.serviceWorker.ready;
+	}).then(function (reg) {
+		DBG && console.log("SW registration succeeded. Scope is "+reg.scope, { reg });
+		navigator.serviceWorker.addEventListener('message', onServiceWorkerMsg);
+		sendMsgToServiceWorker({ type: 'logged_in', login: USER_LOGIN, expire: _sesExpireTimer });
+	}).catch(function (err) {
+		DBG && console.error("SW registration failed with error "+err);
+	});
+}
+
+function onServiceWorkerMsg(event) {
+	var data = event.data || '';
+	var msgType = (data && data.type) ? data.type || '' : data;
+	DBG && console.log('DBG:onServiceWorkerMsg ', { msgType, data: event.data, event })
+	if ('session_expired' === msgType) {
+		_sesExpireTimer = -1;// logout
+		showSesExpireModal()
+	} else if ('logged_in' === msgType) {
+		// jQuery('#session-timer-modal').hide()
+	} else if ('logged_in_again' === msgType) {
+		jQuery('#session-timer-modal').hide()
+		_sesExpireTimer = data.expire;
+		sesExpireTimeoutUpdate();
+		sessionTimeoutStop = false;
+		window.setTimeout(sesAjaxTimerCheck, sessionTimeoutCheckFreq * 1000);
+	}
+}
+
+
+// global.sesExpireTimeoutUpdate = sesExpireTimeoutUpdate;
+// global.sesAjaxTimerCheck = sesAjaxTimerCheck;

+ 5 - 7
SE/session-expire.php

@@ -6,6 +6,7 @@ require_once dirname(__FILE__) . "/se-lib/Lib.php";
 Lib::loadClass('V');
 Lib::loadClass('S');
 Lib::loadClass('User');
+Lib::loadClass('Response');
 
 if (!User::logged()) {
 	die('401: Unauthorized');
@@ -13,13 +14,10 @@ if (!User::logged()) {
 
 S::init();
 
-if ('getTimer' == V::get('task', '', $_REQUEST)) {
-	// just return time diff from last activity
-	$tm = S::timeoutGet();
-	echo $tm;
-	exit;
-} else {
-	S::timeoutUpdate(true);// User is authorized - update logout time
+switch ( V::get('task', '', $_REQUEST) ) {
+	case 'getTimer': Response::sendPlainTextExit( S::timeoutGet() );
+	case 'getAuthStatus': Response::sendJsonExit( S::getAuthStatus() );
+	default: S::timeoutUpdate(true); // User is authorized - update logout time
 }
 
 ?>

+ 171 - 0
SE/sw.js

@@ -0,0 +1,171 @@
+var DBG = DBG || 0;
+var DBG1 = 1;
+// var _clientServerTimeDiff = 0;
+
+
+self.addEventListener('install', onInstall);
+self.addEventListener('activate', onActivate);
+self.addEventListener('fetch', onFetch);
+self.addEventListener('message', onMessage);
+
+
+// self.importScripts('')
+
+/**
+	IAuthStatus {
+		type: string "expired" | "logged_in" | "logged_in_again"
+		login: string
+		expire: int
+		time: timestamp (seconds)
+	}
+ */
+var sessionTimerBuffer = (function () {
+	// var _timeoutId = null;
+	var _lastTimeToExpireSec = null;
+	var _responseTimeMs = null;
+	var _loggedInUser = null; // login
+	var setSessionExpirySec = function (timeToExpire, loggedInUser) {
+		DBG && console.log('DBG:sw:sessionTimerBuffer setSessionExpirySec(', { timeToExpire, loggedInUser }, ')');
+		_lastTimeToExpireSec = parseInt(timeToExpire);
+		_responseTimeMs = (new Date()).getTime();
+		if (loggedInUser) _loggedInUser = loggedInUser;
+	};
+	var getSessionExpirySec = function () {
+		var nowMs = (new Date()).getTime();
+		return (_lastTimeToExpireSec && _responseTimeMs) ? _lastTimeToExpireSec - Math.floor((nowMs - _responseTimeMs) / 1000) : null;
+	};
+	var getDiffLastFetchedMs = function () {
+		var nowMs = (new Date()).getTime();
+		return (_lastTimeToExpireSec && _responseTimeMs) ? (nowMs - _responseTimeMs) : null;
+	};
+	var getLogin = function () {
+		return _loggedInUser;
+	};
+
+	return {
+		setSessionExpirySec: setSessionExpirySec,
+		getSessionExpirySec: getSessionExpirySec,
+		getDiffLastFetchedMs: getDiffLastFetchedMs,
+		getLogin: getLogin,
+	};
+})();
+
+
+function onInstall(event) {
+	DBG && console.log("DBG:sw:install", { event })
+	if (self.skipWaiting) self.skipWaiting();
+}
+function onActivate(event) {
+	DBG && console.log("DBG:sw:activate", { event })
+}
+function onFetch(event) { // proxy all client requests
+	DBG && console.log("DBG:sw:fetch", { url: event.request.url, clientId: event.clientId, event })
+	var url = event.request.url;
+
+	if ('session-expire.php?task=getAuthStatus' === url.substr(url.lastIndexOf('/') + 1)) {
+		var lastFetchedMs = sessionTimerBuffer.getDiffLastFetchedMs();
+		if (!lastFetchedMs || lastFetchedMs > 2000) {
+			event.respondWith(new Promise(function (resolve, reject) {
+				// var clientRequestTime = Math.floor(Date.now() / 1000);
+				// var clientResponseTime = 0;
+				// var serverResponseTime = 0;
+				fetch(event.request).then(function (response) {
+					// clientResponseTime = Math.floor(Date.now() / 1000);
+					return response.text()
+				}).then(function (responseText) {
+					DBG && console.log("DBG:sw:fetch responseText", { url: event.request.url, clientId: event.clientId, responseText })
+					try {
+						var json = JSON.parse(responseText);
+						return json;
+					} catch (e) {
+						throw "session_expired"; // resolve(new Response(responseText, { status: 200 }));
+					}
+				}).then(function (data) { // data: IAuthStatus
+					// serverResponseTime = parseInt(data.time);
+					// _clientServerTimeDiff = serverResponseTime - (clientResponseTime + clientRequestTime) / 2;
+					// DBG && console.log("DBG:sw:getAuthStatus timediff", { clientRequestTime, clientResponseTime, serverResponseTime, _clientServerTimeDiff });
+					var expire = parseInt(data.expire);
+					sessionTimerBuffer.setSessionExpirySec(expire, data.login);
+					resolve(new Response(JSON.stringify(data), { status: 200 }));
+				}).catch(function (err) {
+					resolve(new Response(err, { status: 200 }));
+				})
+			}))
+		} else {
+			var generatedResponseData = {
+				expire: sessionTimerBuffer.getSessionExpirySec(),
+				login: sessionTimerBuffer.getLogin(),
+			}
+			event.respondWith(Promise.resolve(new Response(JSON.stringify(generatedResponseData), { status: 202 }))); // 202 - Accepted
+		}
+	}
+
+	if ('session-expire.php?task=getTimer' === url.substr(url.lastIndexOf('/') + 1)) {
+		var lastFetchedMs = sessionTimerBuffer.getDiffLastFetchedMs();
+		DBG && console.log("DBG:sw:fetch session-expire getTimer", { lastFetchedMs: lastFetchedMs, sesTimerSec: sessionTimerBuffer.getSessionExpirySec(), url: event.request.url, clientId: event.clientId, event });
+		if (!lastFetchedMs || lastFetchedMs > 2000) {
+			event.respondWith(new Promise(function (resolve, reject) {
+				fetch(event.request).then(function (response) {
+					return response.text()
+				}).then(function (data) {
+					sessionTimerBuffer.setSessionExpirySec(data);
+					resolve(new Response("" + data, { status: 200 }));
+				}).catch(function (err) {
+					reject(new Response("" + err, { status: 200 }));
+				})
+			}))
+		} else {
+			event.respondWith(Promise.resolve(new Response("" + sessionTimerBuffer.getSessionExpirySec(), { status: 202 }))); // 202 - Accepted
+		}
+	}
+}
+function onMessage(event) {
+	DBG && console.log("DBG:sw:Message recieved in service worker:", { data: event.data, clientId: event.source.id, event });
+	var data = event.data;
+	var clientId = event.source.id;
+	var msgType = (data && data.type) ? data.type || '' : '';
+	DBG && console.log("DBG:sw:Message type:", { msgType });
+	if ('logged_in' === msgType) {
+		var lastLogin = sessionTimerBuffer.getLogin();
+		var login = data.login || '';
+		var isLoggedInAgain = (lastLogin && login && lastLogin === login);
+		DBG && console.log("DBG:sw:Message logged_in:", { isLoggedInAgain, lastLogin, login, data: event.data, clientId: event.source.id, event });
+		sessionTimerBuffer.setSessionExpirySec(data.expire, data.login);
+		sendStateToOtherClients(
+			clientId,
+			isLoggedInAgain ? Object.assign(data, { type: 'logged_in_again' }) : data
+		);
+	} else {
+		sendStateToAllClients(clientId, data);
+	}
+}
+
+
+sendStateToAllClients = function(clientId, data) {
+	DBG && console.log("DBG:sw:clients (global)", { clients })
+	clients.matchAll().then(function (clients) {
+		clients.forEach(function (client) {
+			client.postMessage(data);
+		})
+	})
+}
+sendStateToOtherClients = function(clientId, data) {
+	DBG && console.log("DBG:sw:clients (global)", { clients })
+	clients.matchAll().then(function (clients) {
+		clients.forEach(function (client) {
+			if (client.id !== clientId) {
+				client.postMessage(data);
+			}
+		})
+	})
+}
+
+// versionCheck();
+//
+// async function versionCheck() {
+// 	const response = await fetch(SW_VERSION_URL);
+// 	const version = await response.text();
+// 	if (version !== VERSION) {
+// 		self.registration.update();
+// 	}
+// }