<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\User\Observer\Backend;

use Magento\Backend\Model\Auth\Session;
use Magento\Backend\Model\UrlInterface;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Exception\State\UserLockedException;
use Magento\Framework\Message\ManagerInterface;
use Magento\User\Model\Backend\Config\ObserverConfig;
use Magento\User\Model\ResourceModel\User as ResourceUser;
use Magento\User\Model\User;
use Magento\Framework\Event\ObserverInterface;
use Magento\User\Model\UserFactory;

/**
 * User backend observer model for authentication
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
 */
class AuthObserver implements ObserverInterface
{
    /**
     * Backend configuration interface
     *
     * @var ObserverConfig
     */
    protected $observerConfig;

    /**
     * Admin user resource model
     *
     * @var ResourceUser
     */
    protected $userResource;

    /**
     * Backend url interface
     *
     * @var UrlInterface
     */
    protected $url;

    /**
     * Backend authorization session
     *
     * @var Session
     */
    protected $authSession;

    /**
     * Factory class for user model
     *
     * @var UserFactory
     */
    protected $userFactory;

    /**
     * Encryption model
     *
     * @var EncryptorInterface
     */
    protected $encryptor;

    /**
     * Message manager interface
     *
     * @var ManagerInterface
     */
    protected $messageManager;

    /**
     * @param ObserverConfig $observerConfig
     * @param ResourceUser $userResource
     * @param UrlInterface $url
     * @param Session $authSession
     * @param UserFactory $userFactory
     * @param EncryptorInterface $encryptor
     * @param ManagerInterface $messageManager
     */
    public function __construct(
        ObserverConfig $observerConfig,
        ResourceUser $userResource,
        UrlInterface $url,
        Session $authSession,
        UserFactory $userFactory,
        EncryptorInterface $encryptor,
        ManagerInterface $messageManager
    ) {
        $this->observerConfig = $observerConfig;
        $this->userResource = $userResource;
        $this->url = $url;
        $this->authSession = $authSession;
        $this->userFactory = $userFactory;
        $this->encryptor = $encryptor;
        $this->messageManager = $messageManager;
    }

    /**
     * Admin locking and password hashing upgrade logic implementation
     *
     * @param EventObserver $observer
     * @return void
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function execute(EventObserver $observer)
    {
        $password = $observer->getEvent()->getPassword();
        /** @var User $user */
        $user = $observer->getEvent()->getUser();
        $authResult = $observer->getEvent()->getResult();

        if (!$authResult && $user->getId()) {
            // update locking information regardless whether user locked or not
            $this->_updateLockingInformation($user);
        }

        // check whether user is locked
        $lockExpires = $user->getLockExpires();
        if ($lockExpires) {
            $lockExpires = new \DateTime($lockExpires);
            if ($lockExpires > new \DateTime()) {
                throw new UserLockedException(
                    __(
                        'The account sign-in was incorrect or your account is disabled temporarily. '
                        . 'Please wait and try again later.'
                    )
                );
            }
        }

        if (!$authResult) {
            return;
        }

        $this->userResource->unlock($user->getId());

        $latestPassword = $this->userResource->getLatestPassword($user->getId());
        $this->_checkExpiredPassword($latestPassword);

        if (!$this->encryptor->validateHashVersion($user->getPassword(), true)) {
            $user->setPassword($password)
                ->setData('force_new_password', true)
                ->save();
        }
    }

    /**
     * Update locking information for the user
     *
     * @param User $user
     * @return void
     */
    private function _updateLockingInformation($user)
    {
        $now = new \DateTime();
        $lockThreshold = $this->observerConfig->getAdminLockThreshold();
        $maxFailures = $this->observerConfig->getMaxFailures();
        if (!($lockThreshold && $maxFailures)) {
            return;
        }
        $failuresNum = (int)$user->getFailuresNum() + 1;
        /** @noinspection PhpAssignmentInConditionInspection */
        if ($firstFailureDate = $user->getFirstFailure()) {
            $firstFailureDate = new \DateTime($firstFailureDate);
        }

        $newFirstFailureDate = false;
        $updateLockExpires = false;
        $lockThreshInterval = new \DateInterval('PT' . $lockThreshold.'S');
        // set first failure date when this is first failure or last first failure expired
        if (1 === $failuresNum
            || !$firstFailureDate
            || ($now->getTimestamp() - $firstFailureDate->getTimestamp()) > $lockThreshold
        ) {
            $newFirstFailureDate = $now;
            // otherwise lock user
        } elseif ($failuresNum >= $maxFailures) {
            $updateLockExpires = $now->add($lockThreshInterval);
        }
        $this->userResource->updateFailure($user, $updateLockExpires, $newFirstFailureDate);
    }

    /**
     * Check whether the latest password is expired
     *
     * Side-effect can be when passwords were changed with different lifetime configuration settings
     *
     * @param array $latestPassword
     * @return void
     */
    private function _checkExpiredPassword($latestPassword)
    {
        if ($latestPassword && $this->observerConfig->_isLatestPasswordExpired($latestPassword)) {
            if ($this->observerConfig->isPasswordChangeForced()) {
                $message = __('It\'s time to change your password.');
            } else {
                $myAccountUrl = $this->url->getUrl('adminhtml/system_account/');
                $message = __('It\'s time to <a href="%1">change your password</a>.', $myAccountUrl);
            }

            $messages = $this->messageManager->getMessages();

            // Remove existing messages with same ID to avoid duplication
            $messages->deleteMessageByIdentifier(User::MESSAGE_ID_PASSWORD_EXPIRED);

            $this->messageManager->addNoticeMessage($message);
            $message = $messages->getLastAddedMessage();
            if ($message) {
                $message->setIdentifier(User::MESSAGE_ID_PASSWORD_EXPIRED)->setIsSticky(true);
                $this->authSession->setPciAdminUserIsPasswordExpired(true);
            }
        }
    }
}
