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