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