1<?php 2 3use dokuwiki\Extension\AuthPlugin; 4use dokuwiki\Logger; 5use dokuwiki\Utf8\Sort; 6 7class auth_plugin_authserversso extends AuthPlugin { 8 const CONF_VAR_AUTH_ID = 'auth_var_id'; 9 const CONF_VAR_AUTH_EMAIL = 'auth_var_email'; 10 const CONF_VAR_AUTH_REALNAME = 'auth_var_realname'; 11 const CONF_AUTH_USERFILE = 'auth_userfile'; 12 13 protected $users = null; 14 15 protected $pattern = array(); 16 17 protected $globalConf = array(); 18 19 public function __construct() { 20 parent::__construct(); 21 if(!@is_readable($this->getConf(self::CONF_AUTH_USERFILE))) { 22 Logger::error("authserversso: Userfile not readable '{$this->getConf(self::CONF_AUTH_USERFILE)}'"); 23 $this->success = false; 24 } else { 25 $this->cando['external'] = true; 26 27 if(@is_writable($this->getConf(self::CONF_AUTH_USERFILE))) { 28 Logger::debug("authserversso: Userfile is writable '{$this->getConf(self::CONF_AUTH_USERFILE)}'"); 29 //$this->cando['addUser'] = true; 30 $this->cando['delUser'] = true; 31 //$this->cando['modLogin'] = false; 32 //$this->cando['modPass'] = false; 33 $this->cando['modMail'] = true; 34 $this->cando['modName'] = true; 35 $this->cando['modGroups'] = true; 36 } 37 $this->cando['getUsers'] = true; 38 $this->cando['getUserCount'] = true; 39 $this->cando['getGroups'] = true; 40 } 41 42 $this->loadConfig(); 43 $this->success = true; 44 } 45 46 // Required 47 public function checkPass($user, $pass) { 48 Logger::debug("authserversso: checkPass '{$user}':'{$pass}' "); 49 return $this->trustExternal($user, $pass); 50 } 51 52 public function getUserData($user, $requireGroups=true) { 53 Logger::debug("authserversso: getUserData {$user}"); 54 if($this->users === null) $this->loadUserData(); 55 return $this->users[$user] ?? false; 56 } 57 58 protected function createUserLine($user, $pass, $name, $mail, $grps) { 59 $groups = implode(',', $grps); 60 $userline = [$user, $pass, $name, $mail, $groups]; 61 $userline = str_replace('\\', '\\\\', $userline); // escape \ as \\ 62 $userline = str_replace(':', '\\:', $userline); // escape : as \: 63 $userline = str_replace('#', '\\#', $userline); // escape # as \# 64 $userline = implode(':', $userline)."\n"; 65 return $userline; 66 } 67 68 public function createUser($user, $pwd, $name, $mail, $grps = null) { 69 global $conf; 70 Logger::debug("authserversso: createUser {$user}"); 71 72 // user mustn't already exist 73 if($this->getUserData($user) !== false) { 74 msg($this->getLang('userexists'), -1); 75 return false; 76 } 77 78 $pass = auth_cryptPassword($pwd); 79 80 // set default group if no groups specified 81 if(!is_array($grps)) $grps = array($conf['defaultgroup']); 82 83 // prepare user line 84 $userline = $this->createUserLine($user, $pass, $name, $mail, $grps); 85 86 if(!io_saveFile($this->getConf(self::CONF_AUTH_USERFILE), $userline, true)) { 87 Logger::error($this->getLang('writefail'), -1); 88 return null; 89 } 90 91 $this->users[$user] = compact('pass', 'name', 'mail', 'grps'); 92 return $pwd; 93 } 94 95 public function modifyUser($user, $changes) { 96 global $ACT; 97 global $conf; 98 Logger::debug("authserversso: modifyUser {$user}"); 99 100 // sanity checks, user must already exist and there must be something to change 101 if(($userinfo = $this->getUserData($user)) === false) { 102 msg($this->getLang('usernotexists'), -1); 103 return false; 104 } 105 106 // don't modify protected users 107 if(!empty($userinfo['protected'])) { 108 msg(sprintf($this->getLang('protected'), hsc($user)), -1); 109 return false; 110 } 111 112 if(!is_array($changes) || !count($changes)) return true; 113 114 // update userinfo with new data, remembering to encrypt any password 115 $newuser = $user; 116 foreach($changes as $field => $value) { 117 if($field == 'user') { 118 $newuser = $value; 119 continue; 120 } 121 if($field == 'pass') $value = auth_cryptPassword($value); 122 $userinfo[$field] = $value; 123 } 124 125 $userline = $this->createUserLine($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $userinfo['grps']); 126 127 if(!io_replaceInFile($this->getConf(self::CONF_AUTH_USERFILE), '/^'.$user.':/', $userline, true)) { 128 msg('There was an error modifying your user data. You may need to register again.', -1); 129 // FIXME, io functions should be fail-safe so existing data isn't lost 130 $ACT = 'register'; 131 return false; 132 } 133 134 $this->users[$newuser] = $userinfo; 135 return true; 136 } 137 138 public function deleteUsers($users) { 139 if(!is_array($users) || empty($users)) return 0; 140 Logger::debug('authserversso: deleteUsers'); 141 142 if($this->users === null) $this->loadUserData(); 143 144 $deleted = array(); 145 foreach($users as $user) { 146 // don't delete protected users 147 if(!empty($this->users[$user]['protected'])) { 148 msg(sprintf($this->getLang('protected'), hsc($user)), -1); 149 continue; 150 } 151 if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/'); 152 } 153 154 if(empty($deleted)) return 0; 155 156 $pattern = '/^('.join('|', $deleted).'):/'; 157 if (!io_deleteFromFile($this->getConf(self::CONF_AUTH_USERFILE), $pattern, true)) { 158 msg($this->getLang('writefail'), -1); 159 return 0; 160 } 161 162 // reload the user list and count the difference 163 $count = count($this->users); 164 $this->loadUserData(); 165 $count -= count($this->users); 166 return $count; 167 } 168 169 public function getUserCount($filter = array()) { 170 //Logger::debug('authserversso: getUserCount'); 171 if($this->users === null) $this->loadUserData(); 172 173 if(!count($filter)) return count($this->users); 174 175 $count = 0; 176 $this->constructPattern($filter); 177 178 foreach($this->users as $user => $info) { 179 $count += $this->filter($user, $info); 180 } 181 182 return $count; 183 } 184 185 public function retrieveUsers($start = 0, $limit = 0, $filter = array()) { 186 //Logger::debug('authserversso: retrieveUsers'); 187 if($this->users === null) $this->loadUserData(); 188 189 Sort::ksort($this->users); 190 191 $i = 0; 192 $count = 0; 193 $out = []; 194 $this->constructPattern($filter); 195 196 foreach($this->users as $user => $info) { 197 if($this->filter($user, $info)) { 198 if($i >= $start) { 199 $out[$user] = $info; 200 $count++; 201 if(($limit > 0) && ($count >= $limit)) break; 202 } 203 $i++; 204 } 205 } 206 207 return $out; 208 } 209 210 public function retrieveGroups($start = 0, $limit = 0) 211 { 212 $groups = []; 213 214 if ($this->users === null) $this->loadUserData(); 215 foreach ($this->users as $info) { 216 $groups = array_merge($groups, array_diff($info['grps'], $groups)); 217 } 218 Sort::ksort($groups); 219 220 if ($limit > 0) { 221 return array_splice($groups, $start, $limit); 222 } 223 return array_splice($groups, $start); 224 } 225 226 public function cleanUser($user) { 227 global $conf; 228 return cleanID(str_replace(':', $conf['sepchar'], $user)); 229 } 230 231 public function cleanGroup($group) { 232 global $conf; 233 return cleanID(str_replace(':', $conf['sepchar'], $group)); 234 } 235 236 protected function loadUserData(){ 237 //Logger::debug('authserversso: load user data'); 238 $this->users = $this->readUserFile($this->getConf(self::CONF_AUTH_USERFILE)); 239 } 240 241 protected function readUserFile($file) { 242 $users = array(); 243 if(!file_exists($file)) return $users; 244 245 Logger::debug('authserversso: read user file'); 246 $lines = file($file); 247 foreach($lines as $line) { 248 $line = preg_replace('/#.*$/', '', $line); //ignore comments 249 $line = trim($line); 250 if(empty($line)) continue; 251 252 $row = $this->splitUserData($line); 253 $row = str_replace('\\:', ':', $row); 254 $row = str_replace('\\\\', '\\', $row); 255 256 $groups = array_values(array_filter(explode(",", $row[4]))); 257 258 $users[$row[0]]['pass'] = $row[1]; 259 $users[$row[0]]['name'] = urldecode($row[2]); 260 $users[$row[0]]['mail'] = $row[3]; 261 $users[$row[0]]['grps'] = $groups; 262 } 263 return $users; 264 } 265 protected function splitUserData($line){ 266 $row = preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5); // allow for : escaped as \: 267 268 if (count($row) < 5) { 269 $row = array_pad($row, 5, ''); 270 Logger::error('User row with less than 5 fields', $row); 271 } 272 273 return $row; 274 } 275 276 protected function filter($user, $info) { 277 foreach($this->pattern as $item => $pattern) { 278 if($item == 'user') { 279 if(!preg_match($pattern, $user)) return false; 280 } else if($item == 'grps') { 281 if(!count(preg_grep($pattern, $info['grps']))) return false; 282 } else { 283 if(!preg_match($pattern, $info[$item])) return false; 284 } 285 } 286 return true; 287 } 288 289 protected function constructPattern($filter) { 290 $this->pattern = array(); 291 foreach($filter as $item => $pattern) { 292 $this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters 293 } 294 } 295 296 /** 297 * Do all authentication 298 * @param string $user Username 299 * @param string $pass Cleartext Password 300 * @param bool $sticky Cookie should not expire 301 * @return bool true on successful auth 302 */ 303 function trustExternal($user, $pass, $sticky=false) { 304 global $USERINFO; 305 global $ACT; 306 global $conf; 307 global $auth; 308 309 //Got a session already ? 310 if($this->hasSession()) { 311 //Logger::debug('authserversso: Session found'); 312 return true; 313 } 314 315 Logger::debug('authserversso: trustExternal: No Session'); 316 317 $userSso = $this->cleanUser($this->getSSOId()); 318 $data = $this->getUserData($userSso); 319 if($data == false) { 320 Logger::debug('authserversso: trustExternal: user does not exist'); 321 $mail = $this->getSSOMail(); 322 $name = $this->getSSOName(); 323 $pwd = auth_pwgen(); 324 $pwd = $this->createUser($userSso, $pwd, $name, $mail); 325 if(!is_null($pwd)) { 326 $data = $this->getUserData($userSso); 327 } 328 } 329 if($data == false) { 330 Logger::debug('authserversso: trustExternal: could not get user'); 331 return false; 332 } 333 $this->setSession($userSso, $data['grps'], $data['mail'], $data['name']); 334 //Logger::debug('authserversso: authenticated user'); 335 return true; 336 } 337 338 private function getSSOId() { 339 return $this->getServerVar($this->getConf(self::CONF_VAR_AUTH_ID)); 340 } 341 342 private function getSSOMail() { 343 $mail = $this->getServerVar($this->getConf(self::CONF_VAR_AUTH_EMAIL)); 344 if(!$mail || !mail_isvalid($mail)) return null; 345 return $mail; 346 } 347 348 private function getSSOName() { 349 return $this->getServerVar($this->getConf(self::CONF_VAR_AUTH_REALNAME)); 350 } 351 352 private function getServerVar($varName) { 353 if(is_null($varName)) return null; 354 if(!array_key_exists($varName, $_SERVER)) return null; 355 $varVal = $_SERVER[$varName]; 356 Logger::debug("authserversso: getServerVar {$varName}:{$varVal}"); 357 return $varVal; 358 } 359 360 private function hasSession() { 361 global $USERINFO; 362 //Logger::debug('authserversso: check hasSession'); 363 if(!empty($_SESSION[DOKU_COOKIE]['auth']['info'])) { 364 //Logger::debug('authserversso: Session found'); 365 $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['info']['name']; 366 $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['info']['mail']; 367 $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['info']['grps']; 368 $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 369 } 370 return false; 371 } 372 373 // Create user session 374 private function setSession($user, $grps, $mail, $name) { 375 global $USERINFO; 376 $USERINFO['name'] = $name; 377 $USERINFO['mail'] = $mail; 378 $USERINFO['grps'] = is_array($grps) ? $grps : array(); 379 $_SESSION[DOKU_COOKIE]['auth']['user'] = $user; 380 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 381 $_SERVER['REMOTE_USER'] = $user; 382 return $_SESSION[DOKU_COOKIE]; 383 } 384}