From a5d164feed380e30a89721bfc0a79c3f13c2cc34 Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Fri, 15 Dec 2023 13:14:58 +0100 Subject: [PATCH 1/5] LDEV-4670 test case for database session expiry (WIP) https://luceeserver.atlassian.net/browse/LDEV-4670 --- test/tickets/LDEV4670.cfc | 118 ++++++++++++++++++++++++++ test/tickets/LDEV4670/Application.cfc | 32 +++++++ test/tickets/LDEV4670/ldev4670.cfm | 31 +++++++ 3 files changed, 181 insertions(+) create mode 100644 test/tickets/LDEV4670.cfc create mode 100644 test/tickets/LDEV4670/Application.cfc create mode 100644 test/tickets/LDEV4670/ldev4670.cfm diff --git a/test/tickets/LDEV4670.cfc b/test/tickets/LDEV4670.cfc new file mode 100644 index 0000000000..3b662c8839 --- /dev/null +++ b/test/tickets/LDEV4670.cfc @@ -0,0 +1,118 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { + + function isMySqlNotSupported() { + var mySql = server.getDatasource("mysql"); + return isEmpty( mysql ); + } + + function run( testResults , testBox ) { + describe( title="Test suite for LDEV-4670", body=function() { + + it( title='Checking datasource session expiry, timezone UTC',skip=isMySqlNotSupported(),body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "UTC" ); + dumpResult( remainingSessions, "UTC" ); + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + it( title='Checking datasource session expiry, timezone PDT -7',skip=isMySqlNotSupported(),body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "PDT" ); + dumpResult( remainingSessions, "PDT" ); + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + it( title='Checking datasource session expiry, timezone AEST +10',skip=isMySqlNotSupported(),body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "AEST" ); + dumpResult( remainingSessions, "AEST" ); + + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + }); + } + + private query function testSessionTimezone( required string timezone ){ + var result = testSession("first cleanout sessions table",{ + action: "purge", + sessionEnable: false, + timezone: arguments.timezone + }); + + admin + action="purgeExpiredSessions" + type="server" + password="#request.SERVERADMINPASSWORD#"; + + result = testSession("dump sessions table",{ + action: "dump", + sessionEnable: false, + timezone: arguments.timezone + }); + + expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 0 ); + + result = testSession("create a session", { + timezone: arguments.timezone + }); + + result = testSession("dump sessions table, should have 1 session",{ + action: "dump", + sessionEnable: false, + timezone: arguments.timezone + }); + + expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 1 ); + + sleep( 1001 ); // session expiry is 1s + + admin + action="purgeExpiredSessions" + type="server" + password="#request.SERVERADMINPASSWORD#"; + + result = testSession("dump sessions table, should have 0 sessions",{ + action: "dump", + sessionEnable: false, + timezone: arguments.timezone + }); + + var remainingSessionCount = deserializeJson( result.filecontent, false ); + + return remainingSessionCount; + } + + private any function testSession ( required string name, required struct args ){ + var uri = createURI("LDEV4670"); + var result = _InternalRequest( + template:"#uri#/ldev4670.cfm", + form: arguments.args + ); + //systemOutput("---- #arguments.name# / #args.timezone# ----- ", true); + //systemOutput(result, true); + //systemOutput(deserializeJson(result.filecontent), true); + //systemOutput(deserializeJson(result.filecontent, false), true); + return result; + } + + private string function epochToDate( epoch ){ + return createObject("java", "java.text.SimpleDateFormat").init("MM/dd/yyyy HH:mm:ss LLL") + .format( createObject("java", "java.util.Date").init( arguments.epoch * 1 ) ); + } + + private void function dumpResult( remainingSessions, timezone ){ + systemOutput("", true); + systemOutput( "#timezone# has #remainingSessions.recordcount# sessions, expires: " + & epochToDate( remainingSessions.expires ) + & ", now: #dateTimeFormat(now(), " yyyy-mm-dd'T'HH:nn:ss:LLL", "UTC" )#", + true ); + var epoch = dateTimeFormat( now(), 'epochms' ); + if ( len( remainingSessions.expires ) ) + systemOutput( "expires: " & remainingSessions.expires & ", now: #epoch#, diff: #epoch-remainingSessions.expires#", true ); + + } + + // Private functions + private string function createURI(string calledName){ + var baseURI = "/test/#listLast(getDirectoryFromPath(getCurrenttemplatepath()),"\/")#/"; + return baseURI & "" & calledName; + } +} \ No newline at end of file diff --git a/test/tickets/LDEV4670/Application.cfc b/test/tickets/LDEV4670/Application.cfc new file mode 100644 index 0000000000..65ce3cae89 --- /dev/null +++ b/test/tickets/LDEV4670/Application.cfc @@ -0,0 +1,32 @@ +component { + param name="form.timezone"; + param name="form.sessionEnable" default="true"; + + this.name = "ldev-4670-mysql-#form.timezone#"; + + + this.sessionManagement = form.sessionEnable; + this.sessionTimeout = createTimeSpan(0,0,0,1); + + mySQL = mySqlCredentials(); + mySQL.storage = true; + datasource = "my-ldev-4670" + this.datasources[datasource] = mySQL; + this.dataSource = datasource; + + this.sessionStorage = datasource; + + this.timezone = form.timezone; // Pacific Time Zone, UTC -7 + + public function onRequestStart() { + // systemOutput(getTimeZone(), true); + setting requesttimeout=10 showdebugOutput=false; + if ( form.sessionEnable ) { + session.running = true; + } + } + + private struct function mySqlCredentials() { + return server.getDatasource("mysql"); + } +} \ No newline at end of file diff --git a/test/tickets/LDEV4670/ldev4670.cfm b/test/tickets/LDEV4670/ldev4670.cfm new file mode 100644 index 0000000000..3979e2406f --- /dev/null +++ b/test/tickets/LDEV4670/ldev4670.cfm @@ -0,0 +1,31 @@ + + + param name="form.action" default=""; + + //systemOutput("LDEV-4670 #form.action#", true); + + settings = getApplicationSettings(); + params = { + name: settings.name + }; + + //systemOutput( params, true ); + + switch (form.action){ + case "purge": + query name="purge_sessions" result="deleted" params=params { + echo("DELETE FROM cf_session_data WHERE name = :name"); + } + echo( deleted.toJson() ); + break; + case "dump": + query name="q_sessions" params=params { + echo("SELECT expires, name, cfid FROM cf_session_data WHERE name = :name"); + } + echo( q_sessions.toJson() ); + break; + default: + echo( session.toJson() ); + } + + \ No newline at end of file From 6c689e91ed919e315fbf3867d392ddf59e6ee87a Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Fri, 15 Dec 2023 13:38:44 +0100 Subject: [PATCH 2/5] fix debug date masks --- test/tickets/LDEV4670.cfc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tickets/LDEV4670.cfc b/test/tickets/LDEV4670.cfc index 3b662c8839..6749d0cc3c 100644 --- a/test/tickets/LDEV4670.cfc +++ b/test/tickets/LDEV4670.cfc @@ -62,7 +62,7 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 1 ); - sleep( 1001 ); // session expiry is 1s + sleep( 2001 ); // session expiry is 1s admin action="purgeExpiredSessions" @@ -94,7 +94,7 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { } private string function epochToDate( epoch ){ - return createObject("java", "java.text.SimpleDateFormat").init("MM/dd/yyyy HH:mm:ss LLL") + return createObject("java", "java.text.SimpleDateFormat").init("yyyy-MM-dd HH:mm:ss:SSS") .format( createObject("java", "java.util.Date").init( arguments.epoch * 1 ) ); } @@ -102,7 +102,7 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { systemOutput("", true); systemOutput( "#timezone# has #remainingSessions.recordcount# sessions, expires: " & epochToDate( remainingSessions.expires ) - & ", now: #dateTimeFormat(now(), " yyyy-mm-dd'T'HH:nn:ss:LLL", "UTC" )#", + & ", now: #dateTimeFormat(now(), " yyyy-mm-dd HH:nn:ss:LLL", "UTC" )#", true ); var epoch = dateTimeFormat( now(), 'epochms' ); if ( len( remainingSessions.expires ) ) From b27dbc9055de1df97ad9da20b154cf5e4efccfd1 Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Fri, 15 Dec 2023 13:39:10 +0100 Subject: [PATCH 3/5] use same function for expiry check as creation --- .../main/java/lucee/runtime/type/scope/storage/db/Ansi92.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/lucee/runtime/type/scope/storage/db/Ansi92.java b/core/src/main/java/lucee/runtime/type/scope/storage/db/Ansi92.java index e1db885fd8..e1af0db76c 100644 --- a/core/src/main/java/lucee/runtime/type/scope/storage/db/Ansi92.java +++ b/core/src/main/java/lucee/runtime/type/scope/storage/db/Ansi92.java @@ -203,7 +203,7 @@ public void clean(Config config, DatasourceConnection dc, int type, StorageScope String strType = VariableInterpreter.scopeInt2String(type); // select SQL sqlSelect = new SQLImpl("SELECT cfid, name FROM " + PREFIX + "_" + strType + "_data WHERE expires <= ?", - new SQLItem[] { new SQLItemImpl(System.currentTimeMillis(), Types.VARCHAR) }); + new SQLItem[] { new SQLItemImpl(now(config), Types.VARCHAR) }); Query query; try { query = new QueryImpl(ThreadLocalPageContext.get(), dc, sqlSelect, -1, -1, null, strType + "_storage"); From 7f30160cf124d8b2a5da79a3b93396821d2bbccf Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Fri, 12 Jan 2024 14:08:17 +0100 Subject: [PATCH 4/5] LDEV-4670 improve tests, add memory test --- test/tickets/LDEV4670.cfc | 159 ++++++++++++++++++++------ test/tickets/LDEV4670/Application.cfc | 41 +++++-- test/tickets/LDEV4670/ldev4670.cfm | 26 ++++- 3 files changed, 175 insertions(+), 51 deletions(-) diff --git a/test/tickets/LDEV4670.cfc b/test/tickets/LDEV4670.cfc index 6749d0cc3c..912bda86e3 100644 --- a/test/tickets/LDEV4670.cfc +++ b/test/tickets/LDEV4670.cfc @@ -1,4 +1,4 @@ -component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { +component extends="org.lucee.cfml.test.LuceeTestCase" labels="session" { function isMySqlNotSupported() { var mySql = server.getDatasource("mysql"); @@ -7,21 +7,46 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { function run( testResults , testBox ) { describe( title="Test suite for LDEV-4670", body=function() { - it( title='Checking datasource session expiry, timezone UTC',skip=isMySqlNotSupported(),body=function( currentSpec ) { - var remainingSessions = testSessionTimezone( "UTC" ); + var remainingSessions = testSessionTimezone( "UTC", "datasource" ); dumpResult( remainingSessions, "UTC" ); expect( remainingSessions.recordcount ).toBe( 0 ); }); it( title='Checking datasource session expiry, timezone PDT -7',skip=isMySqlNotSupported(),body=function( currentSpec ) { - var remainingSessions = testSessionTimezone( "PDT" ); + var remainingSessions = testSessionTimezone( "PDT", "datasource" ); dumpResult( remainingSessions, "PDT" ); expect( remainingSessions.recordcount ).toBe( 0 ); }); it( title='Checking datasource session expiry, timezone AEST +10',skip=isMySqlNotSupported(),body=function( currentSpec ) { - var remainingSessions = testSessionTimezone( "AEST" ); + var remainingSessions = testSessionTimezone( "AEST", "datasource" ); + dumpResult( remainingSessions, "AEST" ); + + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + it( title='Checking memory session expiry, timezone UTC', body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "UTC", "memory" ); + dumpResult( remainingSessions, "UTC" ); + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + it( title='Checking memory session expiry, timezone PDT -7', body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "PDT", "memory" ); + dumpResult( remainingSessions, "PDT" ); + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + it( title='Checking memory session expiry, timezone Europe/Berlin +1', body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "Europe/Berlin", "memory" ); + dumpResult( remainingSessions, "Europe/Berlin" ); + + expect( remainingSessions.recordcount ).toBe( 0 ); + }); + + it( title='Checking memory session expiry, timezone AEST +10', body=function( currentSpec ) { + var remainingSessions = testSessionTimezone( "AEST", "memory" ); dumpResult( remainingSessions, "AEST" ); expect( remainingSessions.recordcount ).toBe( 0 ); @@ -30,65 +55,128 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { }); } - private query function testSessionTimezone( required string timezone ){ - var result = testSession("first cleanout sessions table",{ - action: "purge", - sessionEnable: false, - timezone: arguments.timezone - }); + private query function testSessionTimezone( required string timezone, required string sessionStorageType ){ + systemOutput( " ", true ); + systemOutput(">>>>> testSessionTimezone #arguments.timezone# / #arguments.sessionStorageType# ----- ", true); + if ( arguments.sessionStorageType == "datasource" ) { + var result = testSession("first cleanout sessions table",{ + action: "purge", + sessionEnable: false, + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType + }); + } + systemoutput("purgeExpiredSessions", true); admin action="purgeExpiredSessions" type="server" password="#request.SERVERADMINPASSWORD#"; - result = testSession("dump sessions table",{ - action: "dump", - sessionEnable: false, - timezone: arguments.timezone - }); - - expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 0 ); + if ( arguments.sessionStorageType == "datasource" ) { + result = testSession("dump sessions table",{ + action: "dumpDatabaseSessions", + sessionEnable: false, + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType + }); + expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 0 ); + } result = testSession("create a session", { - timezone: arguments.timezone + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType, + action: "createSession" }); - result = testSession("dump sessions table, should have 1 session",{ - action: "dump", - sessionEnable: false, - timezone: arguments.timezone - }); - - expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 1 ); + var currentSession = duplicate( result.session ); + if ( arguments.sessionStorageType == "datasource" ) { + result = testSession("dump sessions table, should have 1 session", { + action: "dumpDatabaseSessions", + sessionEnable: false, + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType + }); + expect( deserializeJson( result.filecontent, false ).recordcount ).toBe( 1 ); + } + + result = testSession("check that the current session is still active", { + action: "checkSession", + sessionEnable: true, + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType + }, + currentSession // pass in previous session + ); + expect( result.session.cfid ).toBe( currentSession.cfid ); + expect( result.session.requestCount ).toBe( 2 ); - sleep( 2001 ); // session expiry is 1s + // now let the session expire, session expiry is 1s + sleep( 5*60*1001 ); + systemoutput("purgeExpiredSessions", true); admin action="purgeExpiredSessions" type="server" password="#request.SERVERADMINPASSWORD#"; - result = testSession("dump sessions table, should have 0 sessions",{ - action: "dump", + result = testSession("dump sessions from memory, should have 0 sessions", { + action: "dumpMemorySessions", sessionEnable: false, - timezone: arguments.timezone + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType }); var remainingSessionCount = deserializeJson( result.filecontent, false ); + expect ( remainingSessionCount.recordcount ).toBe( 0 ); + + result = testSession("check that the current session was recreated, requestcount 1", { + action: "checkSession", + sessionEnable: true, + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType + }, + currentSession // pass in previous session + ); + + // expect( result.session.cfid ).notToBe( currentSession.cfid ); // doesn't work session gets recreated with the same cfid? + expect( result.session.requestCount ).toBe( 1 ); + + if ( arguments.sessionStorageType == "datasource" ) { + result = testSession("dump sessions table, should have 0 sessions", { + action: "dumpDatabaseSessions", + sessionEnable: false, + timezone: arguments.timezone, + sessionStorageType: arguments.sessionStorageType + }); + remainingSessionCount = deserializeJson( result.filecontent, false ); + } + return remainingSessionCount; } - private any function testSession ( required string name, required struct args ){ + private any function testSession ( required string name, required struct args, struct session={} ){ + systemOutput( "---- #arguments.name# ----- ", true ); var uri = createURI("LDEV4670"); + var cookies = {}; + if ( structCount( arguments.session ) ){ + //systemOutput( arguments.session, true ); + cookies = { + cfid: arguments.session.cfid, + cftoken: arguments.session.cftoken + }; + //systemOutput( "cookies: #cookies.toJson()#", true ); + } + var result = _InternalRequest( template:"#uri#/ldev4670.cfm", - form: arguments.args + form: arguments.args, + cookies: cookies ); - //systemOutput("---- #arguments.name# / #args.timezone# ----- ", true); + //systemOutput(result, true); - //systemOutput(deserializeJson(result.filecontent), true); + systemOutput( deserializeJson( result.filecontent ), true ); //systemOutput(deserializeJson(result.filecontent, false), true); return result; } @@ -100,6 +188,8 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { private void function dumpResult( remainingSessions, timezone ){ systemOutput("", true); + if ( remainingSessions.recordcount eq 0 ) + return; systemOutput( "#timezone# has #remainingSessions.recordcount# sessions, expires: " & epochToDate( remainingSessions.expires ) & ", now: #dateTimeFormat(now(), " yyyy-mm-dd HH:nn:ss:LLL", "UTC" )#", @@ -107,7 +197,6 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="mysql" { var epoch = dateTimeFormat( now(), 'epochms' ); if ( len( remainingSessions.expires ) ) systemOutput( "expires: " & remainingSessions.expires & ", now: #epoch#, diff: #epoch-remainingSessions.expires#", true ); - } // Private functions diff --git a/test/tickets/LDEV4670/Application.cfc b/test/tickets/LDEV4670/Application.cfc index 65ce3cae89..0a2f489e75 100644 --- a/test/tickets/LDEV4670/Application.cfc +++ b/test/tickets/LDEV4670/Application.cfc @@ -1,31 +1,48 @@ component { param name="form.timezone"; param name="form.sessionEnable" default="true"; + param name="form.sessionStorageType"; this.name = "ldev-4670-mysql-#form.timezone#"; - - - this.sessionManagement = form.sessionEnable; - this.sessionTimeout = createTimeSpan(0,0,0,1); - mySQL = mySqlCredentials(); - mySQL.storage = true; - datasource = "my-ldev-4670" - this.datasources[datasource] = mySQL; - this.dataSource = datasource; - - this.sessionStorage = datasource; + this.sessionManagement = form.sessionEnable; + this.sessionTimeout = createTimeSpan(0,0,0,1); + switch ( form.sessionStorageType ){ + case "memory": + this.sessionStorage = "memory"; + break; + case "datasource": + mySQL = mySqlCredentials(); + mySQL.storage = true; + datasource = "my-ldev-4670" + this.datasources[datasource] = mySQL; + this.dataSource = datasource; + this.sessionStorage = datasource; + break; + default: + throw "unsupported sessionStorageType [#form.sessionStorageType#]"; + } + this.timezone = form.timezone; // Pacific Time Zone, UTC -7 public function onRequestStart() { // systemOutput(getTimeZone(), true); setting requesttimeout=10 showdebugOutput=false; if ( form.sessionEnable ) { - session.running = true; + session["running"] = true; + session["sessionStorageType"] = form.sessionStorageType; + param name="session.requestCount" default="0"; + session["requestCount"]++; + session["timezone"] = form.timezone; } } + function onSessionEnd( SessionScope, ApplicationScope ) { + systemOutput("!!!!!!!!!!!!!!!!!!!!!!!! #now()# session ended #cgi.SCRIPT_NAME# #sessionScope.sessionid#", true); + server.LDEV4670_endedSessions[ arguments.sessionScope.sessionid ] = now(); + } + private struct function mySqlCredentials() { return server.getDatasource("mysql"); } diff --git a/test/tickets/LDEV4670/ldev4670.cfm b/test/tickets/LDEV4670/ldev4670.cfm index 3979e2406f..5a4dee87c6 100644 --- a/test/tickets/LDEV4670/ldev4670.cfm +++ b/test/tickets/LDEV4670/ldev4670.cfm @@ -1,6 +1,6 @@ - param name="form.action" default=""; + param name="form.action"; //systemOutput("LDEV-4670 #form.action#", true); @@ -11,21 +11,39 @@ //systemOutput( params, true ); - switch (form.action){ + switch ( form.action ){ case "purge": query name="purge_sessions" result="deleted" params=params { echo("DELETE FROM cf_session_data WHERE name = :name"); } echo( deleted.toJson() ); break; - case "dump": + case "dumpDatabaseSessions": query name="q_sessions" params=params { echo("SELECT expires, name, cfid FROM cf_session_data WHERE name = :name"); } echo( q_sessions.toJson() ); break; - default: + case "dumpMemorySessions": + sess = getPageContext().getCFMLFactory().getScopeContext().getAllCFSessionScopes(); + //systemOutput("getAllCFSessionScopes()", true); + //systemOutput(sess, true); + if ( structKeyExists( sess, settings.name ) ) { + st = sess[ settings.name ]; + keys = structKeyArray( st ); + echo( queryNew( structKeyList( st[ keys[ 1 ] ] ) ,"" , st ).toJson() ); + } else { + echo( queryNew( "cfid,expires" ).toJson() ); + } + break; + case "createSession": + echo("['sessionCreated: #session.cfid#']"); + break; + case "checkSession": echo( session.toJson() ); + break; + default: + throw "unknown action [#form.action#]"; } \ No newline at end of file From d9b42c83ced2c8e5b51dffc503d58b7074376016 Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Fri, 12 Jan 2024 14:39:08 +0100 Subject: [PATCH 5/5] LDEV-4670 revert looong sleep in test case! --- test/tickets/LDEV4670.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tickets/LDEV4670.cfc b/test/tickets/LDEV4670.cfc index 912bda86e3..c55c94666d 100644 --- a/test/tickets/LDEV4670.cfc +++ b/test/tickets/LDEV4670.cfc @@ -112,7 +112,7 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="session" { expect( result.session.requestCount ).toBe( 2 ); // now let the session expire, session expiry is 1s - sleep( 5*60*1001 ); + sleep( 1001 ); systemoutput("purgeExpiredSessions", true); admin