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