1<?php 2/** 3 * Authentication Plugin for authsmf20. 4 * 5 * @package SMF DokuWiki 6 * @file auth.php 7 * @author digger <digger@mysmf.net> 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @version 1.0 beta2 10 */ 11 12/* 13 * Sign in DokuWiki via SMF database. This file is a part of authsmf20 plugin 14 * and must be in plugin directory for correct work. 15 * 16 * Requirements: SMF 2.x with utf8 encoding in database and subdomain independent cookies 17 * SMF - Admin - Server Settings - Cookies and Sessions: Use subdomain independent cookies 18 * Tested with DokuWiki 2017-02-19b "Frusterick Manners" 19*/ 20 21if (!defined('DOKU_INC')) { 22 die(); 23} 24 25/** 26 * SMF 2.0 Authentication class. 27 */ 28class auth_plugin_authsmf20 extends DokuWiki_Auth_Plugin 29{ 30 protected $_smf_db_link = null; 31 32 protected $_smf_conf = array( 33 'path' => '', 34 'boardurl' => '', 35 'db_server' => '', 36 'db_port' => 3306, 37 'db_name' => '', 38 'db_user' => '', 39 'db_passwd' => '', 40 'db_character_set' => '', 41 'db_prefix' => '', 42 ); 43 44 protected 45 $_smf_user_id = 0, 46 $_smf_user_realname = '', 47 $_smf_user_username = '', 48 $_smf_user_email = '', 49 $_smf_user_is_banned = false, 50 $_smf_user_avatar = '', 51 $_smf_user_groups = array(), 52 $_smf_user_profile = '', 53 $_cache = null, 54 $_cache_duration = 0, 55 $_cache_ext_name = '.authsmf20'; 56 57 CONST CACHE_DURATION_UNIT = 86400; 58 59 /** 60 * Constructor. 61 */ 62 public function __construct() 63 { 64 $this->cando['addUser'] = false; 65 $this->cando['delUser'] = false; 66 $this->cando['modLogin'] = false; 67 $this->cando['modPass'] = false; 68 $this->cando['modName'] = false; 69 $this->cando['modMail'] = false; 70 $this->cando['modGroups'] = false; 71 $this->cando['getUsers'] = false; 72 $this->cando['getUserCount'] = false; 73 $this->cando['getGroups'] = true; 74 $this->cando['external'] = true; 75 $this->cando['logout'] = true; 76 77 $this->success = $this->loadConfiguration(); 78 79 if (!$this->success) { 80 msg($this->getLang('config_error'), -1); 81 } 82 } 83 84 /** 85 * Destructor. 86 */ 87 public function __destruct() 88 { 89 $this->disconnectSmfDB(); 90 $this->_cache = null; 91 92 } 93 94 /** 95 * Do all authentication 96 * 97 * @param string $user Username 98 * @param string $pass Cleartext Password 99 * @param boolean $sticky Cookie should not expire 100 * @return boolean True on successful auth 101 */ 102 public function trustExternal($user = '', $pass = '', $sticky = false) 103 { 104 global $USERINFO; 105 106 $sticky ? $sticky = true : $sticky = false; 107 108 if ($this->doLoginCookie()) { 109 return true; // User already logged in 110 } 111 112 if ($user) { 113 $is_logged = $this->checkPass($user, $pass); // Try to login over DokuWiki login form 114 } else { 115 $is_logged = $this->doLoginSSI(); // Try to login over SMF SSI API 116 } 117 118 if (!$is_logged) { 119 if ($user) { 120 msg($this->getLang('login_error'), -1); 121 } 122 return false; 123 } 124 125 $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['user'] = $this->_smf_user_username; 126 $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['mail'] = $this->_smf_user_email; 127 $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['grps'] = $this->_smf_user_groups; 128 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 129 $_SERVER['REMOTE_USER'] = $USERINFO['name']; 130 131 return true; 132 } 133 134 /** 135 * Log off the current user 136 */ 137 public function logOff() 138 { 139 unset($_SESSION[DOKU_COOKIE]); 140 141 // This doesn't work now because SMF SSI API have session logout issue 142 //$link = ssi_logout(DOKU_URL, 'array'); 143 //preg_match('/href="(.+)"/iU', $link, $url); 144 //send_redirect($url[1]); 145 } 146 147 /** 148 * Loads the plugin configuration. 149 * 150 * @return boolean True on success, false otherwise 151 */ 152 private function loadConfiguration() 153 { 154 $ssi_guest_access = true; 155 156 $this->_smf_conf['path'] = rtrim(trim($this->getConf('smf_path')), '\/'); 157 if (!file_exists($this->_smf_conf['path'] . '/SSI.php')) { 158 dbglog('SMF not found in path' . $this->_smf_conf['path']); 159 return false; 160 } else { 161 include_once($this->_smf_conf['path'] . '/SSI.php'); 162 } 163 164 $this->_smf_conf['boardurl'] = $boardurl; 165 $this->_smf_conf['db_server'] = $db_server; 166 $this->_smf_conf['db_name'] = $db_name; 167 $this->_smf_conf['db_user'] = $db_user; 168 $this->_smf_conf['db_passwd'] = $db_passwd; 169 $this->_smf_conf['db_character_set'] = $db_character_set; 170 $this->_smf_conf['db_prefix'] = $db_prefix; 171 172 return (!empty($this->_smf_conf['boardurl'])); 173 } 174 175 /** 176 * Authenticate the user using SMF SSI. 177 * 178 * @return boolean True on successful login 179 */ 180 private function doLoginSSI() 181 { 182 $user_info = ssi_welcome('array'); 183 184 if (empty($user_info['is_logged'])) { 185 return false; 186 } 187 188 $this->_smf_user_id = $user_info['id']; 189 $this->_smf_user_username = $user_info['username']; 190 $this->_smf_user_email = $user_info['email']; 191 $this->getUserGroups(); 192 193 return true; 194 } 195 196 /** 197 * Authenticate the user using DokuWiki Cookie. 198 * 199 * @return boolean True on successful login 200 */ 201 private function doLoginCookie() 202 { 203 global $USERINFO; 204 205 if (empty($_SESSION[DOKU_COOKIE]['auth']['info'])) { 206 return false; 207 } 208 209 $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 210 $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['mail']; 211 $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['grps']; 212 213 $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 214 215 return true; 216 } 217 218 /** 219 * Connect to SMF database. 220 * 221 * @return boolean True on success, false otherwise 222 */ 223 private function connectSmfDB() 224 { 225 if (!$this->_smf_db_link) { 226 $this->_smf_db_link = new mysqli( 227 $this->_smf_conf['db_server'], $this->_smf_conf['db_user'], 228 $this->_smf_conf['db_passwd'], $this->_smf_conf['db_name'], 229 (int)$this->_smf_conf['db_port'] 230 ); 231 232 if (!$this->_smf_db_link || $this->_smf_db_link->connect_error) { 233 $error = 'Cannot connect to database server'; 234 235 if ($this->_smf_db_link) { 236 $error .= ' (' . $this->_smf_db_link->connect_errno . ')'; 237 } 238 dbglog($error); 239 msg($this->getLang('database_error'), -1); 240 $this->_smf_db_link = null; 241 242 return false; 243 } 244 245 if ($this->_smf_conf['db_character_set'] == 'utf8') { 246 $this->_smf_db_link->set_charset('utf8'); 247 } 248 } 249 return ($this->_smf_db_link && $this->_smf_db_link->ping()); 250 } 251 252 /** 253 * Disconnects from SMF database. 254 */ 255 private function disconnectSmfDB() 256 { 257 if ($this->_smf_db_link !== null) { 258 $this->_smf_db_link->close(); 259 $this->_smf_db_link = null; 260 } 261 } 262 263 /** 264 * Get SMF user's groups. 265 * 266 * @return boolean True for success, false otherwise 267 */ 268 private function getUserGroups() 269 { 270 271 if (!$this->connectSmfDB() || !$this->_smf_user_id) { 272 return false; 273 } 274 275 $query = "SELECT mg.group_name, m.id_group 276 FROM {$this->_smf_conf['db_prefix']}members m 277 LEFT JOIN {$this->_smf_conf['db_prefix']}membergroups mg ON mg.id_group = m.id_group OR FIND_IN_SET (mg.id_group, m.additional_groups) OR mg.id_group = m.id_post_group 278 WHERE m.id_member = {$this->_smf_user_id}"; 279 280 $result = $this->_smf_db_link->query($query); 281 282 if (!$result) { 283 dbglog("cannot get groups for user id: {$this->_smf_user_id}"); 284 return false; 285 } 286 287 while ($row = $result->fetch_object()) { 288 if ($row->id_group == 1) { 289 $this->_smf_user_groups[] = 'admin'; // Map SMF Admin to DokuWiki Admin 290 } else { 291 $this->_smf_user_groups[] = $row->group_name; 292 } 293 } 294 295 if (!$this->_smf_user_is_banned) { 296 $this->_smf_user_groups[] = 'user'; 297 } // Banned users as guests 298 $this->_smf_user_groups = array_unique($this->_smf_user_groups); 299 300 $result->close(); 301 unset($row); 302 return true; 303 } 304 305 /** 306 * Return user info 307 * 308 * Returns info about the given user needs to contain 309 * at least these fields: 310 * 311 * name string full name of the user 312 * mail string email address of the user 313 * grps array list of groups the user is in 314 * 315 * @param string $user User name 316 * @param bool $requireGroups Whether or not the returned data must include groups 317 * @return false|array Containing user data or false 318 * 319 * array['realname'] string User's real name 320 * array['username'] string User's username 321 * array['email'] string User's email address 322 * array['smf_user_id'] string User's ID 323 * array['smf_profile'] string User's link to profile 324 * array['smf_user_groups'] array User's groups 325 */ 326 public function getUserData($user, $requireGroups = true) 327 { 328 if (empty($user)) { 329 return false; 330 } 331 332 $user_data = false; 333 334 335 $this->_cache_duration = (int)($this->getConf('smf_cache')); 336 $depends = array('age' => self::CACHE_DURATION_UNIT * $this->_cache_duration); 337 $cache = new cache('authsmf20_getUserData_' . $user, $this->_cache_ext_name); 338 339 340 if (($this->_cache_duration > 0) && $cache->useCache($depends)) { 341 $user_data = unserialize($cache->retrieveCache(false)); 342 } else { 343 344 $cache->removeCache(); 345 346 if (!$this->connectSmfDB()) { 347 return false; 348 } 349 350 $user = $this->_smf_db_link->real_escape_string($user); 351 352 $query = "SELECT m.id_member, m.real_name, m.email_address, m.gender, m.location, m.usertitle, m.personal_text, m.signature, IF(m.avatar = '', a.id_attach, m.avatar) AS avatar 353 FROM {$this->_smf_conf['db_prefix']}members m 354 LEFT JOIN {$this->_smf_conf['db_prefix']}attachments a ON a.id_member = m.id_member AND a.id_msg = 0 355 WHERE member_name = '{$user}'"; 356 357 $result = $this->_smf_db_link->query($query); 358 359 if (!$result) { 360 dbglog("No data found in database for user: {$user}"); 361 return false; 362 } 363 364 $row = $result->fetch_object(); 365 366 $this->_smf_user_id = $row->id_member; 367 $this->getUserGroups(); 368 369 $user_data['smf_user_groups'] = array_unique($this->_smf_user_groups); 370 $user_data['smf_user_id'] = $row->id_member; 371 $user_data['smf_user_username'] = $user; 372 $user_data['smf_user_realname'] = $row->real_name; 373 374 if (empty($user_data['smf_user_realname'])) { 375 $user_data['smf_user_realname'] = $user_data['smf_user_username']; 376 } 377 378 $user_data['smf_user_email'] = $row->email_address; 379 380 if ($row->gender == 1) { 381 $user_data['smf_user_gender'] = 'male'; 382 } elseif ($row->gender == 2) { 383 $user_data['smf_user_gender'] = 'female'; 384 } else { 385 $user_data['smf_user_gender'] = 'unknown'; 386 } 387 388 $user_data['smf_user_location'] = $row->location; 389 $user_data['smf_user_usertitle'] = $row->usertitle; 390 $user_data['smf_personal_text'] = $row->personal_text; 391 $user_data['smf_user_profile'] = $this->_smf_conf['boardurl'] . '/index.php?action=profile;u=' . $this->_smf_user_id; 392 $user_data['smf_user_avatar'] = $this->getAvatarUrl($row->avatar); 393 394 $result->close(); 395 unset($row); 396 397 $cache->storeCache(serialize($user_data)); 398 } 399 400 $cache = null; 401 unset($cache); 402 return $user_data; 403 } 404 405 /** 406 * Retrieve groups 407 * 408 * @param int $start 409 * @param int $limit 410 * @return array|false Containing groups list, false if error 411 */ 412 public function retrieveGroups($start = 0, $limit = 10) 413 { 414 if (!$this->connectSmfDB()) { 415 return false; 416 } 417 418 $query = "SELECT group_name 419 FROM {$this->_smf_conf['db_prefix']}membergroups 420 LIMIT {$start}, {$limit}"; 421 422 $result = $this->_smf_db_link->query($query); 423 424 if (!$result) { 425 dbglog("Cannot get SMF groups list"); 426 return false; 427 } 428 429 while ($row = $result->fetch_object()) { 430 $groups[] = $row->group_name; 431 } 432 433 $result->close(); 434 unset($row); 435 436 return $groups; 437 } 438 439 /** 440 * Checks if the given user exists and the given 441 * plaintext password is correct 442 * 443 * @param string $user User name 444 * @param string $pass Clear text password 445 * @return bool True for success, false otherwise 446 */ 447 public function checkPass($user = '', $pass = '') 448 { 449 $check = ssi_checkPassword($user, $pass, true); 450 451 if (empty($check)) { 452 return false; 453 } 454 455 $user_data = ssi_queryMembers('member_name = {string:user}', array('user' => $user), 1, 'id_member', 'array'); 456 $user_data = array_shift($user_data); 457 458 $this->_smf_user_id = $user_data['id']; 459 $this->_smf_user_username = $user_data['username']; 460 $this->_smf_user_email = $user_data['email']; 461 $this->getUserGroups(); 462 463 return true; 464 } 465 466 /** 467 * Sanitize a given username 468 * 469 * @param string $user username 470 * @return string the cleaned username 471 */ 472 public function cleanUser($user) 473 { 474 return trim($user); 475 } 476 477 /** 478 * Get avatar url 479 * 480 * @param string $avatar 481 * @return string avatar url 482 */ 483 private function getAvatarUrl($avatar = '') 484 { 485 $avatar = trim($avatar); 486 487 // No avatar 488 if (empty($avatar)) { 489 return ''; 490 } elseif ($avatar == (string)(int)$avatar) { 491 // Avatar uploaded as attachment 492 return $this->_smf_conf['boardurl'] . '/index.php?action=dlattach;attach=' . $avatar . ';type=avatar'; 493 } elseif (preg_match('#^https?://#i', $avatar)) { 494 // Avatar is a link to external image 495 return $avatar; 496 } else { 497 // Avatar from SMF library 498 return $this->_smf_conf['boardurl'] . '/avatars/' . $avatar; 499 } 500 // TODO: Custom avatars url 501 // TODO: Default avatar for empty one 502 } 503} 504