1<?php 2/** 3 * DokuWiki Plugin authimap2 (Auth Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Marco Fenoglio <marco.fenoglio@to.infn.it> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12class auth_plugin_authimap2 extends DokuWiki_Auth_Plugin { 13 /** @var array user cache */ 14 protected $users = null; 15 16 /** @var array filter pattern */ 17 protected $_pattern = array(); 18 19 /** @var bool safe version of preg_split */ 20 protected $_pregsplit_safe = false; 21 22 /** 23 * Constructor. 24 */ 25 public function __construct() { 26 parent::__construct(); // for compatibility 27 28 if(!function_exists('imap_open')) { 29 msg('PHP IMAP extension not available, IMAP auth not available.', -1); 30 return; 31 } 32 if(!$this->getConf('server')) { 33 msg('IMAP auth is missing server configuration', -1); 34 return; 35 } 36 if(!$this->getConf('domain')) { 37 msg('IMAP auth is missing domain configuration', -1); 38 return; 39 } 40 41 global $config_cascade; 42 43 if(!@is_readable($config_cascade['plainauth.users']['default'])) { 44 $this->success = false; 45 } else { 46 if(@is_writable($config_cascade['plainauth.users']['default'])) { 47 $this->cando['addUser'] = true; 48 $this->cando['delUser'] = true; 49 $this->cando['modLogin'] = true; 50 $this->cando['modPass'] = false; 51 $this->cando['modName'] = true; 52 $this->cando['modMail'] = true; 53 $this->cando['modGroups'] = true; 54 } 55 $this->cando['getUsers'] = true; 56 $this->cando['getUserCount'] = true; 57 } 58 59 $this->_pregsplit_safe = version_compare(PCRE_VERSION,'6.7','>='); 60 } 61 62 /** 63 * Check user+password 64 * 65 * May be ommited if trustExternal is used. 66 * 67 * @param string $user the user name 68 * @param string $pass the clear text password 69 * @return bool 70 */ 71 public function checkPass($user, $pass) { 72 $userinfo = $this->getUserData($user); 73 if ($userinfo === false) return false; 74 75 $domain = $this->getConf('domain'); 76 $server = $this->getConf('server'); 77 78 $toReturn=false; 79 80 // some servers want the local part, others want the full address as username 81 if($this->getConf('usedomain')) { 82 $login = "$user@$domain"; 83 } else { 84 $login = $user; 85 } 86 // check at imap server 87 #$imap_login = @imap_open("{imap.to.infn.it:993/imap/ssl/novalidate-cert}", 88 $imap_login = @imap_open($server, $login, $pass, OP_READONLY); 89 90 if ($imap_login == false){ 91 $toReturn = false; 92 } 93 else { 94 $toReturn = true; 95 imap_close($imap_login); 96 } 97 98 return $toReturn; 99 } 100 101 /** 102 * Return user info 103 * 104 * Returns info about the given user needs to contain 105 * at least these fields: 106 * 107 * name string full name of the user 108 * mail string email addres of the user 109 * grps array list of groups the user is in 110 * 111 * @param string $user the user name 112 * @return array containing user data or false 113 * @param bool $requireGroups whether or not the returned data must include groups 114 */ 115 public function getUserData($user, $requireGroups=true) { 116 if($this->users === null) $this->_loadUserData(); 117 return isset($this->users[$user]) ? $this->users[$user] : false; 118 } 119 120 /** 121 * Creates a string suitable for saving as a line 122 * in the file database 123 * (delimiters escaped, etc.) 124 * 125 * @param string $user 126 * @param string $pass 127 * @param string $name 128 * @param string $mail 129 * @param array $grps list of groups the user is in 130 * @return string 131 */ 132 protected function _createUserLine($user, $pass, $name, $mail, $grps) { 133 $groups = join(',', $grps); 134 $userline = array($user, $pass, $name, $mail, $groups); 135 $userline = str_replace('\\', '\\\\', $userline); // escape \ as \\ 136 $userline = str_replace(':', '\\:', $userline); // escape : as \: 137 $userline = join(':', $userline)."\n"; 138 return $userline; 139 } 140 141 /** 142 * Create a new User [implement only where required/possible] 143 * 144 * Returns false if the user already exists, null when an error 145 * occurred and true if everything went well. 146 * 147 * The new user HAS TO be added to the default group by this 148 * function! 149 * 150 * Set addUser capability when implemented 151 * 152 * @param string $user 153 * @param string $pass 154 * @param string $name 155 * @param string $mail 156 * @param null|array $grps 157 * @return bool|null 158 */ 159 public function createUser($user, $pass, $name, $mail, $grps = null) { 160 global $conf; 161 global $config_cascade; 162 163 // user mustn't already exist 164 if($this->getUserData($user) !== false) { 165 msg($this->getLang('userexists'), -1); 166 return false; 167 } 168 169 $pass = auth_cryptPassword($pwd); 170 171 // set default group if no groups specified 172 if(!is_array($grps)) $grps = array($conf['defaultgroup']); 173 174 // prepare user line 175 $userline = $this->_createUserLine($user, $pass, $name, $mail, $grps); 176 177 if(!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) { 178 msg($this->getLang('writefail'), -1); 179 return null; 180 } 181 182 $this->users[$user] = compact('pass', 'name', 'mail', 'grps'); 183 return $pwd; 184 } 185 186 /** 187 * Modify user data [implement only where required/possible] 188 * 189 * Set the mod* capabilities according to the implemented features 190 * 191 * @param string $user nick of the user to be changed 192 * @param array $changes array of field/value pairs to be changed (password will be clear text) 193 * @return bool 194 */ 195 public function modifyUser($user, $changes) { 196 global $ACT; 197 global $config_cascade; 198 199 // sanity checks, user must already exist and there must be something to change 200 if(($userinfo = $this->getUserData($user)) === false) { 201 msg($this->getLang('usernotexists'), -1); 202 return false; 203 } 204 205 // don't modify protected users 206 if(!empty($userinfo['protected'])) { 207 msg(sprintf($this->getLang('protected'), hsc($user)), -1); 208 return false; 209 } 210 211 if(!is_array($changes) || !count($changes)) return true; 212 213 // update userinfo with new data, remembering to encrypt any password 214 $newuser = $user; 215 foreach($changes as $field => $value) { 216 if($field == 'user') { 217 $newuser = $value; 218 continue; 219 } 220 if($field == 'pass') $value = auth_cryptPassword($value); 221 $userinfo[$field] = $value; 222 } 223 224 $userline = $this->_createUserLine($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $userinfo['grps']); 225 226 if(!io_replaceInFile($config_cascade['plainauth.users']['default'], '/^'.$user.':/', $userline, true)) { 227 msg('There was an error modifying your user data. You may need to register again.', -1); 228 // FIXME, io functions should be fail-safe so existing data isn't lost 229 $ACT = 'register'; 230 return false; 231 } 232 233 $this->users[$newuser] = $userinfo; 234 return true; 235 } 236 237 /** 238 * Delete one or more users [implement only where required/possible] 239 * 240 * Set delUser capability when implemented 241 * 242 * @param array $users 243 * @return int number of users deleted 244 */ 245 public function deleteUsers($users) { 246 global $config_cascade; 247 248 if(!is_array($users) || empty($users)) return 0; 249 250 if($this->users === null) $this->_loadUserData(); 251 252 $deleted = array(); 253 foreach($users as $user) { 254 // don't delete protected users 255 if(!empty($this->users[$user]['protected'])) { 256 msg(sprintf($this->getLang('protected'), hsc($user)), -1); 257 continue; 258 } 259 if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/'); 260 } 261 262 if(empty($deleted)) return 0; 263 264 $pattern = '/^('.join('|', $deleted).'):/'; 265 if (!io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true)) { 266 msg($this->getLang('writefail'), -1); 267 return 0; 268 } 269 270 // reload the user list and count the difference 271 $count = count($this->users); 272 $this->_loadUserData(); 273 $count -= count($this->users); 274 return $count; 275 } 276 277 /** 278 * Bulk retrieval of user data [implement only where required/possible] 279 * 280 * Set getUsers capability when implemented 281 * 282 * @param int $start index of first user to be returned 283 * @param int $limit max number of users to be returned, 0 for unlimited 284 * @param array $filter array of field/pattern pairs, null for no filter 285 * @return array list of userinfo (refer getUserData for internal userinfo details) 286 */ 287 public function retrieveUsers($start = 0, $limit = 0, $filter = null) { 288 289 if($this->users === null) $this->_loadUserData(); 290 291 ksort($this->users); 292 293 $i = 0; 294 $count = 0; 295 $out = array(); 296 $this->_constructPattern($filter); 297 298 foreach($this->users as $user => $info) { 299 if($this->_filter($user, $info)) { 300 if($i >= $start) { 301 $out[$user] = $info; 302 $count++; 303 if(($limit > 0) && ($count >= $limit)) break; 304 } 305 $i++; 306 } 307 } 308 309 return $out; 310 } 311 312 /** 313 * Return a count of the number of user which meet $filter criteria 314 * [should be implemented whenever retrieveUsers is implemented] 315 * 316 * Set getUserCount capability when implemented 317 * 318 * @param array $filter array of field/pattern pairs, empty array for no filter 319 * @return int 320 */ 321 public function getUserCount($filter = array()) { 322 323 if($this->users === null) $this->_loadUserData(); 324 325 if(!count($filter)) return count($this->users); 326 327 $count = 0; 328 $this->_constructPattern($filter); 329 330 foreach($this->users as $user => $info) { 331 $count += $this->_filter($user, $info); 332 } 333 334 return $count; 335 } 336 337 /** 338 * Return case sensitivity of the backend 339 * 340 * When your backend is caseinsensitive (eg. you can login with USER and 341 * user) then you need to overwrite this method and return false 342 * 343 * @return bool 344 */ 345 public function isCaseSensitive() { 346 return true; 347 } 348 349 /** 350 * Sanitize a given username 351 * 352 * This function is applied to any user name that is given to 353 * the backend and should also be applied to any user name within 354 * the backend before returning it somewhere. 355 * 356 * This should be used to enforce username restrictions. 357 * 358 * @param string $user username 359 * @return string the cleaned username 360 */ 361 public function cleanUser($user) { 362 global $conf; 363 return cleanID(str_replace(':', $conf['sepchar'], $user)); 364 } 365 366 /** 367 * Sanitize a given groupname 368 * 369 * This function is applied to any groupname that is given to 370 * the backend and should also be applied to any groupname within 371 * the backend before returning it somewhere. 372 * 373 * This should be used to enforce groupname restrictions. 374 * 375 * Groupnames are to be passed without a leading '@' here. 376 * 377 * @param string $group groupname 378 * @return string the cleaned groupname 379 */ 380 public function cleanGroup($group) { 381 global $conf; 382 return cleanID(str_replace(':', $conf['sepchar'], $group)); 383 } 384 385 /** 386 * Load all user data 387 * 388 * loads the user file into a datastructure 389 * 390 * @author Andreas Gohr <andi@splitbrain.org> 391 */ 392 protected function _loadUserData() { 393 global $config_cascade; 394 395 $this->users = $this->_readUserFile($config_cascade['plainauth.users']['default']); 396 397 // support protected users 398 if(!empty($config_cascade['plainauth.users']['protected'])) { 399 $protected = $this->_readUserFile($config_cascade['plainauth.users']['protected']); 400 foreach(array_keys($protected) as $key) { 401 $protected[$key]['protected'] = true; 402 } 403 $this->users = array_merge($this->users, $protected); 404 } 405 } 406 407 /** 408 * Read user data from given file 409 * 410 * ignores non existing files 411 * 412 * @param string $file the file to load data from 413 * @return array 414 */ 415 protected function _readUserFile($file) { 416 $users = array(); 417 if(!file_exists($file)) return $users; 418 419 $lines = file($file); 420 foreach($lines as $line) { 421 $line = preg_replace('/#.*$/', '', $line); //ignore comments 422 $line = trim($line); 423 if(empty($line)) continue; 424 425 $row = $this->_splitUserData($line); 426 $row = str_replace('\\:', ':', $row); 427 $row = str_replace('\\\\', '\\', $row); 428 429 $groups = array_values(array_filter(explode(",", $row[4]))); 430 431 $users[$row[0]]['pass'] = $row[1]; 432 $users[$row[0]]['name'] = urldecode($row[2]); 433 $users[$row[0]]['mail'] = $row[3]; 434 $users[$row[0]]['grps'] = $groups; 435 } 436 return $users; 437 } 438 439 protected function _splitUserData($line){ 440 // due to a bug in PCRE 6.6, preg_split will fail with the regex we use here 441 // refer github issues 877 & 885 442 if ($this->_pregsplit_safe){ 443 return preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5); // allow for : escaped as \: 444 } 445 446 $row = array(); 447 $piece = ''; 448 $len = strlen($line); 449 for($i=0; $i<$len; $i++){ 450 if ($line[$i]=='\\'){ 451 $piece .= $line[$i]; 452 $i++; 453 if ($i>=$len) break; 454 } else if ($line[$i]==':'){ 455 $row[] = $piece; 456 $piece = ''; 457 continue; 458 } 459 $piece .= $line[$i]; 460 } 461 $row[] = $piece; 462 463 return $row; 464 } 465 466 /** 467 * return true if $user + $info match $filter criteria, false otherwise 468 * 469 * @author Chris Smith <chris@jalakai.co.uk> 470 * 471 * @param string $user User login 472 * @param array $info User's userinfo array 473 * @return bool 474 */ 475 protected function _filter($user, $info) { 476 foreach($this->_pattern as $item => $pattern) { 477 if($item == 'user') { 478 if(!preg_match($pattern, $user)) return false; 479 } else if($item == 'grps') { 480 if(!count(preg_grep($pattern, $info['grps']))) return false; 481 } else { 482 if(!preg_match($pattern, $info[$item])) return false; 483 } 484 } 485 return true; 486 } 487 488 /** 489 * construct a filter pattern 490 * 491 * @param array $filter 492 */ 493 protected function _constructPattern($filter) { 494 $this->_pattern = array(); 495 foreach($filter as $item => $pattern) { 496 $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters 497 } 498 } 499 500} 501 502// vim:ts=4:sw=4:et: 503