<?php
/**
 * Password Protection
 *
 * This plugin checks passwords and manages restrictions on the access of installed plugins.
 *
 * {@translation
 *
 *   de:   Passwortschutz
 *
 *         Dieses Plugin prüft Passwörter und regelt den Zugriff auf installierte Plugins.
 *
 *   , fr: Mot de Passe
 *
 * }
 *
 * @author     Thomas Meyer
 * @type       security
 * @priority   high
 * @license    http://www.gnu.org/licenses/gpl.txt
 * @active     always
 *
 * @package    yana
 * @subpackage plugins
 */

/**
 * user authentification plugin
 *
 * @access     public
 * @package    yana
 * @subpackage plugins
 */
class plugin_user extends StdClass implements IsPlugin
{
    /**
     * count boundary
     *
     * Maximum number of times a user may enter
     * a wrong password before its account
     * is suspended for $maxFailureTime seconds.
     *
     * @access  private
     * @var     int
     */
    private $maxFailureCount = 3;
    /**
     * time boundary
     *
     * Maximum time in seconds a user's login
     * is blocked after entering a wrong password
     * $maxFailureCount times.
     *
     * @access  private
     * @var     int
     */
    private $maxFailureTime = 300; /* 300 sec. = 5 minutes */
    /**
     * @access  private
     * @static
     * @var     string
     */
    private static $userName = "";
    /**
     * @access  private
     * @static
     * @var     string
     */
    private static $profileId = "";
    /**
     * @access  private
     * @static
     * @var     int
     */
    private static $securityLevel = 0;

    /**
     * constructor
     *
     * @access  public
     * @ignore
     */
    public function __construct()
    {
        global $YANA;
        if (isset($YANA)) {
            self::$userName = YanaUser::getUserName();
            if (!empty(self::$userName)) {
                self::$securityLevel = $YANA->session->getSecurityLevel(self::$userName);
                self::$profileId = Yana::getId();
                $YANA->setVar("SESSION_USER_ID", self::$userName);
                $YANA->setVar("PERMISSION", self::$securityLevel);
            }
            $YANA->setVar("SESSION_ID", session_id());
            $YANA->setVar("SESSION_NAME", session_name());
        }
        SessionManager::addSecurityRule(array(__CLASS__, 'checkSecurityLevel'));
    }

    /**
     * Default event handler
     *
     * returns bool(true) on success and bool(false) on error
     *
     * @access  public
     * @return  bool
     * @param   string  $event  name of the called event in lower-case
     * @param   array   $ARGS   array of arguments passed to the function
     */
    public function _default($event, array $ARGS)
    {
        settype($event, "string");

        /* @var $YANA Yana */
        global $YANA;
        $YANA->language->readFile("user");

        if ($YANA->session->checkPermission(null, $event)) {
            /* access granted */
            if (YanaUser::isLoggedIn()) {
                PluginMenu::getInstance()->setMenuEntry("logout", $YANA->language->getVar("logout"));
            } else {
                PluginMenu::getInstance()->setMenuEntry("login", $YANA->language->getVar("login"));
            }
            return true;
        } else {
            if (!YanaUser::isLoggedIn()) {
                new Warning("403");
                $_SESSION['on_login_goto'] = $event;
                $YANA->exitTo("login");
            } else {
                new Warning("INSUFFICIENT_RIGHTS");
                $YANA->exitTo();
            }
        }
    }

    /**
     * check security level
     *
     * @access  public
     * @static
     * @param   DBStream   $database    database
     * @param   array      $required    required level
     * @param   string     $profileId   profile id
     * @param   string     $action      action
     * @param   string     $userName    user name
     * @return  bool
     *
     * @ignore
     */
    public static function checkSecurityLevel(DBStream $database, array $required, $profileId, $action, $userName)
    {
        if (!isset($required[PluginAnnotation::LEVEL])) {
            return null;
        }
        // skip if nothing to check
        if (empty($required[PluginAnnotation::LEVEL])) {
            return true;
        }

        $requiredLevel = (int) $required[PluginAnnotation::LEVEL];

        if (!YanaUser::isLoggedIn()) {
            return false;
        }

        if ($userName == self::$userName && self::$profileId == $profileId) {
            return $requiredLevel <= self::$securityLevel;
        }

        $securityLevel = (int) SessionManager::getInstance()->getSecurityLevel($userName, $profileId);

        return $requiredLevel <= $securityLevel;
    }

    /**
     * login screen
     *
     * @type        security
     * @template    login_template
     *
     * @access      public
     * @return      bool
     */
    public function login()
    {
        return true;
    }

    /**
     * Send login data screen
     *
     * @type        security
     * @template    USER_LOST_PASSWORD
     *
     * @access      public
     * @return      bool
     */
    public function set_lost_pwd()
    {
        return true;
    }

    /**
     * Send login data screen
     *
     * @type        security
     * @template    message
     * @onsuccess   goto: login, text: send_mail
     * @onerror     goto: set_lost_pwd, text: INPUT_INVALID
     *
     *
     * @access      public
     * @param       array   $ARGS   array of arguments passed to the function
     * @return      bool
     */
    public function get_lost_pwd(array $ARGS)
    {
        global $YANA;
        $sessionManager = SessionManager::getInstance();
        $database = SessionManager::getDatasource();
        // check captcha field
        if ($YANA->callAction("security_check_image", $ARGS) === false) {
            Log::report('SPAM: CAPTCHA not solved, entry has not been created.');
            return false;
        }

        $userMail = mb_strtolower($ARGS['user']);
        // email address is not valid
        if (filter_var($userMail, FILTER_VALIDATE_EMAIL) === false) {
            throw new Alert('mail_invalid');
        }

        // check if user exist in the database
        $user = $database->select('user', array('USER_MAIL', '=', $userMail));
        // clean variables
        unset($userMail);
        // e-mail is not present on the db
        if (count($user) !== 1) {
            throw new Alert('mail_broken');
        }

        /* Get user name and mail.
         *
         * Note that for security reasosns you should not trust the mail address the user
         * entered himself.
         */
        $user = array_pop($user);
        $userName = $user['USER_ID'];
        $recipient = $user['USER_MAIL'];
        unset($user);
        assert('is_string($userName); // $userName must be of type String');
        assert('is_string($recipient); // $recipient must be of type String');
        assert('filter_var($recipient, FILTER_VALIDATE_EMAIL); // $recipient not a valid e-mail');

        // calculate unique recovery-key
        $uniqueKey = uniqid(substr(md5($recipient), 0, 3));

        // update the user record with time() and uniqueID for verification
        assert('!isset($recovery) // Cannot redeclare var $recovery');
        $recovery = array("user_recover_id" => $uniqueKey, "user_recover_utc" => time());
        $setRecoveryId = $database->update("user.{$userName}", $recovery);
        unset($recovery);
        $database->commit();

        // check for successful user record update
        if ($setRecoveryId == false) {
            return false;
        }

        assert('isset($userName); // variable $userName is not set');

        $subject = $YANA->language->getVar('user.6');
        $sender = $YANA->getVar('PROFILE.MAIL');
        assert('filter_var($sender, FILTER_VALIDATE_EMAIL); // $sender not a valid e-mail');

        // get the mail template
        $skinManager = Skin::getInstance();
        $fileName = $skinManager->getFile('USER_LOST_PWD');
        $website = 'http://' . $_SERVER['SERVER_ADDR'] . $_SERVER['PHP_SELF'] .
            '?action=set_reset_pwd&key=' . $uniqueKey;

        // set the mail values
        $mailer = new Mailer($fileName);
        $mailer->subject = $subject;
        $mailer->sender = $sender;
        $mailer->setVar('NAME', $userName);
        $mailer->setVar('TIME', date("m.d.y H:i:s"));
        $mailer->setVar('WEBSITE', $website);
        $result = $mailer->send($recipient);
        // check for mail result
        if ($result) {
            return true;
        } else {
            // mail can not be send
            return false;
        }
    }

    /**
     * Send login data screen
     *
     * @type        security
     * @template    USER_RESET_PWD
     * @onerror     text: 404
     *
     * @access      public
     * @param       string  $key  user id
     * @return      bool
     */
    public function set_reset_pwd($key)
    {
        $this->_getUserId($key);
        return true;
    }

    /**
     * Send login data screen
     *
     * @type        security
     * @template    message
     * @onsuccess   goto: login, text: 200
     * @onerror     goto: login, text: INPUT_INVALID
     *
     * @access      public
     * @param       string  $key         user id
     * @param       string  $new_pwd     new password
     * @param       string  $repeat_pwd  duplicate of new password
     * @return      bool
     */
    public function reset_pwd($key, $new_pwd, $repeat_pwd)
    {
        global $YANA;
        $db = SessionManager::getDatasource();

        // check if user exist in the database
        $userName = $this->_getUserId($key);
        assert('is_string($userName);');

        try {

            $user = YanaUser::getInstance($userName);
            return $this->_setPwd($user, $new_pwd, $repeat_pwd);

        } catch (NotFoundException $e) {
            return false;
        }
    }

    /**
     * Set new password
     *
     * @type        security
     * @template    message
     * @onsuccess   goto: index
     * @onerror     goto: index
     *
     * @access      public
     * @param       string  $new_pwd     new password
     * @param       string  $repeat_pwd  duplicate of new password
     * @param       string  $old_pwd     old password
     * @return      bool
     */
    public function set_pwd($new_pwd, $repeat_pwd, $old_pwd = "")
    {
        global $YANA;
        try {

            $user = YanaUser::getInstance();
            if (!$user->checkPassword($old_pwd)) {
                Error::report('LOGIN_INVALID');
                $YANA->exitTo(); // abort immediately to avoid security risks
                /**
                 * This case exits to the default page (which should be public),
                 * to avoid redirection problems when redirecting to a page which
                 * needs correct login information - for which we already know
                 * that it is false.
                 */
            }
            if (!$this->_setPwd($user, $new_pwd, $repeat_pwd)) {
                throw new Error('500');
            }
            return true;

        } catch (NotFoundException $e) {
            return false;
        }
    }

    /**
     * Set new password
     *
     * @access      private
     * @param       YanaUser  $user         user instance
     * @param       string    $newPwd       new password
     * @param       string    $repeatPwd    new password
     * @return      bool
     */
    private function _setPwd(YanaUser $user, $newPwd, $repeatPwd)
    {
        if ($newPwd !== $repeatPwd) {
            return false;
        }
        try {
            $user->setPassword($newPwd);
            return true;
        } catch (DbError $e) { // unable to set password
            return false;
        }
    }

    /**
     * Check Password reset Ticket
     *
     * @access      private
     * @param       string    $recoveryId recovery id
     * @return      array
     */
    private function _getUserId($recoveryId)
    {
        if (!is_string($recoveryId)) {
            throw new Notice('INPUT_INVALID');
        }

        $database = SessionManager::getDatasource();
        $user = $database->select('user', array('user_recover_id', '=', $recoveryId));

        assert('is_array($user); // $user must be of type array');
        if (count($user) !== 1) {
            throw new Notice('USER_UNKNOWN');
        }

        $user = array_pop($user);
        if (!isset($user['USER_RECOVER_UTC'])) {
            throw new Notice('time_exceed');
        }

        // T - 4h
        if ($user['USER_RECOVER_UTC'] + 14400 < time()) {
            throw new Notice('time_exceed');
        }

        return $user['USER_ID'];
    }

    /**
     * Logout
     *
     * @type        security
     * @template    message
     * @onsuccess   text: logout
     *
     * @access      public
     * @return      bool
     */
    public function logout()
    {
        YanaUser::getInstance()->logout();
        return true;
    }

    /**
     * Check if login data is correct
     *
     * @type        security
     * @template    message
     * @onsuccess   text: LOGIN_OKAY
     * @onerror     template: ALERT, text: LOGIN_INVALID
     *
     * @access      public
     * @param       string  $user  user name
     * @param       string  $pass  password
     * @return      bool
     */
    public function check_login($user, $pass = "")
    {
        // get user instance
        try {
            $userData = YanaUser::getInstance($user);
        } catch (NotFoundException $e) {
            /* delay output if attempt failed to make brute-force attacks more difficult to commit */
            sleep(2);
            return false;
        }

        /* 1. reset failure count if failure time has expired */
        if ($userData->getFailureTime() < time() - $this->maxFailureTime) {
            $userData->resetFailureCount();
        }
        /* 2. exit if the user has 3 times tried to login with a wrong password in last 5 minutes */
        if ($userData->getFailureCount() >= $this->maxFailureCount) {
            throw new Alert('USER_LOGIN_BLOCKED');
        }
        /* 3. error - login has failed */
        if (!$userData->checkPassword($pass)) {
            // delay output if attempt failed to make brute-force attacks more difficult to commit
            if (isset($_SESSION['on_login_goto'])) {
                unset($_SESSION['on_login_goto']);
            }

            /* create a log for each failed login attempt */
            Log::report("Login attempt failed for user '".mb_strtolower($user)."'. Invalid password.");

            /* The sleep-command is introduced for security reasons,
             * to make brute-force attacks on password forms harder.
             */
            sleep(2);
            throw new Alert("LOGIN_INVALID");
        }
        $userData->login();
        self::$userName = $user;

        /* route next action */
        if (isset($_SESSION['on_login_goto'])) {
            $nextAction = $_SESSION['on_login_goto'];
            unset($_SESSION['on_login_goto']);
            Message::report('login_okay');
            global $YANA;
            $YANA->exitTo($nextAction);
        }
        return true;
    }
}

?>