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