]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/DbSession.php
clarify affected rows problem
[SourceForge/phpwiki.git] / lib / DbSession.php
1 <?php rcs_id('$Id: DbSession.php,v 1.29 2005-01-30 23:08:22 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 = '';
162             $res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
163             $res = preg_replace('/s:12:"_cached_html";s:.+",s:4:"hits"/','s:4:"hits"',$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             /*echo "<pre>";
195             print_r($GLOBALS['request']->_user);
196             echo "</pre>";
197             */
198         }
199         // postgres can't handle binary data in a TEXT field.
200         if (isa($dbh, 'DB_pgsql'))
201             $sess_data = base64_encode($sess_data);
202         $qdata = $dbh->quote($sess_data);
203         
204         $res = $dbh->query("UPDATE $table"
205                            . " SET sess_data=$qdata, sess_date=$time, sess_ip=$qip"
206                            . " WHERE sess_id=$qid");
207         $result = $dbh->Affected_Rows();
208         if ( $result === false or $result < 1 ) { // 0 cannot happen, -1 (failure) on mysql
209             $res = $dbh->query("INSERT INTO $table"
210                                . " (sess_id, sess_data, sess_date, sess_ip)"
211                                . " VALUES ($qid, $qdata, $time, $qip)");
212         }
213         $this->_disconnect();
214         return ! DB::isError($res);
215     }
216
217     /**
218      * Destroys a session.
219      *
220      * Removes a session from the table.
221      *
222      * @param  string $id
223      * @return boolean true 
224      * @access private
225      */
226     function destroy ($id) {
227         $dbh = &$this->_connect();
228         $table = $this->_table;
229         $qid = $dbh->quote($id);
230
231         $dbh->query("DELETE FROM $table WHERE sess_id=$qid");
232
233         $this->_disconnect();
234         return true;     
235     }
236
237     /**
238      * Cleans out all expired sessions.
239      *
240      * @param  int $maxlifetime session's time to live.
241      * @return boolean true
242      * @access private
243      */
244     function gc ($maxlifetime) {
245         $dbh = &$this->_connect();
246         $table = $this->_table;
247         $threshold = time() - $maxlifetime;
248
249         $dbh->query("DELETE FROM $table WHERE sess_date < $threshold");
250
251         $this->_disconnect();
252         return true;
253     }
254
255     // WhoIsOnline support
256     // TODO: ip-accesstime dynamic blocking API
257     function currentSessions() {
258         $sessions = array();
259         $dbh = &$this->_connect();
260         $table = $this->_table;
261         $res = $dbh->query("SELECT sess_data,sess_date,sess_ip FROM $table ORDER BY sess_date DESC");
262         if (DB::isError($res) || empty($res))
263             return $sessions;
264         while ($row = $res->fetchRow()) {
265             $data = $row['sess_data'];
266             $date = $row['sess_date'];
267             $ip   = $row['sess_ip'];
268             if (preg_match('|^[a-zA-Z0-9/+=]+$|', $data))
269                 $data = base64_decode($data);
270             if ($date < 908437560 or $date > 1588437560)
271                 $date = 0;
272             // session_data contains the <variable name> + "|" + <packed string>
273             // we need just the wiki_user object (might be array as well)
274             $user = strstr($data,"wiki_user|");
275             $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
276                                 'date' => $date,
277                                 'ip'   => $ip);
278         }
279         $this->_disconnect();
280         return $sessions;
281     }
282 }
283
284 // self-written adodb-sessions
285 class DbSession_ADODB
286 extends DbSession
287 {
288     var $_backend_type = "ADODB";
289
290     function DbSession_ADODB ($dbh, $table) {
291
292         $this->_dbh = $dbh;
293         $this->_table = $table;
294
295         ini_set('session.save_handler','user');
296         session_module_name('user'); // new style
297         session_set_save_handler(array(&$this, 'open'),
298                                  array(&$this, 'close'),
299                                  array(&$this, 'read'),
300                                  array(&$this, 'write'),
301                                  array(&$this, 'destroy'),
302                                  array(&$this, 'gc'));
303         return $this;
304     }
305
306     function _connect() {
307         global $request;
308         static $parsed = false;
309         $dbh = &$this->_dbh;
310         if (!$dbh or !is_resource($dbh->_connectionID)) {
311             if (!$parsed) $parsed = parseDSN($request->_dbi->getParam('dsn'));
312             $this->_dbh = &ADONewConnection($parsed['phptype']); // Probably only MySql works just now
313             $this->_dbh->Connect($parsed['hostspec'],$parsed['username'], 
314                                  $parsed['password'], $parsed['database']);
315             $dbh = &$this->_dbh;                             
316         }
317         return $dbh;
318     }
319     
320     function query($sql) {
321         return $this->_dbh->Execute($sql);
322     }
323
324     function quote($string) {
325         return $this->_dbh->qstr($string);
326     }
327
328     function _disconnect() {
329         if (0 and $this->_dbh)
330             $this->_dbh->close();
331     }
332
333     /**
334      * Opens a session.
335      *
336      * Actually this function is a fake for session_set_save_handle.
337      * @param  string $save_path a path to stored files
338      * @param  string $session_name a name of the concrete file
339      * @return boolean true just a variable to notify PHP that everything 
340      * is good.
341      * @access private
342      */
343     function open ($save_path, $session_name) {
344         //$this->log("_open($save_path, $session_name)");
345         return true;
346     }
347
348     /**
349      * Closes a session.
350      *
351      * This function is called just after <i>write</i> call.
352      *
353      * @return boolean true just a variable to notify PHP that everything 
354      * is good.
355      * @access private
356      */
357     function close() {
358         //$this->log("_close()");
359         return true;
360     }
361
362     /**
363      * Reads the session data from DB.
364      *
365      * @param  string $id an id of current session
366      * @return string
367      * @access private
368      */
369     function read ($id) {
370         //$this->log("_read($id)");
371         $dbh = &$this->_connect();
372         $table = $this->_table;
373         $qid = $dbh->qstr($id);
374         $res = '';
375         $row = $dbh->GetRow("SELECT sess_data FROM $table WHERE sess_id=$qid");
376         if ($row)
377             $res = $row[0];
378         $this->_disconnect();
379         if (!empty($res) and preg_match('|^[a-zA-Z0-9/+=]+$|', $res))
380             $res = base64_decode($res);
381         if (strlen($res) > 4000) {
382             trigger_error("Overlarge session data! ".strlen($res).
383                         " gt. 4000", E_USER_WARNING);
384             //$res = '';
385             $res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
386             $res = preg_replace('/s:12:"_cached_html";s:.+",s:4:"hits"/','s:4:"hits"',$res);
387         }
388         return $res;
389     }
390   
391     /**
392      * Saves the session data into DB.
393      *
394      * Just  a  comment:       The  "write"  handler  is  not 
395      * executed until after the output stream is closed. Thus,
396      * output from debugging statements in the "write" handler
397      * will  never be seen in the browser. If debugging output
398      * is  necessary, it is suggested that the debug output be
399      * written to a file instead.
400      *
401      * @param  string $id
402      * @param  string $sess_data
403      * @return boolean true if data saved successfully  and false
404      * otherwise.
405      * @access private
406      */
407     function write ($id, $sess_data) {
408         
409         $dbh = &$this->_connect();
410         $table = $this->_table;
411         $qid = $dbh->qstr($id);
412         $qip = $dbh->qstr($GLOBALS['request']->get('REMOTE_ADDR'));
413         $time = $dbh->qstr(time());
414
415         // postgres can't handle binary data in a TEXT field.
416         if (isa($dbh, 'ADODB_postgres64'))
417             $sess_data = base64_encode($sess_data);
418         $qdata = $dbh->qstr($sess_data);
419         $rs = $dbh->Execute("UPDATE $table"
420                            . " SET sess_data=$qdata, sess_date=$time, sess_ip=$qip"
421                            . " WHERE sess_id=$qid");
422         $result = $dbh->Affected_Rows();
423         if ( $result === false or $result < 1 ) { // false or int > 0
424             $rs = $dbh->Execute("INSERT INTO $table"
425                                . " (sess_id, sess_data, sess_date, sess_ip)"
426                                . " VALUES ($qid, $qdata, $time, $qip)");
427         }
428         $result = ! $rs->EOF;
429         if ($result) $rs->free();                        
430         $this->_disconnect();
431         return $result;
432     }
433
434     /**
435      * Destroys a session.
436      *
437      * Removes a session from the table.
438      *
439      * @param  string $id
440      * @return boolean true 
441      * @access private
442      */
443     function destroy ($id) {
444         $dbh = &$this->_connect();
445         $table = $this->_table;
446         $qid = $dbh->qstr($id);
447
448         $dbh->Execute("DELETE FROM $table WHERE sess_id=$qid");
449
450         $this->_disconnect();
451         return true;     
452     }
453
454     /**
455      * Cleans out all expired sessions.
456      *
457      * @param  int $maxlifetime session's time to live.
458      * @return boolean true
459      * @access private
460      */
461     function gc ($maxlifetime) {
462         $dbh = &$this->_connect();
463         $table = $this->_table;
464         $threshold = time() - $maxlifetime;
465
466         $dbh->Execute("DELETE FROM $table WHERE sess_date < $threshold");
467
468         $this->_disconnect();
469         return true;
470     }
471
472     // WhoIsOnline support. 
473     // TODO: ip-accesstime dynamic blocking API
474     function currentSessions() {
475         $sessions = array();
476         $dbh = &$this->_connect();
477         $table = $this->_table;
478         $rs = $dbh->Execute("SELECT sess_data,sess_date,sess_ip FROM $table ORDER BY sess_date DESC");
479         if ($rs->EOF) {
480             $rs->free();
481             return $sessions;
482         }
483         while (!$rs->EOF) {
484             $row = $rs->fetchRow();
485             $data = $row[0];
486             $date = $row[1];
487             $ip   = $row[2];
488             if (preg_match('|^[a-zA-Z0-9/+=]+$|', $data))
489                 $data = base64_decode($data);
490             if ($date < 908437560 or $date > 1588437560)
491                 $date = 0;
492             // session_data contains the <variable name> + "|" + <packed string>
493             // we need just the wiki_user object (might be array as well)
494             $user = strstr($data,"wiki_user|");
495             $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
496                                 'date' => $date,
497                                 'ip' => $ip);
498             $rs->MoveNext();
499         }
500         $rs->free();
501         $this->_disconnect();
502         return $sessions;
503     }
504 }
505
506 /** DBA Sessions
507  *  session:
508  *    Index: session_id
509  *   Values: date : IP : data
510  */
511 class DbSession_dba
512 extends DbSession
513 {
514     var $_backend_type = "dba";
515
516     function DbSession_dba (&$dbh, $table) {
517         $this->_dbh = $dbh;
518         ini_set('session.save_handler','user');
519         session_module_name('user'); // new style
520         session_set_save_handler(array(&$this, 'open'),
521                                  array(&$this, 'close'),
522                                  array(&$this, 'read'),
523                                  array(&$this, 'write'),
524                                  array(&$this, 'destroy'),
525                                  array(&$this, 'gc'));
526         return $this;
527     }
528
529     function quote($str) { return $str; }
530     function query($sql) { return false; }
531
532     function _connect() {
533         global $DBParams;
534         $dbh = &$this->_dbh;
535         if (!$dbh) {
536             $directory = '/tmp';
537             $prefix = 'wiki_';
538             $dba_handler = 'gdbm';
539             $timeout = 20;
540             extract($DBParams);
541             $dbfile = "$directory/$prefix" . 'session' . '.' . $dba_handler;
542             $dbh = new DbaDatabase($dbfile, false, $dba_handler);
543             $dbh->set_timeout($timeout);
544             if (!$dbh->open('c')) {
545                 trigger_error(sprintf(_("%s: Can't open dba database"), $dbfile), E_USER_ERROR);
546                 global $request;
547                 $request->finish(fmt("%s: Can't open dba database", $dbfile));
548             }
549             $this->_dbh = &$dbh;
550         }
551         return $dbh;
552     }
553
554     function _disconnect() {
555         if (0 and isset($this->_dbh))
556             $this->_dbh->close();
557     }
558
559     function open ($save_path, $session_name) {
560         $dbh = &$this->_connect();
561         $dbh->open();
562     }
563
564     function close() {
565         if ($this->_dbh)
566             $this->_dbh->close();
567     }
568
569     function read ($id) {
570         $dbh = &$this->_connect();
571         $result = $dbh->get($id);
572         if (!$result) {
573             return false;
574         }
575         list(,,$packed) = explode(':', $result, 3);
576         $this->_disconnect();
577         if (strlen($packed) > 4000) {
578             trigger_error("Overlarge session data!", E_USER_WARNING);
579             $packed = '';
580             //$res = preg_replace('/s:6:"_cache";O:12:"WikiDB_cache".+}$/',"",$res);
581         }
582         return $packed;
583     }
584   
585     function write ($id, $sess_data) {
586         $dbh = &$this->_connect();
587         $time = time();
588         $ip = $GLOBALS['request']->get('REMOTE_ADDR');
589         if (strlen($sess_data) > 4000) {
590             trigger_error("Overlarge session data!", E_USER_WARNING);
591             $sess_data = '';
592         }
593         $dbh->set($id,$time.':'.$ip.':'.$sess_data);
594         $this->_disconnect();
595         return true;
596     }
597
598     function destroy ($id) {
599         $dbh = &$this->_connect();
600         $dbh->delete($id);
601         $this->_disconnect();
602         return true;
603     }
604
605     function gc ($maxlifetime) {
606         $dbh = &$this->_connect();
607         $threshold = time() - $maxlifetime;
608         for ($id = $dbh->firstkey(); $id !== false; $id = $dbh->nextkey()) {
609             $result = $dbh->get($id);
610             list($date,,) = explode(':', $result, 3);
611             //$dbh->query("DELETE FROM $table WHERE sess_date < $threshold");
612             if ($date < $threshold)
613                 $dbh->delete($id);
614         }
615         $this->_disconnect();
616         return true;
617     }
618
619     // WhoIsOnline support. 
620     // TODO: ip-accesstime dynamic blocking API
621     function currentSessions() {
622         $sessions = array();
623         $dbh = &$this->_connect();
624         for ($id = $dbh->firstkey(); $id !== false; $id = $dbh->nextkey()) {
625             $result = $dbh->get($id);
626             list($date,$ip,$packed) = explode(':', $result, 3);
627             if (!$packed) continue;
628             //$data = @unserialize($packed);
629             // session_data contains the <variable name> + "|" + <packed string>
630             // we need just the wiki_user object (might be array as well)
631             if ($date < 908437560 or $date > 1588437560)
632                 $date = 0;
633             $user = strstr($packed, "wiki_user|");
634             $sessions[] = array('wiki_user' => substr($user,10), // from "O:" onwards
635                                 'date' => $date,
636                                 'ip' => $ip);
637         }
638         $this->_disconnect();
639         return $sessions;
640     }
641 }
642
643
644 // Local Variables:
645 // mode: php
646 // tab-width: 8
647 // c-basic-offset: 4
648 // c-hanging-comment-ender-p: nil
649 // indent-tabs-mode: nil
650 // End:
651 ?>