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