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