nordsoftware/yii2-account

View on GitHub
src/components/datacontract/DataContract.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
/*
 * This file is part of Account.
 *
 * (c) 2014 Nord Software
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace nord\yii\account\components\datacontract;

use nord\yii\account\models\Account;
use nord\yii\account\models\AccountLoginHistory;
use nord\yii\account\models\AccountPasswordHistory;
use nord\yii\account\models\AccountProvider;
use nord\yii\account\models\AccountToken;
use nord\yii\account\Module;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidParamException;
use yii\base\Model;
use yii\db\ActiveRecord;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;

class DataContract extends Component implements DataContractInterface
{
    /**
     * @var array map over model statuses to use by this contract.
     */
    public $statusMap = [];

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        $this->initStatusMap();
    }

    /**
     * @inheritdoc
     * @return Account
     */
    public function createAccount(array $config = [])
    {
        return $this->createInternal(Module::CLASS_ACCOUNT, $config, false);
    }

    /**
     * @inheritdoc
     * @return Account model instance.
     */
    public function findAccount($condition)
    {
        return $this->findInternal(Module::CLASS_ACCOUNT, $condition);
    }

    /**
     * @inheritdoc
     */
    public function updateAccountAttributes(ActiveRecord $model, array $config)
    {
        $this->updateAttributesInternal($model, $config);
    }

    /**
     * @inheritdoc
     */
    public function activateAccount(ActiveRecord $model)
    {
        $this->transitionInternal($model, $this->getStatusCode(Module::CLASS_ACCOUNT, Module::STATUS_ACTIVATED));
    }

    /**
     * @inheritdoc
     */
    public function isAccountActivated(ActiveRecord $model)
    {
        if (!Module::getInstance()->enableActivation) {
            return true;
        }
        return $model->status === $this->getStatusCode(Module::CLASS_ACCOUNT, Module::STATUS_ACTIVATED);
    }

    /**
     * @inheritdoc
     */
    public function isAccountLocked(ActiveRecord $model)
    {
        $numAllowedAttempts = Module::getParam(Module::PARAM_NUM_ALLOWED_FAILED_LOGIN_ATTEMPTS);
        $lockoutExpireTime = Module::getParam(Module::PARAM_LOCKOUT_EXPIRE_TIME);

        if ($numAllowedAttempts === 0) {
            return false;
        }

        /** @var ActiveRecord $modelClass */
        $modelClass = Module::getInstance()->getClassName(Module::CLASS_LOGIN_HISTORY);

        /** @var AccountLoginHistory $model */
        $model = $modelClass::find()
            ->where(['accountId' => $model->getPrimaryKey()])
            ->andWhere('UNIX_TIMESTAMP() - UNIX_TIMESTAMP(createdAt) < :expireTime',
                [':expireTime' => $lockoutExpireTime])
            ->andWhere('numFailedAttempts > :numAllowedAttempts', [':numAllowedAttempts' => $numAllowedAttempts])
            ->orderBy('createdAt DESC')
            ->one();

        return $model !== null;
    }

    /**
     * @inheritdoc
     */
    public function isAccountPasswordExpired(ActiveRecord $model)
    {
        $passwordExpireTime = Module::getParam(Module::PARAM_PASSWORD_EXPIRE_TIME);

        if ($passwordExpireTime === 0) {
            return false;
        }

        /** @var ActiveRecord $modelClass */
        $modelClass = Module::getInstance()->getClassName(Module::CLASS_PASSWORD_HISTORY);

        /** @var AccountPasswordHistory $model */
        $model = $modelClass::find()
            ->where(['accountId' => $model->getPrimaryKey()])
            ->andWhere('UNIX_TIMESTAMP() - UNIX_TIMESTAMP(createdAt) > :expireTime',
                [':expireTime' => $passwordExpireTime])
            ->orderBy('createdAt DESC')
            ->one();

        return $model !== null;
    }

    /**
     * @inheritdoc
     */
    public function isAccountPasswordUsed(ActiveRecord $model, $password)
    {
        $models = $this->getAccountPasswordHistory($model);
        foreach ($models as $model) {
            if (Module::getInstance()->getPasswordHasher()->validatePassword($password, $model->password)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @inheritdoc
     */
    public function getAccountNumFailedLoginAttempts(ActiveRecord $model)
    {
        /** @var ActiveRecord $modelClass */
        $modelClass = Module::getInstance()->getClassName(Module::CLASS_LOGIN_HISTORY);

        /** @var AccountLoginHistory $lastEntry */
        $lastEntry = $modelClass::find()
            ->where(['accountId' => $model->getPrimaryKey])
            ->orderBy('createdAt DESC')
            ->one();

        $numFailedAttempts = $lastEntry !== null ? $lastEntry->numFailedAttempts : 0;
        return ++$numFailedAttempts;
    }

    /**
     * @inheritdoc
     * @return AccountPasswordHistory[]
     */
    public function getAccountPasswordHistory(ActiveRecord $model)
    {
        /** @var ActiveRecord $modelClass */
        $modelClass = Module::getInstance()->getClassName(Module::CLASS_PASSWORD_HISTORY);
        return $modelClass::find()
            ->where(['accountId' => $model->getPrimaryKey()])
            ->orderBy('createdAt DESC')
            ->limit(10)
            ->all();
    }

    /**
     * @inheritdoc
     * @return AccountProvider model instance.
     */
    public function createProvider(array $config = [])
    {
        return $this->createInternal(Module::CLASS_PROVIDER, $config, false);
    }

    /**
     * @inheritdoc
     * @return AccountProvider model instance.
     */
    public function findProvider($condition)
    {
        return $this->findInternal(Module::CLASS_PROVIDER, $condition);
    }

    /**
     * @inheritdoc
     * @return AccountLoginHistory model instance.
     */
    public function createLoginHistory(array $config = [])
    {
        return $this->createInternal(Module::CLASS_LOGIN_HISTORY, $config);
    }

    /**
     * @inheritdoc
     * @return AccountPasswordHistory model instance.
     */
    public function createPasswordHistory(array $config = [])
    {
        return $this->createInternal(Module::CLASS_PASSWORD_HISTORY, $config);
    }

    /**
     * @inheritdoc
     * @return string the generated token.
     */
    public function createToken(array $config = [])
    {
        return $this->createInternal(Module::CLASS_TOKEN, $config);
    }

    /**
     * @inheritdoc
     * @return AccountToken
     */
    public function findToken($condition)
    {
        return $this->findInternal(Module::CLASS_TOKEN, $condition);
    }

    /**
     * @inheritdoc
     * @return AccountToken token model.
     */
    public function findValidToken($type, $token)
    {
        $tokenExpireTime = Module::getParam(Module::PARAM_TOKEN_EXPIRE_TIME);

        /** @var AccountToken $modelClass */
        $modelClass = Module::getInstance()->getClassName(Module::CLASS_TOKEN);

        return $modelClass::find()
            ->where([
                'type' => $type,
                'token' => $token,
                'status' => $this->getStatusCode(Module::CLASS_TOKEN, Module::STATUS_UNUSED),
            ])
            ->andWhere('(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(createdAt)) < :expireTime',
                [':expireTime' => $tokenExpireTime])
            ->one();
    }

    /**
     * @inheritdoc
     */
    public function useToken(ActiveRecord $model)
    {
        $this->transitionInternal($model, $this->getStatusCode(Module::CLASS_TOKEN, Module::STATUS_USED));
    }

    /**
     * @inheritdoc
     */
    public function createLoginForm(array $config = [])
    {
        return $this->createModelInternal(Module::CLASS_LOGIN_FORM, $config);
    }

    /**
     * @inheritdoc
     */
    public function createSignupForm(array $config = [])
    {
        return $this->createModelInternal(Module::CLASS_SIGNUP_FORM, $config);
    }

    /**
     * @inheritdoc
     */
    public function createConnectForm(array $config = [])
    {
        return $this->createModelInternal(Module::CLASS_CONNECT_FORM, $config);
    }

    /**
     * @inheritdoc
     */
    public function createForgotPasswordForm(array $config = [])
    {
        return $this->createModelInternal(Module::CLASS_FORGOT_PASSWORD_FORM, $config);
    }

    /**
     * @inheritdoc
     */
    public function createPasswordForm(array $config = [])
    {
        return $this->createModelInternal(Module::CLASS_PASSWORD_FORM, $config);
    }

    /**
     * Returns the status code for a specific model status.
     *
     * @param string $className class name.
     * @param string $status status identifier.
     * @return integer status code.
     */
    public function getStatusCode($className, $status)
    {
        if (!isset($this->statusMap[$className])) {
            throw new InvalidParamException("Trying to get status code for unknown class '$className'.");
        }
        if (!isset($this->statusMap[$className][$status])) {
            throw new InvalidParamException("Trying to get status code for unknown status '$status'.");
        }
        return $this->statusMap[$className][$status];
    }

    /**
     * Initializes the status map.
     */
    protected function initStatusMap()
    {
        $this->statusMap = ArrayHelper::merge(
            [
                Module::CLASS_ACCOUNT => [
                    Module::STATUS_UNACTIVATED => 0,
                    Module::STATUS_ACTIVATED => 1,
                ],
                Module::CLASS_TOKEN => [
                    Module::STATUS_UNUSED => 0,
                    Module::STATUS_USED => 1,
                ],
            ],
            $this->statusMap
        );
    }

    /**
     * Creates a new active record of the given class.
     *
     * @param string $className active record class name.
     * @param array $config object configuration.
     * @param boolean $runSave whether to save the model.
     * @return ActiveRecord model instance.
     * @throws Exception if the model cannot be saved.
     */
    protected function createInternal($className, array $config = [], $runSave = true)
    {
        /** @var ActiveRecord $model */
        $model = $this->createModelInternal($className, $config);

        if ($runSave && !$model->save()) {
            throw new Exception("Failed to save model '$className' with errors '" . Json::encode($model->getErrors()) . "'.");
        }

        return $model;
    }

    /**
     * Creates a new model of the given class.
     *
     * @param string $className model class name.
     * @param array $config object configuration.
     * @return Model model instance.
     */
    protected function createModelInternal($className, array $config = [])
    {
        $modelClass = Module::getInstance()->getClassName($className);
        return new $modelClass($config);
    }

    /**
     * Updates attributes for the given model.
     *
     * @param ActiveRecord $model model instance.
     * @param array $attributes model attributes.
     * @return boolean the result.
     */
    protected function updateAttributesInternal(ActiveRecord $model, array $attributes)
    {
        return $model->updateAttributes($attributes) !== 0;
    }

    /**
     * Returns a model class using the given condition.
     *
     * @param string $className class name.
     * @param mixed $condition search condition.
     * @return ActiveRecord|null model instance or null if not found.
     */
    protected function findInternal($className, $condition)
    {
        /** @var ActiveRecord $modelClass */
        $modelClass = Module::getInstance()->getClassName($className);
        return $modelClass::findOne($condition);
    }

    /**
     * Changes the status of a model class.
     *
     * @param ActiveRecord $model model instance.
     * @param integer $status new status.
     */
    protected function transitionInternal(ActiveRecord $model, $status)
    {
        $this->updateAttributesInternal($model, ['status' => $status]);
    }
}