<?php
/**
 * DokuWiki Plugin evesso (Auth Component)
 *
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
 * @author  Andreas Gohr <andi@splitbrain.org>
 */

// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

class auth_plugin_evesso extends auth_plugin_authplain {

    /**
     * Constructor
     *
     * Sets capabilities.
     */
    public function __construct() {
        parent::__construct();

        $this->cando['external'] = true;
    }

    /**
     * Handle the login
     *
     * This either trusts the session data (if any), processes the second oAuth step or simply
     * executes a normal plugin against local users.
     *
     * @param string $user
     * @param string $pass
     * @param bool   $sticky
     * @return bool
     */
    function trustExternal($user, $pass, $sticky = false) {
        global $USERINFO, $INPUT;

        // check session for existing oAuth login data
        if(isset($_SESSION[DOKU_COOKIE]['auth']['oauth'])) {
            $session = $_SESSION[DOKU_COOKIE]['auth'];
            $servicename = $session['oauth'];
            // check if session data is still considered valid
            if ($this->isSessionValid($session)) {
                $_SERVER['REMOTE_USER'] = $session['user'];
                $USERINFO               = $session['info'];
                return true;
            }
        }

        $existingLoginProcess = false;
        // are we in login progress?
        if(isset($_SESSION[DOKU_COOKIE]['oauth-inprogress'])) {
            $servicename = $_SESSION[DOKU_COOKIE]['oauth-inprogress']['service'];
            $page        = $_SESSION[DOKU_COOKIE]['oauth-inprogress']['id'];
            if (isset($_SESSION[DOKU_COOKIE]['oauth-inprogress']['params'])) {
                $params = $_SESSION[DOKU_COOKIE]['oauth-inprogress']['params'];
            } else {
                $params = array();
            }
            unset($_SESSION[DOKU_COOKIE]['oauth-inprogress']);
            $existingLoginProcess = true;
        }

        // either we're in oauth login or a previous log needs to be rechecked
        if(isset($servicename)) {
            /** @var helper_plugin_evesso $hlp */
            $hlp     = plugin_load('helper', 'evesso');

            /** @var OAuth\Plugin\AbstractAdapter $service */
            $service = $hlp->loadService($servicename);
            if(is_null($service)) {
                $this->cleanLogout();
                return false;
            }

            if($service->checkToken()) {
                $ok = $this->processLogin($sticky, $hlp, $service, $servicename, $page, $params);
                if (!$ok) {
                    $this->cleanLogout();
                    return false;
                }
                return true;
            } else {
                if ($existingLoginProcess) {
                    msg($this->getLang('loginFailed'), -1);
                    $this->cleanLogout();
                    return false;
                } else {
                    // first time here
                    $this->relogin($servicename);
                }
            }

            $this->cleanLogout();
            return false; // something went wrong during oAuth login
        } elseif (isset($_COOKIE[DOKU_COOKIE])) {
            global $INPUT;
            //try cookie
            $cookie = explode('|', $_COOKIE[DOKU_COOKIE]);
            if (isset($cookie[2]) && isset($cookie[3])) {
                $auth = base64_decode($cookie[2], true);
                $servicename = base64_decode($cookie[3], true);
                if ($auth === 'oauth') {
                    $this->relogin($servicename);
                }
            }
        }

        // do the "normal" plain auth login via form
        return auth_login($user, $pass, $sticky);
    }

    /**
     * @param array $session cookie auth session
     *
     * @return bool
     */
    protected function isSessionValid ($session) {
        /** @var helper_plugin_evesso $hlp */
        $hlp     = plugin_load('helper', 'evesso');
        if ($hlp->validBrowserID($session)) {
            if (!$hlp->isSessionTimedOut($session)) {
                return true;
            } elseif (!($hlp->isGETRequest() && $hlp->isDokuPHP())) {
                // only force a recheck on a timed-out session during a GET request on the main script doku.php
                return true;
            }
        }
        return false;
    }

    protected function relogin($servicename) {
        global $INPUT;

        /** @var helper_plugin_evesso $hlp */
        $hlp     = plugin_load('helper', 'evesso');
        $service     = $hlp->loadService($servicename);
        if(is_null($service)) return false;

        // remember service in session
        session_start();
        $_SESSION[DOKU_COOKIE]['oauth-inprogress']['service'] = $servicename;
        $_SESSION[DOKU_COOKIE]['oauth-inprogress']['id']      = $INPUT->str('id');
        $_SESSION[DOKU_COOKIE]['oauth-inprogress']['params']  = $_GET;

        $_SESSION[DOKU_COOKIE]['oauth-done']['$_REQUEST'] = $_REQUEST;

        if (is_array($INPUT->post->param('do'))) {
            $doPost = key($INPUT->post->arr('do'));
        } else {
            $doPost = $INPUT->post->str('do');
        }
        $doGet = $INPUT->get->str('do');
        if (!empty($doPost)) {
            $_SESSION[DOKU_COOKIE]['oauth-done']['do'] = $doPost;
        } elseif (!empty($doGet)) {
            $_SESSION[DOKU_COOKIE]['oauth-done']['do'] = $doGet;
        }

        session_write_close();

        $service->login();
    }

    /**
     * @param                              $sticky
     * @param OAuth\Plugin\AbstractAdapter $service
     * @param string                       $servicename
     * @param string                       $page
     * @param array                        $params
     *
     * @return bool
     */
    protected function processLogin($sticky, $hlp, $service, $servicename, $page, $params = array()) {
        $uinfo = $service->getUser();
        $ok = $this->processUser($uinfo, $hlp, $servicename);
        if(!$ok) {
            return false;
        }
        $this->setUserSession($uinfo, $servicename);
        $this->setUserCookie($uinfo['user'], $sticky, $servicename);
        if(isset($page)) {
            if(!empty($params['id'])) unset($params['id']);
            send_redirect(wl($page, $params, false, '&'));
        }
        return true;
    }

    /**
     * process the user and update the $uinfo array
     *
     * @param $uinfo
     * @param $servicename
     *
     * @return bool
     */
    protected function processUser(&$uinfo, $hlp, $servicename) {
        $uinfo['user'] = $this->cleanUser((string) $uinfo['user']);
        if(!$uinfo['name']) $uinfo['name'] = $uinfo['user'];

        if(!$uinfo['user'] || !$uinfo['mail']) {
            msg("$servicename did not provide the needed user info. Can't log you in", -1);
            return false;
        }

        // see if the user is known already
        $user = $this->getUserByEmail($uinfo['mail']);
        if($user) {
            $sinfo = $this->getUserData($user);
            // check if the user allowed access via this service
            if(!in_array($this->cleanServiceGroup($servicename), $sinfo['grps'])) {
                msg(sprintf($this->getLang('authnotenabled'), $servicename), -1);
                return false;
            }
            $uinfo['user'] = $user;
            $uinfo['name'] = $sinfo['name'];
            if ($this->haveEveGroups($uinfo['grps'])) { //Update groups
                //Remove all eve groups
                foreach($sinfo['grps'] as $group) {
                    if($this->startsWith($group, helper_plugin_evesso::CORPORATION_PREFIX)
                       || $this->startsWith($group, helper_plugin_evesso::ALLIANCE_PREFIX)
                       || $this->startsWith($group, helper_plugin_evesso::FACTION_PREFIX)
                       || $this->startsWith($group, "eve-") //Old Prefix
                            ) {
                        $idx = array_search($group, $sinfo['grps']);
                        if($idx !== false) unset($sinfo['grps'][$idx]);
                    }
                }
                //Merge groups
                $uinfo['grps'] = array_unique(array_merge((array) $uinfo['grps'], $sinfo['grps']));
            } else { //Load groups
                $uinfo['grps'] = $sinfo['grps'];
            }
            //Update groups
            $this->modifyUser($user, array('grps' => $uinfo['grps']));
            //Check group
            if (!$hlp->checkGroups($uinfo['grps'])) {
                msg($this->getLang("rejectedGroup"), -1);
                return false;
            }
            return true; //Existing valid user
        } elseif(actionOK('register') || $this->getConf('register-on-auth')) {
            //Check group before creation
            if (!$hlp->checkGroups($uinfo['grps'])) {
                msg($this->getLang("rejectedGroup"), -1);
                return false;
            }
            //Add user
            $ok = $this->addUser($uinfo, $servicename);
            if(!$ok) {
                msg('something went wrong creating your user account. please try again later.', -1);
                return false;
            }
            return true; //New valid user
        }
        //Not existing valid user and can not create a new user
        msg($this->getLang('addUserNotPossible'), -1);
        return false;
    }

    private function haveEveGroups($groups) {
        if (!isset($groups)) {
            return false;
        }
        foreach($groups as $group) {
            if($this->startsWith($group, helper_plugin_evesso::CORPORATION_PREFIX)
                        || $this->startsWith($group, helper_plugin_evesso::ALLIANCE_PREFIX)     
                        || $this->startsWith($group, helper_plugin_evesso::FACTION_PREFIX)
                        || $this->startsWith($group, "eve-") //Old Prefix
                    ) {
                return true;
            }
        }
        return false;
    }

    private function startsWith($haystack, $needle) {
        $length = strlen($needle);
        return (substr($haystack, 0, $length) === $needle);
    }

    /**
     * new user, create him - making sure the login is unique by adding a number if needed
     *
     * @param array $uinfo user info received from the oAuth service
     * @param string $servicename
     *
     * @return bool
     */
    protected function addUser(&$uinfo, $servicename) {
        global $conf;
        $user = $uinfo['user'];
        $count = '';
        while($this->getUserData($user . $count)) {
            if($count) {
                $count++;
            } else {
                $count = 1;
            }
        }
        $user = $user . $count;
        $uinfo['user'] = $user;
        $groups_on_creation = array();
        $groups_on_creation[] = $conf['defaultgroup'];
        $groups_on_creation[] = $this->cleanServiceGroup($servicename); // add service as group
        $uinfo['grps'] = array_merge((array) $uinfo['grps'], $groups_on_creation);

        $ok = $this->triggerUserMod(
            'create',
            array($user, auth_pwgen($user), $uinfo['name'], $uinfo['mail'], $uinfo['grps'],)
        );
        if(!$ok) {
            return false;
        }

        // send notification about the new user
        $subscription = new Subscription();
        $subscription->send_register($user, $uinfo['name'], $uinfo['mail']);
        return true;
    }

    /**
     * Find a user by his email address
     *
     * @param $mail
     * @return bool|string
     */
    protected function getUserByEmail($mail) {
        if (method_exists($this, "loadUserData")) {
            if($this->users === null) $this->loadUserData();
        } else {
            if($this->users === null) $this->_loadUserData();
        }
        
        $mail = strtolower($mail);

        foreach($this->users as $user => $uinfo) {
            if(strtolower($uinfo['mail']) == $mail) return $user;
        }
         
        return false;
    }

    /**
     * @param array  $data
     * @param string $service
     */
    protected function setUserSession($data, $service) {
        global $USERINFO;

        // set up groups
        if(!is_array($data['grps'])) {
            $data['grps'] = array();
        }
        $data['grps'][] = $this->cleanServiceGroup($service);
        $data['grps']   = array_unique($data['grps']);

        $USERINFO                               = $data;
        $_SERVER['REMOTE_USER']                 = $data['user'];
        $_SESSION[DOKU_COOKIE]['auth']['user']  = $data['user'];
        if (isset($data['pass'])) {
            $_SESSION[DOKU_COOKIE]['auth']['pass']  = $data['pass'];
        }
        $_SESSION[DOKU_COOKIE]['auth']['info']  = $USERINFO;
        $_SESSION[DOKU_COOKIE]['auth']['buid']  = auth_browseruid();
        $_SESSION[DOKU_COOKIE]['auth']['time']  = time();
        $_SESSION[DOKU_COOKIE]['auth']['oauth'] = $service;
    }

    /**
     * @param string $user
     * @param bool   $sticky
     * @param string $servicename
     * @param int    $validityPeriodInSeconds optional, per default 1 Year
     */
    private function setUserCookie($user, $sticky, $servicename, $validityPeriodInSeconds = 31536000) {
        global $conf;

        $cookie = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode('oauth').'|'.base64_encode($servicename);
        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
        $time      = $sticky ? (time() + $validityPeriodInSeconds) : 0;
        setcookie(DOKU_COOKIE,$cookie, $time, $cookieDir, '',($conf['securecookie'] && is_ssl()), true);
    }

    /**
     * Unset additional stuff in session on logout
     */
    public function logOff() {
        parent::logOff();

        $this->cleanLogout();
    }

    /**
     * unset auth cookies and session information
     */
    private function cleanLogout() {
        $hlp = plugin_load('helper', 'evesso');
        $service = $hlp->loadService();
        if (!is_null($service)) {
            $service->logout();
        }
        if(isset($_SESSION[DOKU_COOKIE]['auth'])) {
            unset($_SESSION[DOKU_COOKIE]['auth']);
        }
        $this->setUserCookie('',true,'',-60);
    }

    /**
     * Enhance function to check against duplicate emails
     *
     * @param string $user
     * @param string $pwd
     * @param string $name
     * @param string $mail
     * @param null   $grps
     * @return bool|null|string
     */
    public function createUser($user, $pwd, $name, $mail, $grps = null) {
        if($this->getUserByEmail($mail)) {
            msg($this->getLang('emailduplicate'), -1);
            return false;
        }

        return parent::createUser($user, $pwd, $name, $mail, $grps);
    }

    /**
     * Enhance function to check against duplicate emails
     *
     * @param string $user
     * @param array  $changes
     * @return bool
     */
    public function modifyUser($user, $changes) {
        global $conf;

        if(isset($changes['mail'])) {
            $found = $this->getUserByEmail($changes['mail']);
            if($found != $user) {
                msg($this->getLang('emailduplicate'), -1);
                return false;
            }
        }

        $ok = parent::modifyUser($user, $changes);

        // refresh session cache
        touch($conf['cachedir'] . '/sessionpurge');

        return $ok;
    }

    public function cleanServiceGroup($group) {
        return strtolower($this->cleanGroup($group));
    }

    public function cleanGroup($group) {
        global $conf;
        return str_replace(':', $conf['sepchar'], $group);
    }
}
// vim:ts=4:sw=4:et:
