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