[ci skip] Proper error handling for Sessions on PHP 5

This was actually a PHP bug, see https://wiki.php.net/rfc/session.user.return-value

Also related: #4039
diff --git a/system/libraries/Session/Session_driver.php b/system/libraries/Session/Session_driver.php
index 47376da..64b4bb5 100644
--- a/system/libraries/Session/Session_driver.php
+++ b/system/libraries/Session/Session_driver.php
@@ -74,6 +74,18 @@
 	 */
 	protected $_session_id;
 
+	/**
+	 * Success and failure return values
+	 *
+	 * Necessary due to a bug in all PHP 5 versions where return values
+	 * from userspace handlers are not handled properly. PHP 7 fixes the
+	 * bug, so we need to return different values depending on the version.
+	 *
+	 * @see	https://wiki.php.net/rfc/session.user.return-value
+	 * @var	mixed
+	 */
+	protected $_success, $_failure;
+
 	// ------------------------------------------------------------------------
 
 	/**
@@ -85,6 +97,17 @@
 	public function __construct(&$params)
 	{
 		$this->_config =& $params;
+
+		if (is_php('7'))
+		{
+			$this->_success = TRUE;
+			$this->_failure = FALSE;
+		}
+		else
+		{
+			$this->_success = 0;
+			$this->_failure = -1;
+		}
 	}
 
 	// ------------------------------------------------------------------------
diff --git a/system/libraries/Session/drivers/Session_database_driver.php b/system/libraries/Session/drivers/Session_database_driver.php
index 72b39d1..40a358f 100644
--- a/system/libraries/Session/drivers/Session_database_driver.php
+++ b/system/libraries/Session/drivers/Session_database_driver.php
@@ -125,9 +125,12 @@
 	 */
 	public function open($save_path, $name)
 	{
-		return empty($this->_db->conn_id)
-			? (bool) $this->_db->db_connect()
-			: TRUE;
+		if (empty($this->_db->conn_id) && ! $this->_db->db_connect())
+		{
+			return $this->_failure;
+		}
+
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -201,7 +204,7 @@
 		{
 			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
 			{
-				return FALSE;
+				return $this->_failure;
 			}
 
 			$this->_row_exists = FALSE;
@@ -209,7 +212,7 @@
 		}
 		elseif ($this->_lock === FALSE)
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 
 		if ($this->_row_exists === FALSE)
@@ -224,10 +227,11 @@
 			if ($this->_db->insert($this->_config['save_path'], $insert_data))
 			{
 				$this->_fingerprint = md5($session_data);
-				return $this->_row_exists = TRUE;
+				$this->_row_exists = TRUE;
+				return $this->_success;
 			}
 
-			return FALSE;
+			return $this->_failure;
 		}
 
 		$this->_db->where('id', $session_id);
@@ -247,10 +251,10 @@
 		if ($this->_db->update($this->_config['save_path'], $update_data))
 		{
 			$this->_fingerprint = md5($session_data);
-			return TRUE;
+			return $this->_success;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -264,9 +268,9 @@
 	 */
 	public function close()
 	{
-		return ($this->_lock)
-			? $this->_release_lock()
-			: TRUE;
+		return ($this->_lock && ! $this->_release_lock())
+			? $this->_failure
+			: $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -289,12 +293,19 @@
 				$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
 			}
 
-			return $this->_db->delete($this->_config['save_path'])
-				? ($this->close() && $this->_cookie_destroy())
-				: FALSE;
+			if ( ! $this->_db->delete($this->_config['save_path']))
+			{
+				return $this->_failure;
+			}
 		}
 
-		return ($this->close() && $this->_cookie_destroy());
+		if ($this->close())
+		{
+			$this->_cookie_destroy();
+			return $this->_success;
+		}
+
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -309,7 +320,9 @@
 	 */
 	public function gc($maxlifetime)
 	{
-		return $this->_db->delete($this->_config['save_path'], 'timestamp < '.(time() - $maxlifetime));
+		return ($this->_db->delete($this->_config['save_path'], 'timestamp < '.(time() - $maxlifetime)))
+			? $this->_success
+			: $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -390,4 +403,4 @@
 		return parent::_release_lock();
 	}
 
-}
+}
\ No newline at end of file
diff --git a/system/libraries/Session/drivers/Session_files_driver.php b/system/libraries/Session/drivers/Session_files_driver.php
index 173b437..f0f055f 100644
--- a/system/libraries/Session/drivers/Session_files_driver.php
+++ b/system/libraries/Session/drivers/Session_files_driver.php
@@ -129,7 +129,7 @@
 			.$name // we'll use the session cookie name as a prefix to avoid collisions
 			.($this->_config['match_ip'] ? md5($_SERVER['REMOTE_ADDR']) : '');
 
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -156,13 +156,13 @@
 				if (($this->_file_handle = fopen($this->_file_path.$session_id, 'w+b')) === FALSE)
 				{
 					log_message('error', "Session: File '".$this->_file_path.$session_id."' doesn't exist and cannot be created.");
-					return FALSE;
+					return $this->_failure;
 				}
 			}
 			elseif (($this->_file_handle = fopen($this->_file_path.$session_id, 'r+b')) === FALSE)
 			{
 				log_message('error', "Session: Unable to open file '".$this->_file_path.$session_id."'.");
-				return FALSE;
+				return $this->_failure;
 			}
 
 			if (flock($this->_file_handle, LOCK_EX) === FALSE)
@@ -170,7 +170,7 @@
 				log_message('error', "Session: Unable to obtain lock for file '".$this->_file_path.$session_id."'.");
 				fclose($this->_file_handle);
 				$this->_file_handle = NULL;
-				return FALSE;
+				return $this->_failure;
 			}
 
 			// Needed by write() to detect session_regenerate_id() calls
@@ -187,7 +187,7 @@
 		// See https://github.com/bcit-ci/CodeIgniter/issues/4039
 		elseif ($this->_file_handler === FALSE)
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 		else
 		{
@@ -226,18 +226,18 @@
 		// and we need to close the old handle and open a new one
 		if ($session_id !== $this->_session_id && ( ! $this->close() OR $this->read($session_id) === FALSE))
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 
 		if ( ! is_resource($this->_file_handle))
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 		elseif ($this->_fingerprint === md5($session_data))
 		{
-			return ($this->_file_new)
-				? TRUE
-				: touch($this->_file_path.$session_id);
+			return ( ! $this->_file_new && ! touch($this->_file_path.$session_id))
+				? $this->_failure
+				: $this->_success;
 		}
 
 		if ( ! $this->_file_new)
@@ -260,12 +260,12 @@
 			{
 				$this->_fingerprint = md5(substr($session_data, 0, $written));
 				log_message('error', 'Session: Unable to write data.');
-				return FALSE;
+				return $this->_failure;
 			}
 		}
 
 		$this->_fingerprint = md5($session_data);
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -285,10 +285,9 @@
 			fclose($this->_file_handle);
 
 			$this->_file_handle = $this->_file_new = $this->_session_id = NULL;
-			return TRUE;
 		}
 
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -305,19 +304,31 @@
 	{
 		if ($this->close())
 		{
-			return file_exists($this->_file_path.$session_id)
-				? (unlink($this->_file_path.$session_id) && $this->_cookie_destroy())
-				: TRUE;
+			if (file_exists($this->_file_path.$session_id))
+			{
+				$this->_cookie_destroy();
+				return unlink($this->_file_path.$session_id)
+					? $this->_success
+					: $this->_failure;
+			}
+
+			return $this->_success;
 		}
 		elseif ($this->_file_path !== NULL)
 		{
 			clearstatcache();
-			return file_exists($this->_file_path.$session_id)
-				? (unlink($this->_file_path.$session_id) && $this->_cookie_destroy())
-				: TRUE;
+			if (file_exists($this->_file_path.$session_id))
+			{
+				$this->_cookie_destroy();
+				return unlink($this->_file_path.$session_id)
+					? $this->_success
+					: $this->_failure;
+			}
+
+			return $this->_success;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -335,7 +346,7 @@
 		if ( ! is_dir($this->_config['save_path']) OR ($directory = opendir($this->_config['save_path'])) === FALSE)
 		{
 			log_message('debug', "Session: Garbage collector couldn't list files under directory '".$this->_config['save_path']."'.");
-			return FALSE;
+			return $this->_failure;
 		}
 
 		$ts = time() - $maxlifetime;
@@ -362,7 +373,7 @@
 
 		closedir($directory);
 
-		return TRUE;
+		return $this->_success;
 	}
 
-}
+}
\ No newline at end of file
diff --git a/system/libraries/Session/drivers/Session_memcached_driver.php b/system/libraries/Session/drivers/Session_memcached_driver.php
index 97b8605..760239d 100644
--- a/system/libraries/Session/drivers/Session_memcached_driver.php
+++ b/system/libraries/Session/drivers/Session_memcached_driver.php
@@ -117,7 +117,7 @@
 		{
 			$this->_memcached = NULL;
 			log_message('error', 'Session: Invalid Memcached save path format: '.$this->_config['save_path']);
-			return FALSE;
+			return $this->_failure;
 		}
 
 		foreach ($matches as $match)
@@ -142,10 +142,10 @@
 		if (empty($server_list))
 		{
 			log_message('error', 'Session: Memcached server pool is empty.');
-			return FALSE;
+			return $this->_failure;
 		}
 
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -170,7 +170,7 @@
 			return $session_data;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -188,14 +188,14 @@
 	{
 		if ( ! isset($this->_memcached))
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 		// Was the ID regenerated?
 		elseif ($session_id !== $this->_session_id)
 		{
 			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
 			{
-				return FALSE;
+				return $this->_failure;
 			}
 
 			$this->_fingerprint = md5('');
@@ -210,16 +210,18 @@
 				if ($this->_memcached->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
 				{
 					$this->_fingerprint = $fingerprint;
-					return TRUE;
+					return $this->_success;
 				}
 
-				return FALSE;
+				return $this->_failure;
 			}
 
-			return $this->_memcached->touch($this->_key_prefix.$session_id, $this->_config['expiration']);
+			return $this->_memcached->touch($this->_key_prefix.$session_id, $this->_config['expiration'])
+				? $this->_success
+				: $this->_failure;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -238,14 +240,14 @@
 			isset($this->_lock_key) && $this->_memcached->delete($this->_lock_key);
 			if ( ! $this->_memcached->quit())
 			{
-				return FALSE;
+				return $this->_failure;
 			}
 
 			$this->_memcached = NULL;
-			return TRUE;
+			return $this->_success;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -263,10 +265,11 @@
 		if (isset($this->_memcached, $this->_lock_key))
 		{
 			$this->_memcached->delete($this->_key_prefix.$session_id);
-			return $this->_cookie_destroy();
+			$this->_cookie_destroy();
+			return $this->_success;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -282,7 +285,7 @@
 	public function gc($maxlifetime)
 	{
 		// Not necessary, Memcached takes care of that.
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -299,7 +302,9 @@
 	{
 		if (isset($this->_lock_key))
 		{
-			return $this->_memcached->replace($this->_lock_key, time(), 300);
+			return ($this->_memcached->replace($this->_lock_key, time(), 300))
+				? $this->_success
+				: $this->_failure;
 		}
 
 		// 30 attempts to obtain a lock, in case another request already has it
@@ -316,7 +321,7 @@
 			if ( ! $this->_memcached->set($lock_key, time(), 300))
 			{
 				log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
-				return FALSE;
+				return $this->_failure;
 			}
 
 			$this->_lock_key = $lock_key;
@@ -327,11 +332,11 @@
 		if ($attempt === 30)
 		{
 			log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
-			return FALSE;
+			return $this->_failure;
 		}
 
 		$this->_lock = TRUE;
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
diff --git a/system/libraries/Session/drivers/Session_redis_driver.php b/system/libraries/Session/drivers/Session_redis_driver.php
index b098cc4..e891530 100644
--- a/system/libraries/Session/drivers/Session_redis_driver.php
+++ b/system/libraries/Session/drivers/Session_redis_driver.php
@@ -124,7 +124,7 @@
 	{
 		if (empty($this->_config['save_path']))
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 
 		$redis = new Redis();
@@ -143,10 +143,10 @@
 		else
 		{
 			$this->_redis = $redis;
-			return TRUE;
+			return $this->_success;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -171,7 +171,7 @@
 			return $session_data;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -189,14 +189,14 @@
 	{
 		if ( ! isset($this->_redis))
 		{
-			return FALSE;
+			return $this->_failure;
 		}
 		// Was the ID regenerated?
 		elseif ($session_id !== $this->_session_id)
 		{
 			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
 			{
-				return FALSE;
+				return $this->_failure;
 			}
 
 			$this->_fingerprint = md5('');
@@ -211,16 +211,18 @@
 				if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
 				{
 					$this->_fingerprint = $fingerprint;
-					return TRUE;
+					return $this->_success;
 				}
 
-				return FALSE;
+				return $this->_failure;
 			}
 
-			return $this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_config['expiration']);
+			return ($this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_config['expiration']))
+				? $this->_success
+				: $this->_failure;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -242,7 +244,7 @@
 					isset($this->_lock_key) && $this->_redis->delete($this->_lock_key);
 					if ( ! $this->_redis->close())
 					{
-						return FALSE;
+						return $this->_failure;
 					}
 				}
 			}
@@ -252,10 +254,10 @@
 			}
 
 			$this->_redis = NULL;
-			return TRUE;
+			return $this->_success;
 		}
 
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
@@ -277,10 +279,11 @@
 				log_message('debug', 'Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.');
 			}
 
-			return $this->_cookie_destroy();
+			$this->_cookie_destroy();
+			return $this->_success;
 		}
 
-		return FALSE;
+		return $this->_failure;
 	}
 
 	// ------------------------------------------------------------------------
@@ -296,7 +299,7 @@
 	public function gc($maxlifetime)
 	{
 		// Not necessary, Redis takes care of that.
-		return TRUE;
+		return $this->_success;
 	}
 
 	// ------------------------------------------------------------------------
diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst
index f4e5158..caa4455 100644
--- a/user_guide_src/source/changelog.rst
+++ b/user_guide_src/source/changelog.rst
@@ -25,6 +25,7 @@
 -  Fixed a bug (#4283) - :doc:`String Helper <helpers/string_helper>` function :php:func:`alternator()` couldn't be called without arguments.
 -  Fixed a bug (#4306) - :doc:`Database <database/index>` method ``version()`` didn't work properly with the 'mssql' driver.
 -  Fixed a bug (#4039) - :doc:`Session Library <libraries/sessions>` could generate multiple (redundant) warnings in case of a read failure with the 'files' driver, due to a bug in PHP.
+-  Fixed a bug where :doc:`Session Library <libraries/sessions>` didn't have proper error handling on PHP 5 (due to a PHP bug).
 
 Version 3.0.3
 =============