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->addGroup($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->addGroup($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 if($limit < 0) $limit = 10000; // we don't support no limit 307 if(is_null($filter)) $filter = array(); 308 309 foreach(array('user','name','mail','group') as $key) { 310 if(!isset($filter[$key])) { 311 $filter[$key] = '%'; 312 } else { 313 $filter[$key] = '%'.$filter[$key].'%'; 314 } 315 } 316 $filter['start'] = $start; 317 $filter['end'] = $start + $limit; 318 $filter['limit'] = $limit; 319 320 $result = $this->_query($this->getConf('list-users'), $filter); 321 if(!$result) return array(); 322 $users = array(); 323 foreach($result as $row) { 324 if(!isset($row['user'])) { 325 $this->_debug("Statement did not return 'user' attribute", -1, __LINE__); 326 return array(); 327 } 328 $users[] = $row['user']; 329 } 330 return $users; 331 } 332 333 /** 334 * Return a count of the number of user which meet $filter criteria 335 * 336 * @param array $filter array of field/pattern pairs, empty array for no filter 337 * @return int 338 */ 339 public function getUserCount($filter = array()) { 340 if(is_null($filter)) $filter = array(); 341 342 foreach(array('user','name','mail','group') as $key) { 343 if(!isset($filter[$key])) { 344 $filter[$key] = '%'; 345 } else { 346 $filter[$key] = '%'.$filter[$key].'%'; 347 } 348 } 349 350 $result = $this->_query($this->getConf('count-users'), $filter); 351 if(!$result || !isset($result[0]['count'])) { 352 $this->_debug("Statement did not return 'count' attribute", -1, __LINE__); 353 } 354 return isset($result[0]['count']); 355 } 356 357 /** 358 * Create a new group with the given name 359 * 360 * @param string $group 361 * @return bool 362 */ 363 public function addGroup($group) { 364 $sql = $this->getConf('insert-group'); 365 366 $result = $this->_query($sql, array(':group' => $group)); 367 if($result === false) return false; 368 return true; 369 } 370 371 /** 372 * Retrieve groups 373 * 374 * Set getGroups capability when implemented 375 * 376 * @param int $start 377 * @param int $limit 378 * @return array 379 */ 380 public function retrieveGroups($start = 0, $limit = 0) { 381 $groups = array_keys($this->_selectGroups()); 382 if($groups === false) return array(); 383 384 if(!$limit) { 385 return array_splice($groups, $start); 386 } else { 387 return array_splice($groups, $start, $limit); 388 } 389 } 390 391 /** 392 * Select data of a specified user 393 * 394 * @param string $user the user name 395 * @return bool|array user data, false on error 396 */ 397 protected function _selectUser($user) { 398 $sql = $this->getConf('select-user'); 399 400 $result = $this->_query($sql, array(':user' => $user)); 401 if(!$result) return false; 402 403 if(count($result) > 1) { 404 $this->_debug('Found more than one matching user', -1, __LINE__); 405 return false; 406 } 407 408 $data = array_shift($result); 409 $dataok = true; 410 411 if(!isset($data['user'])) { 412 $this->_debug("Statement did not return 'user' attribute", -1, __LINE__); 413 $dataok = false; 414 } 415 if(!isset($data['hash']) && !isset($data['clear'])) { 416 $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__); 417 $dataok = false; 418 } 419 if(!isset($data['name'])) { 420 $this->_debug("Statement did not return 'name' attribute", -1, __LINE__); 421 $dataok = false; 422 } 423 if(!isset($data['mail'])) { 424 $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__); 425 $dataok = false; 426 } 427 428 if(!$dataok) return false; 429 return $data; 430 } 431 432 /** 433 * Delete a user after removing all their group memberships 434 * 435 * @param string $user 436 * @return bool true when the user was deleted 437 */ 438 protected function _deleteUser($user) { 439 $this->pdo->beginTransaction(); 440 { 441 $userdata = $this->getUserData($user); 442 if($userdata === false) goto FAIL; 443 $allgroups = $this->_selectGroups(); 444 445 // remove group memberships (ignore errors) 446 foreach($userdata['grps'] as $group) { 447 $this->_leaveGroup($userdata, $allgroups[$group]); 448 } 449 450 $ok = $this->_query($this->getConf('delete-user'), $userdata); 451 if($ok === false) goto FAIL; 452 } 453 $this->pdo->commit(); 454 return true; 455 456 FAIL: 457 $this->pdo->rollBack(); 458 return false; 459 } 460 461 /** 462 * Select all groups of a user 463 * 464 * @param array $userdata The userdata as returned by _selectUser() 465 * @return array|bool list of group names, false on error 466 */ 467 protected function _selectUserGroups($userdata) { 468 global $conf; 469 $sql = $this->getConf('select-user-groups'); 470 $result = $this->_query($sql, $userdata); 471 if($result === false) return false; 472 473 $groups = array($conf['defaultgroup']); // always add default config 474 foreach($result as $row) { 475 if(!isset($row['group'])) { 476 $this->_debug("No 'group' field returned in select-user-groups statement"); 477 return false; 478 } 479 $groups[] = $row['group']; 480 } 481 482 $groups = array_unique($groups); 483 sort($groups); 484 return $groups; 485 } 486 487 /** 488 * Select all available groups 489 * 490 * @todo this should be cached 491 * @return array|bool list of all available groups and their properties 492 */ 493 protected function _selectGroups() { 494 $sql = $this->getConf('select-groups'); 495 $result = $this->_query($sql); 496 if($result === false) return false; 497 498 $groups = array(); 499 foreach($result as $row) { 500 if(!isset($row['group'])) { 501 $this->_debug("No 'group' field returned from select-groups statement", -1, __LINE__); 502 return false; 503 } 504 505 // relayout result with group name as key 506 $group = $row['group']; 507 $groups[$group] = $row; 508 } 509 510 ksort($groups); 511 return $groups; 512 } 513 514 515 /** 516 * Adds the user to the group 517 * 518 * @param array $userdata all the user data 519 * @param array $groupdata all the group data 520 * @return bool 521 */ 522 protected function _joinGroup($userdata, $groupdata) { 523 $data = array_merge($userdata, $groupdata); 524 $sql = $this->getConf('join-group'); 525 $result = $this->_query($sql, $data); 526 if($result === false) return false; 527 return true; 528 } 529 530 /** 531 * Removes the user from the group 532 * 533 * @param array $userdata all the user data 534 * @param array $groupdata all the group data 535 * @return bool 536 */ 537 protected function _leaveGroup($userdata, $groupdata) { 538 $data = array_merge($userdata, $groupdata); 539 $sql = $this->getConf('leave-group'); 540 $result = $this->_query($sql, $data); 541 if($result === false) return false; 542 return true; 543 } 544 545 /** 546 * Executes a query 547 * 548 * @param string $sql The SQL statement to execute 549 * @param array $arguments Named parameters to be used in the statement 550 * @return array|bool The result as associative array, false on error 551 */ 552 protected function _query($sql, $arguments = array()) { 553 if(empty($sql)) { 554 $this->_debug('No SQL query given', -1, __LINE__); 555 return false; 556 } 557 558 // prepare parameters - we only use those that exist in the SQL 559 $params = array(); 560 foreach($arguments as $key => $value) { 561 if(is_array($value)) continue; 562 if(is_object($value)) continue; 563 if($key[0] != ':') $key = ":$key"; // prefix with colon if needed 564 if(strpos($sql, $key) !== false) $params[$key] = $value; 565 } 566 567 // execute 568 $sth = $this->pdo->prepare($sql); 569 try { 570 $sth->execute($params); 571 $result = $sth->fetchAll(); 572 } catch(Exception $e) { 573 // report the caller's line 574 $trace = debug_backtrace(); 575 $line = $trace[0]['line']; 576 $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST')); 577 $this->_debug($e, -1, $line); 578 $this->_debug("SQL: <pre>$dsql</pre>", -1, $line); 579 $result = false; 580 } finally { 581 $sth->closeCursor(); 582 $sth = null; 583 } 584 585 return $result; 586 } 587 588 /** 589 * Wrapper around msg() but outputs only when debug is enabled 590 * 591 * @param string|Exception $message 592 * @param int $err 593 * @param int $line 594 */ 595 protected function _debug($message, $err = 0, $line = 0) { 596 if(!$this->getConf('debug')) return; 597 if(is_a($message, 'Exception')) { 598 $err = -1; 599 $msg = $message->getMessage(); 600 if(!$line) $line = $message->getLine(); 601 } else { 602 $msg = $message; 603 } 604 605 if(defined('DOKU_UNITTEST')) { 606 printf("\n%s, %s:%d\n", $msg, __FILE__, $line); 607 } else { 608 msg('authpdo: ' . $msg, $err, $line, __FILE__); 609 } 610 } 611 612 /** 613 * Check if the given config strings are set 614 * 615 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 616 * 617 * @param string[] $keys 618 * @return bool 619 */ 620 protected function _chkcnf($keys) { 621 foreach($keys as $key) { 622 if(!$this->getConf($key)) return false; 623 } 624 625 return true; 626 } 627 628 /** 629 * create an approximation of the SQL string with parameters replaced 630 * 631 * @param string $sql 632 * @param array $params 633 * @param bool $htmlescape Should the result be escaped for output in HTML? 634 * @return string 635 */ 636 protected function _debugSQL($sql, $params, $htmlescape = true) { 637 foreach($params as $key => $val) { 638 if(is_int($val)) { 639 $val = $this->pdo->quote($val, PDO::PARAM_INT); 640 } elseif(is_bool($val)) { 641 $val = $this->pdo->quote($val, PDO::PARAM_BOOL); 642 } elseif(is_null($val)) { 643 $val = 'NULL'; 644 } else { 645 $val = $this->pdo->quote($val); 646 } 647 $sql = str_replace($key, $val, $sql); 648 } 649 if($htmlescape) $sql = hsc($sql); 650 return $sql; 651 } 652} 653 654// vim:ts=4:sw=4:et: 655