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