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