Files
kupshop/bundles/KupShop/UserOauthBundle/Security/OauthUserProvider.php
2025-08-02 16:30:27 +02:00

313 lines
13 KiB
PHP

<?php
namespace KupShop\UserOauthBundle\Security;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
use KupShop\GraphQLBundle\EventListener\JsShopRefreshListener;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\UserBundle\Event\UserRegisteredEvent;
use KupShop\UserOauthBundle\ResourceOwner\ICustomResourceOwner;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class OauthUserProvider implements UserProviderInterface, OAuthAwareUserProviderInterface
{
use \DatabaseCommunication;
/** @var EventDispatcherInterface */
protected $eventDispatcher;
public function __construct(
protected TokenStorageInterface $tokenStorage,
protected SessionInterface $session,
protected RequestStack $requestStack,
) {
}
public function loadUserByUsername($username)
{
return $this->loadUserByIdentifier($username);
}
public function loadUserByIdentifier(string $identifier): UserInterface
{
throw new UserNotFoundException();
}
/**
* Refreshes the user for the account interface.
*
* It is up to the implementation to decide if the user data should be
* totally reloaded (e.g. from the database), or if the UserInterface
* object can just be merged into some internal array of users / identity
* map.
*
* @throws UnsupportedUserException if the account is not supported
*/
public function refreshUser(UserInterface $user): UserInterface
{
if ($user instanceof OauthUser) {
$userObject = $this->getUserByClientID($user->getProvider(), $user->getClientID());
if ($userObject) {
$userObject->activateUser();
return new OauthUser(
$userObject->id,
$userObject->email,
$user->getProvider(),
$user->getClientID(),
['ROLE_USER'],
$userObject
);
} else {
// this usually happens when user unbinds his fb/google account and is currently logged in with it
$this->tokenStorage->setToken(null);
// hack to preserve userMessages
$userMessages = $this->session->get('userMessages');
$this->session->invalidate();
$this->session->set('userMessages', $userMessages);
if (findModule(\Modules::JS_SHOP)) {
// set `jsShopReload: true` session to notify js-cart
$this->session->set(JsShopRefreshListener::SESSION_NAME, true);
}
addUserMessage(translate('oauth', 'login')['invalid_login']);
redirection(path('kupshop_user_login_login'));
}
}
throw new UnsupportedUserException();
}
/**
* Whether this provider supports the given user class.
*
* @param string $class
*/
public function supportsClass($class): bool
{
return $class === OauthUser::class;
}
private function getUserByClientID(string $provider, string $clientID)
{
$providerRow = $this->selectSQL('users_provider_ids', [
'provider' => $provider,
'client_id' => $clientID,
])->fetch();
if ($providerRow) {
$user = \User::createFromId($providerRow['id_user']);
}
return $user ?? false;
}
/**
* Loads the user by a given UserResponseInterface object.
*
* @return UserInterface
*
* @throws UserNotFoundException if the user is not found
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$email = $response->getEmail();
$resourceOwner = $response->getResourceOwner();
$provider = $resourceOwner->getName();
$clientID = $response->getUsername();
$user = $this->getUserByClientID($provider, $clientID ?? '');
$token = $this->tokenStorage->getToken();
if ($user && !$user->isActive()) {
throw new UserNotFoundException();
}
if ($user && $token?->getUser()) {
if ($this->session->get('bind_external_account') === $provider) {
// external account is already linked so refuse bind
$this->session->remove('bind_external_account');
throw new OauthBindRefusedException('Account already binded.');
} else {
// unintended bind (eg: when user is already logged in, but goes to /connect/google without proper session)
throw new ShouldLogOutException('Unintended bind.');
}
}
// bind account to current user
// to restrict binding for non logged users add condition: $token && $token->isAuthenticated()
if (!$user) {
$tokenUsername = $token?->getUser() ? $token->getUserIdentifier() : null;
// unintended bind check
if ($tokenUsername && $this->session->get('bind_external_account') !== $provider) {
throw new OauthBindRefusedException('Unintended bind.');
}
$this->session->remove('bind_external_account');
$user = \User::createFromLogin($tokenUsername ?? $email);
if ($user) {
try {
$user = sqlGetConnection()->transactional(function () use ($user, $provider, $clientID, $email) {
$this->insertSQL(
'users_provider_ids',
[
'id_user' => $user->id,
'provider' => $provider,
'client_id' => $clientID,
'email' => $email,
]
);
return $this->getUserByClientID($provider, $clientID);
});
if ($token?->getUser()) {
if ($user) {
addUserMessage(
sprintf(
translate('oauth', 'login')['bind_success'],
ucfirst($provider),
$email
),
'success'
);
if ($resourceOwner instanceof ICustomResourceOwner) {
$resourceOwner->updateUserData($user, $response);
}
throw new OauthBindSuccessException();
} else {
throw new OauthBindFailedException('User not found.');
}
}
} catch (UniqueConstraintViolationException $e) {
throw new OauthBindFailedException('OAuth already binded.');
}
}
}
// add new user
if (!$user && !empty($email) && !empty($response->getFirstName()) && !empty($response->getLastName())
&& !empty($clientID)
) {
try {
$user = sqlGetConnection()->transactional(function () use ($email, $response, $provider, $clientID) {
$insert = [
'email' => $email,
'name' => $response->getFirstName(),
'surname' => $response->getLastName(),
'date_reg' => date('Y-m-d H:i:s'),
];
if (findModule(\Modules::CURRENCIES)) {
$currencyContext = Contexts::get(CurrencyContext::class);
$insert['currency'] = $currencyContext->getActiveId();
}
if (findModule(\Modules::TRANSLATIONS)) {
$languageContext = Contexts::get(LanguageContext::class);
$insert['id_language'] = $languageContext->getActiveId();
}
try {
$this->insertSQL('users', $insert);
$userId = sqlInsertId();
} catch (UniqueConstraintViolationException) {
if (!($userId = $this->checkNewsletterUser($insert['email'], $insert))) {
// pokud uzivatel existuje a je neaktivni z jineho duvodu, nez newsletter, tak ho nedovolim sparovat/prihlasit
throw new UserNotFoundException();
}
}
$this->insertSQL(
'users_provider_ids',
[
'id_user' => $userId,
'provider' => $provider,
'client_id' => $clientID,
'email' => $email,
]
);
$this->eventDispatcher->dispatch(
new UserRegisteredEvent(\User::createFromId($userId), $response->getResourceOwner()->getName())
);
return $this->getUserByClientID($provider, $clientID);
});
// redirect to user account
addUserMessage(translate('oauth', 'login')['registration_success'], 'success');
$this->requestStack->getMainRequest()->attributes->set('gtm_registration', [
'email' => $email ?? 'no-email',
'firstname' => $response->getFirstName() ?? 'no-firstname',
]);
// force redirection to user settings
$this->session->set('_security.main.target_path', path('settings'));
} catch (UniqueConstraintViolationException $e) {
}
}
// user was not found by clientID, but we already have his email, so we can't register him
// LoginView catches this error and displays translatable message:
// translate('oauth', 'login')['unknown_clientid_with_known_email']
if (!$user) {
throw new ClientIDNotFoundException($email ?? '');
}
if ($resourceOwner instanceof ICustomResourceOwner) {
$resourceOwner->updateUserData($user, $response);
}
if ($this->session->get('oauth_register_redirect')) {
$this->session->set('_security.main.target_path', $this->session->get('oauth_register_redirect'));
$this->session->remove('oauth_register_redirect');
}
// user found or successfully created
return new OauthUser($user->id, $user->email, $provider, $clientID, ['ROLE_USER'], $user);
}
private function checkNewsletterUser(string $email, array $update = []): ?int
{
$user = sqlQueryBuilder()
->select('id, figure, passw, get_news, date_subscribe, date_unsubscribe')
->from('users')
->where(Operator::equals(['email' => $email]))
->execute()->fetchAssociative();
if (!$user) {
return null;
}
// pokud je to neaktivni uzivatel, kterej nejspis vznikl pres newsletter, tak ho rovnou zaktivuju at to prihlaseni pres socky vubec funguje
if ($user['figure'] === 'N' && empty($user['passw']) && ($user['get_news'] === 'Y' || !empty($user['date_subscribe']) || !empty($user['date_unsubscribe']))) {
sqlQueryBuilder()
->update('users')
->directValues(array_merge($update, ['figure' => 'Y']))
->where(Operator::equals(['id' => $user['id']]))
->execute();
} else {
// pokud to neni newsletter uzivatel, tak vracim null
return null;
}
return $user['id'];
}
/**
* @required
*/
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->eventDispatcher = $eventDispatcher;
}
}