* @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @version 1.0 beta2 */ /* * Sign in DokuWiki via SMF database. This file is a part of authsmf20 plugin * and must be in plugin directory for correct work. * * Requirements: SMF 2.x with utf8 encoding in database and subdomain independent cookies * SMF - Admin - Server Settings - Cookies and Sessions: Use subdomain independent cookies * Tested with DokuWiki 2017-02-19b "Frusterick Manners" */ if (!defined('DOKU_INC')) { die(); } /** * SMF 2.0 Authentication class. */ class auth_plugin_authsmf20 extends DokuWiki_Auth_Plugin { protected $_smf_db_link = null; protected $_smf_conf = array( 'path' => '', 'boardurl' => '', 'db_server' => '', 'db_port' => 3306, 'db_name' => '', 'db_user' => '', 'db_passwd' => '', 'db_character_set' => '', 'db_prefix' => '', ); protected $_smf_user_id = 0, $_smf_user_realname = '', $_smf_user_username = '', $_smf_user_email = '', $_smf_user_is_banned = false, $_smf_user_avatar = '', $_smf_user_groups = array(), $_smf_user_profile = '', $_cache = null, $_cache_duration = 0, $_cache_ext_name = '.authsmf20'; CONST CACHE_DURATION_UNIT = 86400; /** * Constructor. */ public function __construct() { $this->cando['addUser'] = false; $this->cando['delUser'] = false; $this->cando['modLogin'] = false; $this->cando['modPass'] = false; $this->cando['modName'] = false; $this->cando['modMail'] = false; $this->cando['modGroups'] = false; $this->cando['getUsers'] = false; $this->cando['getUserCount'] = false; $this->cando['getGroups'] = true; $this->cando['external'] = true; $this->cando['logout'] = true; $this->success = $this->loadConfiguration(); if (!$this->success) { msg($this->getLang('config_error'), -1); } } /** * Destructor. */ public function __destruct() { $this->disconnectSmfDB(); $this->_cache = null; } /** * Do all authentication * * @param string $user Username * @param string $pass Cleartext Password * @param boolean $sticky Cookie should not expire * @return boolean True on successful auth */ public function trustExternal($user = '', $pass = '', $sticky = false) { global $USERINFO; $sticky ? $sticky = true : $sticky = false; if ($this->doLoginCookie()) { return true; // User already logged in } if ($user) { $is_logged = $this->checkPass($user, $pass); // Try to login over DokuWiki login form } else { $is_logged = $this->doLoginSSI(); // Try to login over SMF SSI API } if (!$is_logged) { if ($user) { msg($this->getLang('login_error'), -1); } return false; } $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['user'] = $this->_smf_user_username; $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['mail'] = $this->_smf_user_email; $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['grps'] = $this->_smf_user_groups; $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; $_SERVER['REMOTE_USER'] = $USERINFO['name']; return true; } /** * Log off the current user */ public function logOff() { unset($_SESSION[DOKU_COOKIE]); // This doesn't work now because SMF SSI API have session logout issue //$link = ssi_logout(DOKU_URL, 'array'); //preg_match('/href="(.+)"/iU', $link, $url); //send_redirect($url[1]); } /** * Loads the plugin configuration. * * @return boolean True on success, false otherwise */ private function loadConfiguration() { $ssi_guest_access = true; $this->_smf_conf['path'] = rtrim(trim($this->getConf('smf_path')), '\/'); if (!file_exists($this->_smf_conf['path'] . '/SSI.php')) { dbglog('SMF not found in path' . $this->_smf_conf['path']); return false; } else { include_once($this->_smf_conf['path'] . '/SSI.php'); } $this->_smf_conf['boardurl'] = $boardurl; $this->_smf_conf['db_server'] = $db_server; $this->_smf_conf['db_name'] = $db_name; $this->_smf_conf['db_user'] = $db_user; $this->_smf_conf['db_passwd'] = $db_passwd; $this->_smf_conf['db_character_set'] = $db_character_set; $this->_smf_conf['db_prefix'] = $db_prefix; return (!empty($this->_smf_conf['boardurl'])); } /** * Authenticate the user using SMF SSI. * * @return boolean True on successful login */ private function doLoginSSI() { $user_info = ssi_welcome('array'); if (empty($user_info['is_logged'])) { return false; } $this->_smf_user_id = $user_info['id']; $this->_smf_user_username = $user_info['username']; $this->_smf_user_email = $user_info['email']; $this->getUserGroups(); return true; } /** * Authenticate the user using DokuWiki Cookie. * * @return boolean True on successful login */ private function doLoginCookie() { global $USERINFO; if (empty($_SESSION[DOKU_COOKIE]['auth']['info'])) { return false; } $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['user']; $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['mail']; $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['grps']; $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user']; return true; } /** * Connect to SMF database. * * @return boolean True on success, false otherwise */ private function connectSmfDB() { if (!$this->_smf_db_link) { $this->_smf_db_link = new mysqli( $this->_smf_conf['db_server'], $this->_smf_conf['db_user'], $this->_smf_conf['db_passwd'], $this->_smf_conf['db_name'], (int)$this->_smf_conf['db_port'] ); if (!$this->_smf_db_link || $this->_smf_db_link->connect_error) { $error = 'Cannot connect to database server'; if ($this->_smf_db_link) { $error .= ' (' . $this->_smf_db_link->connect_errno . ')'; } dbglog($error); msg($this->getLang('database_error'), -1); $this->_smf_db_link = null; return false; } if ($this->_smf_conf['db_character_set'] == 'utf8') { $this->_smf_db_link->set_charset('utf8'); } } return ($this->_smf_db_link && $this->_smf_db_link->ping()); } /** * Disconnects from SMF database. */ private function disconnectSmfDB() { if ($this->_smf_db_link !== null) { $this->_smf_db_link->close(); $this->_smf_db_link = null; } } /** * Get SMF user's groups. * * @return boolean True for success, false otherwise */ private function getUserGroups() { if (!$this->connectSmfDB() || !$this->_smf_user_id) { return false; } $query = "SELECT mg.group_name, m.id_group FROM {$this->_smf_conf['db_prefix']}members m 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 WHERE m.id_member = {$this->_smf_user_id}"; $result = $this->_smf_db_link->query($query); if (!$result) { dbglog("cannot get groups for user id: {$this->_smf_user_id}"); return false; } while ($row = $result->fetch_object()) { if ($row->id_group == 1) { $this->_smf_user_groups[] = 'admin'; // Map SMF Admin to DokuWiki Admin } else { $this->_smf_user_groups[] = $row->group_name; } } if (!$this->_smf_user_is_banned) { $this->_smf_user_groups[] = 'user'; } // Banned users as guests $this->_smf_user_groups = array_unique($this->_smf_user_groups); $result->close(); unset($row); return true; } /** * Return user info * * Returns info about the given user needs to contain * at least these fields: * * name string full name of the user * mail string email address of the user * grps array list of groups the user is in * * @param string $user User name * @param bool $requireGroups Whether or not the returned data must include groups * @return false|array Containing user data or false * * array['realname'] string User's real name * array['username'] string User's username * array['email'] string User's email address * array['smf_user_id'] string User's ID * array['smf_profile'] string User's link to profile * array['smf_user_groups'] array User's groups */ public function getUserData($user, $requireGroups = true) { if (empty($user)) { return false; } $user_data = false; $this->_cache_duration = (int)($this->getConf('smf_cache')); $depends = array('age' => self::CACHE_DURATION_UNIT * $this->_cache_duration); $cache = new cache('authsmf20_getUserData_' . $user, $this->_cache_ext_name); if (($this->_cache_duration > 0) && $cache->useCache($depends)) { $user_data = unserialize($cache->retrieveCache(false)); } else { $cache->removeCache(); if (!$this->connectSmfDB()) { return false; } $user = $this->_smf_db_link->real_escape_string($user); $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 FROM {$this->_smf_conf['db_prefix']}members m LEFT JOIN {$this->_smf_conf['db_prefix']}attachments a ON a.id_member = m.id_member AND a.id_msg = 0 WHERE member_name = '{$user}'"; $result = $this->_smf_db_link->query($query); if (!$result) { dbglog("No data found in database for user: {$user}"); return false; } $row = $result->fetch_object(); $this->_smf_user_id = $row->id_member; $this->getUserGroups(); $user_data['smf_user_groups'] = array_unique($this->_smf_user_groups); $user_data['smf_user_id'] = $row->id_member; $user_data['smf_user_username'] = $user; $user_data['smf_user_realname'] = $row->real_name; if (empty($user_data['smf_user_realname'])) { $user_data['smf_user_realname'] = $user_data['smf_user_username']; } $user_data['smf_user_email'] = $row->email_address; if ($row->gender == 1) { $user_data['smf_user_gender'] = 'male'; } elseif ($row->gender == 2) { $user_data['smf_user_gender'] = 'female'; } else { $user_data['smf_user_gender'] = 'unknown'; } $user_data['smf_user_location'] = $row->location; $user_data['smf_user_usertitle'] = $row->usertitle; $user_data['smf_personal_text'] = $row->personal_text; $user_data['smf_user_profile'] = $this->_smf_conf['boardurl'] . '/index.php?action=profile;u=' . $this->_smf_user_id; $user_data['smf_user_avatar'] = $this->getAvatarUrl($row->avatar); $result->close(); unset($row); $cache->storeCache(serialize($user_data)); } $cache = null; unset($cache); return $user_data; } /** * Retrieve groups * * @param int $start * @param int $limit * @return array|false Containing groups list, false if error */ public function retrieveGroups($start = 0, $limit = 10) { if (!$this->connectSmfDB()) { return false; } $query = "SELECT group_name FROM {$this->_smf_conf['db_prefix']}membergroups LIMIT {$start}, {$limit}"; $result = $this->_smf_db_link->query($query); if (!$result) { dbglog("Cannot get SMF groups list"); return false; } while ($row = $result->fetch_object()) { $groups[] = $row->group_name; } $result->close(); unset($row); return $groups; } /** * Checks if the given user exists and the given * plaintext password is correct * * @param string $user User name * @param string $pass Clear text password * @return bool True for success, false otherwise */ public function checkPass($user = '', $pass = '') { $check = ssi_checkPassword($user, $pass, true); if (empty($check)) { return false; } $user_data = ssi_queryMembers('member_name = {string:user}', array('user' => $user), 1, 'id_member', 'array'); $user_data = array_shift($user_data); $this->_smf_user_id = $user_data['id']; $this->_smf_user_username = $user_data['username']; $this->_smf_user_email = $user_data['email']; $this->getUserGroups(); return true; } /** * Sanitize a given username * * @param string $user username * @return string the cleaned username */ public function cleanUser($user) { return trim($user); } /** * Get avatar url * * @param string $avatar * @return string avatar url */ private function getAvatarUrl($avatar = '') { $avatar = trim($avatar); // No avatar if (empty($avatar)) { return ''; } elseif ($avatar == (string)(int)$avatar) { // Avatar uploaded as attachment return $this->_smf_conf['boardurl'] . '/index.php?action=dlattach;attach=' . $avatar . ';type=avatar'; } elseif (preg_match('#^https?://#i', $avatar)) { // Avatar is a link to external image return $avatar; } else { // Avatar from SMF library return $this->_smf_conf['boardurl'] . '/avatars/' . $avatar; } // TODO: Custom avatars url // TODO: Default avatar for empty one } }