Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 181
0.00% covered (danger)
0.00%
0 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCrm
0.00% covered (danger)
0.00%
0 / 181
0.00% covered (danger)
0.00%
0 / 26
6320
0.00% covered (danger)
0.00%
0 / 1
 getUser
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 forceLogin
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getUserFromApi
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 login
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 logout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 check
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 saveUser
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 deleteUser
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 toggleUserOptin
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 setSingleOptin
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 activateUser
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getNewUser
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 unsetActiveUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loginUserWithApi
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCustomer
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 initLdcApi
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 makeApiV4Call
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 bootSessionIfNeeded
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
156
 getUserFromTable
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getUserFromTableByEmail
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getUserFromTableByUuid
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 saveOrUpdateUserToTable
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 deleteUserFromTable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 query
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getWpDb
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * LDCCRM For Wordpress usage
5 *
6 * @category API
7 * @package  None
8 * @author   Jérémy Munsch <jeremy.munsch@ldc.fr>
9 * @license  MIT https://opensource.org/licenses/MIT
10 * @link     /
11 */
12
13namespace LDC\LDCCRM\WordPress;
14
15use LDC\LDCCRM\CrmApiV4;
16use LDC\LDCCRM\Enums\ApiError;
17use LDC\LDCCRM\Enums\LogLevel;
18use LDC\LDCCRM\Exceptions\ApiErrorException;
19use LDC\LDCCRM\Exceptions\CrmException;
20use LDC\LDCCRM\Models\Customer;
21use LDC\LDCCRM\Models\SingleOptin;
22use LDC\LDCCRM\Utils;
23
24/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
25    TABLE LDC CUSTOMERS
26    CREATE TABLE `ldc_wp_customers` (
27    `hashcode` VARCHAR(36) NOT NULL DEFAULT '' COMMENT 'Le hashcode (UUID)',
28    `email` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Le customer email',
29    `uuid` VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'Code unique (inscripton, validation, etc)',
30    PRIMARY KEY (`hashcode`)
31    )
32    COMMENT='Les customers sur un WP liées à la CRM'
33    COLLATE='utf8mb4_unicode_ci';
34*/
35
36/**
37 * Class de gestion des users en session avec Wordpress
38 *
39 * @category API
40 * @package LDCCrmClient
41 * @subpackage Wordpress
42 * @author   Jérémy Munsch <jeremy.munsch@ldc.fr>
43 * @license  MIT https://opensource.org/licenses/MIT
44 * @link     /
45 */
46class WPCrm
47{
48    /**
49     * The session key name
50     *
51     * @var string|null
52     */
53    private static ?string $sessionKey = null;
54
55    /**
56     * The session live status
57     *
58     * @var boolean
59     */
60    private static bool $sessionDestroyed = false;
61
62    /**
63     * The user object
64     *
65     * @var \LDC\LDCCRM\CrmApiV4|null
66     */
67    protected static ?CrmApiV4 $api = null;
68
69    /**
70     * The user object
71     *
72     * @var \LDC\LDCCRM\WordPress\WPCrmUser|null
73     */
74    protected static ?WPCrmUser $user = null;
75
76    /**
77     * Get the current user
78     * return null if the user is not logged in
79     *
80     * @return \LDC\LDCCRM\WordPress\WPCrmUser|null
81     */
82    public static function getUser()
83    {
84        self::bootSessionIfNeeded();
85        return self::$user;
86    }
87
88    /**
89     * Manually login a user
90     * If self::$user is already set.
91     *
92     * @return \LDCCRM\WPCrmUser|null
93     */
94    public static function forceLogin()
95    {
96        self::bootSessionIfNeeded();
97        if ($user = self::getUser()) {
98            $_SESSION['wpcrm'][self::$sessionKey] = json_encode($user);
99        }
100        return $user;
101    }
102
103    /**
104     * Get the current user
105     * return api response code if an error occurs
106     *
107     * @param string $identification The customer hashcode or email.
108     *
109     * @return \LDC\LDCCRM\Wordpress\WPCrmUser|false
110     * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response.
111     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
112     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
113     */
114    public static function getUserFromApi(string $identification)
115    {
116        self::initLdcApi();
117        try {
118            if (!($response = self::makeApiV4Call(self::$api, 'getCustomer', [$identification]))) {
119                return false;
120            }
121        } catch (ApiErrorException $e) {
122            switch ($e->getApiError()) {
123                case ApiError::modelNotFound->value():
124                    return false;
125                default:
126                    throw $e;
127            }
128        }
129        /** @var \LDC\LDCCRM\Models\Customer */
130        $customer = $response;
131        self::setUser($customer);
132        return self::$user;
133    }
134
135    /**
136     * Try to login with CRM API
137     * A session will be started if credentials are valid.
138     *
139     * @param string $email    The customer email.
140     * @param string $password The customer password.
141     *
142     * @return boolean
143     * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response.
144     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
145     * Possibles are : - authFailed, If credentials are not valid.
146     *                 - customerNotActivated, If the account is not activated.
147     */
148    public static function login(string $email, string $password)
149    {
150        self::bootSessionIfNeeded();
151        $validCredentials = self::loginUserWithApi($email, $password);
152
153        if ($validCredentials and ($user = self::getUserFromApi($email))) {
154            self::saveOrUpdateUserToTable();
155            $_SESSION['wpcrm'][self::$sessionKey] = json_encode($user);
156        }
157
158        return $validCredentials;
159    }
160
161    /**
162     * Destroy the customer from session
163     * and therefore logout
164     *
165     * @return void
166     */
167    public static function logout()
168    {
169        self::bootSessionIfNeeded();
170        $_SESSION['wpcrm'] = [];
171        session_destroy();
172        self::$sessionDestroyed = true;
173    }
174
175    /**
176     * Check if the user is logged in.
177     * This means he has an active valid session.
178     *
179     * @return boolean
180     */
181    public static function check()
182    {
183        self::bootSessionIfNeeded();
184        return (bool)self::getUser();
185    }
186
187    /**
188     * Save current customer to
189     * the local database and on crm
190     *
191     * @return boolean true if the operation succeeded false otherwise
192     * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured.
193     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
194     * Possibles are : - validationFailed, If provided data is wrong(CRM side).
195     *                 - modelNotFound, If this customer hashcode could not be found.
196     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
197     */
198    public static function saveUser(): bool
199    {
200        if (!self::getUser() or !($customer = self::getCustomer())) {
201            return false;
202        }
203        try {
204            if (!self::makeApiV4Call($customer, 'save')) {
205                return false;
206            }
207        } catch (ApiErrorException $e) {
208            switch ($e->getApiError()) {
209                case ApiError::modelNotFound->value():
210                    return false;
211                default:
212                    throw $e;
213            }
214        }
215        self::saveOrUpdateUserToTable();
216        return true;
217    }
218
219    /**
220     * Delete current customer from
221     * the local database and the crm
222     *
223     * @return boolean true if the operation succeeded
224     * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured.
225     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
226     * Possibles are : - modelNotFound, If this customer hashcode could not be found.
227     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
228     */
229    public static function deleteUser(): bool
230    {
231        if (!self::getUser() or !($customer = self::getCustomer())) {
232            return false;
233        }
234        try {
235            if (!self::makeApiV4Call($customer, 'delete')) {
236                return false;
237            }
238        } catch (ApiErrorException $e) {
239            switch ($e->getApiError()) {
240                case ApiError::authorizationException->value():
241                case ApiError::modelNotFound->value():
242                    return false;
243                default:
244                    throw $e;
245            }
246        }
247        self::deleteUserFromTable();
248        return true;
249    }
250
251    /**
252     * Toggle the current user optin value
253     * on the current user and the crm
254     *
255     * @return boolean true if the operation succeeded
256     * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured.
257     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
258     * Possibles are : - modelNotFound, If this customer hashcode could not be found.
259     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
260     */
261    public static function toggleUserOptin(): bool
262    {
263        if (!self::getUser() or !($customer = self::getCustomer())) {
264            return false;
265        }
266        try {
267            if (!self::makeApiV4Call($customer, 'save')) {
268                return false;
269            }
270        } catch (ApiErrorException $e) {
271            switch ($e->getApiError()) {
272                case ApiError::authorizationException->value():
273                case ApiError::modelNotFound->value():
274                    return false;
275                default:
276                    throw $e;
277            }
278        }
279        $customer->opt_in = !$customer->opt_in;
280        return true;
281    }
282
283    /**
284     * Set the single optin status for an email
285     * on the crm
286     *
287     * @param string  $email
288     * @param boolean $opt_in
289     *
290     * @return boolean true if the operation succeeded false otherwise
291     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field email could not be validated.
292     * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response.
293     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
294     * Possibles are : - singleOptinCustomerExists, If a customer exists with this email.
295     */
296    public static function setSingleOptin(string $email, bool $opt_in): bool
297    {
298        return self::makeApiV4Call(SingleOptin::class, 'setStatus', [
299            $email, $opt_in
300        ]);
301    }
302
303    /**
304     * Toggle the current user optin value
305     * on the current user and the crm
306     *
307     * @return boolean True if the operation succeeded.
308     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a customer's hashcode could not be validated.
309     * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response.
310     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
311     * Possibles are : - modelNotFound, If this customer hashcode could not be found.
312     *                 - customerAlreadyActive, It the customer is already activated.
313     */
314    public static function activateUser(): bool
315    {
316        if (!self::getUser() or !($customer = self::getCustomer())) {
317            return false;
318        }
319        if ($customer->status) {
320            return true;
321        }
322        if (!self::makeApiV4Call($customer, 'activate')) {
323            return false;
324        }
325        $customer->opt_in = !$customer->opt_in;
326        return true;
327    }
328
329    /**
330     * Generate a new user
331     *
332     * @return WPCrmUser
333     */
334    public static function getNewUser(): WPCrmUser
335    {
336        self::$user = new WPCrmUser();
337        return self::$user;
338    }
339
340    /**
341     * Remove active user
342     * to prevent WPCrm::check to return true
343     *
344     * @return void
345     */
346    public static function unsetActiveUser(): void
347    {
348        self::$user = null;
349    }
350
351    // * --- PRIVATE --- METHODS
352
353    /**
354     * Check customer credentials using the CRM.
355     *
356     * @param string $email    The customer email.
357     * @param string $password The customer password.
358     *
359     * @return boolean
360     * @throws \LDC\LDCCRM\Exceptions\CrmException If an error occurs on server side or on getting response.
361     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs.
362     * Possibles are : - authFailed, If credentials are not valid.
363     *                 - customerNotActivated, If the account is not activated.
364     */
365    private static function loginUserWithApi(string $email, string $password): bool
366    {
367        self::initLdcApi();
368
369        /** @var boolean $response */
370        $response = self::makeApiV4Call(self::$api, 'login', [$email, $password]);
371        return $response;
372    }
373
374    /**
375     * Get all customer infos
376     *
377     * @return \LDC\LDCCRM\Models\Customer|null
378     */
379    private static function getCustomer(): ?Customer
380    {
381        if (!self::getUser()) {
382            return null;
383        }
384        if (!Utils::getRawProperty(self::$user, 'info')) {
385            self::getUserFromApi(self::$user->hashcode);
386        }
387        /** @var \LDC\LDCCRM\Models\Customer */
388        $customer = Utils::getRawProperty(self::$user, 'info');
389        return $customer;
390    }
391
392    /**
393     * Create a user
394     * stores it to the session
395     * and finally saves it to the database if it does not exists
396     *
397     * @param \LDC\LDCCRM\Models\Customer $customer
398     *
399     * @return void
400     */
401    private static function setUser(Customer $customer): void
402    {
403        self::bootSessionIfNeeded();
404        if (!self::$user) {
405            self::$user = new WPCrmUser($customer->hashcode, $customer->email);
406        }
407        self::$user->setInfo($customer);
408    }
409
410    /**
411     * Get the initialized API object
412     *
413     * @return \LDC\LDCCRM\CrmApiV4
414     * @throws \ErrorException If php does not have extension curl.
415     */
416    private static function initLdcApi(): CrmApiV4
417    {
418        if (!self::$api) {
419            if (!extension_loaded('curl')) {
420                throw new \ErrorException(
421                    'WPCrm requires php extension curl'
422                );
423            }
424            self::$api = new CrmApiV4();
425        }
426        return self::$api;
427    }
428
429    /**
430     * Make a call using apiV4
431     *
432     * @param object|string $obj       An object or a class name.
433     * @param string        $method    The method to call.
434     * @param array         $arguments Arguments to pass.
435     * @return mixed|false
436     * @throws \LDC\LDCCRM\Exceptions\ApiErrorException If any of this exception is thrown.
437     * @throws \LDC\LDCCRM\Exceptions\CrmException If a critical error occured.
438     */
439    private static function makeApiV4Call($obj, string $method, array $arguments = [])
440    {
441        try {
442            return \forward_static_call_array([$obj, $method], $arguments);
443        } catch (ApiErrorException $e) {
444            switch ($e->getApiError()) {
445                case ApiError::apiAuthFailed->value():
446                    Utils::log($e->getMessage(), LogLevel::CRIT);
447                    throw $e;
448                default:
449                    Utils::log($e->getMessage(), LogLevel::DEBUG);
450                    throw $e;
451            }
452        } catch (CrmException $e) {
453            Utils::log($e->getMessage(), LogLevel::CRIT);
454            throw $e;
455        }
456    }
457
458    /**
459     * Boot the php session if needed
460     *
461     * @return void
462     */
463    private static function bootSessionIfNeeded(): void
464    {
465        self::$sessionKey = ($_SERVER['HTTP_USER_AGENT'] ?? '') . ($_SERVER['REMOTE_ADDR'] ?? '');
466        /** @var string self::$sessionKey */
467        self::$sessionKey = \strlen(self::$sessionKey) ? self::$sessionKey : (date('d') ? date('d') : '');
468        if (!session_id() && !self::$sessionDestroyed) {
469            session_start();
470        }
471        if (!isset($_SESSION['wpcrm'])) {
472            $_SESSION['wpcrm'] = [];
473        }
474        if (
475            // If session identity is violated.
476            count($_SESSION['wpcrm']) > 1 or
477            // If session timed out.
478            (isset($_SESSION['wpcrm'][self::$sessionKey]) and
479                isset($_SESSION['lastActivity']) and
480                // * 15 minutes session.
481                ($_SESSION['lastActivity'] + (15 * 60)) < time())
482        ) {
483            // Destroy session.
484            self::$sessionDestroyed = true;
485            $_SESSION['wpcrm']      = [];
486        } else {
487            $_SESSION['lastActivity'] = time();
488        }
489        if (!self::$user and isset($_SESSION['wpcrm'][self::$sessionKey])) {
490            self::$user = WPCrmUser::parseFromJson(
491                $_SESSION['wpcrm'][self::$sessionKey]
492            );
493        }
494    }
495
496    /**
497     * Get a customer from table
498     *
499     * @param string $hashcode The customer hashcode.
500     *
501     * @return array|null Database query result in format specified by $output or null on failure.
502     */
503    public static function getUserFromTable(string $hashcode): ?array
504    {
505        return self::getWpDb()->get_row(
506            self::getWpDb()->prepare(
507                'SELECT * FROM `ldc_wp_customers` WHERE `hashcode` = %s',
508                [
509                    'hashcode' => $hashcode
510                ]
511            ),
512            'ARRAY_A'
513        );
514    }
515
516    /**
517     * Get a customer from table by an email
518     *
519     * @param string $email The customer email.
520     *
521     * @return array|null Database query result in format specified by $output or null on failure.
522     */
523    public static function getUserFromTableByEmail(string $email): ?array
524    {
525        return self::getWpDb()->get_row(
526            self::getWpDb()->prepare(
527                'SELECT * FROM `ldc_wp_customers` WHERE `email` = %s',
528                [
529                    'email' => $email
530                ]
531            ),
532            'ARRAY_A'
533        );
534    }
535
536    /**
537     * Get a customer from table by an email
538     *
539     * @param string $uuid The customer transaction uuid.
540     *
541     * @return array|null Database query result in format specified by $output or null on failure.
542     */
543    public static function getUserFromTableByUuid(string $uuid): ?array
544    {
545        return self::getWpDb()->get_row(
546            self::getWpDb()->prepare(
547                'SELECT * FROM `ldc_wp_customers` WHERE `uuid` = %s',
548                [
549                    'uuid' => $uuid
550                ]
551            ),
552            'ARRAY_A'
553        );
554    }
555
556    /**
557     * Save or update the user to the table
558     *
559     * @return integer The number of row affected on the table.
560     */
561    private static function saveOrUpdateUserToTable(): int
562    {
563        if (is_string(self::$user->hashcode) and self::getUserFromTable(self::$user->hashcode)) {
564            return self::query(
565                'UPDATE `ldc_wp_customers` SET `email` = %s, `uuid` = %s WHERE `hashcode` = %s',
566                [
567                    'email' => self::$user->email,
568                    'uuid' => self::$user->uuidToken,
569                    'hashcode' => self::$user->hashcode
570                ]
571            );
572        }
573        return self::query(
574            'INSERT INTO `ldc_wp_customers`(hashcode, email, uuid) VALUES (%s, %s, %s)',
575            [
576                'hashcode' => self::$user->hashcode,
577                'email' => self::$user->email,
578                'uuid' => self::$user->uuidToken
579            ]
580        );
581    }
582
583    /**
584     * Save or update the user to the table
585     *
586     * @return integer The number of row removed from the table.
587     */
588    private static function deleteUserFromTable(): int
589    {
590        return self::query(
591            'DELETE FROM `ldc_wp_customers` WHERE `hashcode` = %s;',
592            [
593                'hashcode' => self::$user->hashcode
594            ]
595        );
596    }
597
598    /**
599     * Query in DB
600     *
601     * @param string $query  La request avec les variables en points d'interrogation.
602     * @param array  $params Les paramètres des la requète.
603     *
604     * @return integer|boolean Boolean true for CREATE, ALTER, TRUNCATE and DROP queries.
605     * Number of rows affected/selected for all other queries. Boolean false on error.
606     */
607    private static function query(string $query, array $params = [])
608    {
609        return self::getWpDb()->query(
610            self::getWpDb()->prepare($query, $params)
611        );
612    }
613
614    /**
615     * Get WP 'pdo instance'
616     *
617     * @return object The $wpdb variable
618     */
619    private static function getWpDb(): object
620    {
621        // phpcs:ignore Squiz.PHP.GlobalKeyword.NotAllowed
622        global $wpdb;
623        return $wpdb;
624    }
625}