1<?php 2/** 3 * DokuWiki Plugin authdrupal7 (Auth Component) 4 * 5 * Authenticate users based on Drupal7 Database 6 * This Plugin provides password checking using drupals algorithms. 7 * 8 * Plugin is widely based on the MySQL authentication backend by 9 * Andreas Gohr <andi@splitbrain.org> 10 * Chris Smith <chris@jalakai.co.uk> 11 * Matthias Grimm <matthias.grimmm@sourceforge.net> 12 * Jan Schumann <js@schumann-it.com> 13 * 14 * Some further ideas were taken from DokuDrupal Drupal 7.x/MySQL authentication backend by 15 * Alex Shepherd <n00bATNOSPAMn00bsys0p.co.uk> 16 * 17 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 18 * @author Matthias Jung <matzekuh@web.de> 19 */ 20 21// must be run within Dokuwiki 22if(!defined('DOKU_INC')) die(); 23 24class auth_plugin_authdrupal7 extends DokuWiki_Auth_Plugin { 25 26 /** @var resource holds the database connection */ 27 protected $dbcon = 0; 28 29 /** 30 * Constructor. 31 */ 32 public function __construct() { 33 parent::__construct(); // for compatibility 34 35 if(!function_exists('mysql_connect')) { 36 $this->_debug("MySQL err: PHP MySQL extension not found.", -1, __LINE__, __FILE__); 37 $this->success = false; 38 return; 39 } 40 41 // set capabilities based upon config strings set 42 if(!$this->getConf('server') || !$this->getConf('user') || !$this->getConf('database')) { 43 $this->_debug("MySQL err: insufficient configuration.", -1, __LINE__, __FILE__); 44 $this->success = false; 45 return; 46 } 47 48 // set capabilities accordingly 49 $this->cando['addUser'] = false; // can Users be created? 50 $this->cando['delUser'] = false; // can Users be deleted? 51 $this->cando['modLogin'] = false; // can login names be changed? 52 $this->cando['modPass'] = false; // can passwords be changed? 53 $this->cando['modName'] = false; // can real names be changed? 54 $this->cando['modMail'] = false; // can emails be changed? 55 $this->cando['modGroups'] = false; // can groups be changed? 56 $this->cando['getUsers'] = false; // FIXME can a (filtered) list of users be retrieved? 57 $this->cando['getUserCount']= true; // can the number of users be retrieved? 58 $this->cando['getGroups'] = false; // FIXME can a list of available groups be retrieved? 59 $this->cando['external'] = false; // does the module do external auth checking? 60 $this->cando['logout'] = true; // can the user logout again? (eg. not possible with HTTP auth) 61 62 // FIXME intialize your auth system and set success to true, if successful 63 $this->success = true; 64 } 65 66 67 /** 68 * Checks if the given user exists and the given plaintext password 69 * is correct. Furtheron it might be checked wether the user is 70 * member of the right group 71 * 72 * @param string $user user who would like access 73 * @param string $pass user's clear text password to check 74 * @return bool 75 * 76 * @author Andreas Gohr <andi@splitbrain.org> 77 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 78 * @author Matthias Jung <matzekuh@web.de> 79 */ 80 public function checkPass($user, $pass) { 81 global $conf; 82 $rc = false; 83 if($this->_openDB()) { 84 $sql = str_replace('%{user}', $this->_escape($user), $this->getConf('checkPass')); 85 $sql = str_replace('%{drupal_prefix}', $this->getConf('drupalPrefix'), $sql); 86 $result = $this->_queryDB($sql); 87 if($result !== false && count($result) == 1) { 88 $rc = $this->_hash_password($pass, $result[0]['pass']) == $result[0]['pass']; 89 } 90 $this->_closeDB(); 91 } 92 return $rc; 93 } 94 95 /** 96 * Hashes the password using drupals hashing algorithms 97 * 98 * @param string $pass user's clear text password to hash 99 * @param string $hashedpw user's pre-hashed password from the database 100 * @return mixed boolean|string hashed password string in case of success, else return boolean false 101 * 102 * @author Matthias Jung <matzekuh@web.de> 103 */ 104 protected function _hash_password($pass, $hashedpw) { 105 $drupalroot = $this->getConf('drupalRoot'); 106 require_once($drupalroot.'includes/password.inc'); 107 if(!function_exists(_password_crypt)) { 108 msg("Drupal installation not found. Please check your configuration.",-1,__LINE__,__FILE__); 109 $this->success = false; 110 } 111 $hash = _password_crypt('sha512', $pass, $hashedpw); 112 return $hash; 113 } 114 115 /** 116 * Return user info 117 * 118 * @author Andreas Gohr <andi@splitbrain.org> 119 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 120 * 121 * @param string $user user login to get data for 122 * @param bool $requireGroups when true, group membership information should be included in the returned array; 123 * when false, it maybe included, but is not required by the caller 124 * @return array|bool 125 */ 126 public function getUserData($user, $requireGroups=true) { 127 if($this->_cacheExists($user, $requireGroups)) { 128 return $this->cacheUserInfo[$user]; 129 } 130 if($this->_openDB()) { 131 $this->_lockTables("READ"); 132 $info = $this->_getUserInfo($user, $requireGroups); 133 $this->_unlockTables(); 134 $this->_closeDB(); 135 } else { 136 $info = false; 137 } 138 return $info; 139 } 140 141 /** 142 * Get a user's information 143 * 144 * The database connection must already be established for this function to work. 145 * 146 * @author Christopher Smith <chris@jalakai.co.uk> 147 * 148 * @param string $user username of the user whose information is being reterieved 149 * @param bool $requireGroups true if group memberships should be included 150 * @param bool $useCache true if ok to return cached data & to cache returned data 151 * 152 * @return mixed false|array false if the user doesn't exist 153 * array containing user information if user does exist 154 */ 155 protected function _getUserInfo($user, $requireGroups=true, $useCache=true) { 156 $info = null; 157 if ($useCache && isset($this->cacheUserInfo[$user])) { 158 $info = $this->cacheUserInfo[$user]; 159 } 160 if (is_null($info)) { 161 $info = $this->_retrieveUserInfo($user); 162 } 163 if (($requireGroups == true) && $info && !isset($info['grps'])) { 164 $info['grps'] = $this->_getGroups($user); 165 } 166 if ($useCache) { 167 $this->cacheUserInfo[$user] = $info; 168 } 169 return $info; 170 } 171 172 /** 173 * retrieveUserInfo 174 * 175 * Gets the data for a specific user. The database connection 176 * must already be established for this function to work. 177 * Otherwise it will return 'false'. 178 * 179 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 180 * @author Matthias Jung <matzekuh@web.de> 181 * 182 * @param string $user user's nick to get data for 183 * @return false|array false on error, user info on success 184 */ 185 protected function _retrieveUserInfo($user) { 186 $sql = str_replace('%{user}', $this->_escape($user), $this->getConf('getUserInfo')); 187 $sql = str_replace('%{drupal_prefix}', $this->getConf('drupalPrefix'), $sql); 188 $result = $this->_queryDB($sql); 189 if($result !== false && count($result)) { 190 $info = $result[0]; 191 return $info; 192 } 193 return false; 194 } 195 196 /** 197 * Retrieves a list of groups the user is a member off. 198 * 199 * The database connection must already be established 200 * for this function to work. Otherwise it will return 201 * false. 202 * 203 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 204 * @author Matthias Jung <matzekuh@web.de> 205 * 206 * @param string $user user whose groups should be listed 207 * @return bool|array false on error, all groups on success 208 */ 209 protected function _getGroups($user) { 210 $groups = array(); 211 if($this->dbcon) { 212 $sql = str_replace('%{user}', $this->_escape($user), $this->getConf('getGroups')); 213 $sql = str_replace('%{drupal_prefix}', $this->getConf('drupalPrefix'), $sql); 214 $result = $this->_queryDB($sql); 215 if($result !== false && count($result)) { 216 foreach($result as $row) { 217 $groups[] = $row['name']; 218 } 219 } 220 return $groups; 221 } 222 return false; 223 } 224 225 /** 226 * Counts users. 227 * 228 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 229 * @author Matthias Jung <matzekuh@web.de> 230 * 231 * @param array $filter filter criteria in item/pattern pairs 232 * @return int count of found users 233 */ 234 public function getUserCount() { 235 $rc = 0; 236 if($this->_openDB()) { 237 $sql = str_replace('%{drupal_prefix}', $this->getConf('drupalPrefix'), $this->getConf('getUserCount')); 238 $result = $this->_queryDB($sql); 239 $rc = $result[0]['num']; 240 $this->_closeDB(); 241 } 242 return $rc; 243 } 244 245 /** 246 * Retrieve groups [implement only where required/possible] 247 * 248 * Set getGroups capability when implemented 249 * 250 * @param int $start 251 * @param int $limit 252 * @return array 253 */ 254 //public function retrieveGroups($start = 0, $limit = 0) { 255 // FIXME implement 256 // return array(); 257 //} 258 259 /** 260 * Return case sensitivity of the backend 261 * 262 * MYSQL is case-insensitive 263 * 264 * @return false 265 */ 266 public function isCaseSensitive() { 267 return false; 268 } 269 270 /** 271 * Check Session Cache validity [implement only where required/possible] 272 * 273 * DokuWiki caches user info in the user's session for the timespan defined 274 * in $conf['auth_security_timeout']. 275 * 276 * This makes sure slow authentication backends do not slow down DokuWiki. 277 * This also means that changes to the user database will not be reflected 278 * on currently logged in users. 279 * 280 * To accommodate for this, the user manager plugin will touch a reference 281 * file whenever a change is submitted. This function compares the filetime 282 * of this reference file with the time stored in the session. 283 * 284 * This reference file mechanism does not reflect changes done directly in 285 * the backend's database through other means than the user manager plugin. 286 * 287 * Fast backends might want to return always false, to force rechecks on 288 * each page load. Others might want to use their own checking here. If 289 * unsure, do not override. 290 * 291 * @param string $user - The username 292 * @return bool 293 */ 294 //public function useSessionCache($user) { 295 // FIXME implement 296 //} 297 298 299 /** 300 * Opens a connection to a database and saves the handle for further 301 * usage in the object. The successful call to this functions is 302 * essential for most functions in this object. 303 * 304 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 305 * 306 * @return bool 307 */ 308 protected function _openDB() { 309 if(!$this->dbcon) { 310 $con = @mysql_connect($this->getConf('server'), $this->getConf('user'), $this->getConf('password')); 311 if($con) { 312 if((mysql_select_db($this->getConf('database'), $con))) { 313 if((preg_match('/^(\d+)\.(\d+)\.(\d+).*/', mysql_get_server_info($con), $result)) == 1) { 314 $this->dbver = $result[1]; 315 $this->dbrev = $result[2]; 316 $this->dbsub = $result[3]; 317 } 318 $this->dbcon = $con; 319 if($this->getConf('charset')) { 320 mysql_query('SET CHARACTER SET "'.$this->getConf('charset').'"', $con); 321 } 322 return true; // connection and database successfully opened 323 } else { 324 mysql_close($con); 325 $this->_debug("MySQL err: No access to database {$this->getConf('database')}.", -1, __LINE__, __FILE__); 326 } 327 } else { 328 $this->_debug( 329 "MySQL err: Connection to {$this->getConf('user')}@{$this->getConf('server')} not possible.", 330 -1, __LINE__, __FILE__ 331 ); 332 } 333 return false; // connection failed 334 } 335 return true; // connection already open 336 } 337 338 /** 339 * Closes a database connection. 340 * 341 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 342 */ 343 protected function _closeDB() { 344 if($this->dbcon) { 345 mysql_close($this->dbcon); 346 $this->dbcon = 0; 347 } 348 } 349 350 /** 351 * Sends a SQL query to the database and transforms the result into 352 * an associative array. 353 * 354 * This function is only able to handle queries that returns a 355 * table such as SELECT. 356 * 357 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 358 * 359 * @param string $query SQL string that contains the query 360 * @return array|false with the result table 361 */ 362 protected function _queryDB($query) { 363 if($this->getConf('debug') >= 2) { 364 msg('MySQL query: '.hsc($query), 0, __LINE__, __FILE__); 365 } 366 $resultarray = array(); 367 if($this->dbcon) { 368 $result = @mysql_query($query, $this->dbcon); 369 if($result) { 370 while(($t = mysql_fetch_assoc($result)) !== false) 371 $resultarray[] = $t; 372 mysql_free_result($result); 373 return $resultarray; 374 } 375 $this->_debug('MySQL err: '.mysql_error($this->dbcon), -1, __LINE__, __FILE__); 376 } 377 return false; 378 } 379 380 /** 381 * Escape a string for insertion into the database 382 * 383 * @author Andreas Gohr <andi@splitbrain.org> 384 * 385 * @param string $string The string to escape 386 * @param boolean $like Escape wildcard chars as well? 387 * @return string 388 */ 389 protected function _escape($string, $like = false) { 390 if($this->dbcon) { 391 $string = mysql_real_escape_string($string, $this->dbcon); 392 } else { 393 $string = addslashes($string); 394 } 395 if($like) { 396 $string = addcslashes($string, '%_'); 397 } 398 return $string; 399 } 400 401 /** 402 * Wrapper around msg() but outputs only when debug is enabled 403 * 404 * @param string $message 405 * @param int $err 406 * @param int $line 407 * @param string $file 408 * @return void 409 */ 410 protected function _debug($message, $err, $line, $file) { 411 if(!$this->getConf('debug')) return; 412 msg($message, $err, $line, $file); 413 } 414 415 /** 416 * Sends a SQL query to the database 417 * 418 * This function is only able to handle queries that returns 419 * either nothing or an id value such as INPUT, DELETE, UPDATE, etc. 420 * 421 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 422 * 423 * @param string $query SQL string that contains the query 424 * @return int|bool insert id or 0, false on error 425 */ 426 protected function _modifyDB($query) { 427 if($this->getConf('debug') >= 2) { 428 msg('MySQL query: '.hsc($query), 0, __LINE__, __FILE__); 429 } 430 if($this->dbcon) { 431 $result = @mysql_query($query, $this->dbcon); 432 if($result) { 433 $rc = mysql_insert_id($this->dbcon); //give back ID on insert 434 if($rc !== false) return $rc; 435 } 436 $this->_debug('MySQL err: '.mysql_error($this->dbcon), -1, __LINE__, __FILE__); 437 } 438 return false; 439 } 440 441 /** 442 * Locked a list of tables for exclusive access so that modifications 443 * to the database can't be disturbed by other threads. The list 444 * could be set with $conf['plugin']['authmysql']['TablesToLock'] = array() 445 * 446 * If aliases for tables are used in SQL statements, also this aliases 447 * must be locked. For eg. you use a table 'user' and the alias 'u' in 448 * some sql queries, the array must looks like this (order is important): 449 * array("user", "user AS u"); 450 * 451 * MySQL V3 is not able to handle transactions with COMMIT/ROLLBACK 452 * so that this functionality is simulated by this function. Nevertheless 453 * it is not as powerful as transactions, it is a good compromise in safty. 454 * 455 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 456 * 457 * @param string $mode could be 'READ' or 'WRITE' 458 * @return bool 459 */ 460 protected function _lockTables($mode) { 461 if($this->dbcon) { 462 $ttl = $this->getConf('TablesToLock'); 463 if(is_array($ttl) && !empty($ttl)) { 464 if($mode == "READ" || $mode == "WRITE") { 465 $sql = "LOCK TABLES "; 466 $cnt = 0; 467 foreach($ttl as $table) { 468 if($cnt++ != 0) $sql .= ", "; 469 $sql .= "$table $mode"; 470 } 471 $this->_modifyDB($sql); 472 return true; 473 } 474 } 475 } 476 return false; 477 } 478 /** 479 * Unlock locked tables. All existing locks of this thread will be 480 * abrogated. 481 * 482 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 483 * 484 * @return bool 485 */ 486 protected function _unlockTables() { 487 if($this->dbcon) { 488 $this->_modifyDB("UNLOCK TABLES"); 489 return true; 490 } 491 return false; 492 } 493 494 /** 495 * Flush cached user information 496 * 497 * @author Christopher Smith <chris@jalakai.co.uk> 498 * 499 * @param string $user username of the user whose data is to be removed from the cache 500 * if null, empty the whole cache 501 */ 502 protected function _flushUserInfoCache($user=null) { 503 if (is_null($user)) { 504 $this->cacheUserInfo = array(); 505 } else { 506 unset($this->cacheUserInfo[$user]); 507 } 508 } 509 /** 510 * Quick lookup to see if a user's information has been cached 511 * 512 * This test does not need a database connection or read lock 513 * 514 * @author Christopher Smith <chris@jalakai.co.uk> 515 * 516 * @param string $user username to be looked up in the cache 517 * @param bool $requireGroups true, if cached info should include group memberships 518 * 519 * @return bool existence of required user information in the cache 520 */ 521 protected function _cacheExists($user, $requireGroups=true) { 522 if (isset($this->cacheUserInfo[$user])) { 523 if (!is_array($this->cacheUserInfo[$user])) { 524 return true; // user doesn't exist 525 } 526 if (!$requireGroups || isset($this->cacheUserInfo[$user]['grps'])) { 527 return true; 528 } 529 } 530 return false; 531 } 532 533} 534 535// vim:ts=4:sw=4:et: 536