]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/DbSession.php
support USE_SAFE_DBSESSION if duplicate sid (INSERT errors) occur
[SourceForge/phpwiki.git] / lib / DbSession.php
1 <?php rcs_id('$Id: DbSession.php,v 1.31 2005-02-05 15:32:59 rurban Exp $');
2
3 /**
4  * Store sessions data in Pear DB / ADODB / dba / ....
5  *
6  * History
7  *
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.
12  */
13 class DbSession
14 {
15     var $_backend;
16     /**
17      * Constructor
18      *
19      * @param mixed $dbh
20      * Pear DB handle, or WikiDB object (from which the Pear DB handle will
21      * be extracted.
22      *
23      * @param string $table
24      * Name of SQL table containing session data.
25      */
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;
34             
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')
39                 return false;
40                 
41             if (class_exists($class)) {
42                 $this->_backend = new $class($backend->_dbh, $table);
43                 return $this->_backend;
44             }
45         }
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);
50         return false;
51     }
52     
53     function currentSessions() {
54         return $this->_backend->currentSessions();
55     }
56     function query($sql) {
57         return $this->_backend->query($sql);
58     }
59     function quote($string) { return $string; }
60 }
61
62 class DbSession_SQL
63 extends DbSession
64 {
65     var $_backend_type = "SQL";
66
67     function DbSession_SQL (&$dbh, $table) {
68
69         $this->_dbh = $dbh;
70         $this->_table = $table;
71
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'),
79                                  array(&$this, 'gc'));
80         return $this;
81     }
82
83     function _connect() {
84         $dbh = &$this->_dbh;
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());
90             }
91         }
92         return $dbh;
93     }
94     
95     function query($sql) {
96         return $this->_dbh->query($sql);
97     }
98     // adds surrounding quotes
99     function quote($string) {
100         return $this->_dbh->quote($string);
101     }
102
103     function _disconnect() {
104         if (0 and $this->_connected)
105             $this->_dbh->disconnect();
106     }
107
108     /**
109      * Opens a session.
110      *
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 
115      * is good.
116      * @access private
117      */
118     function open ($save_path, $session_name) {
119         //$this->log("_open($save_path, $session_name)");
120         return true;
121     }
122
123     /**
124      * Closes a session.
125      *
126      * This function is called just after <i>write</i> call.
127      *
128      * @return boolean true just a variable to notify PHP that everything 
129      * is good.
130      * @access private
131      */
132     function close() {
133         //$this->log("_close()");
134         return true;
135     }
136
137     /**
138      * Reads the session data from DB.
139      *
140      * @param  string $id an id of current session
141      * @return string
142      * @access private
143      */
144     function read ($id) {
145         //$this->log("_read($id)");
146         $dbh = &$this->_connect();
147         $table = $this->_table;
148         $qid = $dbh->quote($id);
149     
150         $res = $dbh->getOne("SELECT sess_data FROM $table WHERE sess_id=$qid");
151
152         $this->_disconnect();
153         if (DB::isError($res) || empty($res))
154             return '';
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 = '';
164         }
165         return $res;
166     }
167   
168     /**
169      * Saves the session data into DB.
170      *
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.
177      *
178      * @param  string $id
179      * @param  string $sess_data
180      * @return boolean true if data saved successfully  and false
181      * otherwise.
182      * @access private
183      */
184     function write ($id, $sess_data) {
185         
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);
194         }
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);
199
200         /* AffectedRows with sessions seems to be instable on certain platforms.
201          * Enable the safe and slow USE_SAFE_DBSESSION then.
202          */
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)");
209         } else {
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)");
218             }
219         }
220         $this->_disconnect();
221         return ! DB::isError($res);
222     }
223
224     /**
225      * Destroys a session.
226      *
227      * Removes a session from the table.
228      *
229      * @param  string $id
230      * @return boolean true 
231      * @access private
232      */
233     function destroy ($id) {
234         $dbh = &$this->_connect();
235         $table = $this->_table;
236         $qid = $dbh->quote($id);
237
238         $dbh->query("DELETE FROM $table WHERE sess_id=$qid");
239
240         $this->_disconnect();
241         return true;     
242     }
243
244     /**
245      * Cleans out all expired sessions.
246      *
247      * @param  int $maxlifetime session's time to live.
248      * @return boolean true
249      * @access private
250      */
251     function gc ($maxlifetime) {
252         $dbh = &$this->_connect();
253         $table = $this->_table;
254         $threshold = time() - $maxlifetime;
255
256         $dbh->query("DELETE FROM $table WHERE sess_date < $threshold");
257
258         $this->_disconnect();
259         return true;
260     }
261
262     // WhoIsOnline support
263     // TODO: ip-accesstime dynamic blocking API
264     function currentSessions() {
265         $sessions = array();
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))
270             return $sessions;
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)
278                 $date = 0;
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
283                                 'date' => $date,
284                                 'ip'   => $ip);
285         }
286         $this->_disconnect();
287         return $sessions;
288     }
289 }
290
291 // self-written adodb-sessions
292 class DbSession_ADODB
293 extends DbSession
294 {
295     var $_backend_type = "ADODB";
296
297     function DbSession_ADODB ($dbh, $table) {
298
299         $this->_dbh = $dbh;
300         $this->_table = $table;
301
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'));
310         return $this;
311     }
312
313     function _connect() {
314         global $request;
315         static $parsed = false;
316         $dbh = &$this->_dbh;
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']);
322             $dbh = &$this->_dbh;                             
323         }
324         return $dbh;
325     }
326     
327     function query($sql) {
328         return $this->_dbh->Execute($sql);
329     }
330
331     function quote($string) {
332         return $this->_dbh->qstr($string);
333     }
334
335     function _disconnect() {
336         if (0 and $this->_dbh)
337             $this->_dbh->close();
338     }
339
340     /**
341      * Opens a session.
342      *
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 
347      * is good.
348      * @access private
349      */
350     function open ($save_path, $session_name) {
351         //$this->log("_open($save_path, $session_name)");
352         return true;
353     }
354
355     /**
356      * Closes a session.
357      *
358      * This function is called just after <i>write</i> call.
359      *
360      * @return boolean true just a variable to notify PHP that everything 
361      * is good.
362      * @access private
363      */
364     function close() {
365         //$this->log("_close()");
366         return true;
367     }
368
369     /**
370      * Reads the session data from DB.
371      *
372      * @param  string $id an id of current session
373      * @return string
374      * @access private
375      */
376     function read ($id) {
377         //$this->log("_read($id)");
378         $dbh = &$this->_connect();
379         $table = $this->_table;
380         $qid = $dbh->qstr($id);
381         $res = '';
382         $row = $dbh->GetRow("SELECT sess_data FROM $table WHERE sess_id=$qid");
383         if ($row)
384             $res = $row[0];
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 = '';
394         }
395         return $res;
396     }
397   
398     /**
399      * Saves the session data into DB.
400      *
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.
407      *
408      * @param  string $id
409      * @param  string $sess_data
410      * @return boolean true if data saved successfully  and false
411      * otherwise.
412      * @access private
413      */
414     function write ($id, $sess_data) {
415         
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());
421
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);
426
427         /* AffectedRows with sessions seems to be instable on certain platforms.
428          * Enable the safe and slow USE_SAFE_DBSESSION then.
429          */
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)");
436         } else {
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)");
445             }
446         }
447         $result = ! $rs->EOF;
448         if ($result) $rs->free();                        
449         $this->_disconnect();
450         return $result;
451     }
452
453     /**
454      * Destroys a session.
455      *
456      * Removes a session from the table.
457      *
458      * @param  string $id
459      * @return boolean true 
460      * @access private
461      */
462     function destroy ($id) {
463         $dbh = &$this->_connect();
464         $table = $this->_table;
465         $qid = $dbh->qstr($id);
466
467         $dbh->Execute("DELETE FROM $table WHERE sess_id=$qid");
468
469         $this->_disconnect();
470         return true;     
471     }
472
473     /**
474      * Cleans out all expired sessions.
475      *
476      * @param  int $maxlifetime session's time to live.
477      * @return boolean true
478      * @access private
479      */
480     function gc ($maxlifetime) {
481         $dbh = &$this->_connect();
482         $table = $this->_table;
483         $threshold = time() - $maxlifetime;
484
485         $dbh->Execute("DELETE FROM $table WHERE sess_date < $threshold");
486
487         $this->_disconnect();
488         return true;
489     }
490
491     // WhoIsOnline support. 
492     // TODO: ip-accesstime dynamic blocking API
493     function currentSessions() {
494         $sessions = array();
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");
498         if ($rs->EOF) {
499             $rs->free();
500             return $sessions;
501         }
502         while (!$rs->EOF) {
503             $row = $rs->fetchRow();
504             $data = $row[0];
505             $date = $row[1];
506             $ip   = $row[2];
507             if (preg_match('|^[a-zA-Z0-9/+=]+$|', $data))
508                 $data = base64_decode($data);
509             if ($date < 908437560 or $date > 1588437560)
510                 $date = 0;
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
515                                 'date' => $date,
516                                 'ip' => $ip);
517             $rs->MoveNext();
518         }
519         $rs->free();
520         $this->_disconnect();
521         return $sessions;
522     }
523 }
524
525 /** DBA Sessions
526  *  session:
527  *    Index: session_id
528  *   Values: date : IP : data
529  */
530 class DbSession_dba
531 extends DbSession
532 {
533     var $_backend_type = "dba";
534
535     function DbSession_dba (&$dbh, $table) {
536         $this->_dbh = $dbh;
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'));
545         return $this;
546     }
547
548     function quote($str) { return $str; }
549     function query($sql) { return false; }
550
551     function _connect() {
552         global $DBParams;
553         $dbh = &$this->_dbh;
554         if (!$dbh) {
555             $directory = '/tmp';
556             $prefix = 'wiki_';
557             $dba_handler = 'gdbm';
558             $timeout = 20;
559             extract($DBParams);
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);
565                 global $request;
566                 $request->finish(fmt("%s: Can't open dba database", $dbfile));
567             }
568             $this->_dbh = &$dbh;
569         }
570         return $dbh;
571     }
572
573     function _disconnect() {
574         if (0 and isset($this->_dbh))
575             $this->_dbh->close();
576     }
577
578     function open ($save_path, $session_name) {
579         $dbh = &$this->_connect();
580         $dbh->open();
581     }
582
583     function close() {
584         if ($this->_dbh)
585             $this->_dbh->close();
586     }
587
588     function read ($id) {
589         $dbh = &$this->_connect();
590         $result = $dbh->get($id);
591         if (!$result) {
592             return false;
593         }
594         list(,,$packed) = explode(':', $result, 3);
595         $this->_disconnect();
596         if (strlen($packed) > 4000) {
597             trigger_error("Overlarge session data!", E_USER_WARNING);
598             $packed = '';
599             //$res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
600         }
601         return $packed;
602     }
603   
604     function write ($id, $sess_data) {
605         $dbh = &$this->_connect();
606         $time = time();
607         $ip = $GLOBALS['request']->get('REMOTE_ADDR');
608         if (strlen($sess_data) > 4000) {
609             trigger_error("Overlarge session data!", E_USER_WARNING);
610             $sess_data = '';
611         }
612         $dbh->set($id,$time.':'.$ip.':'.$sess_data);
613         $this->_disconnect();
614         return true;
615     }
616
617     function destroy ($id) {
618         $dbh = &$this->_connect();
619         $dbh->delete($id);
620         $this->_disconnect();
621         return true;
622     }
623
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)
632                 $dbh->delete($id);
633         }
634         $this->_disconnect();
635         return true;
636     }
637
638     // WhoIsOnline support. 
639     // TODO: ip-accesstime dynamic blocking API
640     function currentSessions() {
641         $sessions = array();
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)
651                 $date = 0;
652             $user = strstr($packed, "wiki_user|");
653             $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
654                                 'date' => $date,
655                                 'ip' => $ip);
656         }
657         $this->_disconnect();
658         return $sessions;
659     }
660 }
661
662
663 // Local Variables:
664 // mode: php
665 // tab-width: 8
666 // c-basic-offset: 4
667 // c-hanging-comment-ender-p: nil
668 // indent-tabs-mode: nil
669 // End:
670 ?>