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