It is important to understand that PHP's default file-based session handling LOCKS the session file, inherently allowing ONLY ONE thread handling any given session at a time.
When you implement a DB-backed session storage and you do not do any locking, you may run into situations where more than one thread is serving the same session, and you may LOSE DATA because the second thread will overwrite any session changes done by the first thread.
You should therefore think about locking the session somehow if you want to have the exact same behavior as with the default file-based implementation. For example, with InnoDB you could do a SELECT ... FOR UPDATE, or you can use the GET_LOCK() function.
session_set_save_handler
(PHP 4, PHP 5)
session_set_save_handler — ユーザー定義のセッション保存関数を設定する
説明
$open
, callable $close
, callable $read
, callable $write
, callable $destroy
, callable $gc
)PHP 5.4 以降は次のプロトタイプでも登録できます。
$sessionhandler
[, bool $register_shutdown = true
] )session_set_save_handler() は、セッションに 関連するデータを保存および取得するために使用されるユーザー定義の セッション保存関数を設定します。この関数は、セッションデータを ローカルデータベースに保存する場合のように PHP セッションにより 提供されるもの以外の保存方法を使用したい場合に有用です。
パラメータ
この関数には二種類のプロトタイプがあります。
-
sessionhandler -
SessionHandlerInterface を実装したクラス、たとえば SessionHandler のインスタンスで、 これをセッションハンドラとして登録する。PHP 5.4 以降限定。
-
register_shutdown -
session_write_close() を register_shutdown_function() 関数として登録するかどうか。
-
open(string $savePath, string $sessionName) -
open コールバックはクラスのコンストラクタのようなもので、セッションを開くときに実行されます。 セッションが自動で開始したり、あるいは手動で session_start() で開始させたりするときに、最初に実行されるコールバック関数がこれです。 成功した場合は
TRUE、失敗した場合はFALSEを返します。 -
close() -
close コールバックはクラスのデストラクタのようなもので、write コールバックがコールされた後で実行されます。 また、session_write_close() がコールされたときにも実行されます。 成功した場合は
TRUE、失敗した場合はFALSEを返します。 -
read(string $sessionId) -
readコールバックは、常にセッションエンコード (シリアライズ) された文字列を返さなければなりません。何もデータを読み込まなかった場合は空文字列を返します。このコールバックは、セッションが開始したときや session_start() がコールされたときに PHP が内部的に実行します。 このコールバックを実行する前に、PHP は
openコールバックを実行します。このコールバックが返す値は、
writeコールバックがストレージに渡した形式とまったく同じシリアライズ形式でなければなりません。 返された値を PHP が自動的にアンシリアライズして、スーパーグローバル $_SESSION に格納します。データの形式は serialize() したものと似ていますが、実際は違う形式であることに注意しましょう。 シリアライズの方法は ini 設定 session.serialize_handler で指定します。 -
write(string $sessionId, string $data) -
writeコールバックは、セッションの保存や終了が必要となったときにコールされます。 このコールバックが受け取るのは、現在のセッション ID とシリアライズ後のスーパーグローバル $_SESSION です。 PHP が内部で利用するシリアライズ方法は、ini 設定 session.serialize_handler で指定します。このコールバックに渡されたシリアライズ後のセッションデータを、 渡されたセッション ID に対応させて格納しなければなりません。 このデータを取得した
readコールバックは、writeコールバックに最初に渡されたのとまったく同じ値を返さなければなりません。このコールバックが実行されるのは、PHP のスクリプトが終了するときか、あるいは明示的に session_write_close() がコールされたときです。この関数の実行後には、PHP が内部的に
closeコールバックを実行することに注意しましょう。注意:
"write" ハンドラは、出力ストリームが閉じてから実行されます。 したがって、"write" ハンドラ内でデバッグ出力を行っても、 それはブラウザに表示されません。 デバッグ出力が必要なら、それをファイルに書き出すようにしましょう。
-
destroy($sessionId) -
このコールバックが実行されるのは、 session_destroy() あるいは session_regenerate_id() (destroy パラメータを
TRUEにした場合) を実行し、セッションを破棄した場合です。 成功した場合はTRUE、失敗した場合はFALSEを返します。 -
gc($lifetime) -
ガベージコレクタコールバックは PHP が内部で定期的に実行し、古いセッションデータを破棄します。 実行頻度は session.gc_probability および session.gc_divisor で設定します。 この関数に渡される有効期限の値は session.gc_maxlifetime で設定できます。 成功した場合は
TRUE、失敗した場合はFALSEを返します。
返り値
成功した場合に TRUE を、失敗した場合に FALSE を返します。
例
例1 自作のセッションハンドラ: 完全なコードは SessionHandlerInterface を参照
このコードは、PHP 5.4.0 以降のバージョンで動作します。 ここでは実行するところだけを示します。完全な例は、上でリンクしている SessionHandlerInterface のページを参照ください。
session_set_save_handler() でオブジェクト指向型のプロトタイプを使っていることと、 シャットダウン関数をその parameter フラグで登録していることに注目しましょう。 オブジェクトをセッション保存ハンドラとして使うときには、この方法をおすすめします。
<?php
class MySessionHandler implements SessionHandlerInterface
{
// ここでインターフェイスを実装します
}
$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();
// $_SESSION への値の設定や格納されている値の取得を進めます
例2 セッション保存ハンドラでオブジェクトを使う例
このコードは、PHP 5.4.0 より前のバージョンで動作します。
この例では、ファイルベースのセッションストレージをつくります。これは
PHP のデフォルトのセッション保存ハンドラである files
と似たものです。この例をさらに拡張すれば、
PHP がサポートするお好みのデータベースを使ってセッションを保存させるようにするのも簡単です。
シャットダウン関数 session_write_close() を、 register_shutdown_function() を使って追加登録していることに注目しましょう。 PHP 5.4.0 より前のバージョンでオブジェクトをセッション保存ハンドラとして使うときには、この方法をおすすめします。
<?php
class FileSessionHandler
{
private $savePath;
function open($savePath, $sessionName)
{
$this->savePath = $savePath;
if (!is_dir($this->savePath)) {
mkdir($this->savePath, 0777);
}
return true;
}
function close()
{
return true;
}
function read($id)
{
return (string)@file_get_contents("$this->savePath/sess_$id");
}
function write($id, $data)
{
return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
}
function destroy($id)
{
$file = "$this->savePath/sess_$id";
if (file_exists($file)) {
unlink($file);
}
return true;
}
function gc($maxlifetime)
{
foreach (glob("$this->savePath/sess_*") as $file) {
if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
unlink($file);
}
}
return true;
}
}
$handler = new FileSessionHandler();
session_set_save_handler(
array($handler, 'open'),
array($handler, 'close'),
array($handler, 'read'),
array($handler, 'write'),
array($handler, 'destroy'),
array($handler, 'gc')
);
// これは、オブジェクトを保存ハンドラとして使ったときの予期せぬ副作用を防ぎます
register_shutdown_function('session_write_close');
session_start();
// $_SESSION への値の設定や格納されている値の取得を進めます
注意
オブジェクトをセッション保存ハンドラとして使うときには、
シャットダウン関数を PHP に登録しておくことが重要です。
これで、PHP スクリプトの終了時に内部的にオブジェクトを破棄する処理による副作用や、
write および close
がコールされてしまうことを防げます。
一般的には、'session_write_close' を
register_shutdown_function() 関数で登録しなければなりません。
PHP 5.4.0 以降では session_register_shutdown() を使えます。あるいは session_set_save_handler() をオブジェクト指向のメソッドとして実行するときに 'register shutdown' フラグを使い、SessionHandlerInterface を実装したインスタンスを渡すという方法もあります。
PHP 5.0.5 以降、write ハンドラおよび
close ハンドラはオブジェクトが破棄されたあとにコールされます。
そのため、これらのハンドラでオブジェクトを使ったり例外をスローしたりすることはできません。
例外をキャッチできないのでその情報をトレースすることもできず、
例外は単純に無視されてしまいます。
しかし、オブジェクトのデストラクタではセッションを使えます。
この「ニワトリが先かタマゴが先か」の問題を解決するために、 デストラクタから session_write_close() をコールできますが、より確実なのは先述のとおりシャットダウン関数を登録することです。
SAPI の種類によっては、スクリプトの終了時にセッションを閉じると 現在の作業ディレクトリが変わってしまうことがあります。これを防ぐには、 事前に session_write_close() でセッションを閉じます。
変更履歴
| バージョン | 説明 |
|---|---|
| 5.4.0 | SessionHandlerInterface でセッションハンドラを実装したり、 SessionHandler で PHP の内部セッションハンドラを公開したりできるようになりました。 |
参考
- 設定ディレクティブ session.save_handler
- 設定ディレクティブ session.serialize_handler
- register_shutdown_function() - シャットダウン時に実行する関数を登録する
- session_register_shutdown() - セッションのシャットダウン関数 (PHP 5.4.0 以降)
It is important to note that autoload handlers don't work inside the read, write, etc. functions, so you have to explicitly require_once all classes you use there.
I write a class that unites whole handler functionality. It's not even needed to save instances of this class in variables. Just add a row:
<?php
new SessionSaveHandler();
?>
and the handler will rule the sessions ;-)
<?php
class SessionSaveHandler {
protected $savePath;
protected $sessionName;
public function __construct() {
session_set_save_handler(
array($this, "open"),
array($this, "close"),
array($this, "read"),
array($this, "write"),
array($this, "destroy"),
array($this, "gc")
);
}
public function open($savePath, $sessionName) {
$this->savePath = $savePath;
$this->sessionName = $sessionName;
return true;
}
public function close() {
// your code if any
return true;
}
public function read($id) {
// your code
}
public function write($id, $data) {
// your code
}
public function destroy($id) {
// your code
}
public function gc($maxlifetime) {
// your code
}
}
new SessionSaveHandler();
?>
I have solved problem with session handler, which needs some other objects (for example DB access object), with register_shutdown_function. Just calling
<?php
register_shutdown_function('session_write_close');
?>
solved my problem. Shutdown functions are called before object destructors.
If you use class for session handling (my prefered way), you can call register_shutdown_function in constructor and you must not mess your code in another place.
Example:
<?php
class SessionHandler {
protected $_db;
public function __construct(PDO $db) {
$this->_db = $db;
register_shutdown_function('session_write_close');
}
function open($save_path, $session_name) {
}
function close() {
}
function read($id) {
}
function write($id, $sess_data) {
}
function destroy($id) {
}
function gc($maxlifetime) {
}
}
?>
I had trouble with committing session data.
To "commit and continue" without closing your session, put this at the top of your "write" method:
<?php
$id = session_id();
session_write_close();
session_id($id);
session_start();
?>
Note that ANY time php generates a new session id, it is not automatically updated in a database. This can be helpful:
<?php
public function resetSessionId()
{
$old = session_id();
session_regenerate_id();
$new = session_id();
SessionHandler::regenerate_id($old,$new);
}
public function regenerate_id($old,$new)
{
$db = mysqli->connect(...);
$db->query('UPDATE sessions SET session_id = \''.$db->escape_string($new).'\'
WHERE session_id = \''.$db->escape_string($old).'\'');
}
?>
I was getting Fatal error: Exception thrown without a stack frame and it took days to figure out the reason. I am using memcache to store sessions and in my custom class I use Memcache class in write method.
I put the code in the write method inside try-catch block and it solved my problem.
I have been done with session problems creating a new and my own session system.
This is very simple, bellow the class file, a good pratice is include these file as your first line:
<?php
class SessionDB {
private $data=null;
private $session_id=null;
private $minutes_to_expire=3600; // TIME TO MAINTAIN DATA ON DB
public function __construct(){
global $SESSION;
if (isset($_COOKIE['session_id'])){
$this->session_id = $_COOKIE['session_id'];
} else {
$this->session_id = md5(microtime().rand(1,9999999999999999999999999)); // GENERATE A RANDOM ID
setcookie('session_id',$this->session_id);
$sql = "INSERT INTO `tb_session_db` (`session_id`, `updated_on`) VALUES ('{$this->session_id}', NOW())";
mysql_query($sql);
}
$sql = "SELECT `value` FROM `tb_session_db` WHERE `session_id`='{$this->session_id}'";
$query = mysql_query($sql);
$this->data = unserialize(mysql_result($query, 0, 'value'));
$SESSION = $this->data;
}
private function expire(){
$date_to_delete = date("Y-m-d H:i:s", time()-60*$this->minutes_to_expire);
$sql = "DELETE FROM `tb_session_db` WHERE `update_on` <= '$date_to_delete'";
mysql_query($sql);
}
public function __destruct(){
global $SESSION;
$this->data = serialize($SESSION);
$sql = "UPDATE `tb_session_db` SET `value`='{$this->data}', `updated_on`=NOW() WHERE `session_id`='{$this->session_id}'";
mysql_query($sql);
$this->expire();
}
}
/*
TABLE STRUCTURE
CREATE TABLE IF NOT EXISTS `tb_session_db` (
`session_id` varchar(32) NOT NULL,
`value` blob,
`updated_on` datetime DEFAULT NULL,
PRIMARY KEY (`session_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
*/
?>
With this file included, connected to MySQL database who has the table `tb_session_db` I can easy do that into my config file:
<?php
require_once('session_db.php');
define('MYSQL_HOST','mysql.example.com');
define('MYSQL_USER','.......');
define('MYSQL_PASS','.......');
define('MYSQL_BASE','.......');
mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASS) or die('Cannot connect');
mysql_select_db(MYSQL_BASE) or die('Cannot select DB');
mysql_set_charset('utf8_general_ci');
$SESSION = null;
global $SESSION;
$session_db = new SessionDB();
?>
To use $SESSION on a function/class method, you must call:
global $SESSION
For who will not work with this over classes or functions, it is a very simple/fast way to work!
You can call the session_write in db object destructor to be shore that you still have a connection to mysql and the session is write.
When storing sessions in a DB, it's usually beneficial to use an existing custom DB object, but this creates problems with the latest version of PHP 5.3.1. This used to work fine on PHP 5.2.x (Linux and Windows).
The problem now is that session_write_close() is not automatically called when execution ends, but rather after all the objects have been destructed, including the DB object!
There are two ways around this, either manually calling session_write_close() at the end of your script(s), or not using the DB object.
I'm sure this is the intended behavior from the beginning.
I pulled a really stupid move. If you are trying to debug your garbage collector, make sure you call the following >>> BEFORE <<< "session_start":
<?php
ini_set('session.gc_probability', 100);
ini_set('session.gc_divisor', 100);
?>
I was sure it was a bug in PHP, but turned out (like 99% of the time) to be me own fault.
A quick note for people having problems storing binary session data. Using prepared statements solves all your quote-related issues! Start reading here http://php.net/manual/en/mysqli-stmt.prepare.php for more information!
session_set_save_handler is used before session_start.if your session is setted as auto start. it will return FALSE value.so you need add session_write_close() before session_set_save_handler to cancel the session's auto start.it likes this:
<?php
/*
qq:290359552
*/
session_write_close(); //cancel the session's auto start,important
function open()
{
...
}
....
session_set_save_handler( ... );
session_start();
?>
after spending 8 hours to find out whats going on..
just for the records, because php.net ignore the real world out there:
debian 5 installs by default the php-suhosin module, which changes the behavior of session_set_save_handler read/write function.
on calling the session write function the session data will be encrypted, and the returning string from the read function are decrypted and verified.
the encrypted data is no more compatible with session_encode/session_decode.
and breaks by default, subdomain handling and multiple host setups where different document roots are used.
for futher information look at:
http://www.hardened-php.net/suhosin/configuration.html
session sample data (debian 4):
test|s:3:"sdf";
session sample data (debian 5, with php-suhosin):
3GdlPEGr2kYgRFDs-pUSoKomZ4fN7r5BM5oKOCMsWNc...
i thing the suhosin patch should report a warning in case of invalid session data, to get a clue whats going wrong.
The "binary" data that is in the session data appears to surround class/object names, and if you pass your session data through a function to sanitize it for SQL injection, you may indeed run in to problems.
For example, using the PDO::quote() function to prepare the data for injection (in my case for SQLite3), it was stopping short as soon as it encountered the first bit of binary data, causing my session information to be corrupted.
This change *must* have happened somewhere in the 5.2 series, because I just started encountering this problem recently on a code base that had been tested & working on earlier versions of PHP 5.2.
This may in fact be a bug - I have not yet checked... but beware, and perhaps using base64 to encode/decode your session data is a good thing to do just to be sure (though you are now left unable to visually inspect serialized session information at the storage level which is a rather big problem for on-the-fly debugging of sessions).
if you simply append the information from session variables every time you'll have many multiples for variables each time they are changed. a simple way to do this is explode the data twice to seperate the variable name from the other relevant information and foreach() check against the stored set. here is a little bit of a mess i wrote to do it.
assuming stored session variables in both database and passed through function:
<?php
$buffer = array();
$buffer = explode('|',$sessiondata);
$buf1 = array();
$buf2 = array();
$finalbuff = '';
foreach($buffer as $i){
$i = explode(';',$i);
foreach($i as $b){
array_push($buf1,$b);
}
}
$buffer = explode('|',$result['data']);
foreach($buffer as $i){ $i = explode(';',$i); foreach($i as $b){ array_push($buf2,$b);}}
$z = 0;
$x = 0;
while($buf2[$z]){
while($buf1[$x]){
if($buf2[$z] == $buf1[$x]){
$buf2[($z+1)] = $buf1[($x+1)];
}
$x+=2;
}
$z+=2;
}
foreach($buf2 as $i){ $finalbuff .= $i; }
?>
$sessiondata is the variable passed through the function and $result['data'] is the data stored in an sql database.
If your using database session handler and your database is in UTF8, but you happen to have a script running in IS0-8859 that tries to put symbols such as £ signs into the database - then the session will corrupt and data will go missing.
You can either - make sure you never put special characters into the session, or - more helpful, do this in your 'write' function
<?php
public static function write( $key, $val )
{
$val = utf8_encode( $val );
$val = preg_replace( '!s:(\d+):"(.*?)";!se', "'s:'.strlen('$2').':\"$2\";'", $val );
//......
?>
probably an overhead on this, but less overhead than loosing data.
You could also use WDDX for storing sessions I suppose
Regarding the SAPIs: The warning mentioned in function's description (that the Current working directory is changed with some SAPIs) is very important.
It means that if your callback 'write' function needs to write to a file in current directory, it will not find it. You have to use absolute path and not rely upon the current working directory.
I thought this warning applies only to some strange environments like Windows, but it happens exactly on Linux + Apache 2.2 + PHP 5.
Some PHP code for memcached session handler.
<?php
class SessionHadler
{
private static $lifetime = 0;
public static function open()
{
self::$lifetime = ini_get('session.gc_maxlifetime');
return true;
}
public static function read($id)
{
return memcached::get("sessions/{$id}");
}
public static function write($id, $data)
{
return memcached::set("sessions/{$id}", $data, self::$lifetime);
}
public static function destroy($id)
{
return memcached::delete("sessions/{$id}");
}
private function __construct(){}
public static function gc(){ return true; }
public static function close(){ return true; }
public function __destruct()
{
session_write_close();
}
}
?>
With regards to db session handling:
Remember if you use the REPLACE INTO method to have your session key as the primary key otherwise you will end up with duplicate records in your table.
http://dev.mysql.com/doc/refman/5.0/en/replace.html
When writing your own session handler, particularly database session handlers, play close attention to garbage cleanup and how it could affect server load.
To pick a round number example:
If you have 1000 requests per minute on session enabled pages, everyone needs a session started but the session garbage cleanup does not need to run every request. Doing so would cause unrequired queries on the database server.
In this example, setting your probability/divisor to 1/1000 would be sufficient to clean up old sessions at a minimum once a minute. If you don't need that kind of granularity, increase the gc divisor.
Finding the tradeoff between clearing up old sessions and server load is the important aspect here.
blow example and ta summary of these comments.
and using the simple native functions of mysql.
<?php
class Session
{
/**
* a database connection resource
* @var resource
*/
private $_sess_db;
/**
* Open the session
* @return bool
*/
public function open() {
if ($this->_sess_db = mysql_connect(SESSION_DB_HOST,
SESSION_DB_USER,
SESSION_DB_PASS)) {
return mysql_select_db(SESSION_DB_DATABASE, $this->_sess_db);
}
return false;
}
/**
* Close the session
* @return bool
*/
public function close() {
return mysql_close($this->_sess_db);
}
/**
* Close the session
* @return bool
*/
public function close() {
return mysql_close($this->_sess_db);
}
/**
* Read the session
* @param int session id
* @return string string of the sessoin
*/
public function read($id) {
$id = mysql_real_escape_string($id);
$sql = sprintf("SELECT `data` FROM `sessions` " .
"WHERE id = '%s'", $id);
if ($result = mysql_query($sql, $this->_sess_db)) {
if (mysql_num_rows($result)) {
$record = mysql_fetch_assoc($result);
return $record['data'];
}
}
return '';
}
/**
* Write the session
* @param int session id
* @param string data of the session
*/
public function write($id, $data) {
$sql = sprintf("REPLACE INTO `sessions` VALUES('%s', '%s', '%s')",
mysql_real_escape_string($id),
mysql_real_escape_string($data),
mysql_real_escape_string(time()));
return mysql_query($sql, $this->_sess_db);
}
/**
* Destoroy the session
* @param int session id
* @return bool
*/
public function destroy($id) {
$sql = sprintf("DELETE FROM `sessions` WHERE `id` = '%s'", $id);
return mysql_query($sql, $this->_sess_db);
}
/**
* Garbage Collector
* @param int life time (sec.)
* @return bool
* @see session.gc_divisor 100
* @see session.gc_maxlifetime 1440
* @see session.gc_probability 1
* @usage execution rate 1/100
* (session.gc_probability/session.gc_divisor)
*/
public function gc($max) {
$sql = sprintf("DELETE FROM `sessions` WHERE `timestamp` < '%s'",
mysql_real_escape_string(time() - $max));
return mysql_query($sql, $this->_sess_db);
}
}
//ini_set('session.gc_probability', 50);
ini_set('session.save_handler', 'user');
$session = new Session();
session_set_save_handler(array($session, 'open'),
array($session, 'close'),
array($session, 'read'),
array($session, 'write'),
array($session, 'destroy'),
array($session, 'gc'));
// below sample main
session_start();
session_regenerate_id(true);
if (isset($_SESSION['counter'])) {
$_SESSION['counter']++;
} else {
$_SESSION['counter'] = 1;
}
?>
It is useful to use MEMORY storage engine in MySQL while handling sessions.
http://dev.mysql.com/doc/refman/5.0/en/memory-storage-engine.html
I think it is very important here to stress that the WRITE method should use UPDATE+INSERT (or mysql specific REPLACE).
There is example code "out there" that uses just UPDATE for the write method, in which case, when session_regenerate_id is called, session data is lost (as an update would fail, as the key has changed).
I've just wasted a whole day due to this (I know I should have thought it through / RTFM, but it is an easy trap to fall into).
When using a custom session handler, if the first callback function (sessOpen in my case) finds no session id, one is set by the time the second argument (sessRead in my case) is called.
Note that for security reasons the Debian and Ubuntu distributions of php do not call _gc to remove old sessions, but instead run /etc/cron.d/php*, which check the value of session.gc_maxlifetime in php.ini and delete the session files in /var/lib/php*. This is all fine, but it means if you write your own session handlers you'll need to explicitly call your _gc function yourself. A good place to do this is in your _close function, like this:
<?php
function _close() {
_gc(get_cfg_var("session.gc_maxlifetime"));
// rest of function goes here
}
?>
Note that if session.auto_start is set to On in the php.ini, your session_set_save_handler will return false as the session has already been initialized.
If you are finding that your code works OK on one machine but doesn't work on another, check to see if session.auto_start is set to On
the behavior, return values, and exact time of calling for these functions is pretty poorly documented here. i thought folks might like to know that:
1) calling session_start() triggers PHP to first call your open function and then call your read function before resuming with the code immediately following your session_start() call.
2) calling session_id('some_value') within your open function WILL NOT SET THE SESSION COOKIE (at least not on my setup - PHP 4.4.1). Assuming you defined some function to validate a session id called my_func(), you might want to do something like this in your open function:
<?php
function _open($save_path, $session_name) {
// check for session id
$sess_id = session_id();
if (empty($sess_id) || !myfunc($sess_id)) {
//session_id is INVALID - generating new
$new_id = md5(uniqid("some random seed here"));
session_id($new_id);
setcookie(session_name(),
$new_id,
0,
"/",
".mydomain.com");
}
return true;
} // _open()
?>
With regards to the read handler, the docs say:
"Read function must return string value always to make save
handler work as expected. Return empty string if there is no
data to read."
I can't emphasize this enough. I just spent half a day trying to figure out why my sessions weren't storing any information. I was blithely returning the results of a query on the database from the read handler. Since there was no match for the new ID, the result was NULL. Since it wasn't a string, sessions were essentially disabled. So, the safe thing might be something like this:
<?php
function sessRead($id)
{
// Look up data
$results = getStuff($id);
// Make sure it's a string
settype($results, 'string');
return $results;
}
?>
Of course, you can do whatever you want with it. But, no matter what, make sure you return a string.
HTH,
Michael
object- and mysql-based session-handler, requires the following table:
CREATE TABLE `ws_sessions` (
`session_id` varchar(255) binary NOT NULL default '',
`session_expires` int(10) unsigned NOT NULL default '0',
`session_data` text,
PRIMARY KEY (`session_id`)
) TYPE=InnoDB;
<?php
class session {
// session-lifetime
var $lifeTime;
// mysql-handle
var $dbHandle;
function open($savePath, $sessName) {
// get session-lifetime
$this->lifeTime = get_cfg_var("session.gc_maxlifetime");
// open database-connection
$dbHandle = @mysql_connect("server","user","password");
$dbSel = @mysql_select_db("database",$dbHandle);
// return success
if(!$dbHandle || !$dbSel)
return false;
$this->dbHandle = $dbHandle;
return true;
}
function close() {
$this->gc(ini_get('session.gc_maxlifetime'));
// close database-connection
return @mysql_close($this->dbHandle);
}
function read($sessID) {
// fetch session-data
$res = mysql_query("SELECT session_data AS d FROM ws_sessions
WHERE session_id = '$sessID'
AND session_expires > ".time(),$this->dbHandle);
// return data or an empty string at failure
if($row = mysql_fetch_assoc($res))
return $row['d'];
return "";
}
function write($sessID,$sessData) {
// new session-expire-time
$newExp = time() + $this->lifeTime;
// is a session with this id in the database?
$res = mysql_query("SELECT * FROM ws_sessions
WHERE session_id = '$sessID'",$this->dbHandle);
// if yes,
if(mysql_num_rows($res)) {
// ...update session-data
mysql_query("UPDATE ws_sessions
SET session_expires = '$newExp',
session_data = '$sessData'
WHERE session_id = '$sessID'",$this->dbHandle);
// if something happened, return true
if(mysql_affected_rows($this->dbHandle))
return true;
}
// if no session-data was found,
else {
// create a new row
mysql_query("INSERT INTO ws_sessions (
session_id,
session_expires,
session_data)
VALUES(
'$sessID',
'$newExp',
'$sessData')",$this->dbHandle);
// if row was created, return true
if(mysql_affected_rows($this->dbHandle))
return true;
}
// an unknown error occured
return false;
}
function destroy($sessID) {
// delete session-data
mysql_query("DELETE FROM ws_sessions WHERE session_id = '$sessID'",$this->dbHandle);
// if session was deleted, return true,
if(mysql_affected_rows($this->dbHandle))
return true;
// ...else return false
return false;
}
function gc($sessMaxLifeTime) {
// delete old sessions
mysql_query("DELETE FROM ws_sessions WHERE session_expires < ".time(),$this->dbHandle);
// return affected rows
return mysql_affected_rows($this->dbHandle);
}
}
$session = new session();
session_set_save_handler(array(&$session,"open"),
array(&$session,"close"),
array(&$session,"read"),
array(&$session,"write"),
array(&$session,"destroy"),
array(&$session,"gc"));
session_start();
// etc...
?>
php doesn't make any checks about PHPSESSID cookie format, it is then important to verify cookie format before making any sql request.
if your read session request is :
SELECT DataValue FROM sessions WHERE SessionID='$aKey'
this generic cookie could succeed to access others session: PHPSESSID=1' OR SessionID LIKE '%>>
for safety, you can make this verification before sql request:
if (! preg_match('/^([0-9a-f]{32})$/i',$aKey)) return NULL;
hope this can be helpful
Ok, after much hairpulling, I've figured out a successful way to store objects in a session using a postgresql savehandler.
<?php
function write ($id, $sess_data)
{
global $sql; // my global db connection object
$sess_data = @pg_escape_bytea(serialize($sess_id)); //works with any type
$sql->Query("delete from sessions where sessionid = '$id'"); // delete old session record
$sql->Query("insert into sessions (sessionid,datavalue) values ('$id', '$sess_data')"); //insert into session table
return true;
}
function read ($id)
{
global $sql;
$result=$sql->Query("select * from sessions where sessionid = '$id'");
if ($sql->rows==1) {
$row=$sql->Fetch(0);
$rval = unserialize(@pg_unescape_bytea($sql->data[2]));
return $rval;
} else {
return "";
}
}
?>
Make the datavalue column (the one used to store the actual session data) in the sessions table type bytea.
The problem apparently lies in the PHP serialize for objects, where there is a CR/LF inserted between the object id and the property array. The CR/LF isn't escaped, and causes a fit for postgresql.
Any data type in PHP can be serialized, so there's no need to distinguish between types.
The object is fully restored, with methods.
Many people are using session_set_save_handler to store the session in a session database, which ofcourse is both valid and smart since it (could) increas security.
What many people forgets, is that session ids can easily be edited by a user as he see fit (by editing a session_cookie for example*)
* If you like to play around to test your site, check Add n edit Cookies extension for firefox.
This might not be a big deal when saving them in a file, since the worst thing that may happen is that the user losts his session and a new one is generated. But when saving to an DB it is*. One should never trust that the server itself add slashes and escapes other vital characters.
* A google search for "SQL Injection" gives 716 000 hits.
Example code, none working:
<?PHP
function read ($session_id)
{
$sql = mysql_query("SELECT * FROM mysessions WHERE session_id=$session_id");
$data= mysql_fetch_array($sql);
}
}
?>
Is obviously flawed. Since setting our session ID to "; drop mysessions; " would create serious problems.
A more suitable approch would be, something in the lines of:
Example code, none working:
<?PHP
function read ($session_id)
{
// ( Code by php.net )
if (get_magic_quotes_gpc()) {
$session_id = stripslashes($session_id);
}
// Quote if not integer
if (!is_numeric($session_id)) {
$session_id = mysql_real_escape_string($session_id);
}
$sql = mysql_query("SELECT * FROM mysessions WHERE session_id=$session_id");
$fieldarray = mysql_fetch_array($sql);
}
}
?>
I quick checked different sample codes and tutorials and none of them actually escaped session ids.
That's my two cents for too night,
Niklas
It seems when you call 'session_name()', php loads the session id automatically from GET ( if the index exists ) and passes it to the 'read' callback method correctly, but the 'write' callback is invoked twice: first the auto-generated session id, then the custom session id
So be aware of what queries you execute inside the callback .. I got crazy because I used a MySQL 'REPLACE' statement to agilize, and I spent a lot of hours trying to understand why 2 rows instead of 1 were being affected ( the first id was inserting, the second updating )
I hope this helps!
Session authentication is not meant to be part of the session handlers.
These handlers only read and write the session data itself, and will not allow you to call the vital function session_regenerate_id().
To add extra authentication routines, call them after session_start returns control. DO NOT put them in your 'open' or 'read' handlers.
Enjoy,
-- Miqro
For some people it might be important to know, that if the standard session handler has been overwritten with session_set_save_handler, no locking is working anymore (between session_read and session_write). The following might happen:
script "A" start .
read session data .
. script "B" start
. read session data
running (30secs) add session data
. write sesion data
. script "B" stop
write session data .
script "A" stop .
If a script "A" runs for a long time (say 30secs) the same user might start another script "B", which also uses a session. Script "B" will start and read session data, even though script "A" is still running. Because script "B" is much faster it will finish its work and write back its session data before script "A" has ended. Now script "A" ends and overwrites all of script "B"'s session data. If you DON'T use session_set_save_handler, this cannot happend, because in this case, PHP will not start script "B" until script "A" ends.
If a session is closed the save-handlers seem to be resetted (in PHP 4.1.2 they are), so you need to run session_set_save_handler() again after e.g. running session_write_close() and restarting the session with session_start();
This is an LDAP implementation that I wrote which works just fine for me. This assumes you've got some global variables with your host and other info. It also uses a custom error function which I have not defined here. Also, it assumes your LDAP server is set up with correct permissions, objectclass, and all that. I used an octet string in LDAP to hold the session data. I had to manually wrap some of this so be careful with cutting and pasting.
<?php
$sessconn=null;
//Custom session handling stored in local LDAP
function ldap_sess_open($save_path,$session_name) {
global $infohost,$infoport,$infodn,$infopw,$sessconn;
$sessconn=@ldap_connect($infohost,$infoport);
if(!@ldap_bind($sessconn,$infodn,$infopw)) {
setError("Failed to open session.");
return false;
}
return true;
}
function ldap_sess_close() {
global $sessconn;
@ldap_close($sessconn);
return true;
}
function ldap_sess_read($id) {
global $sessconn;
$sr=@ldap_search($sessconn,'ou=sessions,o=Company',
"(cn=$id)");
$info=@ldap_get_entries($sessconn,$sr);
if($info['count']>0)
return $info[0]['session'][0];
else
return "";
}
function ldap_sess_write($id,$sess_data) {
global $sessconn;
$update=array();
$update['objectClass']=array('phpsession','top');
$update['session']=$sess_data;
$dn="cn=$id,ou=sessions,o=Company";
@ldap_delete($sessconn,$dn);
@ldap_add($sessconn,$dn,$update);
return true;
}
function ldap_sess_destroy($id) {
global $sessconn;
$dn="cn=$id,ou=sessions,o=Company";
@ldap_delete($sessconn,$dn);
return true;
}
function ldap_sess_gc($maxlifetime) {
global $sessconn;
$sr=@ldap_search($sessconn,'ou=sessions,o=Company',
'(objectClass=phpsession)',array('+','cn'));
$info=@ldap_get_entries($sessconn,$sr);
if($info['count']>0) {
for($i=0;$i<$info['count'];$i++) {
$id=$info[$i]['cn'][0];
$dn="cn=$id,ou=sessions,o=Company";
$modified=stamp2local($info[$i]['modifytimestamp'][0]);
if((time()-$modified)>=$maxlifetime)
@ldap_delete($sessconn,$dn);
}
}
return true;
}
//converts my LDAP timestamps over to Unix timestamps
function stamp2local($ldapstamp) {
$year=substr($ldapstamp,0,4);
$month=substr($ldapstamp,4,2);
$day=substr($ldapstamp,6,2);
$hour=substr($ldapstamp,8,2);
$minute=substr($ldapstamp,10,2);
$stamp=gmmktime($hour,$minute,0,$month,$day,$year);
return $stamp;
}
//Use this so that if your LDAP server goes down people can still
//retain sessions the normal way
$checkds=@ldap_connect($infohost,$infoport);
if(@ldap_bind($checkds,$infodn,$infopw)) {
session_set_save_handler("ldap_sess_open","ldap_sess_close",
"ldap_sess_read","ldap_sess_write","ldap_sess_destroy",
"ldap_sess_gc");
}
@ldap_close($checkds);
?>
I found the need to have a garbage collection function run with my sessions so I had to write my own session handler.
I took the example code above but found that it had a problem, it does not open the session file exclusively.
This means that if you have a frameset where 2 pages are accessing the session at the same time and both modify the session only the last page to finish processing will have its session data saved. The first page will have all of its session data overwritten.
So here is a modified version of a file session handler in PHP that has file locking. (Not supported on FAT or NFS apparently)
<?php
$sess_save_path = $sess_session_name = $fp=null;
function open ($save_path, $session_name) {
global $sess_save_path, $sess_session_name;
$sess_save_path = $save_path;
$sess_session_name = $session_name;
return(true);
}
function close() {
global $fp;
flock($fp, LOCK_UN);
fclose($fp);
$fp=null;
return(true);
}
function read ($id) {
global $sess_save_path, $sess_session_name, $fp;
$sess_file = "$sess_save_path/sess_$id";
if ($fp = @fopen($sess_file, "r+")) {
flock($fp, LOCK_EX);
$sess_data = fread($fp, filesize($sess_file));
return($sess_data);
} else {
return(""); // Must return "" here.
}
}
function write ($id, $sess_data) {
global $sess_save_path, $sess_session_name, $fp;
$sess_file = "$sess_save_path/sess_$id";
if (!empty($fp)) {
fseek($fp,0);
return(fwrite($fp, $sess_data));
} elseif ($fp = @fopen($sess_file, "w")) {
flock($fp, LOCK_EX);
return(fwrite($fp, $sess_data));
} else {
return(false);
}
}
function destroy ($id) {
global $sess_save_path, $sess_session_name;
$sess_file = "$sess_save_path/sess_$id";
return(@unlink($sess_file));
}
function gc ($maxlifetime) {
global $sess_save_path, $sess_session_name;
$sess_file = "$sess_save_path/sess_$id";
return true;
}
session_set_save_handler ("open", "close", "read", "write", "destroy", "gc");
?>
enjoy
When using mySQL for your session handling functions, don't forget to call mysql_select_db() to change the database if you are using a separate database for your session data. Call mysql_select_db() INSIDE every handler function that accesses the database, since if you write session data after accessing another database, it will not change the database to your session database, and therefore, not write the session data.
Just a few words to explain some troubles while using session_set_save_handler(). It appears that internally PHP calls session management functions in this order: open(), read(), write(), close(). Close() function is called even if you does not make a call to sesison_start(), perhaps for some reasons like cleaning.
If you try to redefine these functions and call sessions_set_save_handler() but something doesn't work, (in my case the ssion data hasn't been written) it's a good idea to debug them in the order they are called. They doesn't produce error output to browser but you can use print or echo.
Shortly, if your write() function doesn't work as expected take a look for errors in previous functions - open() and read().
I hope that this will help to save someone few hours debugging.
You can't use the session autostart feature with
session.save_handler = user
set in your php.ini. Use instead the auto_prepend_file directive in the php.ini and point it to your save_handler with an session_start() at the end.
