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