1<?php 2/** 3 * DokuWiki Plugin authpdo (Auth Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { 13 14 /** @var PDO */ 15 protected $pdo; 16 17 /** 18 * Constructor. 19 */ 20 public function __construct() { 21 parent::__construct(); // for compatibility 22 23 if(!class_exists('PDO')) { 24 $this->_debug('PDO extension for PHP not found.', -1, __LINE__); 25 $this->success = false; 26 return; 27 } 28 29 if(!$this->getConf('dsn')) { 30 $this->_debug('No DSN specified', -1, __LINE__); 31 $this->success = false; 32 return; 33 } 34 35 try { 36 $this->pdo = new PDO( 37 $this->getConf('dsn'), 38 $this->getConf('user'), 39 $this->getConf('pass'), 40 array( 41 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array 42 PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names 43 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes 44 ) 45 ); 46 } catch(PDOException $e) { 47 $this->_debug($e); 48 $this->success = false; 49 return; 50 } 51 52 // FIXME set capabilities accordingly 53 //$this->cando['addUser'] = false; // can Users be created? 54 //$this->cando['delUser'] = false; // can Users be deleted? 55 //$this->cando['modLogin'] = false; // can login names be changed? 56 //$this->cando['modPass'] = false; // can passwords be changed? 57 //$this->cando['modName'] = false; // can real names be changed? 58 //$this->cando['modMail'] = false; // can emails be changed? 59 //$this->cando['modGroups'] = false; // can groups be changed? 60 //$this->cando['getUsers'] = false; // can a (filtered) list of users be retrieved? 61 //$this->cando['getUserCount']= false; // can the number of users be retrieved? 62 //$this->cando['getGroups'] = false; // can a list of available groups be retrieved? 63 //$this->cando['external'] = false; // does the module do external auth checking? 64 //$this->cando['logout'] = true; // can the user logout again? (eg. not possible with HTTP auth) 65 66 // FIXME intialize your auth system and set success to true, if successful 67 $this->success = true; 68 } 69 70 /** 71 * Check user+password 72 * 73 * @param string $user the user name 74 * @param string $pass the clear text password 75 * @return bool 76 */ 77 public function checkPass($user, $pass) { 78 79 $data = $this->_selectUser($user); 80 if($data == false) return false; 81 82 if(isset($data['hash'])) { 83 // hashed password 84 $passhash = new PassHash(); 85 return $passhash->verify_hash($pass, $data['hash']); 86 } else { 87 // clear text password in the database O_o 88 return ($pass == $data['clear']); 89 } 90 } 91 92 /** 93 * Return user info 94 * 95 * Returns info about the given user needs to contain 96 * at least these fields: 97 * 98 * name string full name of the user 99 * mail string email addres of the user 100 * grps array list of groups the user is in 101 * 102 * @param string $user the user name 103 * @param bool $requireGroups whether or not the returned data must include groups 104 * @return array containing user data or false 105 */ 106 public function getUserData($user, $requireGroups = true) { 107 $data = $this->_selectUser($user); 108 if($data == false) return false; 109 110 if(isset($data['hash'])) unset($data['hash']); 111 if(isset($data['clean'])) unset($data['clean']); 112 113 if($requireGroups) { 114 $data['grps'] = $this->_selectUserGroups($data); 115 if($data['grps'] === false) return false; 116 } 117 118 return $data; 119 } 120 121 /** 122 * Create a new User [implement only where required/possible] 123 * 124 * Returns false if the user already exists, null when an error 125 * occurred and true if everything went well. 126 * 127 * The new user HAS TO be added to the default group by this 128 * function! 129 * 130 * Set addUser capability when implemented 131 * 132 * @param string $user 133 * @param string $clear 134 * @param string $name 135 * @param string $mail 136 * @param null|array $grps 137 * @return bool|null 138 */ 139 public function createUser($user, $clear, $name, $mail, $grps = null) { 140 global $conf; 141 142 if(($info = $this->getUserData($user, false)) !== false) { 143 msg($this->getLang('userexists'), -1); 144 return false; // user already exists 145 } 146 147 // prepare data 148 if($grps == null) $grps = array(); 149 $grps[] = $conf['defaultgroup']; 150 $grps = array_unique($grps); 151 $hash = auth_cryptPassword($clear); 152 $userdata = compact('user', 'clear', 'hash', 'name', 'mail'); 153 154 // action protected by transaction 155 $this->pdo->beginTransaction(); 156 { 157 // insert the user 158 $ok = $this->_query($this->getConf('insert-user'), $userdata); 159 if($ok === false) goto FAIL; 160 $userdata = $this->getUserData($user, false); 161 if($userdata === false) goto FAIL; 162 163 // create all groups that do not exist, the refetch the groups 164 $allgroups = $this->_selectGroups(); 165 foreach($grps as $group) { 166 if(!isset($allgroups[$group])) { 167 $ok = $this->_insertGroup($group); 168 if($ok === false) goto FAIL; 169 } 170 } 171 $allgroups = $this->_selectGroups(); 172 173 // add user to the groups 174 foreach($grps as $group) { 175 $ok = $this->_joinGroup($userdata, $allgroups[$group]); 176 if($ok === false) goto FAIL; 177 } 178 } 179 $this->pdo->commit(); 180 return true; 181 182 // something went wrong, rollback 183 FAIL: 184 $this->pdo->rollBack(); 185 $this->_debug('Transaction rolled back', 0, __LINE__); 186 return null; // return error 187 } 188 189 /** 190 * Modify user data 191 * 192 * @param string $user nick of the user to be changed 193 * @param array $changes array of field/value pairs to be changed (password will be clear text) 194 * @return bool 195 */ 196 public function modifyUser($user, $changes) { 197 // secure everything in transaction 198 $this->pdo->beginTransaction(); 199 { 200 $olddata = $this->getUserData($user); 201 $oldgroups = $olddata['grps']; 202 unset($olddata['grps']); 203 204 // changing the user name? 205 if(isset($changes['user'])) { 206 if($this->getUserData($changes['user'], false)) goto FAIL; 207 $params = $olddata; 208 $params['newlogin'] = $changes['user']; 209 210 $ok = $this->_query($this->getConf('update-user-login'), $params); 211 if($ok === false) goto FAIL; 212 } 213 214 // changing the password? 215 if(isset($changes['pass'])) { 216 $params = $olddata; 217 $params['clear'] = $changes['pass']; 218 $params['hash'] = auth_cryptPassword($changes['pass']); 219 220 $ok = $this->_query($this->getConf('update-user-pass'), $params); 221 if($ok === false) goto FAIL; 222 } 223 224 // changing info? 225 if(isset($changes['mail']) || isset($changes['name'])) { 226 $params = $olddata; 227 if(isset($changes['mail'])) $params['mail'] = $changes['mail']; 228 if(isset($changes['name'])) $params['name'] = $changes['name']; 229 230 $ok = $this->_query($this->getConf('update-user-info'), $params); 231 if($ok === false) goto FAIL; 232 } 233 234 // changing groups? 235 if(isset($changes['grps'])) { 236 $allgroups = $this->_selectGroups(); 237 238 // remove membership for previous groups 239 foreach($oldgroups as $group) { 240 if(!in_array($group, $changes['grps'])) { 241 $ok = $this->_leaveGroup($olddata, $allgroups[$group]); 242 if($ok === false) goto FAIL; 243 } 244 } 245 246 // create all new groups that are missing 247 $added = 0; 248 foreach($changes['grps'] as $group) { 249 if(!isset($allgroups[$group])) { 250 $ok = $this->_insertGroup($group); 251 if($ok === false) goto FAIL; 252 $added++; 253 } 254 } 255 // reload group info 256 if($added > 0) $allgroups = $this->_selectGroups(); 257 258 // add membership for new groups 259 foreach($changes['grps'] as $group) { 260 if(!in_array($group, $oldgroups)) { 261 $ok = $this->_joinGroup($olddata, $allgroups[$group]); 262 if($ok === false) goto FAIL; 263 } 264 } 265 } 266 267 } 268 $this->pdo->commit(); 269 return true; 270 271 // something went wrong, rollback 272 FAIL: 273 $this->pdo->rollBack(); 274 $this->_debug('Transaction rolled back', 0, __LINE__); 275 return false; // return error 276 } 277 278 /** 279 * Delete one or more users [implement only where required/possible] 280 * 281 * Set delUser capability when implemented 282 * 283 * @param array $users 284 * @return int number of users deleted 285 */ 286 //public function deleteUsers($users) { 287 // FIXME implement 288 // return false; 289 //} 290 291 /** 292 * Bulk retrieval of user data [implement only where required/possible] 293 * 294 * Set getUsers capability when implemented 295 * 296 * @param int $start index of first user to be returned 297 * @param int $limit max number of users to be returned 298 * @param array $filter array of field/pattern pairs, null for no filter 299 * @return array list of userinfo (refer getUserData for internal userinfo details) 300 */ 301 //public function retrieveUsers($start = 0, $limit = -1, $filter = null) { 302 // FIXME implement 303 // return array(); 304 //} 305 306 /** 307 * Return a count of the number of user which meet $filter criteria 308 * [should be implemented whenever retrieveUsers is implemented] 309 * 310 * Set getUserCount capability when implemented 311 * 312 * @param array $filter array of field/pattern pairs, empty array for no filter 313 * @return int 314 */ 315 //public function getUserCount($filter = array()) { 316 // FIXME implement 317 // return 0; 318 //} 319 320 /** 321 * Define a group [implement only where required/possible] 322 * 323 * Set addGroup capability when implemented 324 * 325 * @param string $group 326 * @return bool 327 */ 328 //public function addGroup($group) { 329 // FIXME implement 330 // return false; 331 //} 332 333 /** 334 * Retrieve groups 335 * 336 * Set getGroups capability when implemented 337 * 338 * @param int $start 339 * @param int $limit 340 * @return array 341 */ 342 public function retrieveGroups($start = 0, $limit = 0) { 343 $groups = array_keys($this->_selectGroups()); 344 if($groups === false) return array(); 345 346 if(!$limit) { 347 return array_splice($groups, $start); 348 } else { 349 return array_splice($groups, $start, $limit); 350 } 351 } 352 353 /** 354 * Select data of a specified user 355 * 356 * @param string $user the user name 357 * @return bool|array user data, false on error 358 */ 359 protected function _selectUser($user) { 360 $sql = $this->getConf('select-user'); 361 362 $result = $this->_query($sql, array(':user' => $user)); 363 if(!$result) return false; 364 365 if(count($result) > 1) { 366 $this->_debug('Found more than one matching user', -1, __LINE__); 367 return false; 368 } 369 370 $data = array_shift($result); 371 $dataok = true; 372 373 if(!isset($data['user'])) { 374 $this->_debug("Statement did not return 'user' attribute", -1, __LINE__); 375 $dataok = false; 376 } 377 if(!isset($data['hash']) && !isset($data['clear'])) { 378 $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__); 379 $dataok = false; 380 } 381 if(!isset($data['name'])) { 382 $this->_debug("Statement did not return 'name' attribute", -1, __LINE__); 383 $dataok = false; 384 } 385 if(!isset($data['mail'])) { 386 $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__); 387 $dataok = false; 388 } 389 390 if(!$dataok) return false; 391 return $data; 392 } 393 394 /** 395 * Select all groups of a user 396 * 397 * @param array $userdata The userdata as returned by _selectUser() 398 * @return array|bool list of group names, false on error 399 */ 400 protected function _selectUserGroups($userdata) { 401 global $conf; 402 $sql = $this->getConf('select-user-groups'); 403 $result = $this->_query($sql, $userdata); 404 if($result === false) return false; 405 406 $groups = array($conf['defaultgroup']); // always add default config 407 foreach($result as $row) { 408 if(!isset($row['group'])) { 409 $this->_debug("No 'group' field returned in select-user-groups statement"); 410 return false; 411 } 412 $groups[] = $row['group']; 413 } 414 415 $groups = array_unique($groups); 416 sort($groups); 417 return $groups; 418 } 419 420 /** 421 * Select all available groups 422 * 423 * @todo this should be cached 424 * @return array|bool list of all available groups and their properties 425 */ 426 protected function _selectGroups() { 427 $sql = $this->getConf('select-groups'); 428 $result = $this->_query($sql); 429 if($result === false) return false; 430 431 $groups = array(); 432 foreach($result as $row) { 433 if(!isset($row['group'])) { 434 $this->_debug("No 'group' field returned from select-groups statement", -1, __LINE__); 435 return false; 436 } 437 438 // relayout result with group name as key 439 $group = $row['group']; 440 $groups[$group] = $row; 441 } 442 443 ksort($groups); 444 return $groups; 445 } 446 447 /** 448 * Create a new group with the given name 449 * 450 * @param string $group 451 * @return bool 452 */ 453 protected function _insertGroup($group) { 454 $sql = $this->getConf('insert-group'); 455 456 $result = $this->_query($sql, array(':group' => $group)); 457 if($result === false) return false; 458 return true; 459 } 460 461 /** 462 * Adds the user to the group 463 * 464 * @param array $userdata all the user data 465 * @param array $groupdata all the group data 466 * @return bool 467 */ 468 protected function _joinGroup($userdata, $groupdata) { 469 $data = array_merge($userdata, $groupdata); 470 $sql = $this->getConf('join-group'); 471 $result = $this->_query($sql, $data); 472 if($result === false) return false; 473 return true; 474 } 475 476 /** 477 * Removes the user from the group 478 * 479 * @param array $userdata all the user data 480 * @param array $groupdata all the group data 481 * @return bool 482 */ 483 protected function _leaveGroup($userdata, $groupdata) { 484 $data = array_merge($userdata, $groupdata); 485 $sql = $this->getConf('leave-group'); 486 $result = $this->_query($sql, $data); 487 if($result === false) return false; 488 return true; 489 } 490 491 /** 492 * Executes a query 493 * 494 * @param string $sql The SQL statement to execute 495 * @param array $arguments Named parameters to be used in the statement 496 * @return array|bool The result as associative array, false on error 497 */ 498 protected function _query($sql, $arguments = array()) { 499 if(empty($sql)) { 500 $this->_debug('No SQL query given', -1, __LINE__); 501 return false; 502 } 503 504 // prepare parameters - we only use those that exist in the SQL 505 $params = array(); 506 foreach($arguments as $key => $value) { 507 if(is_array($value)) continue; 508 if(is_object($value)) continue; 509 if($key[0] != ':') $key = ":$key"; // prefix with colon if needed 510 if(strpos($sql, $key) !== false) $params[$key] = $value; 511 } 512 513 // execute 514 $sth = $this->pdo->prepare($sql); 515 try { 516 $sth->execute($params); 517 $result = $sth->fetchAll(); 518 } catch(Exception $e) { 519 // report the caller's line 520 $trace = debug_backtrace(); 521 $line = $trace[0]['line']; 522 $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST')); 523 $this->_debug($e, -1, $line); 524 $this->_debug("SQL: <pre>$dsql</pre>", -1, $line); 525 $result = false; 526 } finally { 527 $sth->closeCursor(); 528 $sth = null; 529 } 530 531 return $result; 532 } 533 534 /** 535 * Wrapper around msg() but outputs only when debug is enabled 536 * 537 * @param string|Exception $message 538 * @param int $err 539 * @param int $line 540 */ 541 protected function _debug($message, $err = 0, $line = 0) { 542 if(!$this->getConf('debug')) return; 543 if(is_a($message, 'Exception')) { 544 $err = -1; 545 $msg = $message->getMessage(); 546 if(!$line) $line = $message->getLine(); 547 } else { 548 $msg = $message; 549 } 550 551 if(defined('DOKU_UNITTEST')) { 552 printf("\n%s, %s:%d\n", $msg, __FILE__, $line); 553 } else { 554 msg('authpdo: ' . $msg, $err, $line, __FILE__); 555 } 556 } 557 558 /** 559 * Check if the given config strings are set 560 * 561 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 562 * 563 * @param string[] $keys 564 * @return bool 565 */ 566 protected function _chkcnf($keys) { 567 foreach($keys as $key) { 568 if(!$this->getConf($key)) return false; 569 } 570 571 return true; 572 } 573 574 /** 575 * create an approximation of the SQL string with parameters replaced 576 * 577 * @param string $sql 578 * @param array $params 579 * @param bool $htmlescape Should the result be escaped for output in HTML? 580 * @return string 581 */ 582 protected function _debugSQL($sql, $params, $htmlescape = true) { 583 foreach($params as $key => $val) { 584 if(is_int($val)) { 585 $val = $this->pdo->quote($val, PDO::PARAM_INT); 586 } elseif(is_bool($val)) { 587 $val = $this->pdo->quote($val, PDO::PARAM_BOOL); 588 } elseif(is_null($val)) { 589 $val = 'NULL'; 590 } else { 591 $val = $this->pdo->quote($val); 592 } 593 $sql = str_replace($key, $val, $sql); 594 } 595 if($htmlescape) $sql = hsc($sql); 596 return $sql; 597 } 598} 599 600// vim:ts=4:sw=4:et: 601