1 <?php rcs_id('$Id: DbSession.php,v 1.31 2005-02-05 15:32:59 rurban Exp $');
4 * Store sessions data in Pear DB / ADODB / dba / ....
8 * Originally by Stanislav Shramko <stanis@movingmail.com>
9 * Minor rewrite by Reini Urban <rurban@x-ray.at> for Phpwiki.
10 * Quasi-major rewrite/decruft/fix by Jeff Dairiki <dairiki@dairiki.org>.
11 * ADODB and dba classes by Reini Urban.
20 * Pear DB handle, or WikiDB object (from which the Pear DB handle will
23 * @param string $table
24 * Name of SQL table containing session data.
26 function DbSession(&$dbh, $table = 'session') {
27 // Coerce WikiDB to PearDB or ADODB.
28 // Todo: adodb/dba handlers
29 $db_type = $dbh->getParam('dbtype');
30 if (isa($dbh, 'WikiDB')) {
31 $backend = &$dbh->_backend;
32 $db_type = substr(get_class($dbh),7);
33 $class = "DbSession_".$db_type;
35 // < 4.1.2 crash on dba sessions at session_write_close().
36 // (Tested with 4.1.1 and 4.1.2)
37 // Didn't try postgres sessions.
38 if (!check_php_version(4,1,2) and $db_type == 'dba')
41 if (class_exists($class)) {
42 $this->_backend = new $class($backend->_dbh, $table);
43 return $this->_backend;
46 //Fixme: E_USER_WARNING ignored!
47 trigger_error(sprintf(_("Your WikiDB DB backend '%s' cannot be used for DbSession.")." ".
48 _("Set USE_DB_SESSION to false."),
49 $db_type), E_USER_WARNING);
53 function currentSessions() {
54 return $this->_backend->currentSessions();
56 function query($sql) {
57 return $this->_backend->query($sql);
59 function quote($string) { return $string; }
65 var $_backend_type = "SQL";
67 function DbSession_SQL (&$dbh, $table) {
70 $this->_table = $table;
72 ini_set('session.save_handler','user');
73 session_module_name('user'); // new style
74 session_set_save_handler(array(&$this, 'open'),
75 array(&$this, 'close'),
76 array(&$this, 'read'),
77 array(&$this, 'write'),
78 array(&$this, 'destroy'),
85 $this->_connected = is_resource($dbh->connection);
86 if (!$this->_connected) {
87 $res = $dbh->connect($dbh->dsn);
88 if (DB::isError($res)) {
89 error_log("PhpWiki::DbSession::_connect: " . $res->getMessage());
95 function query($sql) {
96 return $this->_dbh->query($sql);
98 // adds surrounding quotes
99 function quote($string) {
100 return $this->_dbh->quote($string);
103 function _disconnect() {
104 if (0 and $this->_connected)
105 $this->_dbh->disconnect();
111 * Actually this function is a fake for session_set_save_handle.
112 * @param string $save_path a path to stored files
113 * @param string $session_name a name of the concrete file
114 * @return boolean true just a variable to notify PHP that everything
118 function open ($save_path, $session_name) {
119 //$this->log("_open($save_path, $session_name)");
126 * This function is called just after <i>write</i> call.
128 * @return boolean true just a variable to notify PHP that everything
133 //$this->log("_close()");
138 * Reads the session data from DB.
140 * @param string $id an id of current session
144 function read ($id) {
145 //$this->log("_read($id)");
146 $dbh = &$this->_connect();
147 $table = $this->_table;
148 $qid = $dbh->quote($id);
150 $res = $dbh->getOne("SELECT sess_data FROM $table WHERE sess_id=$qid");
152 $this->_disconnect();
153 if (DB::isError($res) || empty($res))
155 if (isa($dbh, 'DB_pgsql'))
156 //if (preg_match('|^[a-zA-Z0-9/+=]+$|', $res))
157 $res = base64_decode($res);
158 if (strlen($res) > 4000) {
159 trigger_error("Overlarge session data! ".strlen($res).
160 " gt. 4000", E_USER_WARNING);
161 $res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
162 $res = preg_replace('/s:12:"_cached_html";s:.+",s:4:"hits"/','s:4:"hits"',$res);
163 if (strlen($res) > 4000) $res = '';
169 * Saves the session data into DB.
171 * Just a comment: The "write" handler is not
172 * executed until after the output stream is closed. Thus,
173 * output from debugging statements in the "write" handler
174 * will never be seen in the browser. If debugging output
175 * is necessary, it is suggested that the debug output be
176 * written to a file instead.
179 * @param string $sess_data
180 * @return boolean true if data saved successfully and false
184 function write ($id, $sess_data) {
186 $dbh = &$this->_connect();
187 //$dbh->unlock(false,1);
188 $table = $this->_table;
189 $qid = $dbh->quote($id);
190 $qip = $dbh->quote($GLOBALS['request']->get('REMOTE_ADDR'));
191 $time = $dbh->quote(time());
192 if (DEBUG and $sess_data == 'wiki_user|N;') {
193 trigger_error("delete empty session $qid", E_USER_WARNING);
195 // postgres can't handle binary data in a TEXT field.
196 if (isa($dbh, 'DB_pgsql'))
197 $sess_data = base64_encode($sess_data);
198 $qdata = $dbh->quote($sess_data);
200 /* AffectedRows with sessions seems to be instable on certain platforms.
201 * Enable the safe and slow USE_SAFE_DBSESSION then.
203 if (USE_SAFE_DBSESSION) {
204 $dbh->query("DELETE FROM $table"
205 . " WHERE sess_id=$qid");
206 $res = $dbh->query("INSERT INTO $table"
207 . " (sess_id, sess_data, sess_date, sess_ip)"
208 . " VALUES ($qid, $qdata, $time, $qip)");
210 $res = $dbh->query("UPDATE $table"
211 . " SET sess_data=$qdata, sess_date=$time, sess_ip=$qip"
212 . " WHERE sess_id=$qid");
213 $result = $dbh->AffectedRows();
214 if ( $result === false or $result < 1 ) { // 0 cannot happen: time, -1 (failure) on mysql
215 $res = $dbh->query("INSERT INTO $table"
216 . " (sess_id, sess_data, sess_date, sess_ip)"
217 . " VALUES ($qid, $qdata, $time, $qip)");
220 $this->_disconnect();
221 return ! DB::isError($res);
225 * Destroys a session.
227 * Removes a session from the table.
230 * @return boolean true
233 function destroy ($id) {
234 $dbh = &$this->_connect();
235 $table = $this->_table;
236 $qid = $dbh->quote($id);
238 $dbh->query("DELETE FROM $table WHERE sess_id=$qid");
240 $this->_disconnect();
245 * Cleans out all expired sessions.
247 * @param int $maxlifetime session's time to live.
248 * @return boolean true
251 function gc ($maxlifetime) {
252 $dbh = &$this->_connect();
253 $table = $this->_table;
254 $threshold = time() - $maxlifetime;
256 $dbh->query("DELETE FROM $table WHERE sess_date < $threshold");
258 $this->_disconnect();
262 // WhoIsOnline support
263 // TODO: ip-accesstime dynamic blocking API
264 function currentSessions() {
266 $dbh = &$this->_connect();
267 $table = $this->_table;
268 $res = $dbh->query("SELECT sess_data,sess_date,sess_ip FROM $table ORDER BY sess_date DESC");
269 if (DB::isError($res) || empty($res))
271 while ($row = $res->fetchRow()) {
272 $data = $row['sess_data'];
273 $date = $row['sess_date'];
274 $ip = $row['sess_ip'];
275 if (preg_match('|^[a-zA-Z0-9/+=]+$|', $data))
276 $data = base64_decode($data);
277 if ($date < 908437560 or $date > 1588437560)
279 // session_data contains the <variable name> + "|" + <packed string>
280 // we need just the wiki_user object (might be array as well)
281 $user = strstr($data,"wiki_user|");
282 $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
286 $this->_disconnect();
291 // self-written adodb-sessions
292 class DbSession_ADODB
295 var $_backend_type = "ADODB";
297 function DbSession_ADODB ($dbh, $table) {
300 $this->_table = $table;
302 ini_set('session.save_handler','user');
303 session_module_name('user'); // new style
304 session_set_save_handler(array(&$this, 'open'),
305 array(&$this, 'close'),
306 array(&$this, 'read'),
307 array(&$this, 'write'),
308 array(&$this, 'destroy'),
309 array(&$this, 'gc'));
313 function _connect() {
315 static $parsed = false;
317 if (!$dbh or !is_resource($dbh->_connectionID)) {
318 if (!$parsed) $parsed = parseDSN($request->_dbi->getParam('dsn'));
319 $this->_dbh = &ADONewConnection($parsed['phptype']); // Probably only MySql works just now
320 $this->_dbh->Connect($parsed['hostspec'],$parsed['username'],
321 $parsed['password'], $parsed['database']);
327 function query($sql) {
328 return $this->_dbh->Execute($sql);
331 function quote($string) {
332 return $this->_dbh->qstr($string);
335 function _disconnect() {
336 if (0 and $this->_dbh)
337 $this->_dbh->close();
343 * Actually this function is a fake for session_set_save_handle.
344 * @param string $save_path a path to stored files
345 * @param string $session_name a name of the concrete file
346 * @return boolean true just a variable to notify PHP that everything
350 function open ($save_path, $session_name) {
351 //$this->log("_open($save_path, $session_name)");
358 * This function is called just after <i>write</i> call.
360 * @return boolean true just a variable to notify PHP that everything
365 //$this->log("_close()");
370 * Reads the session data from DB.
372 * @param string $id an id of current session
376 function read ($id) {
377 //$this->log("_read($id)");
378 $dbh = &$this->_connect();
379 $table = $this->_table;
380 $qid = $dbh->qstr($id);
382 $row = $dbh->GetRow("SELECT sess_data FROM $table WHERE sess_id=$qid");
385 $this->_disconnect();
386 if (!empty($res) and preg_match('|^[a-zA-Z0-9/+=]+$|', $res))
387 $res = base64_decode($res);
388 if (strlen($res) > 4000) {
389 trigger_error("Overlarge session data! ".strlen($res).
390 " gt. 4000", E_USER_WARNING);
391 $res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
392 $res = preg_replace('/s:12:"_cached_html";s:.+",s:4:"hits"/','s:4:"hits"',$res);
393 if (strlen($res) > 4000) $res = '';
399 * Saves the session data into DB.
401 * Just a comment: The "write" handler is not
402 * executed until after the output stream is closed. Thus,
403 * output from debugging statements in the "write" handler
404 * will never be seen in the browser. If debugging output
405 * is necessary, it is suggested that the debug output be
406 * written to a file instead.
409 * @param string $sess_data
410 * @return boolean true if data saved successfully and false
414 function write ($id, $sess_data) {
416 $dbh = &$this->_connect();
417 $table = $this->_table;
418 $qid = $dbh->qstr($id);
419 $qip = $dbh->qstr($GLOBALS['request']->get('REMOTE_ADDR'));
420 $time = $dbh->qstr(time());
422 // postgres can't handle binary data in a TEXT field.
423 if (isa($dbh, 'ADODB_postgres64'))
424 $sess_data = base64_encode($sess_data);
425 $qdata = $dbh->qstr($sess_data);
427 /* AffectedRows with sessions seems to be instable on certain platforms.
428 * Enable the safe and slow USE_SAFE_DBSESSION then.
430 if (USE_SAFE_DBSESSION) {
431 $dbh->Execute("DELETE FROM $table"
432 . " WHERE sess_id=$qid");
433 $rs = $dbh->Execute("INSERT INTO $table"
434 . " (sess_id, sess_data, sess_date, sess_ip)"
435 . " VALUES ($qid, $qdata, $time, $qip)");
437 $rs = $dbh->Execute("UPDATE $table"
438 . " SET sess_data=$qdata, sess_date=$time, sess_ip=$qip"
439 . " WHERE sess_id=$qid");
440 $result = $dbh->Affected_Rows();
441 if ( $result === false or $result < 1 ) { // false or int > 0
442 $rs = $dbh->Execute("INSERT INTO $table"
443 . " (sess_id, sess_data, sess_date, sess_ip)"
444 . " VALUES ($qid, $qdata, $time, $qip)");
447 $result = ! $rs->EOF;
448 if ($result) $rs->free();
449 $this->_disconnect();
454 * Destroys a session.
456 * Removes a session from the table.
459 * @return boolean true
462 function destroy ($id) {
463 $dbh = &$this->_connect();
464 $table = $this->_table;
465 $qid = $dbh->qstr($id);
467 $dbh->Execute("DELETE FROM $table WHERE sess_id=$qid");
469 $this->_disconnect();
474 * Cleans out all expired sessions.
476 * @param int $maxlifetime session's time to live.
477 * @return boolean true
480 function gc ($maxlifetime) {
481 $dbh = &$this->_connect();
482 $table = $this->_table;
483 $threshold = time() - $maxlifetime;
485 $dbh->Execute("DELETE FROM $table WHERE sess_date < $threshold");
487 $this->_disconnect();
491 // WhoIsOnline support.
492 // TODO: ip-accesstime dynamic blocking API
493 function currentSessions() {
495 $dbh = &$this->_connect();
496 $table = $this->_table;
497 $rs = $dbh->Execute("SELECT sess_data,sess_date,sess_ip FROM $table ORDER BY sess_date DESC");
503 $row = $rs->fetchRow();
507 if (preg_match('|^[a-zA-Z0-9/+=]+$|', $data))
508 $data = base64_decode($data);
509 if ($date < 908437560 or $date > 1588437560)
511 // session_data contains the <variable name> + "|" + <packed string>
512 // we need just the wiki_user object (might be array as well)
513 $user = strstr($data,"wiki_user|");
514 $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
520 $this->_disconnect();
528 * Values: date : IP : data
533 var $_backend_type = "dba";
535 function DbSession_dba (&$dbh, $table) {
537 ini_set('session.save_handler','user');
538 session_module_name('user'); // new style
539 session_set_save_handler(array(&$this, 'open'),
540 array(&$this, 'close'),
541 array(&$this, 'read'),
542 array(&$this, 'write'),
543 array(&$this, 'destroy'),
544 array(&$this, 'gc'));
548 function quote($str) { return $str; }
549 function query($sql) { return false; }
551 function _connect() {
557 $dba_handler = 'gdbm';
560 $dbfile = "$directory/$prefix" . 'session' . '.' . $dba_handler;
561 $dbh = new DbaDatabase($dbfile, false, $dba_handler);
562 $dbh->set_timeout($timeout);
563 if (!$dbh->open('c')) {
564 trigger_error(sprintf(_("%s: Can't open dba database"), $dbfile), E_USER_ERROR);
566 $request->finish(fmt("%s: Can't open dba database", $dbfile));
573 function _disconnect() {
574 if (0 and isset($this->_dbh))
575 $this->_dbh->close();
578 function open ($save_path, $session_name) {
579 $dbh = &$this->_connect();
585 $this->_dbh->close();
588 function read ($id) {
589 $dbh = &$this->_connect();
590 $result = $dbh->get($id);
594 list(,,$packed) = explode(':', $result, 3);
595 $this->_disconnect();
596 if (strlen($packed) > 4000) {
597 trigger_error("Overlarge session data!", E_USER_WARNING);
599 //$res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
604 function write ($id, $sess_data) {
605 $dbh = &$this->_connect();
607 $ip = $GLOBALS['request']->get('REMOTE_ADDR');
608 if (strlen($sess_data) > 4000) {
609 trigger_error("Overlarge session data!", E_USER_WARNING);
612 $dbh->set($id,$time.':'.$ip.':'.$sess_data);
613 $this->_disconnect();
617 function destroy ($id) {
618 $dbh = &$this->_connect();
620 $this->_disconnect();
624 function gc ($maxlifetime) {
625 $dbh = &$this->_connect();
626 $threshold = time() - $maxlifetime;
627 for ($id = $dbh->firstkey(); $id !== false; $id = $dbh->nextkey()) {
628 $result = $dbh->get($id);
629 list($date,,) = explode(':', $result, 3);
630 //$dbh->query("DELETE FROM $table WHERE sess_date < $threshold");
631 if ($date < $threshold)
634 $this->_disconnect();
638 // WhoIsOnline support.
639 // TODO: ip-accesstime dynamic blocking API
640 function currentSessions() {
642 $dbh = &$this->_connect();
643 for ($id = $dbh->firstkey(); $id !== false; $id = $dbh->nextkey()) {
644 $result = $dbh->get($id);
645 list($date,$ip,$packed) = explode(':', $result, 3);
646 if (!$packed) continue;
647 //$data = @unserialize($packed);
648 // session_data contains the <variable name> + "|" + <packed string>
649 // we need just the wiki_user object (might be array as well)
650 if ($date < 908437560 or $date > 1588437560)
652 $user = strstr($packed, "wiki_user|");
653 $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
657 $this->_disconnect();
667 // c-hanging-comment-ender-p: nil
668 // indent-tabs-mode: nil