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