1<?php
2/**
3 * DokuWiki Plugin oauth (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_oauth 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_oauth $hlp */
87            $hlp     = plugin_load('helper', 'oauth');
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_oauth $hlp */
139        $hlp     = plugin_load('helper', 'oauth');
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_oauth $hlp */
155        $hlp     = plugin_load('helper', 'oauth');
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            $uinfo['grps'] = array_merge((array) $uinfo['grps'], $sinfo['grps']);
237        } elseif(actionOK('register') || $this->getConf('register-on-auth')) {
238            $ok = $this->addUser($uinfo, $servicename);
239            if(!$ok) {
240                msg('something went wrong creating your user account. please try again later.', -1);
241                return false;
242            }
243        } else {
244            msg($this->getLang('addUser not possible'), -1);
245            return false;
246        }
247        return true;
248    }
249
250    /**
251     * new user, create him - making sure the login is unique by adding a number if needed
252     *
253     * @param array $uinfo user info received from the oAuth service
254     * @param string $servicename
255     *
256     * @return bool
257     */
258    protected function addUser(&$uinfo, $servicename) {
259        global $conf;
260        $user = $uinfo['user'];
261        $count = '';
262        while($this->getUserData($user . $count)) {
263            if($count) {
264                $count++;
265            } else {
266                $count = 1;
267            }
268        }
269        $user = $user . $count;
270        $uinfo['user'] = $user;
271        $groups_on_creation = array();
272        $groups_on_creation[] = $conf['defaultgroup'];
273        $groups_on_creation[] = $this->cleanGroup($servicename); // add service as group
274        $uinfo['grps'] = array_merge((array) $uinfo['grps'], $groups_on_creation);
275
276        $ok = $this->triggerUserMod(
277            'create',
278            array($user, auth_pwgen($user), $uinfo['name'], $uinfo['mail'], $groups_on_creation,)
279        );
280        if(!$ok) {
281            return false;
282        }
283
284        // send notification about the new user
285        $subscription = new Subscription();
286        $subscription->send_register($user, $uinfo['name'], $uinfo['mail']);
287        return true;
288    }
289
290    /**
291     * Find a user by his email address
292     *
293     * @param $mail
294     * @return bool|string
295     */
296    protected function getUserByEmail($mail) {
297        if($this->users === null){
298            if(is_callable([$this, '_loadUserData'])) {
299                $this->_loadUserData();
300            } else {
301                $this->loadUserData();
302            }
303        }
304        $mail = strtolower($mail);
305
306        foreach($this->users as $user => $uinfo) {
307            if(strtolower($uinfo['mail']) == $mail) return $user;
308        }
309
310        return false;
311    }
312
313    /**
314     * @param array  $data
315     * @param string $service
316     */
317    protected function setUserSession($data, $service) {
318        global $USERINFO;
319        global $conf;
320
321        // set up groups
322        if(!is_array($data['grps'])) {
323            $data['grps'] = array();
324        }
325        $data['grps'][] = $this->cleanGroup($service);
326        $data['grps']   = array_unique($data['grps']);
327
328        $USERINFO                               = $data;
329        $_SERVER['REMOTE_USER']                 = $data['user'];
330        $_SESSION[DOKU_COOKIE]['auth']['user']  = $data['user'];
331        $_SESSION[DOKU_COOKIE]['auth']['pass']  = $data['pass'];
332        $_SESSION[DOKU_COOKIE]['auth']['info']  = $USERINFO;
333        $_SESSION[DOKU_COOKIE]['auth']['buid']  = auth_browseruid();
334        $_SESSION[DOKU_COOKIE]['auth']['time']  = time();
335        $_SESSION[DOKU_COOKIE]['auth']['oauth'] = $service;
336    }
337
338    /**
339     * @param string $user
340     * @param bool   $sticky
341     * @param string $servicename
342     * @param int    $validityPeriodInSeconds optional, per default 1 Year
343     */
344    private function setUserCookie($user, $sticky, $servicename, $validityPeriodInSeconds = 31536000) {
345        $cookie = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode('oauth').'|'.base64_encode($servicename);
346        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
347        $time      = $sticky ? (time() + $validityPeriodInSeconds) : 0;
348        setcookie(DOKU_COOKIE,$cookie, $time, $cookieDir, '',($conf['securecookie'] && is_ssl()), true);
349    }
350
351    /**
352     * Unset additional stuff in session on logout
353     */
354    public function logOff() {
355        parent::logOff();
356
357        $this->cleanLogout();
358    }
359
360    /**
361     * unset auth cookies and session information
362     */
363    private function cleanLogout() {
364        if(isset($_SESSION[DOKU_COOKIE]['oauth-done'])) {
365            unset($_SESSION[DOKU_COOKIE]['oauth-done']);
366        }
367        if(isset($_SESSION[DOKU_COOKIE]['auth'])) {
368            unset($_SESSION[DOKU_COOKIE]['auth']);
369        }
370        $this->setUserCookie('',true,'',-60);
371    }
372
373    /**
374     * Enhance function to check against duplicate emails
375     *
376     * @param string $user
377     * @param string $pwd
378     * @param string $name
379     * @param string $mail
380     * @param null   $grps
381     * @return bool|null|string
382     */
383    public function createUser($user, $pwd, $name, $mail, $grps = null) {
384        if($this->getUserByEmail($mail)) {
385            msg($this->getLang('emailduplicate'), -1);
386            return false;
387        }
388
389        return parent::createUser($user, $pwd, $name, $mail, $grps);
390    }
391
392    /**
393     * Enhance function to check aainst duplicate emails
394     *
395     * @param string $user
396     * @param array  $changes
397     * @return bool
398     */
399    public function modifyUser($user, $changes) {
400        global $conf;
401
402        if(isset($changes['mail'])) {
403            $found = $this->getUserByEmail($changes['mail']);
404            if($found && $found != $user) {
405                msg($this->getLang('emailduplicate'), -1);
406                return false;
407            }
408        }
409
410        $ok = parent::modifyUser($user, $changes);
411
412        // refresh session cache
413        touch($conf['cachedir'] . '/sessionpurge');
414
415        return $ok;
416    }
417
418}
419
420// vim:ts=4:sw=4:et:
421