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 io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true); 202 203 // reload the user list and count the difference 204 $count = count($this->users); 205 $this->_loadUserData(); 206 $count -= count($this->users); 207 return $count; 208 } 209 210 /** 211 * Return a count of the number of user which meet $filter criteria 212 * 213 * @author Chris Smith <chris@jalakai.co.uk> 214 * 215 * @param array $filter 216 * @return int 217 */ 218 public function getUserCount($filter = array()) { 219 220 if($this->users === null) $this->_loadUserData(); 221 222 if(!count($filter)) return count($this->users); 223 224 $count = 0; 225 $this->_constructPattern($filter); 226 227 foreach($this->users as $user => $info) { 228 $count += $this->_filter($user, $info); 229 } 230 231 return $count; 232 } 233 234 /** 235 * Bulk retrieval of user data 236 * 237 * @author Chris Smith <chris@jalakai.co.uk> 238 * 239 * @param int $start index of first user to be returned 240 * @param int $limit max number of users to be returned 241 * @param array $filter array of field/pattern pairs 242 * @return array userinfo (refer getUserData for internal userinfo details) 243 */ 244 public function retrieveUsers($start = 0, $limit = 0, $filter = array()) { 245 246 if($this->users === null) $this->_loadUserData(); 247 248 ksort($this->users); 249 250 $i = 0; 251 $count = 0; 252 $out = array(); 253 $this->_constructPattern($filter); 254 255 foreach($this->users as $user => $info) { 256 if($this->_filter($user, $info)) { 257 if($i >= $start) { 258 $out[$user] = $info; 259 $count++; 260 if(($limit > 0) && ($count >= $limit)) break; 261 } 262 $i++; 263 } 264 } 265 266 return $out; 267 } 268 269 /** 270 * Only valid pageid's (no namespaces) for usernames 271 * 272 * @param string $user 273 * @return string 274 */ 275 public function cleanUser($user) { 276 global $conf; 277 return cleanID(str_replace(':', $conf['sepchar'], $user)); 278 } 279 280 /** 281 * Only valid pageid's (no namespaces) for groupnames 282 * 283 * @param string $group 284 * @return string 285 */ 286 public function cleanGroup($group) { 287 global $conf; 288 return cleanID(str_replace(':', $conf['sepchar'], $group)); 289 } 290 291 /** 292 * Load all user data 293 * 294 * loads the user file into a datastructure 295 * 296 * @author Andreas Gohr <andi@splitbrain.org> 297 */ 298 protected function _loadUserData() { 299 global $config_cascade; 300 301 $this->users = array(); 302 303 if(!@file_exists($config_cascade['plainauth.users']['default'])) return; 304 305 $lines = file($config_cascade['plainauth.users']['default']); 306 foreach($lines as $line) { 307 $line = preg_replace('/#.*$/', '', $line); //ignore comments 308 $line = trim($line); 309 if(empty($line)) continue; 310 311 $row = explode(":", $line, 5); 312 $groups = array_values(array_filter(explode(",", $row[4]))); 313 314 $this->users[$row[0]]['pass'] = $row[1]; 315 $this->users[$row[0]]['name'] = urldecode($row[2]); 316 $this->users[$row[0]]['mail'] = $row[3]; 317 $this->users[$row[0]]['grps'] = $groups; 318 } 319 } 320 321 /** 322 * return true if $user + $info match $filter criteria, false otherwise 323 * 324 * @author Chris Smith <chris@jalakai.co.uk> 325 * 326 * @param string $user User login 327 * @param array $info User's userinfo array 328 * @return bool 329 */ 330 protected function _filter($user, $info) { 331 foreach($this->_pattern as $item => $pattern) { 332 if($item == 'user') { 333 if(!preg_match($pattern, $user)) return false; 334 } else if($item == 'grps') { 335 if(!count(preg_grep($pattern, $info['grps']))) return false; 336 } else { 337 if(!preg_match($pattern, $info[$item])) return false; 338 } 339 } 340 return true; 341 } 342 343 /** 344 * construct a filter pattern 345 * 346 * @param array $filter 347 */ 348 protected function _constructPattern($filter) { 349 $this->_pattern = array(); 350 foreach($filter as $item => $pattern) { 351 $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters 352 } 353 } 354}