1<?php 2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */ 3 4/** 5 * Storage driver for use against PEAR MDB 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: MDB.php 256753 2008-04-04 07:57:02Z aashley $ 22 * @link http://pear.php.net/package/Auth 23 * @since File available since Release 1.2.3 24 */ 25 26/** 27 * Include Auth_Container base class 28 */ 29require_once 'Auth/Container.php'; 30/** 31 * Include PEAR MDB package 32 */ 33require_once 'MDB.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 MDB 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.2.3 50 */ 51class Auth_Container_MDB 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_MDB() [constructor] 77 78 /** 79 * Constructor of the container class 80 * 81 * Initate connection to the database via PEAR::MDB 82 * 83 * @param string Connection data or MDB object 84 * @return object Returns an error object if something went wrong 85 */ 86 function Auth_Container_MDB($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_MDB::_connect() called.', AUTH_LOG_DEBUG); 113 if (is_string($dsn) || is_array($dsn)) { 114 $this->db =& MDB::connect($dsn, $this->options['db_options']); 115 } elseif (is_subclass_of($dsn, 'mdb_common')) { 116 $this->db = $dsn; 117 } elseif (is_object($dsn) && MDB::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 (MDB::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']); 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]); 140 $this->options['final_table'] = implode('.', $t); 141 } 142 $this->options['final_usernamecol'] = $this->db->quoteIdentifier($this->options['usernamecol']); 143 $this->options['final_passwordcol'] = $this->db->quoteIdentifier($this->options['passwordcol']); 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, 'mdb_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_MDB::query() called.', AUTH_LOG_DEBUG); 191 $err = $this->_prepare(); 192 if ($err !== true) { 193 return $err; 194 } 195 return $this->db->query($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); 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']); 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_MDB::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 316 $query = sprintf("SELECT %s FROM %s WHERE %s = %s", 317 $sql_from, 318 $this->options['final_table'], 319 $this->options['final_usernamecol'], 320 $this->db->getTextValue($username) 321 ); 322 323 // check if there is an optional parameter db_where 324 if ($this->options['db_where'] != '') { 325 // there is one, so add it to the query 326 $query .= " AND ".$this->options['db_where']; 327 } 328 329 $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); 330 331 $res = $this->db->getRow($query, null, null, null, MDB_FETCHMODE_ASSOC); 332 333 if (MDB::isError($res) || PEAR::isError($res)) { 334 return PEAR::raiseError($res->getMessage(), $res->getCode()); 335 } 336 if (!is_array($res)) { 337 $this->activeUser = ''; 338 return false; 339 } 340 341 // Perform trimming here before the hashing 342 $password = trim($password, "\r\n"); 343 $res[$this->options['passwordcol']] = trim($res[$this->options['passwordcol']], "\r\n"); 344 345 // If using Challenge Response md5 the pass with the secret 346 if ($isChallengeResponse) { 347 $res[$this->options['passwordcol']] = 348 md5($res[$this->options['passwordcol']].$this->_auth_obj->session['loginchallenege']); 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 355 if ($this->verifyPassword($password, 356 $res[$this->options['passwordcol']], 357 $this->options['cryptType'])) { 358 // Store additional field values in the session 359 foreach ($res as $key => $value) { 360 if ($key == $this->options['passwordcol'] || 361 $key == $this->options['usernamecol']) { 362 continue; 363 } 364 365 $this->log('Storing additional field: '.$key, AUTH_LOG_DEBUG); 366 // Use reference to the auth object if exists 367 // This is because the auth session variable can change so a static 368 // call to setAuthData does not make sense 369 $this->_auth_obj->setAuthData($key, $value); 370 } 371 return true; 372 } 373 374 $this->activeUser = $res[$this->options['usernamecol']]; 375 return false; 376 } 377 378 // }}} 379 // {{{ listUsers() 380 381 /** 382 * Returns a list of users from the container 383 * 384 * @return mixed array|PEAR_Error 385 * @access public 386 */ 387 function listUsers() 388 { 389 $this->log('Auth_Container_MDB::listUsers() called.', AUTH_LOG_DEBUG); 390 $err = $this->_prepare(); 391 if ($err !== true) { 392 return PEAR::raiseError($err->getMessage(), $err->getCode()); 393 } 394 395 $retVal = array(); 396 397 //Check if db_fields contains a *, if so assume all columns are selected 398 if ( is_string($this->options['db_fields']) 399 && strstr($this->options['db_fields'], '*')) { 400 $sql_from = '*'; 401 } else { 402 $sql_from = $this->options['final_usernamecol'] 403 .', '.$this->options['final_passwordcol']; 404 405 if (strlen($fields = $this->_quoteDBFields()) > 0) { 406 $sql_from .= ', '.$fields; 407 } 408 } 409 410 $query = sprintf('SELECT %s FROM %s', 411 $sql_from, 412 $this->options['final_table'] 413 ); 414 415 // check if there is an optional parameter db_where 416 if ($this->options['db_where'] != '') { 417 // there is one, so add it to the query 418 $query .= " WHERE ".$this->options['db_where']; 419 } 420 421 $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); 422 423 $res = $this->db->getAll($query, null, null, null, MDB_FETCHMODE_ASSOC); 424 425 if (MDB::isError($res)) { 426 return PEAR::raiseError($res->getMessage(), $res->getCode()); 427 } else { 428 foreach ($res as $user) { 429 $user['username'] = $user[$this->options['usernamecol']]; 430 $retVal[] = $user; 431 } 432 } 433 $this->log('Found '.count($retVal).' users.', AUTH_LOG_DEBUG); 434 return $retVal; 435 } 436 437 // }}} 438 // {{{ addUser() 439 440 /** 441 * Add user to the storage container 442 * 443 * @access public 444 * @param string Username 445 * @param string Password 446 * @param mixed Additional information that are stored in the DB 447 * 448 * @return mixed True on success, otherwise error object 449 */ 450 function addUser($username, $password, $additional = "") 451 { 452 $this->log('Auth_Container_MDB::addUser() called.', AUTH_LOG_DEBUG); 453 $err = $this->_prepare(); 454 if ($err !== true) { 455 return PEAR::raiseError($err->getMessage(), $err->getCode()); 456 } 457 458 if (isset($this->options['cryptType']) && $this->options['cryptType'] == 'none') { 459 $cryptFunction = 'strval'; 460 } elseif (isset($this->options['cryptType']) && function_exists($this->options['cryptType'])) { 461 $cryptFunction = $this->options['cryptType']; 462 } else { 463 $cryptFunction = 'md5'; 464 } 465 466 $password = $cryptFunction($password); 467 468 $additional_key = ''; 469 $additional_value = ''; 470 471 if (is_array($additional)) { 472 foreach ($additional as $key => $value) { 473 if ($this->options['auto_quote']) { 474 $additional_key .= ', ' . $this->db->quoteIdentifier($key); 475 } else { 476 $additional_key .= ', ' . $key; 477 } 478 $additional_value .= ', ' . $this->db->getTextValue($value); 479 } 480 } 481 482 $query = sprintf("INSERT INTO %s (%s, %s%s) VALUES (%s, %s%s)", 483 $this->options['final_table'], 484 $this->options['final_usernamecol'], 485 $this->options['final_passwordcol'], 486 $additional_key, 487 $this->db->getTextValue($username), 488 $this->db->getTextValue($password), 489 $additional_value 490 ); 491 492 $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); 493 494 $res = $this->query($query); 495 496 if (MDB::isError($res)) { 497 return PEAR::raiseError($res->getMessage(), $res->code); 498 } 499 return true; 500 } 501 502 // }}} 503 // {{{ removeUser() 504 505 /** 506 * Remove user from the storage container 507 * 508 * @access public 509 * @param string Username 510 * 511 * @return mixed True on success, otherwise error object 512 */ 513 function removeUser($username) 514 { 515 $this->log('Auth_Container_MDB::removeUser() called.', AUTH_LOG_DEBUG); 516 $err = $this->_prepare(); 517 if ($err !== true) { 518 return PEAR::raiseError($err->getMessage(), $err->getCode()); 519 } 520 521 $query = sprintf("DELETE FROM %s WHERE %s = %s", 522 $this->options['final_table'], 523 $this->options['final_usernamecol'], 524 $this->db->getTextValue($username) 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 $query .= " AND ".$this->options['db_where']; 531 } 532 533 $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); 534 535 $res = $this->query($query); 536 537 if (MDB::isError($res)) { 538 return PEAR::raiseError($res->getMessage(), $res->code); 539 } 540 return true; 541 } 542 543 // }}} 544 // {{{ changePassword() 545 546 /** 547 * Change password for user in the storage container 548 * 549 * @param string Username 550 * @param string The new password (plain text) 551 */ 552 function changePassword($username, $password) 553 { 554 $this->log('Auth_Container_MDB::changePassword() called.', AUTH_LOG_DEBUG); 555 $err = $this->_prepare(); 556 if ($err !== true) { 557 return PEAR::raiseError($err->getMessage(), $err->getCode()); 558 } 559 560 if (isset($this->options['cryptType']) && $this->options['cryptType'] == 'none') { 561 $cryptFunction = 'strval'; 562 } elseif (isset($this->options['cryptType']) && function_exists($this->options['cryptType'])) { 563 $cryptFunction = $this->options['cryptType']; 564 } else { 565 $cryptFunction = 'md5'; 566 } 567 568 $password = $cryptFunction($password); 569 570 $query = sprintf("UPDATE %s SET %s = %s WHERE %s = %s", 571 $this->options['final_table'], 572 $this->options['final_passwordcol'], 573 $this->db->getTextValue($password), 574 $this->options['final_usernamecol'], 575 $this->db->getTextValue($username) 576 ); 577 578 // check if there is an optional parameter db_where 579 if ($this->options['db_where'] != '') { 580 // there is one, so add it to the query 581 $query .= " AND ".$this->options['db_where']; 582 } 583 584 $this->log('Running SQL against MDB: '.$query, AUTH_LOG_DEBUG); 585 586 $res = $this->query($query); 587 588 if (MDB::isError($res)) { 589 return PEAR::raiseError($res->getMessage(), $res->code); 590 } 591 return true; 592 } 593 594 // }}} 595 // {{{ supportsChallengeResponse() 596 597 /** 598 * Determine if this container supports 599 * password authentication with challenge response 600 * 601 * @return bool 602 * @access public 603 */ 604 function supportsChallengeResponse() 605 { 606 return in_array($this->options['cryptType'], array('md5', 'none', '')); 607 } 608 609 // }}} 610 // {{{ getCryptType() 611 612 /** 613 * Returns the selected crypt type for this container 614 * 615 * @return string Function used to crypt the password 616 */ 617 function getCryptType() 618 { 619 return $this->options['cryptType']; 620 } 621 622 // }}} 623 624} 625?> 626