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 /** 21 * Constructor 22 * 23 * Carry out sanity checks to ensure the object is 24 * able to operate. Set capabilities. 25 * 26 * @author Christopher Smith <chris@jalakai.co.uk> 27 */ 28 public function __construct() { 29 parent::__construct(); 30 global $config_cascade; 31 32 if(!@is_readable($config_cascade['plainauth.users']['default'])) { 33 $this->success = false; 34 } else { 35 if(@is_writable($config_cascade['plainauth.users']['default'])) { 36 $this->cando['addUser'] = true; 37 $this->cando['delUser'] = true; 38 $this->cando['modLogin'] = true; 39 $this->cando['modPass'] = true; 40 $this->cando['modName'] = true; 41 $this->cando['modMail'] = true; 42 $this->cando['modGroups'] = true; 43 } 44 $this->cando['getUsers'] = true; 45 $this->cando['getUserCount'] = true; 46 } 47 } 48 49 /** 50 * Check user+password 51 * 52 * Checks if the given user exists and the given 53 * plaintext password is correct 54 * 55 * @author Andreas Gohr <andi@splitbrain.org> 56 * @param string $user 57 * @param string $pass 58 * @return bool 59 */ 60 public function checkPass($user, $pass) { 61 $userinfo = $this->getUserData($user); 62 if($userinfo === false) return false; 63 64 return auth_verifyPassword($pass, $this->users[$user]['pass']); 65 } 66 67 /** 68 * Return user info 69 * 70 * Returns info about the given user needs to contain 71 * at least these fields: 72 * 73 * name string full name of the user 74 * mail string email addres of the user 75 * grps array list of groups the user is in 76 * 77 * @author Andreas Gohr <andi@splitbrain.org> 78 * @param string $user 79 * @return array|bool 80 */ 81 public function getUserData($user) { 82 if($this->users === null) $this->_loadUserData(); 83 return isset($this->users[$user]) ? $this->users[$user] : false; 84 } 85 86 /** 87 * Create a new User 88 * 89 * Returns false if the user already exists, null when an error 90 * occurred and true if everything went well. 91 * 92 * The new user will be added to the default group by this 93 * function if grps are not specified (default behaviour). 94 * 95 * @author Andreas Gohr <andi@splitbrain.org> 96 * @author Chris Smith <chris@jalakai.co.uk> 97 * 98 * @param string $user 99 * @param string $pwd 100 * @param string $name 101 * @param string $mail 102 * @param array $grps 103 * @return bool|null|string 104 */ 105 public function createUser($user, $pwd, $name, $mail, $grps = null) { 106 global $conf; 107 global $config_cascade; 108 109 // user mustn't already exist 110 if($this->getUserData($user) !== false) return false; 111 112 $pass = auth_cryptPassword($pwd); 113 114 // set default group if no groups specified 115 if(!is_array($grps)) $grps = array($conf['defaultgroup']); 116 117 // prepare user line 118 $groups = join(',', $grps); 119 $userline = join(':', array($user, $pass, $name, $mail, $groups))."\n"; 120 121 if(io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) { 122 $this->users[$user] = compact('pass', 'name', 'mail', 'grps'); 123 return $pwd; 124 } 125 126 msg( 127 'The '.$config_cascade['plainauth.users']['default']. 128 ' file is not writable. Please inform the Wiki-Admin', -1 129 ); 130 return null; 131 } 132 133 /** 134 * Modify user data 135 * 136 * @author Chris Smith <chris@jalakai.co.uk> 137 * @param string $user nick of the user to be changed 138 * @param array $changes array of field/value pairs to be changed (password will be clear text) 139 * @return bool 140 */ 141 public function modifyUser($user, $changes) { 142 global $ACT; 143 global $config_cascade; 144 145 // sanity checks, user must already exist and there must be something to change 146 if(($userinfo = $this->getUserData($user)) === false) return false; 147 if(!is_array($changes) || !count($changes)) return true; 148 149 // update userinfo with new data, remembering to encrypt any password 150 $newuser = $user; 151 foreach($changes as $field => $value) { 152 if($field == 'user') { 153 $newuser = $value; 154 continue; 155 } 156 if($field == 'pass') $value = auth_cryptPassword($value); 157 $userinfo[$field] = $value; 158 } 159 160 $groups = join(',', $userinfo['grps']); 161 $userline = join(':', array($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $groups))."\n"; 162 163 if(!$this->deleteUsers(array($user))) { 164 msg('Unable to modify user data. Please inform the Wiki-Admin', -1); 165 return false; 166 } 167 168 if(!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) { 169 msg('There was an error modifying your user data. You should register again.', -1); 170 // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page 171 $ACT = 'register'; 172 return false; 173 } 174 175 $this->users[$newuser] = $userinfo; 176 return true; 177 } 178 179 /** 180 * Remove one or more users from the list of registered users 181 * 182 * @author Christopher Smith <chris@jalakai.co.uk> 183 * @param array $users array of users to be deleted 184 * @return int the number of users deleted 185 */ 186 public function deleteUsers($users) { 187 global $config_cascade; 188 189 if(!is_array($users) || empty($users)) return 0; 190 191 if($this->users === null) $this->_loadUserData(); 192 193 $deleted = array(); 194 foreach($users as $user) { 195 if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/'); 196 } 197 198 if(empty($deleted)) return 0; 199 200 $pattern = '/^('.join('|', $deleted).'):/'; 201 202 if(io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true)) { 203 foreach($deleted as $user) unset($this->users[$user]); 204 return count($deleted); 205 } 206 207 // problem deleting, reload the user list and count the difference 208 $count = count($this->users); 209 $this->_loadUserData(); 210 $count -= count($this->users); 211 return $count; 212 } 213 214 /** 215 * Return a count of the number of user which meet $filter criteria 216 * 217 * @author Chris Smith <chris@jalakai.co.uk> 218 * 219 * @param array $filter 220 * @return int 221 */ 222 public function getUserCount($filter = array()) { 223 224 if($this->users === null) $this->_loadUserData(); 225 226 if(!count($filter)) return count($this->users); 227 228 $count = 0; 229 $this->_constructPattern($filter); 230 231 foreach($this->users as $user => $info) { 232 $count += $this->_filter($user, $info); 233 } 234 235 return $count; 236 } 237 238 /** 239 * Bulk retrieval of user data 240 * 241 * @author Chris Smith <chris@jalakai.co.uk> 242 * 243 * @param int $start index of first user to be returned 244 * @param int $limit max number of users to be returned 245 * @param array $filter array of field/pattern pairs 246 * @return array userinfo (refer getUserData for internal userinfo details) 247 */ 248 public function retrieveUsers($start = 0, $limit = 0, $filter = array()) { 249 250 if($this->users === null) $this->_loadUserData(); 251 252 ksort($this->users); 253 254 $i = 0; 255 $count = 0; 256 $out = array(); 257 $this->_constructPattern($filter); 258 259 foreach($this->users as $user => $info) { 260 if($this->_filter($user, $info)) { 261 if($i >= $start) { 262 $out[$user] = $info; 263 $count++; 264 if(($limit > 0) && ($count >= $limit)) break; 265 } 266 $i++; 267 } 268 } 269 270 return $out; 271 } 272 273 /** 274 * Only valid pageid's (no namespaces) for usernames 275 * 276 * @param string $user 277 * @return string 278 */ 279 public function cleanUser($user) { 280 global $conf; 281 return cleanID(str_replace(':', $conf['sepchar'], $user)); 282 } 283 284 /** 285 * Only valid pageid's (no namespaces) for groupnames 286 * 287 * @param string $group 288 * @return string 289 */ 290 public function cleanGroup($group) { 291 global $conf; 292 return cleanID(str_replace(':', $conf['sepchar'], $group)); 293 } 294 295 /** 296 * Load all user data 297 * 298 * loads the user file into a datastructure 299 * 300 * @author Andreas Gohr <andi@splitbrain.org> 301 */ 302 protected function _loadUserData() { 303 global $config_cascade; 304 305 $this->users = array(); 306 307 if(!@file_exists($config_cascade['plainauth.users']['default'])) return; 308 309 $lines = file($config_cascade['plainauth.users']['default']); 310 foreach($lines as $line) { 311 $line = preg_replace('/#.*$/', '', $line); //ignore comments 312 $line = trim($line); 313 if(empty($line)) continue; 314 315 $row = explode(":", $line, 5); 316 $groups = array_values(array_filter(explode(",", $row[4]))); 317 318 $this->users[$row[0]]['pass'] = $row[1]; 319 $this->users[$row[0]]['name'] = urldecode($row[2]); 320 $this->users[$row[0]]['mail'] = $row[3]; 321 $this->users[$row[0]]['grps'] = $groups; 322 } 323 } 324 325 /** 326 * return true if $user + $info match $filter criteria, false otherwise 327 * 328 * @author Chris Smith <chris@jalakai.co.uk> 329 * 330 * @param string $user User login 331 * @param array $info User's userinfo array 332 * @return bool 333 */ 334 protected function _filter($user, $info) { 335 foreach($this->_pattern as $item => $pattern) { 336 if($item == 'user') { 337 if(!preg_match($pattern, $user)) return false; 338 } else if($item == 'grps') { 339 if(!count(preg_grep($pattern, $info['grps']))) return false; 340 } else { 341 if(!preg_match($pattern, $info[$item])) return false; 342 } 343 } 344 return true; 345 } 346 347 /** 348 * construct a filter pattern 349 * 350 * @param array $filter 351 */ 352 protected function _constructPattern($filter) { 353 $this->_pattern = array(); 354 foreach($filter as $item => $pattern) { 355 $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters 356 } 357 } 358}