1<?php
2/**
3 * DokuWiki Plugin evesso (Auth Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <andi@splitbrain.org>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12class auth_plugin_evesso extends auth_plugin_authplain {
13
14    /**
15     * Constructor
16     *
17     * Sets capabilities.
18     */
19    public function __construct() {
20        parent::__construct();
21
22        $this->cando['external'] = true;
23    }
24
25    /**
26     * Handle the login
27     *
28     * This either trusts the session data (if any), processes the second oAuth step or simply
29     * executes a normal plugin against local users.
30     *
31     * @param string $user
32     * @param string $pass
33     * @param bool   $sticky
34     * @return bool
35     */
36    function trustExternal($user, $pass, $sticky = false) {
37        global $USERINFO, $INPUT;
38
39        // check session for existing oAuth login data
40        if(isset($_SESSION[DOKU_COOKIE]['auth']['oauth'])) {
41            $session = $_SESSION[DOKU_COOKIE]['auth'];
42            $servicename = $session['oauth'];
43            // check if session data is still considered valid
44            if ($this->isSessionValid($session)) {
45                $_SERVER['REMOTE_USER'] = $session['user'];
46                $USERINFO               = $session['info'];
47                return true;
48            }
49        }
50
51        $existingLoginProcess = false;
52        // are we in login progress?
53        if(isset($_SESSION[DOKU_COOKIE]['oauth-inprogress'])) {
54            $servicename = $_SESSION[DOKU_COOKIE]['oauth-inprogress']['service'];
55            $page        = $_SESSION[DOKU_COOKIE]['oauth-inprogress']['id'];
56            if (isset($_SESSION[DOKU_COOKIE]['oauth-inprogress']['params'])) {
57                $params = $_SESSION[DOKU_COOKIE]['oauth-inprogress']['params'];
58            } else {
59                $params = array();
60            }
61            unset($_SESSION[DOKU_COOKIE]['oauth-inprogress']);
62            $existingLoginProcess = true;
63        }
64
65        // either we're in oauth login or a previous log needs to be rechecked
66        if(isset($servicename)) {
67            /** @var helper_plugin_evesso $hlp */
68            $hlp     = plugin_load('helper', 'evesso');
69
70            /** @var OAuth\Plugin\AbstractAdapter $service */
71            $service = $hlp->loadService($servicename);
72            if(is_null($service)) {
73                $this->cleanLogout();
74                return false;
75            }
76
77            if($service->checkToken()) {
78                $ok = $this->processLogin($sticky, $hlp, $service, $servicename, $page, $params);
79                if (!$ok) {
80                    $this->cleanLogout();
81                    return false;
82                }
83                return true;
84            } else {
85                if ($existingLoginProcess) {
86                    msg($this->getLang('loginFailed'), -1);
87                    $this->cleanLogout();
88                    return false;
89                } else {
90                    // first time here
91                    $this->relogin($servicename);
92                }
93            }
94
95            $this->cleanLogout();
96            return false; // something went wrong during oAuth login
97        } elseif (isset($_COOKIE[DOKU_COOKIE])) {
98            global $INPUT;
99            //try cookie
100            $cookie = explode('|', $_COOKIE[DOKU_COOKIE]);
101            if (isset($cookie[2]) && isset($cookie[3])) {
102                $auth = base64_decode($cookie[2], true);
103                $servicename = base64_decode($cookie[3], true);
104                if ($auth === 'oauth') {
105                    $this->relogin($servicename);
106                }
107            }
108        }
109
110        // do the "normal" plain auth login via form
111        return auth_login($user, $pass, $sticky);
112    }
113
114    /**
115     * @param array $session cookie auth session
116     *
117     * @return bool
118     */
119    protected function isSessionValid ($session) {
120        /** @var helper_plugin_evesso $hlp */
121        $hlp     = plugin_load('helper', 'evesso');
122        if ($hlp->validBrowserID($session)) {
123            if (!$hlp->isSessionTimedOut($session)) {
124                return true;
125            } elseif (!($hlp->isGETRequest() && $hlp->isDokuPHP())) {
126                // only force a recheck on a timed-out session during a GET request on the main script doku.php
127                return true;
128            }
129        }
130        return false;
131    }
132
133    protected function relogin($servicename) {
134        global $INPUT;
135
136        /** @var helper_plugin_evesso $hlp */
137        $hlp     = plugin_load('helper', 'evesso');
138        $service     = $hlp->loadService($servicename);
139        if(is_null($service)) return false;
140
141        // remember service in session
142        session_start();
143        $_SESSION[DOKU_COOKIE]['oauth-inprogress']['service'] = $servicename;
144        $_SESSION[DOKU_COOKIE]['oauth-inprogress']['id']      = $INPUT->str('id');
145        $_SESSION[DOKU_COOKIE]['oauth-inprogress']['params']  = $_GET;
146
147        $_SESSION[DOKU_COOKIE]['oauth-done']['$_REQUEST'] = $_REQUEST;
148
149        if (is_array($INPUT->post->param('do'))) {
150            $doPost = key($INPUT->post->arr('do'));
151        } else {
152            $doPost = $INPUT->post->str('do');
153        }
154        $doGet = $INPUT->get->str('do');
155        if (!empty($doPost)) {
156            $_SESSION[DOKU_COOKIE]['oauth-done']['do'] = $doPost;
157        } elseif (!empty($doGet)) {
158            $_SESSION[DOKU_COOKIE]['oauth-done']['do'] = $doGet;
159        }
160
161        session_write_close();
162
163        $service->login();
164    }
165
166    /**
167     * @param                              $sticky
168     * @param OAuth\Plugin\AbstractAdapter $service
169     * @param string                       $servicename
170     * @param string                       $page
171     * @param array                        $params
172     *
173     * @return bool
174     */
175    protected function processLogin($sticky, $hlp, $service, $servicename, $page, $params = array()) {
176        $uinfo = $service->getUser();
177        $ok = $this->processUser($uinfo, $hlp, $servicename);
178        if(!$ok) {
179            return false;
180        }
181        $this->setUserSession($uinfo, $servicename);
182        $this->setUserCookie($uinfo['user'], $sticky, $servicename);
183        if(isset($page)) {
184            if(!empty($params['id'])) unset($params['id']);
185            send_redirect(wl($page, $params, false, '&'));
186        }
187        return true;
188    }
189
190    /**
191     * process the user and update the $uinfo array
192     *
193     * @param $uinfo
194     * @param $servicename
195     *
196     * @return bool
197     */
198    protected function processUser(&$uinfo, $hlp, $servicename) {
199        $uinfo['user'] = $this->cleanUser((string) $uinfo['user']);
200        if(!$uinfo['name']) $uinfo['name'] = $uinfo['user'];
201
202        if(!$uinfo['user'] || !$uinfo['mail']) {
203            msg("$servicename did not provide the needed user info. Can't log you in", -1);
204            return false;
205        }
206
207        // see if the user is known already
208        $user = $this->getUserByEmail($uinfo['mail']);
209        if($user) {
210            $sinfo = $this->getUserData($user);
211            // check if the user allowed access via this service
212            if(!in_array($this->cleanServiceGroup($servicename), $sinfo['grps'])) {
213                msg(sprintf($this->getLang('authnotenabled'), $servicename), -1);
214                return false;
215            }
216            $uinfo['user'] = $user;
217            $uinfo['name'] = $sinfo['name'];
218            if ($this->haveEveGroups($uinfo['grps'])) { //Update groups
219                //Remove all eve groups
220                foreach($sinfo['grps'] as $group) {
221                    if($this->startsWith($group, helper_plugin_evesso::CORPORATION_PREFIX)
222                       || $this->startsWith($group, helper_plugin_evesso::ALLIANCE_PREFIX)
223                       || $this->startsWith($group, helper_plugin_evesso::FACTION_PREFIX)
224                       || $this->startsWith($group, "eve-") //Old Prefix
225                            ) {
226                        $idx = array_search($group, $sinfo['grps']);
227                        if($idx !== false) unset($sinfo['grps'][$idx]);
228                    }
229                }
230                //Merge groups
231                $uinfo['grps'] = array_unique(array_merge((array) $uinfo['grps'], $sinfo['grps']));
232            } else { //Load groups
233                $uinfo['grps'] = $sinfo['grps'];
234            }
235            //Update groups
236            $this->modifyUser($user, array('grps' => $uinfo['grps']));
237            //Check group
238            if (!$hlp->checkGroups($uinfo['grps'])) {
239                msg($this->getLang("rejectedGroup"), -1);
240                return false;
241            }
242            return true; //Existing valid user
243        } elseif(actionOK('register') || $this->getConf('register-on-auth')) {
244            //Check group before creation
245            if (!$hlp->checkGroups($uinfo['grps'])) {
246                msg($this->getLang("rejectedGroup"), -1);
247                return false;
248            }
249            //Add user
250            $ok = $this->addUser($uinfo, $servicename);
251            if(!$ok) {
252                msg('something went wrong creating your user account. please try again later.', -1);
253                return false;
254            }
255            return true; //New valid user
256        }
257        //Not existing valid user and can not create a new user
258        msg($this->getLang('addUserNotPossible'), -1);
259        return false;
260    }
261
262    private function haveEveGroups($groups) {
263        if (!isset($groups)) {
264            return false;
265        }
266        foreach($groups as $group) {
267            if($this->startsWith($group, helper_plugin_evesso::CORPORATION_PREFIX)
268                        || $this->startsWith($group, helper_plugin_evesso::ALLIANCE_PREFIX)
269                        || $this->startsWith($group, helper_plugin_evesso::FACTION_PREFIX)
270                        || $this->startsWith($group, "eve-") //Old Prefix
271                    ) {
272                return true;
273            }
274        }
275        return false;
276    }
277
278    private function startsWith($haystack, $needle) {
279        $length = strlen($needle);
280        return (substr($haystack, 0, $length) === $needle);
281    }
282
283    /**
284     * new user, create him - making sure the login is unique by adding a number if needed
285     *
286     * @param array $uinfo user info received from the oAuth service
287     * @param string $servicename
288     *
289     * @return bool
290     */
291    protected function addUser(&$uinfo, $servicename) {
292        global $conf;
293        $user = $uinfo['user'];
294        $count = '';
295        while($this->getUserData($user . $count)) {
296            if($count) {
297                $count++;
298            } else {
299                $count = 1;
300            }
301        }
302        $user = $user . $count;
303        $uinfo['user'] = $user;
304        $groups_on_creation = array();
305        $groups_on_creation[] = $conf['defaultgroup'];
306        $groups_on_creation[] = $this->cleanServiceGroup($servicename); // add service as group
307        $uinfo['grps'] = array_merge((array) $uinfo['grps'], $groups_on_creation);
308
309        $ok = $this->triggerUserMod(
310            'create',
311            array($user, auth_pwgen($user), $uinfo['name'], $uinfo['mail'], $uinfo['grps'],)
312        );
313        if(!$ok) {
314            return false;
315        }
316
317        // send notification about the new user
318        $subscription = new Subscription();
319        $subscription->send_register($user, $uinfo['name'], $uinfo['mail']);
320        return true;
321    }
322
323    /**
324     * Find a user by his email address
325     *
326     * @param $mail
327     * @return bool|string
328     */
329    protected function getUserByEmail($mail) {
330        if (method_exists($this, "loadUserData")) {
331            if($this->users === null) $this->loadUserData();
332        } else {
333            if($this->users === null) $this->_loadUserData();
334        }
335
336        $mail = strtolower($mail);
337
338        foreach($this->users as $user => $uinfo) {
339            if(strtolower($uinfo['mail']) == $mail) return $user;
340        }
341
342        return false;
343    }
344
345    /**
346     * @param array  $data
347     * @param string $service
348     */
349    protected function setUserSession($data, $service) {
350        global $USERINFO;
351
352        // set up groups
353        if(!is_array($data['grps'])) {
354            $data['grps'] = array();
355        }
356        $data['grps'][] = $this->cleanServiceGroup($service);
357        $data['grps']   = array_unique($data['grps']);
358
359        $USERINFO                               = $data;
360        $_SERVER['REMOTE_USER']                 = $data['user'];
361        $_SESSION[DOKU_COOKIE]['auth']['user']  = $data['user'];
362        if (isset($data['pass'])) {
363            $_SESSION[DOKU_COOKIE]['auth']['pass']  = $data['pass'];
364        }
365        $_SESSION[DOKU_COOKIE]['auth']['info']  = $USERINFO;
366        $_SESSION[DOKU_COOKIE]['auth']['buid']  = auth_browseruid();
367        $_SESSION[DOKU_COOKIE]['auth']['time']  = time();
368        $_SESSION[DOKU_COOKIE]['auth']['oauth'] = $service;
369    }
370
371    /**
372     * @param string $user
373     * @param bool   $sticky
374     * @param string $servicename
375     * @param int    $validityPeriodInSeconds optional, per default 1 Year
376     */
377    private function setUserCookie($user, $sticky, $servicename, $validityPeriodInSeconds = 31536000) {
378        global $conf;
379
380        $cookie = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode('oauth').'|'.base64_encode($servicename);
381        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
382        $time      = $sticky ? (time() + $validityPeriodInSeconds) : 0;
383        setcookie(DOKU_COOKIE,$cookie, $time, $cookieDir, '',($conf['securecookie'] && is_ssl()), true);
384    }
385
386    /**
387     * Unset additional stuff in session on logout
388     */
389    public function logOff() {
390        parent::logOff();
391
392        $this->cleanLogout();
393    }
394
395    /**
396     * unset auth cookies and session information
397     */
398    private function cleanLogout() {
399        $hlp = plugin_load('helper', 'evesso');
400        $service = $hlp->loadService();
401        if (!is_null($service)) {
402            $service->logout();
403        }
404        if(isset($_SESSION[DOKU_COOKIE]['auth'])) {
405            unset($_SESSION[DOKU_COOKIE]['auth']);
406        }
407        $this->setUserCookie('',true,'',-60);
408    }
409
410    /**
411     * Enhance function to check against duplicate emails
412     *
413     * @param string $user
414     * @param string $pwd
415     * @param string $name
416     * @param string $mail
417     * @param null   $grps
418     * @return bool|null|string
419     */
420    public function createUser($user, $pwd, $name, $mail, $grps = null) {
421        if($this->getUserByEmail($mail)) {
422            msg($this->getLang('emailduplicate'), -1);
423            return false;
424        }
425
426        return parent::createUser($user, $pwd, $name, $mail, $grps);
427    }
428
429    /**
430     * Enhance function to check against duplicate emails
431     *
432     * @param string $user
433     * @param array  $changes
434     * @return bool
435     */
436    public function modifyUser($user, $changes) {
437        global $conf;
438
439        if(isset($changes['mail'])) {
440            $found = $this->getUserByEmail($changes['mail']);
441            if($found != $user) {
442                msg($this->getLang('emailduplicate'), -1);
443                return false;
444            }
445        }
446
447        $ok = parent::modifyUser($user, $changes);
448
449        // refresh session cache
450        touch($conf['cachedir'] . '/sessionpurge');
451
452        return $ok;
453    }
454
455    public function cleanServiceGroup($group) {
456        return strtolower($this->cleanGroup($group));
457    }
458
459    public function cleanGroup($group) {
460        global $conf;
461        return str_replace(':', $conf['sepchar'], $group);
462    }
463}
464// vim:ts=4:sw=4:et:
465