'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 * @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 * @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 * @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 * @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 * @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 * @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 * @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 * @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 * @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!
Please try resetting password once again,
' . '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!
Please try resetting password once again,
' . '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 * @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 * @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 * @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 * @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 * @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 * @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 * @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 * @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 * @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 * @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], ], ]; } }