1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
3
4/**
5 * Storage driver for use against PEAR DB
6 *
7 * PHP versions 4 and 5
8 *
9 * LICENSE: This source file is subject to version 3.01 of the PHP license
10 * that is available through the world-wide-web at the following URI:
11 * http://www.php.net/license/3_01.txt.  If you did not receive a copy of
12 * the PHP License and are unable to obtain it through the web, please
13 * send a note to license@php.net so we can mail you a copy immediately.
14 *
15 * @category   Authentication
16 * @package    Auth
17 * @author     Martin Jansen <mj@php.net>
18 * @author     Adam Ashley <aashley@php.net>
19 * @copyright  2001-2006 The PHP Group
20 * @license    http://www.php.net/license/3_01.txt  PHP License 3.01
21 * @version    CVS: $Id: DB.php 256753 2008-04-04 07:57:02Z aashley $
22 * @link       http://pear.php.net/package/Auth
23 */
24
25/**
26 * Include Auth_Container base class
27 */
28require_once 'Auth/Container.php';
29/**
30 * Include PEAR DB
31 */
32require_once 'DB.php';
33
34/**
35 * Storage driver for fetching login data from a database
36 *
37 * This storage driver can use all databases which are supported
38 * by the PEAR DB abstraction layer to fetch login data.
39 *
40 * @category   Authentication
41 * @package    Auth
42 * @author     Martin Jansen <mj@php.net>
43 * @author     Adam Ashley <aashley@php.net>
44 * @copyright  2001-2006 The PHP Group
45 * @license    http://www.php.net/license/3_01.txt  PHP License 3.01
46 * @version    Release: @package_version@  File: $Revision: 256753 $
47 * @link       http://pear.php.net/package/Auth
48 */
49class Auth_Container_DB extends Auth_Container
50{
51
52    // {{{ properties
53
54    /**
55     * Additional options for the storage container
56     * @var array
57     */
58    var $options = array();
59
60    /**
61     * DB object
62     * @var object
63     */
64    var $db = null;
65    var $dsn = '';
66
67    /**
68     * User that is currently selected from the DB.
69     * @var string
70     */
71    var $activeUser = '';
72
73    // }}}
74    // {{{ Auth_Container_DB [constructor]
75
76    /**
77     * Constructor of the container class
78     *
79     * Save the initial options passed to the container. Initiation of the DB
80     * connection is no longer performed here and is only done when needed.
81     *
82     * @param  string Connection data or DB object
83     * @return object Returns an error object if something went wrong
84     */
85    function Auth_Container_DB($dsn)
86    {
87        $this->_setDefaults();
88
89        if (is_array($dsn)) {
90            $this->_parseOptions($dsn);
91
92            if (empty($this->options['dsn'])) {
93                PEAR::raiseError('No connection parameters specified!');
94            }
95        } else {
96            $this->options['dsn'] = $dsn;
97        }
98    }
99
100    // }}}
101    // {{{ _connect()
102
103    /**
104     * Connect to database by using the given DSN string
105     *
106     * @access private
107     * @param  string DSN string
108     * @return mixed  Object on error, otherwise bool
109     */
110    function _connect($dsn)
111    {
112        $this->log('Auth_Container_DB::_connect() called.', AUTH_LOG_DEBUG);
113
114        if (is_string($dsn) || is_array($dsn)) {
115            $this->db = DB::Connect($dsn, $this->options['db_options']);
116        } elseif (is_subclass_of($dsn, 'db_common')) {
117            $this->db = $dsn;
118        } elseif (DB::isError($dsn)) {
119            return PEAR::raiseError($dsn->getMessage(), $dsn->getCode());
120        } else {
121            return PEAR::raiseError('The given dsn was not valid in file ' . __FILE__ . ' at line ' . __LINE__,
122                                    41,
123                                    PEAR_ERROR_RETURN,
124                                    null,
125                                    null
126                                    );
127        }
128
129        if (DB::isError($this->db) || PEAR::isError($this->db)) {
130            return PEAR::raiseError($this->db->getMessage(), $this->db->getCode());
131        } else {
132            return true;
133        }
134    }
135
136    // }}}
137    // {{{ _prepare()
138
139    /**
140     * Prepare database connection
141     *
142     * This function checks if we have already opened a connection to
143     * the database. If that's not the case, a new connection is opened.
144     *
145     * @access private
146     * @return mixed True or a DB error object.
147     */
148    function _prepare()
149    {
150        if (!DB::isConnection($this->db)) {
151            $res = $this->_connect($this->options['dsn']);
152            if (DB::isError($res) || PEAR::isError($res)) {
153                return $res;
154            }
155        }
156        if ($this->options['auto_quote'] && $this->db->dsn['phptype'] != 'sqlite') {
157            if (strpos('.', $this->options['table']) === false) {
158                $this->options['final_table'] = $this->db->quoteIdentifier($this->options['table']);
159            } else {
160                $t = explode('.', $this->options['table']);
161                for ($i = 0, $count = count($t); $i < $count; $i++)
162                    $t[$i] = $this->db->quoteIdentifier($t[$i]);
163                $this->options['final_table'] = implode('.', $t);
164            }
165            $this->options['final_usernamecol'] = $this->db->quoteIdentifier($this->options['usernamecol']);
166            $this->options['final_passwordcol'] = $this->db->quoteIdentifier($this->options['passwordcol']);
167        } else {
168            $this->options['final_table'] = $this->options['table'];
169            $this->options['final_usernamecol'] = $this->options['usernamecol'];
170            $this->options['final_passwordcol'] = $this->options['passwordcol'];
171        }
172        return true;
173    }
174
175    // }}}
176    // {{{ query()
177
178    /**
179     * Prepare query to the database
180     *
181     * This function checks if we have already opened a connection to
182     * the database. If that's not the case, a new connection is opened.
183     * After that the query is passed to the database.
184     *
185     * @access public
186     * @param  string Query string
187     * @return mixed  a DB_result object or DB_OK on success, a DB
188     *                or PEAR error on failure
189     */
190    function query($query)
191    {
192        $err = $this->_prepare();
193        if ($err !== true) {
194            return $err;
195        }
196        return $this->db->query($query);
197    }
198
199    // }}}
200    // {{{ _setDefaults()
201
202    /**
203     * Set some default options
204     *
205     * @access private
206     * @return void
207     */
208    function _setDefaults()
209    {
210        $this->options['table']       = 'auth';
211        $this->options['usernamecol'] = 'username';
212        $this->options['passwordcol'] = 'password';
213        $this->options['dsn']         = '';
214        $this->options['db_fields']   = '';
215        $this->options['cryptType']   = 'md5';
216        $this->options['db_options']  = array();
217        $this->options['db_where']    = '';
218        $this->options['auto_quote']  = true;
219    }
220
221    // }}}
222    // {{{ _parseOptions()
223
224    /**
225     * Parse options passed to the container class
226     *
227     * @access private
228     * @param  array
229     */
230    function _parseOptions($array)
231    {
232        foreach ($array as $key => $value) {
233            if (isset($this->options[$key])) {
234                $this->options[$key] = $value;
235            }
236        }
237    }
238
239    // }}}
240    // {{{ _quoteDBFields()
241
242    /**
243     * Quote the db_fields option to avoid the possibility of SQL injection.
244     *
245     * @access private
246     * @return string A properly quoted string that can be concatenated into a
247     * SELECT clause.
248     */
249    function _quoteDBFields()
250    {
251        if (isset($this->options['db_fields'])) {
252            if (is_array($this->options['db_fields'])) {
253                if ($this->options['auto_quote']) {
254                    $fields = array();
255                    foreach ($this->options['db_fields'] as $field) {
256                        $fields[] = $this->db->quoteIdentifier($field);
257                    }
258                    return implode(', ', $fields);
259                } else {
260                    return implode(', ', $this->options['db_fields']);
261                }
262            } else {
263                if (strlen($this->options['db_fields']) > 0) {
264                    if ($this->options['auto_quote']) {
265                        return $this->db->quoteIdentifier($this->options['db_fields']);
266                    } else {
267                        return $this->options['db_fields'];
268                    }
269                }
270            }
271        }
272
273        return '';
274    }
275
276    // }}}
277    // {{{ fetchData()
278
279    /**
280     * Get user information from database
281     *
282     * This function uses the given username to fetch
283     * the corresponding login data from the database
284     * table. If an account that matches the passed username
285     * and password is found, the function returns true.
286     * Otherwise it returns false.
287     *
288     * @param   string Username
289     * @param   string Password
290     * @param   boolean If true password is secured using a md5 hash
291     *                  the frontend and auth are responsible for making sure the container supports
292     *                  challenge response password authentication
293     * @return  mixed  Error object or boolean
294     */
295    function fetchData($username, $password, $isChallengeResponse=false)
296    {
297        $this->log('Auth_Container_DB::fetchData() called.', AUTH_LOG_DEBUG);
298        // Prepare for a database query
299        $err = $this->_prepare();
300        if ($err !== true) {
301            return PEAR::raiseError($err->getMessage(), $err->getCode());
302        }
303
304        // Find if db_fields contains a *, if so assume all columns are selected
305        if (is_string($this->options['db_fields'])
306            && strstr($this->options['db_fields'], '*')) {
307            $sql_from = "*";
308        } else {
309            $sql_from = $this->options['final_usernamecol'].
310                ", ".$this->options['final_passwordcol'];
311
312            if (strlen($fields = $this->_quoteDBFields()) > 0) {
313                $sql_from .= ', '.$fields;
314            }
315        }
316
317        $query = "SELECT ".$sql_from.
318                " FROM ".$this->options['final_table'].
319                " WHERE ".$this->options['final_usernamecol']." = ".$this->db->quoteSmart($username);
320
321        // check if there is an optional parameter db_where
322        if ($this->options['db_where'] != '') {
323            // there is one, so add it to the query
324            $query .= " AND ".$this->options['db_where'];
325        }
326
327        $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG);
328
329        $res = $this->db->getRow($query, null, DB_FETCHMODE_ASSOC);
330
331        if (DB::isError($res)) {
332            return PEAR::raiseError($res->getMessage(), $res->getCode());
333        }
334
335        if (!is_array($res)) {
336            $this->activeUser = '';
337            return false;
338        }
339
340        // Perform trimming here before the hashihg
341        $password = trim($password, "\r\n");
342        $res[$this->options['passwordcol']] = trim($res[$this->options['passwordcol']], "\r\n");
343
344        // If using Challenge Response md5 the pass with the secret
345        if ($isChallengeResponse) {
346            $res[$this->options['passwordcol']] = md5($res[$this->options['passwordcol']]
347                    .$this->_auth_obj->session['loginchallenege']);
348
349            // UGLY cannot avoid without modifying verifyPassword
350            if ($this->options['cryptType'] == 'md5') {
351                $res[$this->options['passwordcol']] = md5($res[$this->options['passwordcol']]);
352            }
353
354            //print " Hashed Password [{$res[$this->options['passwordcol']]}]<br/>\n";
355        }
356
357        if ($this->verifyPassword($password,
358                                  $res[$this->options['passwordcol']],
359                                  $this->options['cryptType'])) {
360            // Store additional field values in the session
361            foreach ($res as $key => $value) {
362                if ($key == $this->options['passwordcol'] ||
363                    $key == $this->options['usernamecol']) {
364                    continue;
365                }
366
367                $this->log('Storing additional field: '.$key, AUTH_LOG_DEBUG);
368
369                // Use reference to the auth object if exists
370                // This is because the auth session variable can change so a
371                // static call to setAuthData does not make sence
372                $this->_auth_obj->setAuthData($key, $value);
373            }
374            return true;
375        }
376        $this->activeUser = $res[$this->options['usernamecol']];
377        return false;
378    }
379
380    // }}}
381    // {{{ listUsers()
382
383    /**
384     * Returns a list of users from the container
385     *
386     * @return mixed
387     * @access public
388     */
389    function listUsers()
390    {
391        $this->log('Auth_Container_DB::listUsers() called.', AUTH_LOG_DEBUG);
392        $err = $this->_prepare();
393        if ($err !== true) {
394            return PEAR::raiseError($err->getMessage(), $err->getCode());
395        }
396
397        $retVal = array();
398
399        // Find if db_fields contains a *, if so assume all col are selected
400        if (   is_string($this->options['db_fields'])
401            && strstr($this->options['db_fields'], '*')) {
402            $sql_from = "*";
403        } else {
404            $sql_from = $this->options['final_usernamecol'].
405                ", ".$this->options['final_passwordcol'];
406
407            if (strlen($fields = $this->_quoteDBFields()) > 0) {
408                $sql_from .= ', '.$fields;
409            }
410        }
411
412        $query = sprintf("SELECT %s FROM %s",
413                         $sql_from,
414                         $this->options['final_table']
415                         );
416
417        // check if there is an optional parameter db_where
418        if ($this->options['db_where'] != '') {
419            // there is one, so add it to the query
420            $query .= " WHERE ".$this->options['db_where'];
421        }
422
423        $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG);
424
425        $res = $this->db->getAll($query, null, DB_FETCHMODE_ASSOC);
426
427        if (DB::isError($res)) {
428            return PEAR::raiseError($res->getMessage(), $res->getCode());
429        } else {
430            foreach ($res as $user) {
431                $user['username'] = $user[$this->options['usernamecol']];
432                $retVal[] = $user;
433            }
434        }
435        $this->log('Found '.count($retVal).' users.', AUTH_LOG_DEBUG);
436        return $retVal;
437    }
438
439    // }}}
440    // {{{ addUser()
441
442    /**
443     * Add user to the storage container
444     *
445     * @access public
446     * @param  string Username
447     * @param  string Password
448     * @param  mixed  Additional information that are stored in the DB
449     *
450     * @return mixed True on success, otherwise error object
451     */
452    function addUser($username, $password, $additional = "")
453    {
454        $this->log('Auth_Container_DB::addUser() called.', AUTH_LOG_DEBUG);
455        $err = $this->_prepare();
456        if ($err !== true) {
457            return PEAR::raiseError($err->getMessage(), $err->getCode());
458        }
459
460        if (   isset($this->options['cryptType'])
461            && $this->options['cryptType'] == 'none') {
462            $cryptFunction = 'strval';
463        } elseif (   isset($this->options['cryptType'])
464                  && function_exists($this->options['cryptType'])) {
465            $cryptFunction = $this->options['cryptType'];
466        } else {
467            $cryptFunction = 'md5';
468        }
469
470        $password = $cryptFunction($password);
471
472        $additional_key   = '';
473        $additional_value = '';
474
475        if (is_array($additional)) {
476            foreach ($additional as $key => $value) {
477                if ($this->options['auto_quote']) {
478                    $additional_key .= ', ' . $this->db->quoteIdentifier($key);
479                } else {
480                    $additional_key .= ', ' . $key;
481                }
482                $additional_value .= ", " . $this->db->quoteSmart($value);
483            }
484        }
485
486        $query = sprintf("INSERT INTO %s (%s, %s%s) VALUES (%s, %s%s)",
487                         $this->options['final_table'],
488                         $this->options['final_usernamecol'],
489                         $this->options['final_passwordcol'],
490                         $additional_key,
491                         $this->db->quoteSmart($username),
492                         $this->db->quoteSmart($password),
493                         $additional_value
494                         );
495
496        $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG);
497
498        $res = $this->query($query);
499
500        if (DB::isError($res)) {
501            return PEAR::raiseError($res->getMessage(), $res->getCode());
502        } else {
503            return true;
504        }
505    }
506
507    // }}}
508    // {{{ removeUser()
509
510    /**
511     * Remove user from the storage container
512     *
513     * @access public
514     * @param  string Username
515     *
516     * @return mixed True on success, otherwise error object
517     */
518    function removeUser($username)
519    {
520        $this->log('Auth_Container_DB::removeUser() called.', AUTH_LOG_DEBUG);
521
522        $err = $this->_prepare();
523        if ($err !== true) {
524            return PEAR::raiseError($err->getMessage(), $err->getCode());
525        }
526
527        // check if there is an optional parameter db_where
528        if ($this->options['db_where'] != '') {
529            // there is one, so add it to the query
530            $where = " AND ".$this->options['db_where'];
531        } else {
532            $where = '';
533        }
534
535        $query = sprintf("DELETE FROM %s WHERE %s = %s %s",
536                         $this->options['final_table'],
537                         $this->options['final_usernamecol'],
538                         $this->db->quoteSmart($username),
539                         $where
540                         );
541
542        $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG);
543
544        $res = $this->query($query);
545
546        if (DB::isError($res)) {
547           return PEAR::raiseError($res->getMessage(), $res->getCode());
548        } else {
549          return true;
550        }
551    }
552
553    // }}}
554    // {{{ changePassword()
555
556    /**
557     * Change password for user in the storage container
558     *
559     * @param string Username
560     * @param string The new password (plain text)
561     */
562    function changePassword($username, $password)
563    {
564        $this->log('Auth_Container_DB::changePassword() called.', AUTH_LOG_DEBUG);
565        $err = $this->_prepare();
566        if ($err !== true) {
567            return PEAR::raiseError($err->getMessage(), $err->getCode());
568        }
569
570        if (   isset($this->options['cryptType'])
571            && $this->options['cryptType'] == 'none') {
572            $cryptFunction = 'strval';
573        } elseif (   isset($this->options['cryptType'])
574                  && function_exists($this->options['cryptType'])) {
575            $cryptFunction = $this->options['cryptType'];
576        } else {
577            $cryptFunction = 'md5';
578        }
579
580        $password = $cryptFunction($password);
581
582        // check if there is an optional parameter db_where
583        if ($this->options['db_where'] != '') {
584            // there is one, so add it to the query
585            $where = " AND ".$this->options['db_where'];
586        } else {
587            $where = '';
588        }
589
590        $query = sprintf("UPDATE %s SET %s = %s WHERE %s = %s %s",
591                         $this->options['final_table'],
592                         $this->options['final_passwordcol'],
593                         $this->db->quoteSmart($password),
594                         $this->options['final_usernamecol'],
595                         $this->db->quoteSmart($username),
596                         $where
597                         );
598
599        $this->log('Running SQL against DB: '.$query, AUTH_LOG_DEBUG);
600
601        $res = $this->query($query);
602
603        if (DB::isError($res)) {
604            return PEAR::raiseError($res->getMessage(), $res->getCode());
605        } else {
606            return true;
607        }
608    }
609
610    // }}}
611    // {{{ supportsChallengeResponse()
612
613    /**
614     * Determine if this container supports
615     * password authentication with challenge response
616     *
617     * @return bool
618     * @access public
619     */
620    function supportsChallengeResponse()
621    {
622        return in_array($this->options['cryptType'], array('md5', 'none', ''));
623    }
624
625    // }}}
626    // {{{ getCryptType()
627
628    /**
629      * Returns the selected crypt type for this container
630      */
631    function getCryptType()
632    {
633        return($this->options['cryptType']);
634    }
635
636    // }}}
637
638}
639?>
640