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