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