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