* @license MIT https://opensource.org/licenses/MIT * @link / */ namespace LDC\LDCCRM\WordPress; use LDC\LDCCRM\CrmApiV4; use LDC\LDCCRM\Enums\ApiError; use LDC\LDCCRM\Enums\LogLevel; use LDC\LDCCRM\Exceptions\ApiErrorException; use LDC\LDCCRM\Exceptions\CrmException; use LDC\LDCCRM\Models\Customer; use LDC\LDCCRM\Models\SingleOptin; use LDC\LDCCRM\Utils; /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found TABLE LDC CUSTOMERS CREATE TABLE `ldc_wp_customers` ( `hashcode` VARCHAR(36) NOT NULL DEFAULT '' COMMENT 'Le hashcode (UUID)', `email` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Le customer email', `uuid` VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'Code unique (inscripton, validation, etc)', PRIMARY KEY (`hashcode`) ) COMMENT='Les customers sur un WP liées à la CRM' COLLATE='utf8mb4_unicode_ci'; */ /** * Class de gestion des users en session avec Wordpress * * @category API * @package LDCCrmClient * @subpackage Wordpress * @author Jérémy Munsch * @license MIT https://opensource.org/licenses/MIT * @link / */ class WPCrm { /** * The session key name * * @var string|null */ private static ?string $sessionKey = null; /** * The session live status * * @var boolean */ private static bool $sessionDestroyed = false; /** * The user object * * @var \LDC\LDCCRM\CrmApiV4|null */ protected static ?CrmApiV4 $api = null; /** * The user object * * @var \LDC\LDCCRM\WordPress\WPCrmUser|null */ protected static ?WPCrmUser $user = null; /** * Get the current user * return null if the user is not logged in * * @return \LDC\LDCCRM\WordPress\WPCrmUser|null */ public static function getUser() { self::bootSessionIfNeeded(); return self::$user; } /** * Manually login a user * If self::$user is already set. * * @return \LDCCRM\WPCrmUser|null */ public static function forceLogin() { self::bootSessionIfNeeded(); if ($user = self::getUser()) { $_SESSION['wpcrm'][self::$sessionKey] = json_encode($user); } return $user; } /** * Get the current user * return api response code if an error occurs * * @param string $identification The customer hashcode or email. * * @return \LDC\LDCCRM\Wordpress\WPCrmUser|false * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ public static function getUserFromApi(string $identification) { self::initLdcApi(); try { if (!($response = self::makeApiV4Call(self::$api, 'getCustomer', [$identification]))) { return false; } } catch (ApiErrorException $e) { switch ($e->getApiError()) { case ApiError::modelNotFound->value(): return false; default: throw $e; } } /** @var \LDC\LDCCRM\Models\Customer */ $customer = $response; self::setUser($customer); return self::$user; } /** * Try to login with CRM API * A session will be started if credentials are valid. * * @param string $email The customer email. * @param string $password The customer password. * * @return boolean * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - authFailed, If credentials are not valid. * - customerNotActivated, If the account is not activated. */ public static function login(string $email, string $password) { self::bootSessionIfNeeded(); $validCredentials = self::loginUserWithApi($email, $password); if ($validCredentials and ($user = self::getUserFromApi($email))) { self::saveOrUpdateUserToTable(); $_SESSION['wpcrm'][self::$sessionKey] = json_encode($user); } return $validCredentials; } /** * Destroy the customer from session * and therefore logout * * @return void */ public static function logout() { self::bootSessionIfNeeded(); $_SESSION['wpcrm'] = []; session_destroy(); self::$sessionDestroyed = true; } /** * Check if the user is logged in. * This means he has an active valid session. * * @return boolean */ public static function check() { self::bootSessionIfNeeded(); return (bool)self::getUser(); } /** * Save current customer to * the local database and on crm * * @return boolean true if the operation succeeded false otherwise * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - validationFailed, If provided data is wrong(CRM side). * - modelNotFound, If this customer hashcode could not be found. * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ public static function saveUser(): bool { if (!self::getUser() or !($customer = self::getCustomer())) { return false; } try { if (!self::makeApiV4Call($customer, 'save')) { return false; } } catch (ApiErrorException $e) { switch ($e->getApiError()) { case ApiError::modelNotFound->value(): return false; default: throw $e; } } self::saveOrUpdateUserToTable(); return true; } /** * Delete current customer from * the local database and the crm * * @return boolean true if the operation succeeded * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - modelNotFound, If this customer hashcode could not be found. * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ public static function deleteUser(): bool { if (!self::getUser() or !($customer = self::getCustomer())) { return false; } try { if (!self::makeApiV4Call($customer, 'delete')) { return false; } } catch (ApiErrorException $e) { switch ($e->getApiError()) { case ApiError::authorizationException->value(): case ApiError::modelNotFound->value(): return false; default: throw $e; } } self::deleteUserFromTable(); return true; } /** * Toggle the current user optin value * on the current user and the crm * * @return boolean true if the operation succeeded * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - modelNotFound, If this customer hashcode could not be found. * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ public static function toggleUserOptin(): bool { if (!self::getUser() or !($customer = self::getCustomer())) { return false; } try { $customer->opt_in = !$customer->opt_in; if (!self::makeApiV4Call($customer, 'save')) { return false; } } catch (ApiErrorException $e) { switch ($e->getApiError()) { case ApiError::authorizationException->value(): case ApiError::modelNotFound->value(): return false; default: throw $e; } } return true; } /** * Set the single optin status for an email * on the crm * * @param string $email * @param boolean $opt_in * * @return boolean true if the operation succeeded false otherwise * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field email could not be validated. * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - singleOptinCustomerExists, If a customer exists with this email. */ public static function setSingleOptin(string $email, bool $opt_in): bool { return self::makeApiV4Call(SingleOptin::class, 'setStatus', [ $email, $opt_in ]); } /** * Activate the current user * * @return boolean True if the operation succeeded. * @throws \LDC\LDCCRM\Exceptions\ValidationException If a customer's hashcode could not be validated. * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - modelNotFound, If this customer hashcode could not be found. * - customerAlreadyActive, It the customer is already activated. */ public static function activateUser(): bool { if (!self::getUser() or !($customer = self::getCustomer())) { return false; } if ($customer->status) { return true; } try { if (!self::makeApiV4Call($customer, 'activate')) { return false; } } catch (ApiErrorException $e) { switch ($e->getApiError()) { case ApiError::authorizationException->value(): case ApiError::modelNotFound->value(): return false; default: throw $e; } } return true; } /** * Generate a new user * * @return WPCrmUser */ public static function getNewUser(): WPCrmUser { self::$user = new WPCrmUser(); return self::$user; } /** * Remove active user * to prevent WPCrm::check to return true * * @return void */ public static function unsetActiveUser(): void { self::$user = null; } // * --- PRIVATE --- METHODS /** * Check customer credentials using the CRM. * * @param string $email The customer email. * @param string $password The customer password. * * @return boolean * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response. * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * Possibles are : - authFailed, If credentials are not valid. * - customerNotActivated, If the account is not activated. */ private static function loginUserWithApi(string $email, string $password): bool { self::initLdcApi(); /** @var boolean $response */ $response = self::makeApiV4Call(self::$api, 'login', [$email, $password]); return $response; } /** * Get all customer infos * * @return \LDC\LDCCRM\Models\Customer|null */ private static function getCustomer(): ?Customer { if (!self::getUser()) { return null; } if (!Utils::getRawProperty(self::$user, 'info')) { self::getUserFromApi(self::$user->hashcode); } /** @var \LDC\LDCCRM\Models\Customer */ $customer = Utils::getRawProperty(self::$user, 'info'); return $customer; } /** * Create a user * stores it to the session * and finally saves it to the database if it does not exists * * @param \LDC\LDCCRM\Models\Customer $customer * * @return void */ private static function setUser(Customer $customer): void { self::bootSessionIfNeeded(); if (!self::$user) { self::$user = new WPCrmUser($customer->hashcode, $customer->email); } self::$user->setInfo($customer); } /** * Get the initialized API object * * @return \LDC\LDCCRM\CrmApiV4 * @throws \ErrorException If php does not have extension curl. */ private static function initLdcApi(): CrmApiV4 { if (!self::$api) { if (!extension_loaded('curl')) { throw new \ErrorException( 'WPCrm requires php extension curl' ); } self::$api = new CrmApiV4(); } return self::$api; } /** * Make a call using apiV4 * * @param object|string $obj An object or a class name. * @param string $method The method to call. * @param array $arguments Arguments to pass. * @return mixed|false * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any of this exception is thrown. * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured. */ private static function makeApiV4Call($obj, string $method, array $arguments = []) { try { return \forward_static_call_array([$obj, $method], $arguments); } catch (ApiErrorException $e) { switch ($e->getApiError()) { case ApiError::apiAuthFailed: Utils::log($e->getMessage(), LogLevel::CRIT); throw $e; default: Utils::log($e->getMessage(), LogLevel::DEBUG); throw $e; } } catch (CrmException $e) { Utils::log($e->getMessage(), LogLevel::CRIT); throw $e; } } /** * Boot the php session if needed * * @return void */ private static function bootSessionIfNeeded(): void { self::$sessionKey = ($_SERVER['HTTP_USER_AGENT'] ?? '') . ($_SERVER['REMOTE_ADDR'] ?? ''); /** @var string self::$sessionKey */ self::$sessionKey = \strlen(self::$sessionKey) ? self::$sessionKey : (date('d') ? date('d') : ''); if (!session_id() && !self::$sessionDestroyed) { session_start(); } if (!isset($_SESSION['wpcrm'])) { $_SESSION['wpcrm'] = []; } if ( // If session identity is violated. count($_SESSION['wpcrm']) > 1 or // If session timed out. (isset($_SESSION['wpcrm'][self::$sessionKey]) and isset($_SESSION['lastActivity']) and // * 15 minutes session. ($_SESSION['lastActivity'] + (15 * 60)) < time()) ) { // Destroy session. self::$sessionDestroyed = true; $_SESSION['wpcrm'] = []; } else { $_SESSION['lastActivity'] = time(); } if (!self::$user and isset($_SESSION['wpcrm'][self::$sessionKey])) { self::$user = WPCrmUser::parseFromJson( $_SESSION['wpcrm'][self::$sessionKey] ); } } /** * Get a customer from table * * @param string $hashcode The customer hashcode. * * @return array|null Database query result in format specified by $output or null on failure. */ public static function getUserFromTable(string $hashcode): ?array { return self::getWpDb()->get_row( self::getWpDb()->prepare( 'SELECT * FROM `ldc_wp_customers` WHERE `hashcode` = %s', [ 'hashcode' => $hashcode ] ), 'ARRAY_A' ); } /** * Get a customer from table by an email * * @param string $email The customer email. * * @return array|null Database query result in format specified by $output or null on failure. */ public static function getUserFromTableByEmail(string $email): ?array { return self::getWpDb()->get_row( self::getWpDb()->prepare( 'SELECT * FROM `ldc_wp_customers` WHERE `email` = %s', [ 'email' => $email ] ), 'ARRAY_A' ); } /** * Get a customer from table by an email * * @param string $uuid The customer transaction uuid. * * @return array|null Database query result in format specified by $output or null on failure. */ public static function getUserFromTableByUuid(string $uuid): ?array { return self::getWpDb()->get_row( self::getWpDb()->prepare( 'SELECT * FROM `ldc_wp_customers` WHERE `uuid` = %s', [ 'uuid' => $uuid ] ), 'ARRAY_A' ); } /** * Save or update the user to the table * * @return integer The number of row affected on the table. */ private static function saveOrUpdateUserToTable(): int { if (is_string(self::$user->hashcode) and self::getUserFromTable(self::$user->hashcode)) { return self::query( 'UPDATE `ldc_wp_customers` SET `email` = %s, `uuid` = %s WHERE `hashcode` = %s', [ 'email' => self::$user->email, 'uuid' => self::$user->uuidToken, 'hashcode' => self::$user->hashcode ] ); } return self::query( 'INSERT INTO `ldc_wp_customers`(hashcode, email, uuid) VALUES (%s, %s, %s)', [ 'hashcode' => self::$user->hashcode, 'email' => self::$user->email, 'uuid' => self::$user->uuidToken ] ); } /** * Save or update the user to the table * * @return integer The number of row removed from the table. */ private static function deleteUserFromTable(): int { return self::query( 'DELETE FROM `ldc_wp_customers` WHERE `hashcode` = %s;', [ 'hashcode' => self::$user->hashcode ] ); } /** * Query in DB * * @param string $query La request avec les variables en points d'interrogation. * @param array $params Les paramètres des la requète. * * @return integer|boolean Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. * Number of rows affected/selected for all other queries. Boolean false on error. */ private static function query(string $query, array $params = []) { return self::getWpDb()->query( self::getWpDb()->prepare($query, $params) ); } /** * Get WP 'pdo instance' * * @return object The $wpdb variable */ private static function getWpDb(): object { // phpcs:ignore Squiz.PHP.GlobalKeyword.NotAllowed global $wpdb; return $wpdb; } }