first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

798
class/class.User.php Normal file
View File

@@ -0,0 +1,798 @@
<?php
use KupShop\CatalogBundle\Util\FavoriteProductsUtil;
use KupShop\ContentBundle\View\Exception\ValidationException;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Util\CartFactory;
use KupShop\UserBundle;
use KupShop\UserBundle\Exception\PhoneValidationException;
use KupShop\UserBundle\Security\LegacyPasswordEncoder;
use KupShop\UserBundle\Security\UserProvider;
use KupShop\UserBundle\Util\PhoneNumberValidator;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
#[AllowDynamicProperties]
class UserBase implements ArrayAccess
{
use DatabaseCommunication;
/**
* User ID.
*
* @var int
*/
public $id = -1;
/** @var string */
public $id_language;
public $email;
public $passw;
public $invoice = [];
public $delivery = [];
public $gender;
public $transport;
public $groups;
protected static $fields = ['name', 'surname', 'firm', 'street', 'city', 'zip', 'country', 'custom_address', 'state', 'phone'];
private $userPriceLevel;
private $idUserPriceLevel;
public $custom_data;
public $currency;
public $figure;
public $id_pricelist;
public $types;
public static function createFromSpec($spec)
{
// Nemůže se dát QueryHint na master. Používá se při každém requestu (UserContext).
$qb = sqlQueryBuilder()->select('u.*')->from('users', 'u')
->where($spec);
if (findModule(Modules::PRICE_LEVELS)) {
$qb->addSelect('upl.id_price_level as idUserPriceLevel')
->leftJoin('u', 'users_dealer_price_level', 'upl', 'u.id = upl.id_user');
}
if (findModule(Modules::USERS_GROUPS_TYPES)) {
$qb->addSelect('GROUP_CONCAT(distinct ug.types) as types')
->leftJoin('u', 'users_groups_relations', 'ugr', 'ugr.id_user = u.id')
->leftJoin('ugr', 'users_groups', 'ug', 'ug.id = ugr.id_group')
->groupBy('u.id');
}
$SQL = $qb->execute();
$data = sqlFetchAssoc($SQL);
if (empty($data)) {
return null;
}
$ret = new User();
$ret->loadData($data);
return $ret;
}
public static function getFields()
{
return self::$fields;
}
/**
* @deprecated Use user context
*/
public static function getCurrentUser()
{
return Contexts::get(UserContext::class)->getActive();
}
/**
* @deprecated Use user context
*/
public static function getCurrentUserId()
{
return Contexts::get(UserContext::class)->getActiveId();
}
public static function createFromId($id)
{
return static::createFromSpec(Operator::equals(['u.id' => $id]));
}
public static function createFromLogin($login)
{
return static::createFromSpec(
Operator::andX(
Operator::equals(['email' => $login]),
Operator::equals(['figure' => 'Y']))
);
}
public static function createFromUserKey($userKey)
{
return static::createFromSpec(Operator::equals(['user_key' => $userKey, 'figure' => 'Y']));
}
public static function createFromFeedToken($feed_token)
{
return static::createFromSpec(Operator::equals(['feed_token' => $feed_token, 'figure' => 'Y']));
}
/** Login current user and notify cart.
*/
public function login($userKey = null, $skipSymfonyLogin = false)
{
// Update user key
if (empty($this->user_key)) {
sqlQuery('UPDATE users SET user_key=REPLACE(UUID(), "-", ""), date_logged = NOW() WHERE id=:id', ['id' => $this->id]);
} else {
sqlQuery('UPDATE users SET date_logged = NOW() WHERE id=:id', ['id' => $this->id]);
}
$this->activateUser();
// Notify cart
$cart = ServiceContainer::getService(CartFactory::class)->create();
$cart->userLoggedIn($this->id);
// Notify favorite products
$favoriteProductsUtil = ServiceContainer::getService(FavoriteProductsUtil::class);
$favoriteProductsUtil->userLoggedIn((int) $this->id, FavoriteProductsUtil::getCookieProducts());
if (!$skipSymfonyLogin && !isFunctionalTests()) {
$this->loginToSymfony();
}
}
protected function loginToSymfony()
{
$userProvider = ServiceContainer::getService(UserProvider::class);
$user = $userProvider->loadUserByUsername($this->email);
$security = ServiceContainer::getService(\Symfony\Bundle\SecurityBundle\Security::class);
$security->login($user, 'form_login', 'main');
}
public function loadData($data)
{
foreach ($data as $name => $value) {
$this->$name = $value;
}
if (!empty($data['custom_data'])) {
$this->custom_data = json_decode($data['custom_data'], true);
}
if (!empty($data['types'])) {
$config = Config::get();
$this->types = explode(',', $data['types']);
$this->types = array_combine($this->types, array_filter($config['Modules'][\Modules::USERS_GROUPS_TYPES], function ($value, $key) {
return in_array($key, $this->types);
}, ARRAY_FILTER_USE_BOTH));
}
$this->types = $this->types ?: [];
if ($this->isActive()) {
$this->types = array_merge($this->types, ['logged' => ['name' => translate('LoggedUser', 'settings', false, true)]]);
}
}
public function getChangePasswordHash(?DateTime $date = null)
{
if (!$date) {
$date = new DateTime();
}
return md5($this->email.$this->passw.$date->format('Y-m-d'));
}
public function updatePassword($plaintext)
{
$encoder = ServiceContainer::getService(LegacyPasswordEncoder::class);
$this->passw = $encoder->encodePassword($plaintext, '');
sqlQueryBuilder()
->update('users')
->directValues(
[
'passw' => $this->passw,
'date_updated' => (new DateTime())->format('Y-m-d H:i:s'),
]
)
->where(Operator::equals(['id' => $this->id]))
->execute();
}
public function fetchAddresses(?array $user = null)
{
if ($user === null) {
$user = sqlQueryBuilder()
->select('*')
->from('users')
->where(Operator::equals(['id' => $this->id]))
->execute()->fetchAssociative();
}
if (!$user) {
return false;
}
// invoice
foreach (self::getFields() as $field) {
$this->invoice[$field] = $user[$field];
}
$this->invoice['email'] = $user['email'];
$this->invoice['ico'] = $user['ico'];
$this->invoice['dic'] = $user['dic'];
$this->invoice['copy_email'] = $user['copy_email'];
if (empty($this->gender)) {
$this->gender = $user['gender'] ?? null;
}
if (empty($this->transport)) {
$this->transport = $user['prefer_transport'];
}
$this->invoice['phone'] = '';
if ($user['phone'] != '') {
$this->invoice['phone'] = $user['phone'];
}
if ($user['mobile'] != '') {
$this->invoice['mobile'] = $user['mobile'];
}
// delivery
foreach (self::getFields() as $field) {
$this->delivery[$field] = $user['delivery_'.$field];
}
if (!empty($user['custom_data'])) {
$this->custom_data = json_decode($user['custom_data'], true);
}
return true;
}
public function updateAddresses($invoice, $delivery, $in_person = false)
{
$this->sanitizeAddress($invoice);
$this->invoice = array_merge($this->invoice, $invoice);
if (!is_null($delivery)) {
$this->sanitizeAddress($delivery);
$this->delivery = array_merge($this->delivery, $delivery);
}
$errors = array_keys($this->checkAddresses($in_person));
if (empty($errors)) {
return null;
}
return reset($errors);
}
public function checkAddresses($in_person = false): array
{
$errors = [];
if (empty($this->invoice['email'])) {
$errors[10] = true;
}
if (empty($this->invoice['name']) || empty($this->invoice['surname'])) {
$errors[6] = true;
}
$this->invoice['phone'] = trim(strtr($this->invoice['phone'], [' ' => '']));
if (findModule(\Modules::USERS, \Modules::SUB_USERS_PHONE_LOGIN)) {
$validator = ServiceContainer::getService(PhoneNumberValidator::class);
try {
$validated = $validator->validate($this->invoice['phone']);
} catch (PhoneValidationException $e) {
throw new ValidationException(translate('error', 'user')['invalid_phone']);
}
$this->invoice['phone'] = $validated->dbNumber;
}
if (empty($this->invoice['phone'])) {
$errors[11] = true;
}
$country = getVal('country', $this->invoice);
$isCZorSK = $country == 'SK' || $country == 'CZ';
if ($isCZorSK) {
if ($phone = $this->checkPhoneNumber($this->invoice['phone'], $country)) {
$this->invoice['phone'] = $phone;
} else {
$errors[14] = true;
}
} else {
$phone = preg_replace('/[^0-9+]/', '', $this->invoice['phone']);
$this->invoice['phone'] = $phone;
}
$d_country = getVal('country', $this->delivery);
$d_isCZorSK = $d_country == 'SK' || $d_country == 'CZ';
if (!empty($this->delivery['phone'])) {
$this->delivery['phone'] = trim(strtr($this->delivery['phone'], [' ' => '']));
if ($d_isCZorSK) {
if ($phone = $this->checkPhoneNumber($this->delivery['phone'], $d_country)) {
$this->delivery['phone'] = $phone;
} else {
$errors[14] = true;
}
} else {
$phone = preg_replace('/[^0-9+]/', '', $this->delivery['phone']);
$this->delivery['phone'] = $phone;
}
}
if (!$in_person) {
if (empty($this->invoice['zip'])) {
$errors[9] = true;
}
if (empty($this->invoice['city'])) {
$errors[8] = true;
}
if (empty($this->invoice['street'])) {
$errors[7] = true;
}
if ($isCZorSK) {
if (!empty($this->invoice['zip']) && !preg_match('/^[0-9]{5}$/', $this->invoice['zip'])) {
$errors[13] = true;
}
}
if ($d_isCZorSK) {
if (!empty($this->delivery['zip']) && !preg_match('/^[0-9]{5}$/', $this->delivery['zip'])) {
$errors[13] = true;
}
}
if (empty($this->invoice['country'])) {
$errors[17] = true;
}
}
return $errors;
}
public function checkPhoneNumber($phone = '', $country = 'CZ')
{
if (preg_match('/^((?:\+|00)[0-9]{3}|0)?(\d{6,9})$/', trim(strtr($phone, [' ' => '', '/' => ''])), $matches)) {
if (empty($matches[1])) {
if ($country == 'SK') {
$matches[1] = '+421';
} else {
$matches[1] = '+420';
}
} elseif (substr($matches[1], 0, 2) == '00') {
$matches[1] = '+'.substr($matches[1], 2);
} elseif ($matches[1] == '0') {
// Slovaci maji takhle uchylne nulu na zacatku
$matches[1] = '+421';
} elseif (substr($matches[1], 0, 1) != '+') {
$matches[1] = '+'.$matches[1];
}
$phone = $matches[1].$matches[2];
return $phone;
} else {
return false;
}
}
public function sanitizeRegistration($password = null)
{
$error = false;
$qb = sqlQueryBuilder()->select('COUNT(id)')
->from('users');
$where = Operator::equals(['email' => $this->invoice['email']]);
if (findModule(\Modules::USERS, \Modules::SUB_USERS_PHONE_LOGIN)) {
$where = Operator::orX(
Operator::equals(['email' => $this->invoice['email']]),
Operator::equals(['phone' => $this->invoice['phone']])
);
}
$qb->where($where);
if (!is_null($password)) {
$qb->andWhere(Operator::equals(['figure' => 'Y']));
}
if ($this->id > 0) {
$qb->andWhere(Operator::not(Operator::equals(['id' => $this->id])));
}
$no = $qb->execute()->fetchOne();
if ($no > 0) {
$error = 15;
} else {
if ($this->id) {
if (strlen($password) > 0 && $this->sanitizePassword($password)) {
$error = 16;
} elseif ($this->sanitizePassword($password)) {
$error = 16;
}
}
}
return $error;
}
public function sanitizePassword($password)
{
$error = false;
if (strlen($password) < 6) {
$error = 16;
}
return $error;
}
protected function sanitizeAddress(&$address)
{
// kontrola poslanych dat z formulare
foreach (self::getFields() as $field) {
$address[$field] = StringUtil::unicode_trim(getVal($field, $address));
}
if (($address['country'] == 'CZ') || ($address['country'] == 'SK')) {
$address['zip'] = preg_replace('/[^0-9]/', '', $address['zip']);
}
$address['ico'] = StringUtil::unicode_trim(getVal('ico', $address));
$address['dic'] = StringUtil::unicode_trim(getVal('dic', $address));
$address['phone'] = StringUtil::unicode_trim(getVal('phone', $address));
$address['email'] = StringUtil::unicode_trim(getVal('email', $address));
$address['copy_email'] = StringUtil::unicode_trim(getVal('copy_email', $address));
if (findModule('currencies') && empty($address['currency'])) {
$address['currency'] = Contexts::get(CurrencyContext::class)->getActiveId();
}
}
// used as update too
public function update()
{
$unreg_user_id = null;
$SQL = sqlQuery('SELECT id FROM '.getTableName('users').' WHERE email = :email AND figure=\'N\'', ['email' => $this->invoice['email']]);
if (sqlNumRows($SQL) == 1) {
$unreg_user_id = sqlFetchArray($SQL)['id'];
}
$fields = [];
// invoice
foreach (self::getFields() as $field) {
$fields[$field] = $this->invoice[$field] ?? '';
}
// delivery
foreach (self::getFields() as $field) {
$fields['delivery_'.$field] = $this->delivery[$field] ?? '';
}
$fields['ico'] = $this->invoice['ico'] ?? '';
$fields['dic'] = $this->invoice['dic'] ?? '';
$fields['phone'] = $this->invoice['phone'] ?? '';
$fields['email'] = $this->invoice['email'] ?? '';
$fields['copy_email'] = $this->invoice['copy_email'] ?? '';
if (isset($this->invoice['gender'])) {
$fields['gender'] = $this->invoice['gender'];
}
if (isset($this->invoice['birthdate'])) {
$fields['birthdate'] = $this->invoice['birthdate'];
}
if (isset($this->invoice['transport'])) {
$fields['prefer_transport'] = $this->invoice['transport'] ?: null;
}
if (findModule('currencies') && !empty($this->invoice['currency'])) {
$fields['currency'] = $this->invoice['currency'];
$this->currency = $this->invoice['currency'];
}
if (!empty($this->custom_data)) {
$fields['custom_data'] = json_encode($this->custom_data);
}
if (empty($this->email)) {
$this->email = $fields['email'] ?? '';
}
$userContext = Contexts::get(UserContext::class);
if ($userContext->getActiveId() > 0) {
$fields['date_updated'] = date('Y-m-d H:i');
$this->id = $userContext->getActiveId();
$this->updateSQL('users', $fields, ['id' => $this->id]);
return $this->id;
} else {
if (!isset($this->id_language)) {
$languageContext = Contexts::get(LanguageContext::class);
$this->id_language = $languageContext->getActiveId();
}
$fields['id_language'] = $this->id_language;
$fields['date_reg'] = date('Y-m-d H:i');
if ($unreg_user_id) {
$fields['figure'] = 'Y';
$this->id = $unreg_user_id;
$this->updateSQL('users', $fields, ['id' => $this->id]);
// dispatch registered event
$this->sendUserRegisteredEvent(true);
return $this->id;
} else {
if ($this->insertSQL('users', $fields)) {
$this->id = sqlInsertId();
// dispatch registered event
$this->sendUserRegisteredEvent(true);
return $this->id;
}
}
}
return false;
}
public function getGreeting($prioritize = 'name')
{
if (empty($this->invoice['name'])) {
$this->fetchAddresses();
}
return Greeting::getGreeting($this->invoice['name'], $this->invoice['surname'], $prioritize);
}
public function getPriceLevel()
{
if ($this->userPriceLevel === null) {
// save `false` on null, so it is cached and not loaded again
$this->userPriceLevel = static::getUserPriceLevel($this->id) ?? false;
}
return $this->userPriceLevel ?: null;
}
public function getUserKey()
{
return $this->user_key;
}
public static function getUserPriceLevel($id)
{
$price_level = null;
if (!empty($id)) {
$pl = sqlQuery('SELECT * FROM users_dealer_price_level WHERE id_user=:id_user', ['id_user' => $id]);
if (sqlNumRows($pl) == 1) {
$pl = sqlFetchArray($pl);
$price_level = ServiceContainer::getService(\KupShop\CatalogBundle\PriceLevel::class);
$price_level = $price_level->createFromDB($pl['id_price_level']);
}
}
return $price_level;
}
public function getUserPriceLevelId()
{
return $this->idUserPriceLevel;
}
public function getUserCurrency()
{
if (isset($this->currency)) {
return $this->currency;
}
return null;
}
public function getGroups()
{
if (is_null($this->groups)) {
$this->groups = sqlFetchAll(sqlQueryBuilder()->select('ug.*')
->from('users_groups_relations', 'ugr')
->join('ugr', 'users_groups', 'ug', 'ug.id=ugr.id_group')
->where(Operator::equals(['ugr.id_user' => $this->id]))
->orderBy('ug.id'), 'id');
foreach ($this->groups as &$group) {
$group['data'] = json_decode($group['data'] ?: '', true) ?: [];
}
}
return $this->groups;
}
public function getShoppingLists(): ?array
{
if (!findModule(Modules::SHOPPING_LIST)) {
return null;
}
if (!isset($this->shoppingLists)) {
$this->shoppingLists = sqlQueryBuilder()
->select('sl.*, count(p.id) as pocet')
->from('shopping_list', 'sl')
->leftJoin('sl', 'shopping_list_products', 'psl', 'psl.id_shopping_list = sl.id')
->leftJoin('psl', 'products', 'p', 'psl.id_product = p.id AND p.figure = "Y"')
->andWhere(Operator::equals(['sl.id_user' => $this->id]))
->groupBy('sl.id')
->orderBy('label = "favorites"', 'DESC')
->addOrderBy('sl.name, sl.id', 'ASC')
->execute()->fetchAllAssociative();
}
return $this->shoppingLists;
}
public function isType(string $type): bool
{
return !empty($this->types[$type]);
}
public function getType(string $type): ?array
{
return $this->types[$type];
}
public function hasGroupId($groupId)
{
return isset($this->getGroups()[$groupId]);
}
public function isActive(): bool
{
return $this->figure === 'Y';
}
public function isRegistered(): bool
{
return $this->isActive();
}
/**
* Activate not logged user.
*
* @return bool
*/
public function activateUser()
{
if ($this->id > 0) {
Contexts::get(UserContext::class)->activate($this->id);
if (findModule(Modules::PRICE_LEVELS) && $this->idUserPriceLevel) {
$priceLevelContext = ServiceContainer::getService(\KupShop\KupShopBundle\Context\PriceLevelContext::class);
$priceLevelContext->activate($this->idUserPriceLevel);
}
return true;
}
return false;
}
public function getAllOrdersPrice()
{
return sqlQueryBuilder()->select('SUM(total_price)')
->from('orders')
->where('id_user=:id_user')
->andWhere('status_storno!=1')
->setParameter('id_user', $this->id)
->execute()->fetchColumn();
}
/**
* Implements ArrayAccess interface.
*/
public function offsetSet($offset, $value): void
{
$this->{$offset} = $value;
}
public function offsetExists($offset): bool
{
return isset($this->{$offset});
}
public function offsetUnset($offset): void
{
unset($this->{$offset});
}
public function offsetGet($offset): mixed
{
return isset($this->{$offset}) ? $this->{$offset} : null;
}
public function setCustomData($key, $value)
{
$customData = $this->getCustomData(true);
$customData[$key] = $value;
$this->setCustomDataAll($customData);
}
public function getCustomData(bool $forceLoad = false)
{
if ($forceLoad && $this->id > 0) {
$data = sqlQueryBuilder()->select('custom_data')
->from('users')
->where(Operator::equals(['id' => $this->id]))
->sendToMaster() // Může volat setCustomData kdokoli, tak když by nešlo na master, ztrácí se data :-(
->execute()->fetchOne();
$this->custom_data = json_decode($data, true);
}
return $this->custom_data;
}
private function setCustomDataAll($customData)
{
$this->custom_data = $customData;
if ($this->id > 0) {
$encoded = json_encode($customData);
$this->updateSQL('users', ['custom_data' => $encoded], ['id' => $this->id]);
}
}
private function sendUserRegisteredEvent(bool $refreshUser = false): void
{
$user = $this;
if ($refreshUser) {
$user = QueryHint::withRouteToMaster(fn () => static::createFromId($this->id));
}
$this->getEventDispatcher()->dispatch(
new UserBundle\Event\UserRegisteredEvent($user)
);
}
private function getEventDispatcher(): EventDispatcherInterface
{
return ServiceContainer::getService('event_dispatcher');
}
}
if (empty($subclass)) {
class User extends UserBase
{
}
}