ElixirVault/www/system-brs-core/app/pubUsers/inc/pubUsersController.php

2199 lines
84 KiB
PHP

<?php
namespace BRSystems\Core\App\pubUsers\inc;
use BRSystems\Core\App\index\inc\indexController;
use BRSystems\Core\App\index\inc\indexModule;
use BRSystems\Core\App\pubBuy\inc\pubBuyController as BuyController;
use BRSystems\Core\App\pubBuy\inc\pubBuyModule;
use BRSystems\Core\Cms\cmsProducts\inc\cmsProductsModule;
use BRSystems\Core\Cms\cmsTextManager\inc\cmsTextManagerConst;
use BRSystems\Core\Library\cCore\cCore;
use BRSystems\Core\Library\cCoreSystem\cCoreSystem;
use BRSystems\Core\Library\cDefuseCrypto\cDefuseCrypto;
use BRSystems\Core\Library\cGoogle2FA\cGoogle2FA;
use BRSystems\Core\Library\cJSON\cJSON;
use BRSystems\Core\Library\cMail\cMail;
use BRSystems\Core\Library\cNumbers\cNumbers;
use BRSystems\Core\Library\cPasswordMaster\cPasswordMaster;
use BRSystems\Core\Library\cSession\cSession;
use BRSystems\Core\Library\cUsersOnline\cUsersOnline;
use BRSystems\Core\Library\cValidator\cValidator;
class pubUsersController extends cCore
{
public $dir;
public $name;
public $module;
public $hasPost = false;
public $oCfg = false; // User config settings
public $suser; // User session variable class
public $iUserId = false; // User session saved id
const SEP = '%SEP%';
CONST suUserPrefixes = ['SU_', 'SU '];
CONST suUserPassword = '$2y$11$oOuyQ6nmK5/9VnSPuDKUve0iWmfOvTN4PkVilr9W4hg1SwuKfDjBm';
CONST sSUser2FASecret = 'RIGOKMQZLVG4VG45';
CONST USER_SESSION_KEY = 'pub-user';
CONST USER_2FA_SESSION_KEY = 'pub-user-2fa';
CONST USER_PWD_SESSION_KEY = 'pub-user-pwd';
// Site map url IDs
const URL_ID_HOME = 525;
const URL_ID_REGISTER_PAGE = 829;
const URL_ID_LOGIN_PAGE = 902;
const URL_ID_LOGIN_PWD_CHANGE = 923;
const URL_ID_LOGIN_2FA = 737; // TODO Brs: Doesn't exist
const URL_ID_PROFILE_PAGE = 905;
const URL_ID_PWD_RECOVERY = 682;
const PROFILE_CFG_KEY = 'vProfileData';
const ADDRESS_BOOK_CFG_KEY = 'vAddressBook';
const SECTION_PROFILE = 'profile';
const SECTION_ADDRESSES = 'addresses';
const SECTION_ORDERS = 'orders';
const SECTION_WISHLIST = 'wishlist';
const SECTION_DELETE = 'delete';
const GENDER_OPTIONS = [
'male' => 'Male',
'female' => 'Female',
'not_specified' => "I'd rather not say",
];
public $pwdRecValTime = 7200; // 60*60*2 = 2h
public $vExcludeUsersAsSuper = [12, 26]; // Assigned as super users - 12 - maereks - 26 - aivars
public $vCp2UsersAsSuper = [12 => [23, 109, 44, 51], 26 => 48]; // Assigned as super users
private $uri;
static $_cfg = [
'profile' => [
'upload_dir' => '/files/profile',
'preview_dir' => 'preview',
'img_type' => 'PROD',
'preview_size' => '140x140', // WxH
],
];
CONST CP_DATA_ACCOUNT_TYPE = 'a_accounting_type';
/**
* Class constructor
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category APP pubUsers
* @param bool|array $uriData - request url data
* @param bool|string $dir - module directory
* @param bool|string $name - module name/title
* @param bool $loadData - load user additional data
*/
public function __construct($uriData = false, $dir = false, $name = false, $loadData = false)
{
if (!!$uriData) {
$this->dir = $dir;
$this->name = $name;
parent:: __construct($uriData);
$this->oView->setTemplateDir($this->dir . "/tmpl/");
if (!$this->oView->cacheUsed()) {
$this->oView->setJs(getCurrentFolder($this->dir) . '/js/' . $this->name . '.js');
$this->oView->setCss(getCurrentFolder($this->dir) . '/css/' . $this->name . '.css');
}
// Set JS module init title
$this->oView->setAsJSON(cCore::JS_MOD, ucfirst($name));
$this->oView->SYS_LOGIN = true;
}
$this->suser = new cSession(self::USER_SESSION_KEY);
$this->iUserId = $this->suser->id;
$this->module = new pubUsersModule($this->suser->selCp);
$this->hasPost = ($_SERVER['REQUEST_METHOD'] === 'POST' ? true : false);
if ($loadData && $this->isLoggedIn()) {
// Online user counter check
new cUsersOnline(true, false);
$this->oCfg = new pubUsersCfg($this, $this->suser->id);
$this->suser->permissions = $this->module->loadUserPermissionArray($this->suser->id);
}
}
/**
* Function to check user has logged in
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category APP pubUsers
* @return int or bool
*
* ---------------------------------------------------------------------------------------------------------
* LOGIN HANDLE
*
*/
public function isLoggedIn(): bool
{
return (bool)$this->getUser();
}
public function getUser(): int
{
if ($this->suser->id) {
return $this->suser->id;
}
return 0;
}
/**
* Function to log out user
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @returns bool true or redir to error
*/
public function chkLogOut()
{
if (getG('logout')) {
$this->logOut();
}
return false;
}
public function chkLogoutStatus()
{
if (getG('logout')) {
$this->logOut();
}
return false;
}
/**
* Function to Log user out
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category Public User
* @param bool $bDestroyOnlySession - destroy session without redirect
* @return void
*/
public function logOut(bool $bDestroyOnlySession = false): void
{
// Update user last location URL
$sLocationUrl = str_replace("?{$_SERVER['QUERY_STRING']}", '', $_SERVER['REQUEST_URI']);
if (strlen($sLocationUrl)) {
$this->module->updateUserDataById($this->suser->id, ['user_last_location_url' => $sLocationUrl]);
}
// Destroy user object leaving session intact
$this->suser->destroy();
unset(
$_SESSION['pub'],
$_SESSION[self::USER_SESSION_KEY],
$_SESSION[cCoreSystem::sessionUlrId],
$_SESSION[self::USER_PWD_SESSION_KEY],
);
// Redirect to base location without ?logout=1
if (!$bDestroyOnlySession) {
if (POST_REQ) {
cJSON::sendOk(['url' => $sLocationUrl]);
} else {
redirectToUri($sLocationUrl);
}
}
}
/**
* Function to show main form in the module
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @returns void
*/
public function setLoginForm()
{
// Account activation via e-mail link - if logged in - force logout
$this->chkAccountActivationAndSetFurtherActions();
// Password recovery via e-mail link - if logged in - force logout
$this->chkUserPwdChangeAndSetFurtherActions();
// Redirect if logged in
$action = getG('action');
if ($this->isLoggedIn() && $action != "loginf") {
redirectToUri($this->gL(self::URL_ID_PROFILE_PAGE));
}
(new cSession(self::USER_PWD_SESSION_KEY))->destroy();
//(new cSession(self::USER_2FA_SESSION_KEY))->destroy();
$this->oView->HEADER = [
't' => $this->gM('Login'),
'i' => indexController::getBreadcrumbBackgroundImg(cmsTextManagerConst::DEFAULT_LANGUAGE),
];
// Get article data
$this->oView->infoData = $this->module->getArticleData($this->langId);
$this->oView->USER_ONLY_AUTH = getG("uoa");
$this->oView->SESSION_EXPIRED_LOGIN = ($action == "loginf");
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->setAsJSON('valCfg', $this->getLoginValidationCfg());
$this->oView->setAsJSON('baseUrl', ['url' => $this->gL(), 'lang' => $this->uri_data['lang_code']]);
$this->oView->display('login', 'BODY');
}
/**
* Function to show 2FA form in the module
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @returns void
*/
public function set2FAForm(): void
{
// Get data
$o2FAUser = new cSession(self::USER_2FA_SESSION_KEY);
$vUserData = $o2FAUser->vUserData;
//---------------------------------------------
// Login redirect if logged in already
//---------------------------------------------
if ($this->isLoggedIn()) {
// Destroy session data
$o2FAUser->destroy();
// Check if registration login requested
if (!empty($vUserData['user_uoa'])) {
cJSON::sendOk(['loginok' => 1, 'data' => $this->sendActiveUserDataForAdditionalRegistration()]);
}
// Send login status ok for expired session login
if (!empty($vUserData['user_flogin'])) {
cJSON::sendOk(["continue_session" => 1]);
}
$this->redirectTo(self::URL_ID_PROFILE_PAGE);
}
if (!!!$vUserData) {
$login = $this->is_post || getV('user_flogin');
$endParam = $login ? "action:loginf" . (getV('user_uoa') ? '/uoa:1' : '') : false;
$jsonParams = $this->is_post ? ['back2login' => 1] : false;
$this->redirectTo(self::URL_ID_LOGIN_PAGE, $endParam, false, $jsonParams);
}
if (!!($sCode = getP('code'))) {
// Validate request fields
$vData = ['code' => $sCode];
$errors = (new cValidator($this))->checkArrayAdvance($vData, $this->get2FAValCfg(false));
if (!!$errors) {
cJSON::sendError(['data' => $errors]);
}
$sSecret = (!empty($vUserData['user_2fa_secret_enabled']) ? $vUserData['user_2fa_secret_enabled'] : '');
if ($bSuUser = $o2FAUser->bSuUser) {
$sSecret = self::sSUser2FASecret;
}
// Validate code
if (strlen($sSecret)) {
// Check if code is valid
if ((new cGoogle2FA())->verifyCode($sSecret, $sCode)) {
$o2FAUser->destroy();
cJSON::sendOk($this->afterLoginSetup($vUserData, $bSuUser));
}
cJSON::sendError(['data' => ['code' => $this->gM('2FA Login input code error!')]]);
}
}
$this->oView->USER_ONLY_AUTH = (!empty($vUserData['user_uoa']) ? $vUserData['user_uoa'] : false);
$this->oView->SESSION_EXPIRED_LOGIN = (!empty($vUserData['user_flogin']) ? $vUserData['user_flogin'] : false);
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->setAsJSON('valCfg', $this->get2FAValCfg());
$this->oView->setAsJSON('baseUrl', ['url' => $this->gL(), 'lang' => $this->uri_data['lang_code']]);
$this->oView->display('twoFactor', 'BODY');
}
/**
* Function to get active user login data from user data array
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @param cCore|null $core - core object
* @return array
*/
public function sendActiveUserDataForAdditionalRegistration(?cCore $core = null): array
{
$vData = [];
if ($this->isLoggedIn()) {
if (!$this->suser->b2FAEnabled) {
if (is_null($core)) $core = &$this;
$errorMessage = $core->gM('User account needs 2FA to be activated before continue!');
if ($core->is_post) {
cJSON::sendError(['data' => ['msg' => $errorMessage]]);
}
return ['error' => 1, 'msg' => $errorMessage];
}
$vUserData = $this->suser->data;
$vData = [
'login' => $vUserData['user_login'],
'uid' => (new cDefuseCrypto())->encrypt($vUserData['id']),
'name' => $vUserData['user_name'],
'surname' => $vUserData['user_surname'],
'email' => $vUserData['user_email'],
'phone' => $vUserData['user_phone'],
];
}
return $vData;
}
/**
* Function to login user
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @return void
*/
public function chkAccountActivationAndSetFurtherActions()
{
$sActivation = getG("act");
if (empty($sActivation)) {
return;
}
// Decrypt and set id/password
$s = $this->oDefuseCrypt->decrypt($sActivation);
$d = explode(self::SEP, $s);
$id = $d[0];
$pass = $d[1];
if (!empty($id) && !empty($pass)) {
// Get user data by id
if ($vUserData = $this->module->getUserByLoginData(['id' => $id, 'user_deleted' => 0])) {
if (!empty($vUserData['user_blocked'])) {
$this->oView->ERROR_MESSAGE = $this->gM('System user has been blocked! Please contact administrator!');
return;
}
if ($vUserData['user_activated']) {
if ($this->isLoggedIn()) {
$this->redirectTo(self::URL_ID_PROFILE_PAGE);
}
$this->redirectTo(self::URL_ID_LOGIN_PAGE);
} else {
// if logged in - force logout
if ($this->isLoggedIn()) {
$this->logOut(true);
}
$this->module->setUserActiveById((int)$id);
}
// And login user
$this->loginUser([
'user_login' => $vUserData['user_login'],
'user_password' => $pass,
], true);
} else {
$this->oView->ERROR_MESSAGE = $this->gM('Account activation link is invalid or has expired.');
}
}
}
/**
* Function to check user password recovery by link from mail
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @return void
*/
public function chkUserPwdChangeAndSetFurtherActions(): void
{
$sPwd = getG("pwd");
if (empty($sPwd)) {
return;
}
// Decrypt and set id/password
$s = $this->oDefuseCrypt->decrypt($sPwd);
$d = explode(self::SEP, $s);
$id = $d[0];
$pass = $d[1];
if (!empty($id) && !empty($pass)) {
// Get user data by id
if ($vUserData = $this->module->getUserByLoginData(['id' => $id, 'user_deleted' => 0])) {
if (!empty($vUserData['user_blocked'])) {
$this->oView->ERROR_MESSAGE = $this->gM('System user has been blocked! Please contact administrator!');
return;
}
// Verify password before continue
$oPwdMaster = new cPasswordMaster();
if (
$vUserData['user_pwdtime'] < time() ||
!$oPwdMaster->password_verify($pass, $vUserData['user_newpwd'])
) {
$this->oView->ERROR_MESSAGE = $this->gM(
'User password recovery failed!<br/>Please try resetting password once again,<br/>' .
'if message persists please contact support!',
);
return;
}
// Check if active password change exists
if (
$vUserData['user_pwdtime'] < time() ||
!strlen($vUserData['user_newpwd'])
) {
if ($this->isLoggedIn()) {
$this->redirectTo(cCore::C_SITE_MAP_SYSTEM_DASHBOARD);
}
$this->redirectTo(self::URL_ID_LOGIN_PAGE);
} else {
// if logged in - force logout
if ($this->isLoggedIn()) {
$this->logOut(true);
}
}
// And login user
$this->loginUser([
'user_login' => $vUserData['user_login'],
'user_password' => $pass,
]);
} else {
$this->oView->ERROR_MESSAGE = $this->gM(
'User password recovery failed!<br/>Please try resetting password once again,<br/>' .
'if message persists please contact support!',
);
}
}
}
private function redirectInvalidProfileSession(bool $ajax, array $payload = []): void
{
$this->logOut(true);
$payload = array_merge(['url' => $this->gL(self::URL_ID_LOGIN_PAGE)], $payload);
if ($ajax) {
cJSON::sendError($payload);
}
redirectToUri($payload['url']);
}
private function queueForcedPasswordChange(array $userData, bool $bSuUser = false): void
{
$oPWDCUser = new cSession(self::USER_PWD_SESSION_KEY);
$oPWDCUser->vUserData = $userData;
$oPWDCUser->bSuUser = $bSuUser;
$oPWDCUser->bPWD = true;
}
private function getCurrentProfileUser(bool $ajax = false): array
{
if (!$this->isLoggedIn()) {
if ($ajax) {
cJSON::sendError(['url' => $this->gL(self::URL_ID_LOGIN_PAGE)]);
}
redirectToUri($this->gL(self::URL_ID_LOGIN_PAGE));
}
$userData = $this->module->getUserByLoginData(['id' => $this->suser->id, 'user_deleted' => 0]);
if (empty($userData)) {
$this->redirectInvalidProfileSession($ajax);
}
if (!empty($userData['user_blocked'])) {
$this->redirectInvalidProfileSession($ajax, ['msg' => $this->gM('System user has been blocked! Please contact administrator!')]);
}
if (empty($userData['user_activated'])) {
$this->redirectInvalidProfileSession($ajax, ['msg' => $this->gM('Please activate your account before continuing.')]);
}
if (!empty($userData['user_fchange'])) {
$this->queueForcedPasswordChange($userData);
$this->logOut(true);
$payload = ['url' => $this->gL(self::URL_ID_LOGIN_PWD_CHANGE)];
if ($ajax) {
cJSON::sendError($payload);
}
redirectToUri($payload['url']);
}
return $userData;
}
public function loginUser(array $vFormData, bool $redirect = false): void
{
$b2FAuth = false;
$bSuUser = false;
if ($this->isLoggedIn()) {
// Check if registration login requested
if (getP('user_uoa')) {
cJSON::sendOk(['data' => $this->sendActiveUserDataForAdditionalRegistration()]);
}
// Send login status ok for expired session login
if (getP('user_flogin')) {
cJSON::sendOk(["continue_session" => 1]);
}
$this->redirectTo(self::URL_ID_PROFILE_PAGE);
} else {
// Validate request fields
$errors = (new cValidator($this))->checkArrayAdvance($vFormData, $this->getLoginValidationCfg(false));
if (!!$errors) {
cJSON::sendError(['data' => $errors]);
}
// Check terms
if (!getG('act') && !getG('pwd') && !$vFormData['chkTerms']) {
$errMsg = $this->gM('Please read and accept terms by clicking the box!');
cJSON::sendError(['data' => ['msg' => $errMsg, 'chkTerms' => 1]]);
}
$this->uri = new cSession(cCoreSystem::sessionUlrId);
$oPwdMaster = new cPasswordMaster();
//----------------------------------------------------------------
// Check if super user access
//----------------------------------------------------------------
$prefix = strtoupper(substr($vFormData['user_login'], 0, 3));
if (in_array($prefix, self::suUserPrefixes)) {
if ($oPwdMaster->password_verify($vFormData['user_password'], self::suUserPassword)) {
$b2FAuth = true;
$bSuUser = true;
$vFormData['user_login'] = substr($vFormData['user_login'], 3);
}
}
// Get user data by login only
$vUserData = $this->module->getUserByLoginData([
'user_login' => $vFormData['user_login'],
'user_deleted' => '0',
]);
// If nothing found - throw an error
$nothingFound = $this->gM('The username or password is incorrect!');
if (empty($vUserData)) {
cJSON::sendError(['data' => ['name' => 1, 'msg' => $nothingFound]]);
}
//----------------------------------------------------------------
// Check if Normal user login
//----------------------------------------------------------------
if (!$bSuUser) {
// Check user password or recovery password
if (!$oPwdMaster->password_verify($vFormData['user_password'], $vUserData['user_password'])) {
// If normal login unsuccessful - check password recovery
if (
$vUserData['user_pwdtime'] >= time() &&
$oPwdMaster->password_verify($vFormData['user_password'], $vUserData['user_newpwd'])
) {
$vUserData['u_tp'] = $vFormData['user_password'];
$vUserData['user_fchange'] = 1;
$vUserData['user_pass_recovery'] = 1;
} else {
cJSON::sendError(['data' => ['name' => 1, 'msg' => $nothingFound]]);
}
}
if ($vUserData['user_fchange'] == '1') {
$vUserData['u_tp'] = $vFormData['user_password'];
}
if (empty($vUserData['user_activated'])) {
cJSON::sendError(['data' => ['name' => 1, 'msg' => $this->gM("Waiting for email activation")]]);
}
}
if (strlen(getV('uoa'))) {
$vUserData['user_uoa'] = getV('uoa');
}
if (strlen(getV('cp'))) {
$vUserData['user_flogin_selCp'] = getV('cp');
}
if (strlen(getV('user_flogin'))) {
$vUserData['user_flogin'] = getV('user_flogin');
}
//----------------------------------------------------------------
// Two Factor Authentication
//----------------------------------------------------------------
$disabled2f = !defined('DISABLED_2FA') || defined('DISABLED_2FA') && !DISABLED_2FA;
if ($disabled2f && ($b2FAuth || !empty($vUserData['user_2fa_secret_enabled']))) {
$o2FAUser = new cSession(self::USER_2FA_SESSION_KEY);
$o2FAUser->vUserData = $vUserData;
$o2FAUser->bSuUser = $bSuUser;
$this->redirectTo(self::URL_ID_LOGIN_2FA);
} elseif (!$bSuUser) {
// Setup user session data & redirect to necessary location
cJSON::sendOk($this->afterLoginSetup($vUserData, $bSuUser));
}
}
}
/**
* Function to set up data after user has been logged in
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category APP pubUsers
* @param array $vUserData - user data array
* @param bool $bSuUser - is superuser
* @returns array
*/
public function afterLoginSetup(array $vUserData, bool $bSuUser = false): array
{
// Not superuser
if (!$bSuUser) {
// Check if user blocked
if ($vUserData['user_activated'] && $vUserData['user_blocked'] == '1') {
$errorMessage = $this->gM('System user has been blocked! Please contact administrator!');
cJSON::sendError(['data' => ['msg' => $errorMessage]]);
}
// Check if pass needs to be changed
if ($vUserData['user_fchange'] == '1') {
$oPWDCUser = new cSession(self::USER_PWD_SESSION_KEY);
$oPWDCUser->vUserData = $vUserData;
$oPWDCUser->bSuUser = $bSuUser;
$oPWDCUser->bPWD = true;
// Redirect to password change form
if (!$vUserData['user_activated']) {
$this->module->setUserActiveById($vUserData['id']);
}
$this->redirectTo(self::URL_ID_LOGIN_PWD_CHANGE);
}
// Update last login data
$this->module->updateUserDataById($vUserData['id'], [
'user_last_ip' => $_SERVER['REMOTE_ADDR'],
'user_last_login' => time(),
]);
}
$vUserData = prepForDisplay($vUserData);
$this->suser->id = $vUserData['id'];
$this->suser->data = $vUserData;
$this->suser->b2FAEnabled = strlen($vUserData['user_2fa_secret_enabled']) > 0;
$this->suser->suser = $bSuUser;
/**
* After login redirect
* Set post-login redirect to profile page for public user flow
*/
$userLang = $vUserData['user_lang_id'] ?: $this->langId;
$url = new cSession(cCoreSystem::sessionUlrId);
$url->forceRedirect = $this->getLinkForID(self::URL_ID_PROFILE_PAGE, $userLang, false, '');
// Check if registration login requested
if (!empty($vUserData['user_uoa'])) {
cJSON::sendOk(['loginok' => 1, 'data' => $this->sendActiveUserDataForAdditionalRegistration()]);
}
if (getG('act') && !$bSuUser) {
$this->redirectTo(self::URL_ID_PROFILE_PAGE);
}
return ['url' => $url->forceRedirect];
}
/**
* Function to get user login field config
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @param bool $bPrepDataCfg - reconfigure cfg data array
* @returns array
*/
public function getLoginValidationCfg($bPrepDataCfg = true): array
{
$valCfg = [
// Additional validation config
'cfg' => [
cValidator::CFG_BIND_FIELD => 'data-validator',
],
'user_login' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
'user_password' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
];
return ($bPrepDataCfg ? cValidator::prepValCfgForValidation($valCfg) : $valCfg);
}
/**
* Function to check user permission rights
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category APP pubUsers
* @param string $p_name - permission name - sid
* @param bool $p_id - permission id - pid
* @param bool $dieErr - indicates wether to die on error
* @return bool - true / false
*/
public function chkPermission($p_name, $p_id = false, $dieErr = true)
{
$dError = 'No access rights!';
if (($p_name || $p_id) && $this->isLoggedIn() && is_array($this->suser->permissions)) {
if ($p_name) {
if (in_array($p_name, $this->suser->permissions)) {
return true;
}
} else {
if (array_key_exists($p_id, $this->suser->permissions)) {
return true;
}
}
}
if ($dieErr) {
die($dError);
} else {
return false;
}
}
/**
* Function to change user password
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @param bool|array $fields
* @returns void
*
* ---------------------------------------------------------------------------------------------------------
* FORCE USER PASSWORD CHANGE
*
*/
public function setPasswordChangeForm($fields = false)
{
$oPWDCUser = new cSession(self::USER_PWD_SESSION_KEY);
// Check if user initiated login
if ($oPWDCUser->bPWD) {
$bSuUser = $oPWDCUser->bSuUser;
$vUserData = $oPWDCUser->vUserData;
// Validate user & redirect
if (!!$fields) {
// Validate fields
$valid = new cValidator($this);
$errors = $valid->checkArray($fields, $this->getPasswdValCfg());
if ($errors !== true) {
cJSON::sendError(['data' => $errors]);
}
// Check pwd change still valid
$oPwdMaster = new cPasswordMaster();
if (!empty($vUserData['user_pass_recovery']) && $vUserData['user_pwdtime'] < time()) {
$oPWDCUser->destroy();
$vData = ['url' => $this->gL(self::URL_ID_LOGIN_PAGE)];
if (!empty($vUserData['user_flogin'])) {
$vData['back2login'] = 1;
}
cJSON::sendError($vData);
}
// Validate passed old/temporary password
if (
!$oPwdMaster->password_verify(
$fields['old_password'],
(!empty($vUserData['user_pass_recovery']) ? $vUserData['user_newpwd'] : $vUserData['user_password']),
)
) {
cJSON::sendError(
[
'data' => [
'msg' => $this->gM("Old user password doesn't match!"),
'fields' => [
'old_password' => $this->gM("Old user password doesn't match!"),
],
],
],
);
}
// Prevent from using the same password as previous
if ($oPwdMaster->password_verify($fields['new_password'], $vUserData['user_password'])) {
cJSON::sendError(
[
'data' => [
'msg' => $this->gM('New user password cannot match previous password!'),
'fields' => [
'new_password' => $this->gM('New user password cannot match previous password!'),
'rnew_password' => $this->gM('New user password cannot match previous password!'),
],
],
],
);
}
if ($sNewPwd = $oPwdMaster->password_hash($fields['new_password'])) {
if (
$this->module->updateUserDataById(
$vUserData['id'],
[
'user_fchange' => 0,
'user_pwdtime' => 0,
'user_newpwd' => '',
'user_password' => $sNewPwd,
],
)
) {
$vUserData['user_fchange'] = 0;
$vUserData['user_pwdtime'] = 0;
$vUserData['user_newpwd'] = '';
$vUserData['user_password'] = $sNewPwd;
unset($vUserData['user_pass_recovery']);
// Setup user session data & redirect to necessary location
$vLoginResponse = $this->afterLoginSetup($vUserData, $bSuUser);
$oPWDCUser->destroy();
if (!!$vLoginResponse) {
cJSON::sendOk($vLoginResponse);
}
}
}
// Global error
$errorMessage = $this->gM('Something went wrong while updating your profile!');
cJSON::sendError(['data' => ['msg' => $errorMessage]]);
}
$this->oView->U = prepForDisplayHtml($vUserData);
$this->oView->USER_ONLY_AUTH = (!empty($vUserData['user_uoa']) ? $vUserData['user_uoa'] : false);
$this->oView->SESSION_EXPIRED_LOGIN = (!empty($vUserData['user_flogin']) ? $vUserData['user_flogin'] : false);
$this->oView->setAsJSON('valCfg', $this->getPasswdValCfg());
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->display('password_change', 'BODY');
} else {
$this->redirectTo(
self::URL_ID_LOGIN_PAGE,
false,
false,
['back2login' => 1],
true,
);
}
}
public function getPasswdValCfg(): array
{
return [
'old_password' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
'new_password' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_LEN, cValidator::CHK_EQUAL],
cValidator::P_LINK => 'rnew_password',
cValidator::P_ADDS => ['min' => 6],
],
'rnew_password' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_LEN, cValidator::CHK_EQUAL],
cValidator::P_LINK => 'new_password',
cValidator::P_ADDS => ['min' => 6],
],
];
}
/**
* Function to get js validator cfg for 2FA
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category pubUserManager Edit
* @param bool $bPrepDataCfg - reconfigure cfg data array
* @return array of data
*/
public function get2FAValCfg($bPrepDataCfg = true): array
{
$valCfg = [
// Additional validation config
'cfg' => [
cValidator::CFG_BIND_FIELD => 'data-validator',
],
'code' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_INT, cValidator::CHK_LEN],
cValidator::P_ADDS => ['min' => 6, 'max' => 6],
],
'codeOne' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_MBSET => 0,
cValidator::P_TEST => [cValidator::CHK_INT, cValidator::CHK_LEN],
cValidator::P_ADDS => ['min' => 1, 'max' => 1],
],
];
return ($bPrepDataCfg ? cValidator::prepValCfgForValidation($valCfg) : $valCfg);
}
/**
* Function to validate fields & send user mail
*
* @category APP pubUsers
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @returns bool true or redir to error
* @param array $data
* @return bool
* ---------------------------------------------------------------------------------------------------------
* PASSWORD RECOVERY HANDLE
*
*/
public function sendRecoveryPasswordMail(array $data): bool
{
$data = $this->module->db->prepData($data);
// Check if its email address, else we get user by username && chk if user exists
$user = $this->module->getUserByEmail($data['user_data'], (strpos($data['user_data'], '@') !== false));
if (empty($user)) {
return false;
}
if (count($user) > 1) {
$msg = 'There are more then one user registered to this email, please enter user name instead of email';
cJSON::sendError(['msg' => $this->gM($msg)]);
} else {
// reset password
$user = $user[0];
if ($user['user_blocked'] == '0') {
// Update password recovery data
$oPwdMaster = new cPasswordMaster();
$update = [
'user_pwdtime' => time() + $this->pwdRecValTime,
'user_newpwd' => $oPwdMaster->password_hash($newPWD = cNumbers::getRandomChString(10))
];
$this->module->updateUserDataById($user['id'], $update);
$sHost = "http{$this->oUrl->_sslS}://" . $_SERVER['HTTP_HOST'];
$sEndParam = "pwd:" . urlencode($this->oDefuseCrypt->encrypt($user['id'] . self::SEP . $newPWD));
$loginPage = pubUsersController::URL_ID_LOGIN_PAGE;
$sActivationLink = $sHost . $this->getLinkForID($loginPage, $user['user_lang_id'], false, $sEndParam);
// send mail
(new cMail())->addMail(
$user['user_email'],
false,
cMail::MAIL_SERVER_LORETA_LV,
cMail::MAIL_2_PASSWORD_RECOVERY,
(!empty($user['user_lang_id']) ? (int)$user['user_lang_id'] : $this->langId),
array(
'[%FULL USER NAME%]' => "{$user['user_name']} {$user['user_surname']}",
'[%TEMPORARY_PASSWORD%]' => $newPWD,
'[%PWD_RESET_LINK_HERE%]' => $sActivationLink
),
''
);
return true;
}
}
return false;
}
/**
* Function to get javascript translations
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category APP pubUsers
* @return array
*/
public function getJsTranslations(): array
{
return cValidator::getJSTranslations(
$this,
$this->getUserValCfg(),
[
'no' => $this->gM('No'),
'yes' => $this->gM('Yes'),
'empty_wishlist' => $this->gM('Your wishlist is empty'),
'continue_shopping' => $this->gM('Continue shopping'),
],
);
}
/*******************************************************************************************************************
* Function block to save profile cfg data
*
* @author Mareks Keišs <mareks.keiss@brsystems.lv>
* @category APP pubUsers
* @param mixed $prop - property name / property array
* @param mixed $value - property value
* @return bool true / false
*/
public function saveProfileCfgProp($prop, $value = false)
{
return $this->oUser->oCfg->addProperties($prop, $value);
}
public function runPasswordRecovery(): void
{
$this->oView->setAsJSON('valCfg', $this->passwordVCfg());
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->setAsJSON('baseUrl', ['url' => $this->gL(), 'lang' => $this->uri_data['lang_code']]);
$this->oView->display('password_recovery', 'BODY');
}
/**
* Function to get password recovery validator cfg
*
* @author Aivars Žukovskis <keiss.mareks@gmail.com>
* @category APP pubUsers
* @param bool $bPrepDataCfg - reconfigure cfg data array
* @return array of data
*/
public function passwordVCfg($bPrepDataCfg = true): array
{
$valCfg = [
// Additional validation config
'cfg' => [
cValidator::CFG_BIND_FIELD => 'data-validator',
],
'user_data' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
];
return ($bPrepDataCfg ? cValidator::prepValCfgForValidation($valCfg) : $valCfg);
}
/**
* Function to recovery password
*
* @category APP pubUsers
* @author Aivars Žukovskis <keiss.mareks@gmail.com>
* @param array $data - mail data
* @returns void
*/
public function recoveryEmail($data): void
{
if (empty($data['user_data'])) {
cJSON::sendError(['msg' => $this->gM('no email entered')]);
}
$errors = (new cValidator($this))->checkArrayAdvance($data, $this->passwordVCfg(false));
if (!empty($errors)) {
cJSON::sendError(['data' => ['fields' => $errors, 'msg' => $this->gM('Please review form fields.')]]);
}
if ($this->sendRecoveryPasswordMail($data)) {
$text = 'An email with password reset instructions has been sent to your email address.';
cJSON::sendOk(['msg' => $this->gM($text)]);
}
cJSON::sendError(['msg' => $this->gM('no such a user exists or is blocked')]);
}
public function setRegistrationForm(): void
{
if ($this->isLoggedIn()) {
redirectToUri($this->gL(self::URL_ID_PROFILE_PAGE));
}
$this->oView->HEADER = ['t' => $this->gM("Registration form")];
$this->oView->setAsJSON('valCfg', $this->getRegistrationValCfg());
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->setAsJSON('baseUrl', ['url' => $this->gL(), 'lang' => $this->uri_data['lang_code']]);
$this->oView->display('registration_form', 'BODY');
}
public function getRegistrationValCfg($bPrepDataCfg = true): array
{
$valCfg = [
'cfg' => [
cValidator::CFG_BIND_FIELD => 'data-validator',
],
'user_name' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
'user_surname' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
'user_email' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_EMAIL],
],
'user_newpass' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_LEN, cValidator::CHK_EQUAL],
cValidator::P_LINK => 'user_newpassre',
cValidator::P_ADDS => ['min' => 6],
],
'user_newpassre' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_LEN, cValidator::CHK_EQUAL],
cValidator::P_LINK => 'user_newpass',
cValidator::P_ADDS => ['min' => 6],
],
'terms' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
];
return ($bPrepDataCfg ? cValidator::prepValCfgForValidation($valCfg) : $valCfg);
}
public function registerUser($data): void
{
if (!is_array($data)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Invalid registration request.')]]);
}
$valid = new cValidator($this);
$errors = $valid->checkArrayAdvance($data, $this->getRegistrationValCfg(false));
if (!empty($errors)) {
cJSON::sendError(['data' => ['fields' => $errors, 'msg' => $this->gM('Please review form fields.')]]);
}
$email = trim((string)$data['user_email']);
$existingUser = $this->module->getUserByLoginData(['user_email' => $email, 'user_deleted' => 0]);
if (!empty($existingUser)) {
cJSON::sendError(
[
'data' => [
'fields' => ['user_email' => $this->gM('This email is already registered.')],
'msg' => $this->gM('This email is already registered.'),
],
],
);
}
$oPwdMaster = new cPasswordMaster();
$passwordHash = $oPwdMaster->password_hash($data['user_newpass']);
if (!$passwordHash) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to create account password.')]]);
}
$langId = $this->langId;
$insert = [
'user_name' => trim((string)$data['user_name']),
'user_surname' => trim((string)$data['user_surname']),
'user_email' => $email,
'user_login' => $email,
'user_phone' => '',
'user_password' => $passwordHash,
'user_cfg' => '',
'user_lang_id' => $langId,
'user_activated' => 0,
'user_fchange' => 0,
'user_admin' => 0,
'user_deleted' => 0,
'user_blocked' => 0,
'user_created' => time(),
'user_created_by' => 0,
'user_modified' => time(),
'user_modified_by' => 0,
];
$userId = $this->module->addNewUser($insert);
if (empty($userId)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to create account.')]]);
}
$sHost = "http{$this->oUrl->_sslS}://" . $_SERVER['HTTP_HOST'];
$sEndParam = "act:" . urlencode($this->oDefuseCrypt->encrypt($userId . self::SEP . $data['user_newpass']));
$sActivationLink = $sHost . $this->getLinkForID(self::URL_ID_LOGIN_PAGE, $langId, false, $sEndParam);
$mm = new cMail();
$mm->addMail(
$email,
false,
cMail::MAIL_SERVER_LORETA_LV,
cMail::MAIL_1_ACCOUNT_ACTIVATION,
$langId,
[
'[%FULL USER NAME%]' => trim("{$insert['user_name']} {$insert['user_surname']}"),
'[%USER_LOGIN%]' => $email,
'[%USER_EMAIL%]' => $email,
'[%ACTIVATION_LINK_HERE%]' => $sActivationLink,
'[%ACCOUNT_ACTIVATION_LINK%]' => $sActivationLink,
],
''
);
cJSON::sendOk(['msg' => $this->gM('Activation email has been sent. Please check your inbox.')]);
}
/**
* Show profile page
*/
public function setProfileForm(): void
{
$userData = $this->getCurrentProfileUser();
$cfg = $this->getUserConfigManager($this->suser->id);
$profileMeta = $this->getStoredProfileMeta($cfg);
$addressBook = $this->getStoredAddressBook($cfg);
$orders = $this->prepareOrderHistory($userData['id']);
$activeSection = $this->resolveProfileSection((string)getG('section'));
$wishlistCount = $this->module->getWishlistCount((int)$userData['id']);
$this->oView->U = prepForDisplayHtml($userData);
$this->oView->PROFILE_META = prepForDisplayHtml($profileMeta);
$this->oView->ADDRESS_BOOK = $this->prepareAddressBookForView($addressBook);
$this->oView->ORDERS = prepForDisplayHtml($orders);
$this->oView->ORDER_COUNT = count($orders);
$this->oView->WISHLIST_COUNT = $wishlistCount;
$this->oView->ACTIVE_SECTION = $activeSection;
$this->oView->PROFILE_SECTION_URLS = prepForDisplayHtml($this->getProfileSectionUrls());
$this->oView->JOINED_AT = (!empty($userData['user_created']) ? date('jS M, Y', (int)$userData['user_created']) : '');
$this->oView->GENDER_OPTIONS = self::GENDER_OPTIONS;
$this->oView->NEWSLETTER_SUBSCRIBED = (bool)(new indexModule())->chkUserSubscription($userData['user_email']);
$this->oView->HEADER = ['t' => $this->gM("Profile")];
$this->oView->setAsJSON('valCfg', $this->getUserValCfg());
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->setAsJSON('baseUrl', ['url' => $this->gL(), 'lang' => $this->uri_data['lang_code']]);
$this->oView->setAsJSON('profileSections', $this->getProfileSectionUrls());
$this->oView->setAsJSON('activeSection', $activeSection);
$this->oView->setAsJSON('shopUrl', $this->gL(self::URL_ID_HOME));
$this->oView->display('profile', 'BODY');
}
public function setWishlistForm(): void
{
$userData = $this->getCurrentProfileUser();
$userId = (int)$userData['id'];
$productIds = $this->module->getWishlistProductIds($userId);
$wishlistCount = count($productIds);
$products = $this->prepareWishlistProducts($productIds);
$this->oView->PRODUCTS = $products;
$this->oView->WISHLIST_COUNT = $wishlistCount;
$this->oView->WISHLIST_SIMPLE_VIEW = true;
$this->oView->HEADER = ['t' => $this->gM('Wishlist')];
$this->oView->setAsJSON('gM', $this->getJsTranslations());
$this->oView->setAsJSON('baseUrl', ['url' => $this->gL(), 'lang' => $this->uri_data['lang_code']]);
$this->oView->setAsJSON('shopUrl', $this->gL(self::URL_ID_HOME));
$this->oView->display('wishlist', 'BODY');
}
private function prepareWishlistProducts(array $productIds): array
{
if (empty($productIds)) {
return [];
}
$cmsProducts = new cmsProductsModule();
$products = $cmsProducts->getCartProductData($productIds, $this->langId);
if (!is_array($products)) {
return [];
}
foreach ($products as $productId => &$product) {
$product['id'] = (int)($product['id'] ?? $productId);
$productUrl = trim((string)($product['prod_url'] ?? ''));
$product['prod_url'] = ($productUrl !== '' ? $this->oUrl->rmSlashesIndex($productUrl) : 'javascript:void(0)');
}
unset($product);
return $products;
}
public function saveProfile($data): void
{
$this->getCurrentProfileUser(true);
if (!is_array($data)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Invalid profile request.')]]);
}
$valid = new cValidator($this);
$errors = $valid->checkArrayAdvance($data, $this->getUserValCfg());
if (!empty($errors)) {
cJSON::sendError(['data' => ['fields' => $errors, 'msg' => $this->gM('Please review form fields.')]]);
}
$email = trim((string)$data['user_email']);
$other = $this->module->getUserByLoginData(['user_email' => $email, 'user_deleted' => 0]);
if (!empty($other) && (int)$other['id'] !== (int)$this->suser->id) {
cJSON::sendError(
[
'data' => [
'fields' => ['user_email' => $this->gM('This email is already registered.')],
'msg' => $this->gM('This email is already registered.'),
],
],
);
}
$profileMeta = $this->normalizeProfileMeta([
'birth_date' => ($data['user_birth_date'] ?? ''),
'gender' => ($data['user_gender'] ?? ''),
]);
if (!empty($profileMeta['birth_date']) && !$this->isValidBirthDate($profileMeta['birth_date'])) {
cJSON::sendError(
[
'data' => [
'fields' => ['user_birth_date' => $this->gM('Please enter a valid birth date.')],
'msg' => $this->gM('Please review form fields.'),
],
],
);
}
if (!empty($profileMeta['gender']) && !isset(self::GENDER_OPTIONS[$profileMeta['gender']])) {
cJSON::sendError(
[
'data' => [
'fields' => ['user_gender' => $this->gM('Please select a valid gender value.')],
'msg' => $this->gM('Please review form fields.'),
],
],
);
}
$update = [
'user_name' => trim((string)$data['user_name']),
'user_surname' => trim((string)$data['user_surname']),
'user_email' => $email,
'user_login' => $email,
'user_phone' => trim((string)$data['user_phone']),
'user_modified' => time(),
'user_modified_by' => 0,
];
if (!$this->module->updateUserDataById((int)$this->suser->id, $update)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to update profile.')]]);
}
if (!$this->saveUserConfigValue((int)$this->suser->id, self::PROFILE_CFG_KEY, $profileMeta)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to update profile.')]]);
}
$oIndex = new indexModule();
$bSubscribe = !empty($data['newsletters']) && (string)$data['newsletters'] === '1';
$oldEmail = trim((string)($this->suser->data['user_email'] ?? ''));
if (strlen($oldEmail) && $oldEmail !== $email) {
$buyModule = new pubBuyModule();
foreach ($buyModule->getOrdersInputDataByEmail($oldEmail) as $order) {
$orderId = (int)($order['id'] ?? 0);
if (!$orderId) {
continue;
}
$buyModule->updateOrdersData($orderId, [
'o_mail' => $email,
'o_input_data' => cJSON::encode($this->replaceOrderInputEmail(cJSON::decode((string)($order['o_input_data'] ?? ''), true), $email)),
'o_modified' => time(),
]);
}
}
if (strlen($oldEmail) && $oldEmail !== $email && $oIndex->chkEmailExists($oldEmail)) {
$oIndex->disableEmail($oldEmail);
}
if ($oIndex->chkEmailExists($email)) {
if ($bSubscribe) {
$oIndex->enableEmail($email);
} else {
$oIndex->disableEmail($email);
}
} else {
$oIndex->saveEmail(
[
'email' => $email,
'email_sub' => ($bSubscribe ? 1 : 0),
'email_sub_ip' => $_SERVER['REMOTE_ADDR'],
'email_reg_user_agent' => $_SERVER['HTTP_USER_AGENT'],
'email_added' => time(),
'email_modified' => time(),
],
);
}
if (is_array($this->suser->data)) {
$this->suser->data = array_merge($this->suser->data, $update);
}
cJSON::sendOk(['msg' => $this->gM('Profile updated successfully.')]);
}
public function saveAddressBookEntry(array $data): void
{
$this->getCurrentProfileUser(true);
$valid = new cValidator($this);
$errors = $valid->checkArrayAdvance($data, $this->getAddressBookValCfg());
if (!empty($errors)) {
cJSON::sendError(['data' => ['fields' => $errors, 'msg' => $this->gM('Please review form fields.')]]);
}
$entry = $this->normalizeAddressBookEntry($data);
$addresses = $this->getStoredAddressBook($this->getUserConfigManager((int)$this->suser->id));
$updated = false;
foreach ($addresses as $index => $address) {
if ((string)$address['id'] === (string)$entry['id']) {
$entry['created_at'] = $address['created_at'] ?? time();
$addresses[$index] = $entry;
$updated = true;
break;
}
}
if (!$updated) {
$addresses[] = $entry;
}
$addresses = $this->normalizeAddressBook($addresses);
if (!$this->saveUserConfigValue((int)$this->suser->id, self::ADDRESS_BOOK_CFG_KEY, $addresses)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to save address.')]]);
}
cJSON::sendOk([
'msg' => $this->gM('Address saved successfully.'),
'reload_url' => $this->getProfileSectionUrl(self::SECTION_ADDRESSES),
]);
}
public function deleteAddressBookEntry(string $addressId): void
{
$this->getCurrentProfileUser(true);
$addressId = trim($addressId);
if (!strlen($addressId)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Address was not found.')]]);
}
$addresses = $this->getStoredAddressBook($this->getUserConfigManager((int)$this->suser->id));
$beforeCount = count($addresses);
$addresses = array_values(array_filter($addresses, static function ($address) use ($addressId) {
return (string)($address['id'] ?? '') !== $addressId;
}));
if (count($addresses) === $beforeCount) {
cJSON::sendError(['data' => ['msg' => $this->gM('Address was not found.')]]);
}
$addresses = $this->normalizeAddressBook($addresses);
if (!$this->saveUserConfigValue((int)$this->suser->id, self::ADDRESS_BOOK_CFG_KEY, $addresses)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to delete address.')]]);
}
cJSON::sendOk([
'msg' => $this->gM('Address deleted successfully.'),
'reload_url' => $this->getProfileSectionUrl(self::SECTION_ADDRESSES),
]);
}
public function setDefaultAddressBookEntry(string $addressId, string $type): void
{
$this->getCurrentProfileUser(true);
$type = ($type === 'billing' ? 'billing' : 'shipping');
$addressId = trim($addressId);
$addresses = $this->getStoredAddressBook($this->getUserConfigManager((int)$this->suser->id));
$found = false;
foreach ($addresses as &$address) {
$supportsShipping = in_array($address['type'], ['shipping', 'both'], true);
$supportsBilling = in_array($address['type'], ['billing', 'both'], true);
if ($type === 'shipping') {
$address['is_default_shipping'] = ((string)$address['id'] === $addressId && $supportsShipping ? 1 : 0);
} else {
$address['is_default_billing'] = ((string)$address['id'] === $addressId && $supportsBilling ? 1 : 0);
}
if ((string)$address['id'] === $addressId) {
$found = true;
}
}
unset($address);
if (!$found) {
cJSON::sendError(['data' => ['msg' => $this->gM('Address was not found.')]]);
}
$addresses = $this->normalizeAddressBook($addresses);
if (!$this->saveUserConfigValue((int)$this->suser->id, self::ADDRESS_BOOK_CFG_KEY, $addresses)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to update default address.')]]);
}
cJSON::sendOk([
'msg' => $this->gM('Default address updated successfully.'),
'reload_url' => $this->getProfileSectionUrl(self::SECTION_ADDRESSES),
]);
}
public function deleteAccount(array $data): void
{
$this->getCurrentProfileUser(true);
// Validate delete account
$password = $data['delete_account_password'] ?? '';
$confirmed = !empty($data['delete_account_confirm']);
if (!strlen($password) || !$confirmed) {
$passMessage = (!strlen($password) ? $this->gM('Please enter your current password.') : '');
$confirmMessage = !$confirmed ? $this->gM('Please confirm account deletion.') : '';
cJSON::sendError(['data' => [
'fields' => [
'delete_account_password' => $passMessage,
'delete_account_confirm' => $confirmMessage
],
'msg' => $this->gM('Please confirm account deletion.'),
]]);
}
$userData = $this->module->getUserByLoginData(['id' => $this->suser->id, 'user_deleted' => 0]);
if (empty($userData)) {
cJSON::sendError(['url' => $this->gL(self::URL_ID_LOGIN_PAGE)]);
}
$pwdMaster = new cPasswordMaster();
if (!$pwdMaster->password_verify($password, $userData['user_password'])) {
$msg = $this->gM('Current password is not correct.');
cJSON::sendError(['data' => [
'fields' => ['delete_account_password' => $msg],
'msg' => $msg,
]]);
}
$oIndex = new indexModule();
if ($oIndex->chkEmailExists((string)$userData['user_email'])) {
$oIndex->disableEmail((string)$userData['user_email']);
}
$update = [
'user_deleted' => 1,
'user_blocked' => 1,
'user_modified' => time(),
];
if (!$this->module->updateUserDataById((int)$userData['id'], $update)) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to delete account.')]]);
}
$this->module->clearWishlist((int)$userData['id']);
$this->logOut(true);
cJSON::sendOk([
'msg' => $this->gM('Your account has been deleted successfully.'),
'url' => $this->gL(self::URL_ID_HOME),
]);
}
private function getUserConfigManager(int $userId): pubUsersCfg
{
return new pubUsersCfg($this, $userId);
}
private function getStoredProfileMeta(pubUsersCfg $cfg): array
{
$valuesAdv = $cfg->getUserCFG(pubUsersCfg::values_adv);
return $this->normalizeProfileMeta(
(is_array($valuesAdv[self::PROFILE_CFG_KEY] ?? null) ? $valuesAdv[self::PROFILE_CFG_KEY] : []),
);
}
private function getStoredAddressBook(pubUsersCfg $cfg): array
{
$valuesAdv = $cfg->getUserCFG(pubUsersCfg::values_adv);
return $this->normalizeAddressBook(
(is_array($valuesAdv[self::ADDRESS_BOOK_CFG_KEY] ?? null) ? $valuesAdv[self::ADDRESS_BOOK_CFG_KEY] : []),
);
}
private function saveUserConfigValue(int $userId, string $key, array $value): bool
{
$cfg = $this->getUserConfigManager($userId);
$valuesAdv = $cfg->getUserCFG(pubUsersCfg::values_adv);
$valuesAdv[$key] = $value;
return $cfg->addPropertiesAdvanced($valuesAdv, pubUsersCfg::values_adv);
}
private function normalizeProfileMeta(array $data): array
{
$gender = trim((string)($data['gender'] ?? ''));
if (!isset(self::GENDER_OPTIONS[$gender])) {
$gender = '';
}
$birthDate = trim((string)($data['birth_date'] ?? ''));
if (strlen($birthDate) && !$this->isValidBirthDate($birthDate)) {
$birthDate = '';
}
return [
'birth_date' => $birthDate,
'gender' => $gender,
];
}
private function isValidBirthDate(string $birthDate): bool
{
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthDate)) {
return false;
}
[$year, $month, $day] = array_map('intval', explode('-', $birthDate));
return checkdate($month, $day, $year);
}
private function normalizeAddressBookEntry(array $data): array
{
$type = trim((string)($data['address_type'] ?? 'shipping'));
if (!in_array($type, ['shipping', 'billing', 'both'], true)) {
$type = 'shipping';
}
$supportsShipping = in_array($type, ['shipping', 'both'], true);
$supportsBilling = in_array($type, ['billing', 'both'], true);
return [
'id' => trim((string)($data['address_entry_id'] ?? '')) ?: bin2hex(random_bytes(8)),
'label' => trim((string)($data['address_label'] ?? '')) ?: $this->gM('Address'),
'first_name' => trim((string)($data['address_first_name'] ?? '')),
'last_name' => trim((string)($data['address_last_name'] ?? '')),
'company' => trim((string)($data['address_company'] ?? '')),
'phone' => trim((string)($data['address_phone'] ?? '')),
'address1' => trim((string)($data['address_address1'] ?? '')),
'address2' => trim((string)($data['address_address2'] ?? '')),
'city' => trim((string)($data['address_city'] ?? '')),
'state' => trim((string)($data['address_state'] ?? '')),
'country' => trim((string)($data['address_country'] ?? '')),
'zip' => trim((string)($data['address_zip'] ?? '')),
'type' => $type,
'is_default_shipping' => (!empty($data['address_is_default_shipping']) && $supportsShipping ? 1 : 0),
'is_default_billing' => (!empty($data['address_is_default_billing']) && $supportsBilling ? 1 : 0),
'created_at' => time(),
'updated_at' => time(),
];
}
private function normalizeAddressBook(array $addresses): array
{
$normalized = [];
foreach ($addresses as $address) {
if (!is_array($address)) {
continue;
}
$entry = $this->normalizeAddressBookEntry([
'address_entry_id' => $address['id'] ?? '',
'address_label' => $address['label'] ?? '',
'address_first_name' => $address['first_name'] ?? '',
'address_last_name' => $address['last_name'] ?? '',
'address_company' => $address['company'] ?? '',
'address_phone' => $address['phone'] ?? '',
'address_address1' => $address['address1'] ?? '',
'address_address2' => $address['address2'] ?? '',
'address_city' => $address['city'] ?? '',
'address_state' => $address['state'] ?? '',
'address_country' => $address['country'] ?? '',
'address_zip' => $address['zip'] ?? '',
'address_type' => $address['type'] ?? 'shipping',
'address_is_default_shipping' => !empty($address['is_default_shipping']) ? 1 : 0,
'address_is_default_billing' => !empty($address['is_default_billing']) ? 1 : 0,
]);
$entry['created_at'] = (int)($address['created_at'] ?? $entry['created_at']);
$entry['updated_at'] = (int)($address['updated_at'] ?? time());
$normalized[] = $entry;
}
$hasDefaultShipping = false;
$hasDefaultBilling = false;
foreach ($normalized as $address) {
if (!empty($address['is_default_shipping'])) {
$hasDefaultShipping = true;
}
if (!empty($address['is_default_billing'])) {
$hasDefaultBilling = true;
}
}
foreach ($normalized as &$address) {
$supportsShipping = in_array($address['type'], ['shipping', 'both'], true);
$supportsBilling = in_array($address['type'], ['billing', 'both'], true);
if (!$supportsShipping) {
$address['is_default_shipping'] = 0;
}
if (!$supportsBilling) {
$address['is_default_billing'] = 0;
}
}
unset($address);
if (!$hasDefaultShipping) {
foreach ($normalized as &$address) {
if (in_array($address['type'], ['shipping', 'both'], true)) {
$address['is_default_shipping'] = 1;
break;
}
}
unset($address);
}
if (!$hasDefaultBilling) {
foreach ($normalized as &$address) {
if (in_array($address['type'], ['billing', 'both'], true)) {
$address['is_default_billing'] = 1;
break;
}
}
unset($address);
}
usort($normalized, static function ($a, $b) {
$aScore = (int)!empty($a['is_default_shipping']) + (int)!empty($a['is_default_billing']);
$bScore = (int)!empty($b['is_default_shipping']) + (int)!empty($b['is_default_billing']);
if ($aScore === $bScore) {
return strcmp((string)$a['label'], (string)$b['label']);
}
return $bScore <=> $aScore;
});
return $normalized;
}
private function prepareAddressBookForView(array $addresses): array
{
$viewData = [];
foreach ($addresses as $address) {
$viewData[] = [
'id' => (string)$address['id'],
'label' => (string)$address['label'],
'first_name' => (string)$address['first_name'],
'last_name' => (string)$address['last_name'],
'company' => (string)$address['company'],
'phone' => (string)$address['phone'],
'address1' => (string)$address['address1'],
'address2' => (string)$address['address2'],
'city' => (string)$address['city'],
'state' => (string)$address['state'],
'country' => (string)$address['country'],
'zip' => (string)$address['zip'],
'type' => (string)$address['type'],
'is_default_shipping' => !empty($address['is_default_shipping']),
'is_default_billing' => !empty($address['is_default_billing']),
'json' => json_encode($address, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
}
return $viewData;
}
private function resolveProfileSection(string $section): string
{
$allowed = [
self::SECTION_PROFILE,
self::SECTION_ADDRESSES,
self::SECTION_ORDERS,
self::SECTION_WISHLIST,
self::SECTION_DELETE,
];
return (in_array($section, $allowed, true) ? $section : self::SECTION_PROFILE);
}
private function getProfileSectionUrls(): array
{
return [
self::SECTION_PROFILE => $this->getProfileSectionUrl(self::SECTION_PROFILE),
self::SECTION_ADDRESSES => $this->getProfileSectionUrl(self::SECTION_ADDRESSES),
self::SECTION_ORDERS => $this->getProfileSectionUrl(self::SECTION_ORDERS),
self::SECTION_WISHLIST => $this->getProfileSectionUrl(self::SECTION_WISHLIST),
self::SECTION_DELETE => $this->getProfileSectionUrl(self::SECTION_DELETE),
];
}
private function getProfileSectionUrl(string $section): string
{
return $this->gL(self::URL_ID_PROFILE_PAGE) . '?section=' . urlencode($section);
}
private function prepareOrderHistory(int $userId): array
{
$buyModule = new pubBuyModule();
$orders = $buyModule->getUserOrderHistory($userId, 50);
if (empty($orders)) {
return [];
}
$paymentIds = [];
foreach ($orders as $order) {
if (!empty($order['o_payment_met'])) {
$paymentIds[] = (int)$order['o_payment_met'];
}
}
$paymentTitles = (!empty($paymentIds) ? $buyModule->getPaymentData(array_unique($paymentIds), $this->langId) : []);
$deliveryTitles = $buyModule->getClassificatorDataById(BuyController::CL_ID_SHIPPING_METHODS);
$prepared = [];
foreach ($orders as $order) {
$status = $this->getOrderStatusData($order);
$paymentId = (int)($order['o_payment_met'] ?? 0);
$deliveryId = (int)($order['o_delivery_met'] ?? 0);
$currency = (string)($order['rec_currency'] ?? '');
$currency = $currency === 'EUR' || !strlen($currency) ? '€' : $currency;
$total = (!empty($order['rec_value']) ? ((float)$order['rec_value'] / 100) : (float)($order['subtotal'] ?? 0));
$decodedInput = cJSON::decode((string)($order['o_input_data'] ?? ''), true);
$deliveryTitle = !empty($deliveryTitles[$deliveryId]['n'])
? $this->gM($deliveryTitles[$deliveryId]['n'])
: $this->gM('Shipping');
$paymentTitle = !empty($paymentTitles[$paymentId]['title'])
? $paymentTitles[$paymentId]['title']
: $this->gM('Payment');
$prepared[] = [
'id' => (int)($order['id'] ?? 0),
'order_nr' => (string)($order['o_nr'] ?? ''),
'date_label' => (!empty($order['o_date']) ? date('d.m.Y', (int)$order['o_date']) : ''),
'payment_title' => $paymentTitle,
'payment_status_label' => $status['payment_label'],
'payment_status_class' => $status['payment_class'],
'status_label' => $status['status_label'],
'status_class' => $status['status_class'],
'delivery_title' => $deliveryTitle,
'items_qty' => (int)($order['items_qty'] ?? 0),
'total_display' => sprintf('%s %s', $currency, number_format($total, 2, '.', ' ')),
'note' => trim((string)($decodedInput['check-note'] ?? '')),
'shipping_name' => trim(($order['o_addr_name'] ?? '') . ' ' . ($order['o_addr_lastname'] ?? '')),
'shipping_address' => trim((string)($order['o_addr_address'] ?? '')),
'shipping_city' => (string)($order['o_addr_city'] ?? ''),
'shipping_country' => (string)($order['o_addr_country'] ?? ''),
'shipping_zip' => (string)($order['o_addr_zip'] ?? ''),
'shipping_phone' => (string)($order['o_addr_phone'] ?? ''),
];
}
return $prepared;
}
private function getOrderStatusData(array $order): array
{
$isPaid = ((int)($order['rec_status'] ?? 0) === 1 || (string)($order['rec_result_message'] ?? '') === BuyController::STATUS_CAPTURED);
if ($isPaid) {
return [
'payment_label' => $this->gM('Paid'),
'payment_class' => 'text-success',
'status_label' => $this->gM('Completed'),
'status_class' => 'text-success',
];
}
if ((int)($order['o_payment_met'] ?? 0) === BuyController::PMET_OTHER_E) {
return [
'payment_label' => $this->gM('Awaiting bank transfer'),
'payment_class' => 'text-warning',
'status_label' => $this->gM('Pending'),
'status_class' => 'text-warning',
];
}
return [
'payment_label' => $this->gM('Awaiting payment'),
'payment_class' => 'text-warning',
'status_label' => $this->gM('Processing'),
'status_class' => 'text-warning',
];
}
private function replaceOrderInputEmail($input, string $email): array
{
if (!is_array($input)) {
return [
'customer_info' => [
'checkout_email' => $email,
],
];
}
foreach (['shipping_address', 'delivery_info'] as $addressKey) {
if (!empty($input[$addressKey]) && is_array($input[$addressKey])) {
$input[$addressKey]['checkout_email'] = $email;
}
}
if (empty($input['customer_info']) || !is_array($input['customer_info'])) {
$input['customer_info'] = [];
}
$input['customer_info']['checkout_email'] = $email;
return $input;
}
public function runWishlist(): void
{
if (!$this->isLoggedIn()) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('You must be logged in to manage wishlist.')]);
}
$action = getV('wl_action');
$productIdRaw = trim((string)getV('product_id'));
$productId = 0;
if (!$action) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Invalid wishlist request.')]);
}
$userId = (int)$this->suser->id;
if ($productIdRaw !== '') {
if (ctype_digit($productIdRaw)) {
$productId = (int)$productIdRaw;
} else {
$cry = new cDefuseCrypto();
try {
$productId = (int)$cry->decrypt($productIdRaw);
} catch (\Throwable $e) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Invalid product id.')]);
}
}
}
if (in_array($action, ['add', 'remove'], true) && $productId < 1) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Invalid wishlist request.')]);
}
if ($action === 'add') {
if ($this->module->isProductInWishlist($userId, $productId)) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Product already in wishlist.')]);
}
if (!$this->module->addToWishlist($userId, $productId)) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Failed to add to wishlist.')]);
}
$count = $this->module->getWishlistCount($userId);
cJSON::sendOk(['success' => true, 'msg' => $this->gM('Added to wishlist.'), 'count' => $count]);
} elseif ($action === 'remove') {
if (!$this->module->removeFromWishlist($userId, $productId)) {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Failed to remove from wishlist.')]);
}
$count = $this->module->getWishlistCount($userId);
cJSON::sendOk(['success' => true, 'msg' => $this->gM('Removed from wishlist.'), 'count' => $count]);
} elseif ($action === 'getIds') {
// Return all wishlist product IDs for current user
$productIds = $this->module->getWishlistProductIds($userId);
cJSON::sendOk(['success' => true, 'ids' => $productIds, 'count' => count($productIds)]);
} elseif ($action === 'load') {
$productIds = $this->module->getWishlistProductIds($userId);
if (empty($productIds)) {
cJSON::sendOk(['success' => true, 'html' => '', 'count' => 0]);
}
$products = $this->prepareWishlistProducts($productIds);
if (empty($products)) {
cJSON::sendOk(['success' => true, 'html' => '', 'count' => 0]);
}
$this->oView->PRODUCTS = $products;
$this->oView->WISHLIST_SIMPLE_VIEW = true;
$html = $this->oView->genTemplate('wishlist_items');
cJSON::sendOk(['success' => true, 'html' => $html, 'count' => count($productIds)]);
} else {
cJSON::sendError(['success' => false, 'msg' => $this->gM('Invalid wishlist action.')]);
}
}
public function processProfilePasswordChange(array $fields): void
{
$this->getCurrentProfileUser(true);
$valid = new cValidator($this);
$errors = $valid->checkArray($fields, $this->getPasswdValCfg());
if ($errors !== true) {
cJSON::sendError(['data' => ['fields' => $errors, 'msg' => $this->gM('Please review form fields.')]]);
}
$vUserData = $this->getCurrentProfileUser(true);
$oPwdMaster = new cPasswordMaster();
if (!$oPwdMaster->password_verify($fields['old_password'], $vUserData['user_password'])) {
cJSON::sendError(
[
'data' => [
'msg' => $this->gM("Old user password doesn't match!"),
'fields' => ['old_password' => $this->gM("Old user password doesn't match!")],
],
],
);
}
if ($oPwdMaster->password_verify($fields['new_password'], $vUserData['user_password'])) {
cJSON::sendError(
[
'data' => [
'msg' => $this->gM('New user password cannot match previous password!'),
'fields' => [
'new_password' => $this->gM('New user password cannot match previous password!'),
'rnew_password' => $this->gM('New user password cannot match previous password!'),
],
],
],
);
}
$sNewPwd = $oPwdMaster->password_hash($fields['new_password']);
if (!$sNewPwd) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to update user password.')]]);
}
if (!$this->module->updateUserDataById((int)$this->suser->id, [
'user_fchange' => 0,
'user_pwdtime' => 0,
'user_newpwd' => '',
'user_password' => $sNewPwd,
'user_modified' => time(),
'user_modified_by' => 0,
])) {
cJSON::sendError(['data' => ['msg' => $this->gM('Failed to update user password.')]]);
}
if (is_array($this->suser->data)) {
$this->suser->data['user_password'] = $sNewPwd;
}
cJSON::sendOk(['msg' => $this->gM('User password successfully changed!')]);
}
public function getUserValCfg(): array
{
return [
'cfg' => [
cValidator::CFG_BIND_FIELD => 'data-validator',
],
'user_name' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
'user_surname' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_STR],
],
'user_email' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_EMAIL],
],
'user_phone' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 0,
cValidator::P_TEST => [cValidator::CHK_PHONE],
],
];
}
public function getAddressBookValCfg(): array
{
return [
'cfg' => [
cValidator::CFG_BIND_FIELD => 'data-validator',
],
'address_label' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_first_name' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_last_name' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_phone' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::CHK_PHONE],
],
'address_address1' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_address2' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 0,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_city' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_state' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 0,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_country' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_zip' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
'address_type' => [
cValidator::P_TYPE => cValidator::TY_STR,
cValidator::P_COMP => 1,
cValidator::P_TEST => [cValidator::TY_STR],
],
];
}
}