<?php
/*
 * This file is part of MODX Revolution.
 *
 * Copyright (c) MODX, LLC. All Rights Reserved.
 *
 * For complete copyright and license information, see the COPYRIGHT and LICENSE
 * files found in the top-level directory of this distribution.
 */

/**
 * Properly log in the user and set up the session.
 *
 * @package modx
 * @subpackage processors.security
 */

class modSecurityLoginProcessor extends modProcessor {

    /** @var  modUser */
    public $user;
    public $username;
    public $givenPassword;
    public $rememberme;
    public $lifetime;
    public $loginContext;
    public $addContexts;
    public $isMgr;

    /**
     * {@inheritDoc}
     * @return boolean
     */
    public function initialize() {
        $this->username = $this->getProperty('username');
        $this->givenPassword = $this->getProperty('password');
        if (!$this->username || !$this->givenPassword) {
            return $this->modx->lexicon('login_cannot_locate_account');
        }

        $this->rememberme = ($this->getProperty('rememberme', false) == true);
        $this->lifetime = (int)$this->getProperty('lifetime', $this->modx->getOption('session_cookie_lifetime', null,0));
        $this->loginContext = $this->getProperty('login_context', $this->modx->context->get('key'));
        $this->addContexts = $this->getProperty('add_contexts', array());
        $this->addContexts = empty($this->addContexts) ? array() : explode(',', $this->addContexts);
        /* Events are fired based on the primary loginContext */
        $this->isMgr = ($this->loginContext == 'mgr');

        return true;
    }

    public function getLanguageTopics() {
        return array('login');
    }

    /**
     * Fire event at the start of login process
     * @return string
     */
    public function fireOnBeforeLoginEvent() {
        $onBeforeLoginParams = array(
            'username' => $this->username,
            'password' => $this->givenPassword,
            'attributes' => array(
                'rememberme' => & $this->rememberme,
                'lifetime' => & $this->lifetime,
                'loginContext' => & $this->loginContext,
                'addContexts' => & $this->addContexts,
            )
        );

        $response = $this->modx->invokeEvent($this->isMgr ? "OnBeforeManagerLogin" : "OnBeforeWebLogin", $onBeforeLoginParams);

        if (is_array($response)) {
            foreach ($response as $key => $value) {
                if ($value !== true) {
                    return $value;
                }
            }
        }

        return '';
    }

    /**
     * Load user with profile and user settings
     * @return bool|null|string
     */
    public function getUser() {
        /** @var $user modUser */
        $this->user = $this->modx->getObjectGraph('modUser', '{"Profile":{},"UserSettings":{}}',
            array ('modUser.username' => $this->username));

        return $this->fireOnUserNotFoundEvent();
    }

    /**
     * Fire event when user with this username is not found
     * @return bool|null|string
     */
    public function fireOnUserNotFoundEvent() {
        if (!$this->user) {
            $OnUserNotFoundParams = array(
                'user' => &$this->user,
                'username' => $this->username,
                'password' => $this->givenPassword,
                'attributes' => array(
                    'rememberme' => $this->rememberme,
                    'lifetime' => $this->lifetime,
                    'loginContext' => $this->loginContext,
                    'addContexts' => $this->addContexts,
                )
            );
            $ru = $this->modx->invokeEvent("OnUserNotFound", $OnUserNotFoundParams);
            if (!empty($ru)) {
                foreach ($ru as $obj) {
                    if (is_object($obj) && $obj instanceof modUser) {
                        $this->user = $obj;
                        break;
                    }
                }
            }
            if (!is_object($this->user) || !($this->user instanceof modUser)) {
                return $this->modx->lexicon('login_cannot_locate_account');
            }
        }

        return false;
    }

    /**
     * Check if user is not active or blocked
     * @return bool|null|string
     */
    public function checkIsBlocked() {
        if (!$this->user->get('active')) {
            return $this->modx->lexicon('login_user_inactive');
        }

        /** @var modUserProfile $profile */
        $profile = $this->user->Profile;
        $flc = $profile->get('failedlogincount');

        /* Update block state */
        if ($profile->get('blockeduntil') < time()) {
            if ($profile->get('blockeduntil') > 0) {
                $profile->set('failedlogincount', 0);
                $flc = 0;
            }
            $profile->set('blockeduntil', 0);
            $profile->save();
        }
        if ($this->user->passwordMatches($this->givenPassword) && $profile->get('blockeduntil') < time()) {
            $profile->set('failedlogincount', 0);
            $profile->save();
            $flc = 0;
        }
        else {
            $flc++;
        }
        if ($flc >= $this->modx->getOption('failed_login_attempts') &&
            $profile->get('blockeduntil') < time()) {
                $profile->set('blockeduntil', time() + (60 * $this->modx->getOption('blocked_minutes')));
                $profile->save();
        }
        
        /* Validate block state */
        if ($flc >= $this->modx->getOption('failed_login_attempts') &&
            $profile->get('blockeduntil') > time()) {
            return $this->modx->lexicon('login_blocked_too_many_attempts');
        }
        if ($profile->get('blocked')) {
            return $this->modx->lexicon('login_blocked_admin');
        }
        if ($profile->get('blockeduntil') > time()) {
            return $this->modx->lexicon('login_blocked_error');
        }
        if ($profile->get('blockedafter') > 0 && $profile->get('blockedafter') < time()) {
            return $this->modx->lexicon('login_blocked_error');
        }

        return false;    
    }

    /**
     * Check user settings related to authentication
     * @return bool|null|string
     */
    public function checkUserSettings() {
        /**
         * @var string $settingPK
         * @var modUserSetting $setting
         */
        foreach ($this->user->UserSettings as $settingPK => $setting) {
            if ($setting->get('key') == 'allowed_ip') {
                $ip = $this->modx->request->getClientIp();
                $ip = $ip['ip'];
                if (!in_array($ip, explode(',', str_replace(' ', '', $setting->get('value'))))) {
                    return $this->modx->lexicon('login_blocked_ip');
                }
            }

            if ($setting->get('key') == 'allowed_days') {
                $date = getdate();
                $day = $date['wday'] + 1;
                if (strpos($setting->get('value'), "{$day}") === false) {
                    return $this->modx->lexicon('login_blocked_time');
                }
            }
        }

        return false;
    }

    /**
     * Actions before user is logged in
     * @return bool|null|string
     */
    public function beforeLogin() {
        $preventLogin = $this->fireOnBeforeLoginEvent();
        if (!empty($preventLogin)) {
            return $preventLogin;
        }

        $preventLogin = $this->getUser();
        if (!empty($preventLogin)) {
            return $preventLogin;
        }

        $preventLogin = $this->checkIsBlocked();
        if (!empty($preventLogin)) {
            return $preventLogin;
        }

        $preventLogin = $this->checkUserSettings();
        if (!empty($preventLogin)) {
            return $preventLogin;
        }

        return false;
    }

    /**
     * Fire event just before password check
     * @return array|bool
     */
    public function fireOnAuthenticationEvent() {
        $loginParams = array(
            "user"       => & $this->user,
            "password"   => $this->givenPassword,
            "rememberme" => $this->rememberme,
            "lifetime" => $this->lifetime,
            "loginContext" => & $this->loginContext,
            "addContexts" => & $this->addContexts,
        );

        return $this->modx->invokeEvent($this->isMgr ? "OnManagerAuthentication" : "OnWebAuthentication", $loginParams);
    }

    /**
     * Update failed login count
     */
    public function failedLogin() {
        if (!isset($_SESSION['login_failed'])) {
            $_SESSION['login_failed'] = 0;
        }
        $flc = ((integer) $this->user->Profile->get('failedlogincount')) + 1;
        $this->user->Profile->set('failedlogincount', $flc);
        $this->user->Profile->save();
        $_SESSION['login_failed']++;
    }

    /** Check user password
     *
     * @param $rt
     * @return bool|null|string
     */
    public function checkPassword($rt) {
        /* check if plugin authenticated the user */
        if (!$rt || (is_array($rt) && !in_array(true, $rt))) {
            /* check user password - local authentication */
            if (!$this->user->passwordMatches($this->givenPassword)) {
                $this->failedLogin();
                return $this->modx->lexicon('login_username_password_incorrect');
            }
        }
        else if ($rt && (is_array($rt) && !in_array(true, $rt, true))) {
            $error = "";
            foreach ($rt as $msg) {
                if (!empty($msg)) {
                    $error .= $msg."\n";
                }
            }
            return $error;
        }

        return false;
    }

    /**
     * Remember user in session by login contexts
     */
    public function addSessionContexts() {
        $contexts = array_merge(array($this->loginContext), $this->addContexts);
        foreach ($contexts as $loginCtx) {
            $this->user->addSessionContext($loginCtx);
            $_SESSION['modx.' . $loginCtx . '.session.cookie.lifetime'] = $this->rememberme ? $this->lifetime : 0;
        }
    }

    /**
     * Fire after login event
     */
    public function fireAfterLoginEvent() {
        $postLoginParams = array(
            'user' => $this->user,
            'attributes' => array(
                'rememberme' => $this->rememberme,
                'lifetime' => $this->lifetime,
                'loginContext' => $this->loginContext,
                'addContexts' => $this->addContexts,
            )
        );

        $this->modx->invokeEvent($this->isMgr ? "OnManagerLogin" : "OnWebLogin", $postLoginParams);
    }

    /** Prepare response for mgr context
     *
     * @param $userToken
     * @param $returnUrl
     * @return array
     */
    public function prepareMgrResponse($userToken, $returnUrl) {
        $managerUrl = $this->modx->getOption('url_scheme') . $this->modx->getOption('http_host') . $this->modx->getOption('manager_url', null, MODX_MANAGER_URL);
        $manager_login_startup_url = !empty($returnUrl) && (strpos($returnUrl, '://') === false || strpos($returnUrl, $managerUrl) === 0)
            ? $returnUrl
            : $managerUrl;
        if (!empty($manager_login_startup)) {
            $manager_login_startup= intval($manager_login_startup);
            if ($manager_login_startup) $manager_login_startup_url .= '?id=' . $manager_login_startup;
        }
        return array(
            'url' => $manager_login_startup_url,
            'token' => $userToken,
        );
    }

    /** Prepare response for non-mgr contexts
     *
     * @param $userToken
     * @param $returnUrl
     * @return array
     */
    public function prepareWebResponse($userToken, $returnUrl) {
        $siteUrl = $this->modx->getOption('site_url', null, MODX_SITE_URL);
        $login_startup_url = !empty($returnUrl) && (strpos($returnUrl, '://') === false || strpos($returnUrl, $siteUrl) === 0)
            ? $returnUrl
            : '';
        if (!empty($login_startup)) {
            $login_startup = intval($login_startup);
            if ($login_startup) {
                $login_startup_url = $this->modx->makeUrl($login_startup, $this->loginContext, '', 'full');
            }
        }
        return array(
            'url' => $login_startup_url,
            'token' => $userToken,
        );
    }

    /** Prepare response depending on the login context
     *
     * @return array
     */
    public function prepareResponse() {
        $userToken = $this->user->getUserToken($this->modx->context->get('key'));
        $returnUrl = $this->getProperty('returnUrl', '');

        switch ($this->loginContext) {
            case 'mgr':
                $response = $this->prepareMgrResponse($userToken, $returnUrl);
                break;
            case 'web':
            default:
                $response = $this->prepareWebResponse($userToken, $returnUrl);
        }

        return $response;
    }

    /** Actions after user is logged in
     *
     * @return array
     */
    public function afterLogin() {
        $this->addSessionContexts();
        if ($this->loginContext == 'mgr') {
            $this->modx->user = null;
            $this->modx->getUser('mgr', true);
            if (!$this->modx->hasPermission('frames')) {
                $this->modx->runProcessor('security/logout');
                return $this->failure($this->modx->lexicon('access_denied'));
            }
        }
        $this->fireAfterLoginEvent();

        $this->modx->logManagerAction('login','modContext',$this->loginContext, $this->user->get('id'));

        return $this->cleanup($this->prepareResponse());
    }

    /**
     * {@inheritDoc}
     */
    public function process() {
        $preventLogin = $this->beforeLogin();
        if (!empty($preventLogin)) {
            return $this->failure($preventLogin);
        }

        $canLogin = $this->fireOnAuthenticationEvent();
        $preventLogin = $this->checkPassword($canLogin);
        if (!empty($preventLogin)) {
            return $this->failure($preventLogin);
        }

        return $this->afterLogin();
    }

    /** Return the response
     *
     * @param $response array
     * @return array
     */
    public function cleanup($response) {
        return $this->success('', $response);
    }
}

return 'modSecurityLoginProcessor';
