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