1<?php 2/** 3 * DokuWiki Plugin authnc (Auth Component) 4 * 5 * The commented functions are kept fore reference or later implementation. 6 * 7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 8 * @author Henrik Jürges <ratzeputz@rtzptz.xyz> 9 */ 10 11// must be run within Dokuwiki 12if (!defined('DOKU_INC')) { 13 die(); 14} 15 16class auth_plugin_authnc extends DokuWiki_Auth_Plugin 17{ 18 19 protected $con = NULL; 20 21 protected $curl = NULL; 22 23 /** 24 * Constructor. 25 */ 26 public function __construct() 27 { 28 parent::__construct(); // for compatibility 29 global $config_cascade; 30 global $config; 31 32 $this->curl = curl_init(); 33 $options = array( 34 CURLOPT_HTTPGET => 1, // default, but make clear 35 CURLOPT_RETURNTRANSFER => TRUE, 36 CURLOPT_HTTPHEADER => array("OCS-APIRequest:true"), 37 ); 38 curl_setopt_array($this->curl, $options); 39 40 $this->cando['addUser'] = false; // can Users be created? 41 $this->cando['delUser'] = false; // can Users be deleted? 42 $this->cando['modLogin'] = false; // can login names be changed? 43 $this->cando['modPass'] = false; // can passwords be changed? 44 $this->cando['modName'] = false; // can real names be changed? 45 $this->cando['modMail'] = false; // can emails be changed? 46 $this->cando['modGroups'] = false; // can groups be changed? 47 $this->cando['getUsers'] = true; // can a (filtered) list of users be retrieved? 48 $this->cando['getUserCount']= true; // can the number of users be retrieved? 49 $this->cando['getGroups'] = true; // can a list of available groups be retrieved? 50 $this->cando['external'] = true; // does the module do external auth checking? 51 $this->cando['logout'] = true; // can the user logout again? (eg. not possible with HTTP auth) 52 53 if (!function_exists('curl_init') || ! $this->server_online()) { 54 $this->success = false; 55 } 56 $this->success = true; 57 } 58 59 60 /** 61 * Log off the current user [ OPTIONAL ] 62 */ 63 public function logOff() 64 { 65 // return nothing to log out 66 curl_close($this->curl); 67 } 68 69 /** 70 * Do all authentication [ OPTIONAL ] 71 * 72 * @param string $user Username 73 * @param string $pass Cleartext Password 74 * @param bool $sticky Cookie should not expire 75 * 76 * @return bool true on successful auth 77 */ 78 public function trustExternal($user, $pass, $sticky = false) 79 { 80 global $USERINFO; 81 global $conf; 82 $sticky ? $sticky = true : $sticky = false; //sanity check 83 84 // check only if a user tries to log in, otherwise the function is called with every pageload 85 if (!empty($user)) { 86 // try the login 87 $server = $this->con . 'users/' . $user; 88 $xml = $this->nc_request($server, $user, $pass); 89 $logged_in = false; 90 if ($xml && $xml->meta->status == "ok") { 91 // hurray, we're succeded 92 93 $logged_in = true; 94 } else { 95 $msg = $xml ? " with error " . $xml->meta->message : " connection error"; 96 msg("Failed to log in " . $msg); 97 } 98 99 if ($logged_in) { 100 $groups = array(); 101 foreach ($xml->data->groups->element as $grp) { 102 $groups[] = (string)$grp; 103 } 104 // set the globals if authed 105 $USERINFO['name'] = (string)$xml->data->displayname; 106 $USERINFO['mail'] = (string)$xml->data->email; 107 $USERINFO['grps'] = $groups; 108 $_SERVER['REMOTE_USER'] = $user; 109 $_SESSION[DOKU_COOKIE]['auth']['user'] = $user; 110 $_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass; 111 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 112 } 113 } 114 115 // check if already logged in 116 if (!empty($_SESSION[DOKU_COOKIE]['auth']['info'])) { 117 $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['info']['name']; 118 $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['info']['mail']; 119 $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['info']['grps']; 120 $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 121 $logged_in = true; 122 } 123 return $logged_in; 124 } 125 126 /** 127 * Check user+password 128 * 129 * May be ommited if trustExternal is used. 130 * 131 * @param string $user the user name 132 * @param string $pass the clear text password 133 * 134 * @return bool 135 */ 136 public function checkPass($user, $pass) 137 { 138 return false; 139 } 140 141 /** 142 * Return user info 143 * 144 * Returns info about the given user needs to contain 145 * at least these fields: 146 * 147 * name string full name of the user 148 * mail string email addres of the user 149 * grps array list of groups the user is in 150 * 151 * @param string $user the user name 152 * @param bool $requireGroups whether or not the returned data must include groups 153 * 154 * @return array containing user data or false 155 */ 156 public function getUserData($user, $requireGroups=true) 157 { 158 global $USERINFO; 159 $self['user'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 160 $self['name'] = $USERINFO['name']; 161 $self['mail'] = $USERINFO['mail']; 162 if ($requireGroups) { 163 $self['grps'] = $USERINFO['grps']; 164 } 165 return $self; 166 } 167 168 /** 169 * Create a new User [implement only where required/possible] 170 * 171 * Returns false if the user already exists, null when an error 172 * occurred and true if everything went well. 173 * 174 * The new user HAS TO be added to the default group by this 175 * function! 176 * 177 * Set addUser capability when implemented 178 * 179 * @param string $user 180 * @param string $pass 181 * @param string $name 182 * @param string $mail 183 * @param null|array $grps 184 * 185 * @return bool|null 186 */ 187 //public function createUser($user, $pass, $name, $mail, $grps = null) 188 //{ 189 // FIXME implement 190 // return null; 191 //} 192 193 /** 194 * Modify user data [implement only where required/possible] 195 * 196 * Set the mod* capabilities according to the implemented features 197 * 198 * @param string $user nick of the user to be changed 199 * @param array $changes array of field/value pairs to be changed (password will be clear text) 200 * 201 * @return bool 202 */ 203 //public function modifyUser($user, $changes) 204 //{ 205 // FIXME implement 206 // return false; 207 //} 208 209 /** 210 * Delete one or more users [implement only where required/possible] 211 * 212 * Set delUser capability when implemented 213 * 214 * @param array $users 215 * 216 * @return int number of users deleted 217 */ 218 //public function deleteUsers($users) 219 //{ 220 // FIXME implement 221 // return false; 222 //} 223 224 /** 225 * Bulk retrieval of user data [implement only where required/possible] 226 * 227 * Set getUsers capability when implemented 228 * 229 * @param int $start index of first user to be returned 230 * @param int $limit max number of users to be returned, 0 for unlimited 231 * @param array $filter array of field/pattern pairs, null for no filter 232 * 233 * @return array list of userinfo (refer getUserData for internal userinfo details) 234 */ 235 public function retrieveUsers($start = 0, $limit = 0, $filter = null) 236 { 237 global $USERINFO; 238 $server = $this->con . 'users'; 239 $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']); 240 if (! $xml || ! $xml->data->users) { 241 msg("Retrieving user list failed"); 242 return array(); 243 } 244 245 $users = array(); 246 $self['user'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 247 $self['name'] = $USERINFO['name']; 248 $self['mail'] = $USERINFO['mail']; 249 $self['grps'] = $USERINFO['grps']; 250 $users[] = $self; 251 foreach($xml->data->users->element as $user) { 252 // Request the user information for every user, this may take a while 253 if ($user == $_SESSION[DOKU_COOKIE]['auth']['user']) { 254 continue; // Skip the session user 255 } 256 257 $server = $this->con . 'users/' . (string)$user; 258 $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']); 259 if ($xml && $xml->meta->status == "ok" && $xml->data->enabled == '1') { 260 $usr['user'] = (string)$user; 261 $usr['name'] = (string)$xml->data->displayname; 262 $usr['mail'] = (string)$xml->data->email; 263 $groups = array(); 264 foreach ($xml->data->groups->element as $grp) { 265 $groups[] = (string)$grp; 266 } 267 $usr['grps'] = $groups; 268 $users[] = $usr; 269 } 270 } 271 return $users; 272 } 273 274 /** 275 * Return a count of the number of user which meet $filter criteria 276 * [should be implemented whenever retrieveUsers is implemented] 277 * 278 * Set getUserCount capability when implemented 279 * 280 * @param array $filter array of field/pattern pairs, empty array for no filter 281 * 282 * @return int 283 */ 284 public function getUserCount($filter = array()) 285 { 286 $server = $this->con . 'users'; 287 $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']); 288 if (! $xml || ! $xml->data->users) { 289 msg("Retrieving user count failed"); 290 return 0; 291 } 292 return count($xml->data->users->element); 293 } 294 295 /** 296 * Define a group [implement only where required/possible] 297 * 298 * Set addGroup capability when implemented 299 * 300 * @param string $group 301 * 302 * @return bool 303 */ 304 //public function addGroup($group) 305 //{ 306 // FIXME implement 307 // return false; 308 //} 309 310 /** 311 * Retrieve groups [implement only where required/possible] 312 * 313 * Set getGroups capability when implemented 314 * 315 * @param int $start 316 * @param int $limit 317 * 318 * @return array 319 */ 320 public function retrieveGroups($start = 0, $limit = 0) 321 { 322 $server = $this->con . 'groups'; 323 $xml = $this->nc_request($server, $_SESSION[DOKU_COOKIE]['auth']['user'], $_SESSION[DOKU_COOKIE]['auth']['pass']); 324 if (! $xml || ! $xml->data->groups) { 325 msg("Retrieving groups failed"); 326 return array(); 327 } 328 $groups = array(); 329 foreach ($xml->data->groups->element as $grp) { 330 msg((string) $grp); 331 $groups[(string)$grp] = (string)$grp; 332 } 333 return $groups; 334 } 335 336 /** 337 * Return case sensitivity of the backend 338 * 339 * When your backend is caseinsensitive (eg. you can login with USER and 340 * user) then you need to overwrite this method and return false 341 * 342 * @return bool 343 */ 344 public function isCaseSensitive() 345 { 346 return true; 347 } 348 349 /** 350 * Sanitize a given username 351 * 352 * This function is applied to any user name that is given to 353 * the backend and should also be applied to any user name within 354 * the backend before returning it somewhere. 355 * 356 * This should be used to enforce username restrictions. 357 * 358 * @param string $user username 359 * @return string the cleaned username 360 */ 361 public function cleanUser($user) 362 { 363 return $user; 364 } 365 366 /** 367 * Sanitize a given groupname 368 * 369 * This function is applied to any groupname that is given to 370 * the backend and should also be applied to any groupname within 371 * the backend before returning it somewhere. 372 * 373 * This should be used to enforce groupname restrictions. 374 * 375 * Groupnames are to be passed without a leading '@' here. 376 * 377 * @param string $group groupname 378 * 379 * @return string the cleaned groupname 380 */ 381 public function cleanGroup($group) 382 { 383 return $group; 384 } 385 386 /** 387 * Check Session Cache validity [implement only where required/possible] 388 * 389 * DokuWiki caches user info in the user's session for the timespan defined 390 * in $conf['auth_security_timeout']. 391 * 392 * This makes sure slow authentication backends do not slow down DokuWiki. 393 * This also means that changes to the user database will not be reflected 394 * on currently logged in users. 395 * 396 * To accommodate for this, the user manager plugin will touch a reference 397 * file whenever a change is submitted. This function compares the filetime 398 * of this reference file with the time stored in the session. 399 * 400 * This reference file mechanism does not reflect changes done directly in 401 * the backend's database through other means than the user manager plugin. 402 * 403 * Fast backends might want to return always false, to force rechecks on 404 * each page load. Others might want to use their own checking here. If 405 * unsure, do not override. 406 * 407 * @param string $user - The username 408 * 409 * @return bool 410 */ 411 //public function useSessionCache($user) 412 //{ 413 // FIXME implement 414 //} 415 416 protected function server_online() { 417 if ($this->con) return true; // some link is already set 418 // check if the server is reachable by opening a socket 419 $host = explode(':', $this->getConf('server')); 420 $fp = fSockOpen('ssl:' . $host[1], $this->getConf('port'), $errno, $errstr, 5); 421 if (!$fp) return false; // server is not reachable 422 $this->con = $this->getConf('server') . ':' . $this->getConf('port') . '/' . $this->getConf('ocs-path'); 423 return true; // no more error checking, assume reachable 424 } 425 426 /** 427 * Send a request to the nextcloud instance. 428 * 429 * Returns the parsed xml file or NULL if 430 * the request or parsing failed. 431 * 432 * At some point curl generates an invalid syntax 998 error 433 * see https://www.freedesktop.org/wiki/Specifications/open-collaboration-services/ 434 * and https://help.nextcloud.com/t/api-error-creating-user-failure-998-invalid-query/56530 435 * 436 * @param string $url request url, shall return xml 437 * @param string $user the user name 438 * @param string $pass the users password 439 * 440 * @return object the parsed xml or NULL 441 */ 442 protected function nc_request($url, $user, $pass) { 443 curl_setopt($this->curl, CURLOPT_USERPWD, $user . ':' . $pass); 444 curl_setopt($this->curl, CURLOPT_URL, $url); 445 if ($result = curl_exec($this->curl)) { 446 return simplexml_load_string($result); 447 } else { 448 msg('Request failed with error: ' . curl_error($ch) . '. Return code: ' . $result); 449 return NULL; 450 } 451 } 452} 453