I’ve written a custom session handler class which works for the most part, until I try to log my users out and destroy the session, then I keep getting the following error:
ErrorException: 0
session_write_close(): Failed to write session data using user defined save handler. (session.save_path: D:xampptmp)
I am running PHP Version 7.3.1 on XAMPP.
This is my custom class
class CornerstoneSessionHandler implements SessionHandlerInterface {
/**
* Construct the Session
* No parameters required, nothing will be returned
*/
public function __construct() {
// Set the handler to overide SESSION
session_set_save_handler(
array($this, "open"),
array($this, "close"),
array($this, "read"),
array($this, "write"),
array($this, "destroy"),
array($this, "gc")
);
// Set the shutdown function
register_shutdown_function('session_write_close');
/** Define and initialise the Session Handler */
session_start();
}
/**
* Set the open callback
*
* @param string $savePath
* @param string $sessionName
*
* @return bool return value should be true for success or false for failure
*/
public function open($savePath, $sessionName) {
// Make the $csdb global accessible
global $csdb;
// Check that the DB connection is set
return ((!empty($csdb)) && $csdb->isConnected() != 1) ? FALSE : TRUE ;
}
/**
* Set the close callback
*
* @return bool return value can only be true for success
*/
public function close() {
return TRUE;
}
/**
* Set the read callback
*
* @param string $sessionID
*
* @return string return value should be the session data or an empty string
*/
public function read($sessionID) {
// Make the $csdb global accessible
global $csdb;
// Get the session from the database
$csdb->query_prepared("SELECT session_data FROM cs_session WHERE session_id=?", [$sessionID]);
// If results returned, continue
if($csdb->getNum_Rows() > 0) {
// Get the data
$result = $csdb->get_row(NULL);
return $result->session_data;
} else { // Else return an empty string
return '';
}
}
/**
* Set the write callback
*
* @param string $sessionID
* @param string $data
*
* @return bool return value should be true for success or false for failure
*/
public function write($sessionID, $data) {
// Make the $csdb global accessible
global $csdb;
// Set the time stamp
$access_dtm = new DateTime();
// Replace the data
$csdb->query_prepared('REPLACE INTO cs_session(session_id, session_ip_address, session_data , session_access_dtm) VALUES (?, ?, ?, ?)', [$sessionID, $_SERVER['REMOTE_ADDR'], $data, $access_dtm->format('Y-m-d H:i:s')]);
// If modified a success, return true
if($csdb->getNum_Rows() > 0) {
return TRUE;
} else { // Else, return false
return FALSE;
}
}
/**
* Set the destroy callback
*
* @param string $sessionID
*
* @return bool return value should be true for success or false for failure
*/
public function destroy($sessionID) {
// Make the $csdb global accessible
global $csdb;
// Delete the session from the database
$csdb->delete('cs_session', where(eq('session_id', $sessionID)));
// If results returned, return true
if($csdb->affectedRows() > 0) {
return TRUE;
} else { // Else, return false
return FALSE;
}
}
/**
* Set the garbage collector callback
*
* @param string $lifetime
*
* @return bool return value should be true for success or false for failure
*/
public function gc($lifetime) {
// Make the $csdb global accessible
global $csdb;
// Set the date calculation
$expiredTime = new DateTime();
$expiredTime->modify('-' . $lifetime . ' seconds');
// Get the session from the database
$csdb->delete('cs_session', where(lt('session_access_dtm', $expiredTime->format('Y-m-d H:i:s'))));
// If results deleted, return true
if($csdb->affectedRows() > 0) {
return TRUE;
} else { // Else, return false
return FALSE;
}
}
}
I am using ezSQL as my database handler.
This is the code in the top of all my pages
/**
* Set session data so that logins work properly over all browsers
* This should fix login errors some users can face
* More info can be found at {@link https://www.php.net/manual/en/session.security.ini.php the php user manual}
*/
# PREVENTING SESSION HIJACKING
# Prevents javascript XSS attacks aimed to steal the session ID
ini_set('session.cookie_httponly', 1);
# Make sure the cookie lifetime is set to '0'
ini_set('session.cookie_lifetime', 0);
# Adds entropy into the randomization of the session ID, as PHP's random number
# generator has some known flaws
ini_set('session.entropy_file', '/dev/urandom');
# Uses a strong hash
ini_set('session.hash_function', 'whirlpool');
# Set the session save location (best for shared servers)
# Uncomment out the next line if you would like to set a custom path and haven't already set the value in your `php.ini` file.
# ini_set('session.save_path',realpath(ABSPATH . 'tmp' . DIRECTORY_SEPARATOR));
# Note: The folder referenced above must exist for it to work.
# Set the session garbage collection lifetime to custom defined minutes (PHP default is 24 minutes)
ini_set('session.gc_maxlifetime', (int)get_option("session_expire") * MINUTE_IN_SECONDS);
# Enable session garbage collection with a 1% chance of
# running on each session_start()
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 100);
# Uses a secure connection (HTTPS) if possible
# Allow cookies to be sent over insecure connections if not an HTTPS site
(SITE_HTTPS) ? ini_set('session.cookie_secure', 1) : ini_set('session.cookie_secure', false);
# PREVENTING SESSION FIXATION
# Session ID cannot be passed through URLs
# so only use cookies to store the session id on the client side
ini_set('session.use_only_cookies', 1);
# Set a custom session name
session_name('CSSESSID');
# Load the session class
new CornerstoneSessionHandler();
# Start the session if it's not already started
if (session_status() == PHP_SESSION_NONE) {session_start();}
The weird thing is, even though it’s giving me that error, if I check the database the data is being updated no worries.
This is my logout code
/**
* Logout the user
*
* @return bool
*/
public static function logoutUser() {
// Make the $fear global accessible
global $fear; // Direct access check
// Check if session set (just in case) and start if it isn't
if(session_id() == '') {session_start();}
// Delete the $_SESSION data set in `authenticateUser()`
unset($_SESSION['HTTP_USER_AGENT']);
unset($_SESSION['_cs-uid']);
unset($_SESSION['_cs-ue']);
unset($_SESSION['_cs-ul']);
unset($_SESSION['_cs-un']);
/**
* Get the "ext.auth.php" file and run `clearCustomAuth()` function
* to clear any custom set $_SESSION items
*/
require_once( get_lib_path('ext.auth.php') );
clearCustomAuth();
// Regenerate a new session ID just to be sure
session_regenerate_id();
// Destroy the session
session_destroy();
// Check if the $_COOKIE data if set
if(isset( $_COOKIE['_cs-ti'] ) && !empty( $_COOKIE['_cs-ti'] )) {
// Delete the cookie token from the database
if(!self::deleteAuthCookie($_COOKIE['_cs-ti'])) {
// Return false if cookie couldn't be deleted
return false;
}
}
// Return true if run all the way
return true;
}
I’ve tried shifting around the session_regenerate_id()
but if I put it after the session_destroy()
it says there’s no session to regenerate, but if I put it before the session_destroy()
or don’t have it at all, my session is deleted from my database but I can still see it in my storage in my inspector on Firefox.
It only gives me an error when trying to delete a session from the database. Everything else works no worries! I’ve manually tried to delete a session from the database in testing and I still get that error.
Is anyone able to see what I might be doing wrong or how I can fix it? I’ve only just learnt about this so pretty new to it. If I need to supply more information, just let me know. I’ve been searching over the web for nearly 2 days with no luck.
2
Answers
I don’t see a reason why you need to set a shutdown handler. Sessions are automatically saved when php shuts down, even when you get a fatal error.
Also the write, destroy and gc should always return true, even if no rows were updated / deleted. This seems counter intuitive, but php considers deleting a non existing session also a success.
You can use
session_start
optional parameters