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