YetiForceCompany/YetiForceCRM

View on GitHub
modules/Settings/BruteForce/models/Module.php

Summary

Maintainability
A
1 hr
Test Coverage
F
39%
<?php

/**
 * Brute force model class.
 *
 * @copyright YetiForce S.A.
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
 * @author    YetiForce S.A.
 */
class Settings_BruteForce_Module_Model extends Settings_Vtiger_Module_Model
{
    const UNBLOCKED = 0;
    const BLOCKED = 1;
    const UNBLOCKED_BY_USER = 2;

    private $isBlocked;
    private $blockedId;

    /**
     * Function includes class instances.
     *
     * @return Settings_BruteForce_Module_Model
     */
    public static function getCleanInstance()
    {
        $instance = new self();
        $instance->setData(self::getBruteForceSettings());

        return $instance;
    }

    /**
     * Function verifies if module is active.
     *
     * @return bool
     */
    public function isActive()
    {
        return (bool) $this->get('active');
    }

    /**
     * Function returns module configuration.
     *
     * @return array
     */
    public static function getBruteForceSettings()
    {
        if (App\Cache::has('BruteForce', 'Settings')) {
            return App\Cache::get('BruteForce', 'Settings');
        }
        $row = (new App\Db\Query())->from('a_#__bruteforce')->one();
        App\Cache::save('BruteForce', 'Settings', $row, App\Cache::LONG);

        return $row;
    }

    /**
     * Function gets unsuccessful login attempts data of blocked users.
     *
     * @return array
     */
    public function getBlockedIp()
    {
        $time = $this->get('timelock');
        $blockDate = new DateTime();
        $blockDate->modify("-$time minutes");

        $query = (new \App\Db\Query())
            ->select(['id', 'attempts', 'ip', 'time'])
            ->from('a_#__bruteforce_blocked')
            ->andWhere(['>=', 'time', $blockDate->format('Y-m-d H:i:s')])
            ->andWhere(['blocked' => self::BLOCKED])
            ->andWhere(['>=', 'attempts', $this->get('attempsnumber')]);

        return $query->createCommand()->queryAll();
    }

    /**
     * Functions gets data from login history.
     *
     * @param array $data
     *
     * @return array
     */
    public function getLoginHistoryData($data)
    {
        $query = (new \App\Db\Query())
            ->select(['usersName' => new \yii\db\Expression('DISTINCT user_name'), 'browser' => new \yii\db\Expression('browser')])
            ->from('vtiger_loginhistory')
            ->where(['status' => ['Failed login', 'Blocked IP', 'ForgotPasswordNoUserFound'], 'user_ip' => $data['ip']])
            ->andWhere(['>=', 'login_time', $data['time']]);
        $historyData = $query->createCommand()->queryAllByGroup(2);
        $users = array_keys($historyData);
        $browsers = [];
        foreach ($historyData as $browsersUserName) {
            $browsers = array_merge($browsers, $browsersUserName);
        }
        $data['usersName'] = implode(', ', $users);
        $data['browsers'] = implode(', ', array_unique($browsers));

        return $data;
    }

    /**
     * Function verifies if user is blocked.
     *
     * @return bool
     */
    public function isBlockedIp()
    {
        if (isset($this->isBlocked)) {
            return $this->isBlocked;
        }
        $time = $this->get('timelock');
        $blockDate = new DateTime();
        $blockDate->modify("-$time minutes");
        $ip = \App\RequestUtil::getRemoteIP(true);
        $this->blockedId = (new \App\Db\Query())
            ->select(['id'])
            ->from('a_#__bruteforce_blocked')
            ->where(['>', 'time', $blockDate->format('Y-m-d H:i:s')])
            ->andWhere(['ip' => $ip])
            ->andWhere(['blocked' => self::BLOCKED])
            ->scalar();

        return $this->isBlocked = (!empty($this->blockedId));
    }

    /**
     * Function increases the number of unsuccessful login attempts.
     */
    public function incAttempts()
    {
        if (!empty($this->blockedId)) {
            \App\Db::getInstance('admin')->createCommand()
                ->update('a_#__bruteforce_blocked', [
                    'attempts' => new \yii\db\Expression('attempts + 1'),
                ], ['id' => $this->blockedId])
                ->execute();
        }
    }

    /**
     * Function updates unsuccessful login attempts.
     */
    public function updateBlockedIp()
    {
        \App\Log::trace('Start ' . __METHOD__);
        $db = \App\Db::getInstance('admin');
        $time = $this->get('timelock');
        $date = new DateTime();
        $checkData = $date->modify("-$time minutes")->format('Y-m-d H:i:s');
        $ip = \App\RequestUtil::getRemoteIP(true);

        $bfData = (new \App\Db\Query())
            ->select(['id', 'attempts'])
            ->from('a_#__bruteforce_blocked')
            ->where(['>=', 'time', $checkData])
            ->andWhere(['blocked' => self::UNBLOCKED])
            ->andWhere(['ip' => $ip])->one();
        if (!$bfData) {
            $this->setBlockedIp($ip);
        } else {
            $attempts = ++$bfData['attempts'];
            $blocked = $attempts >= $this->get('attempsnumber') ? self::BLOCKED : self::UNBLOCKED;
            $db->createCommand()
                ->update('a_#__bruteforce_blocked', [
                    'attempts' => $attempts,
                    'blocked' => $blocked,
                ], ['id' => $bfData['id']])
                ->execute();
            $this->isBlocked = self::BLOCKED === $blocked;
            $this->blockedId = $bfData['id'];
        }
        $this->clearBlockedByIp($ip, $checkData);
        \App\Log::trace('End ' . __METHOD__);
    }

    /**
     * Function adds unsuccessful login attempt to database.
     *
     * @param string $ip - User IP
     *
     * @return int - Created records ID
     */
    private function setBlockedIp($ip)
    {
        $db = \App\Db::getInstance('admin');
        $db->createCommand()->insert('a_#__bruteforce_blocked', [
            'ip' => $ip,
            'time' => date('Y-m-d H:i:s'),
            'attempts' => 1,
            'blocked' => self::UNBLOCKED,
        ])->execute();
        $this->isBlocked = false;

        return $this->blockedId = $db->getLastInsertID('a_#__bruteforce_blocked_id_seq');
    }

    /**
     * Function removes redundant entries from database.
     *
     * @param string $ip   - User IP
     * @param string $data - Cut-off date of users block condition
     */
    private function clearBlockedByIp($ip, $data)
    {
        $db = \App\Db::getInstance('admin');
        $db->createCommand()->delete('a_#__bruteforce_blocked', [
            'and', ['<', 'time', $data],
            ['blocked' => self::UNBLOCKED],
            ['ip' => $ip], ])->execute();
    }

    /**
     * Function unblocks user.
     *
     * @param int $id - Record ID
     */
    public static function unBlock($id)
    {
        return \App\Db::getInstance('admin')->createCommand()
            ->update('a_#__bruteforce_blocked', [
                'blocked' => self::UNBLOCKED_BY_USER,
                'userid' => \App\User::getCurrentUserRealId(),
            ], ['id' => $id])
            ->execute();
    }

    /**
     * Function returns a list of users who are administrators.
     *
     * @return array - List of users
     */
    public static function getAdminUsers()
    {
        return \App\Fields\Owner::getInstance()->getUsers(false, 'Active', false, false, true);
    }

    /**
     * Function updates module configuration.
     *
     * @param array $data - Configuration data
     */
    public static function updateConfig($data)
    {
        $db = \App\Db::getInstance('admin');
        $db->createCommand()
            ->update('a_#__bruteforce', [
                'attempsnumber' => $data['attempsnumber'],
                'timelock' => $data['timelock'],
                'active' => $data['active'],
                'sent' => $data['sent'],
            ])->execute();
        $db->createCommand()->delete('a_#__bruteforce_users')->execute();
        if (!empty($data['selectedUsers'])) {
            $users = !\is_array($data['selectedUsers']) ? [$data['selectedUsers']] : $data['selectedUsers'];
            foreach ($users as $userId) {
                $db->createCommand()->insert('a_#__bruteforce_users', ['id' => $userId])->execute();
            }
        }
        App\Cache::delete('BruteForce', 'Settings');
    }

    /**
     * Function returns table of users selected for notifications.
     *
     * @return array - ID list of users
     */
    public static function getUsersForNotifications()
    {
        return (new \App\Db\Query())->from('a_#__bruteforce_users')->createCommand()->queryColumn();
    }

    /**
     * Function sends notifications.
     */
    public function sendNotificationEmail()
    {
        \App\Log::trace('Start ' . __METHOD__);
        if (!empty($this->get('sent'))) {
            $usersId = self::getUsersForNotifications();
            if (0 === \count($usersId)) {
                \App\Log::trace('End ' . __METHOD__ . ' - No brute force users found to send email');
                return false;
            }
            $emails = [];
            foreach ($usersId as $id) {
                $recordModel = Vtiger_Record_Model::getInstanceById($id, 'Users');
                $emails[] = $recordModel->get('email1');
            }
            $configBruteForce = self::getBruteForceSettings();
            \App\Mailer::sendFromTemplate([
                'template' => 'BruteForceSecurityRiskHasBeenDetected',
                'moduleName' => 'Users',
                'to' => $emails,
                'ip' => \App\RequestUtil::getRemoteIP(true),
                'time' => (new DateTime())->modify("-{$configBruteForce['timelock']} minutes")->format('Y-m-d H:i:s'),
            ]);
        }
        \App\Log::trace('End ' . __METHOD__);
    }
}