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