1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26use PrestaShop\PrestaShop\Adapter\CoreException;
27use PrestaShop\PrestaShop\Adapter\ServiceLocator;
28
29/***
30 * Class CustomerCore
31 */
32class CustomerCore extends ObjectModel
33{
34    /** @var int Customer ID */
35    public $id;
36
37    /** @var int Shop ID */
38    public $id_shop;
39
40    /** @var int ShopGroup ID */
41    public $id_shop_group;
42
43    /** @var string Secure key */
44    public $secure_key;
45
46    /** @var string protected note */
47    public $note;
48
49    /** @var int Gender ID */
50    public $id_gender = 0;
51
52    /** @var int Default group ID */
53    public $id_default_group;
54
55    /** @var int Current language used by the customer */
56    public $id_lang;
57
58    /** @var string Lastname */
59    public $lastname;
60
61    /** @var string Firstname */
62    public $firstname;
63
64    /** @var string Birthday (yyyy-mm-dd) */
65    public $birthday = null;
66
67    /** @var string e-mail */
68    public $email;
69
70    /** @var bool Newsletter subscription */
71    public $newsletter;
72
73    /** @var string Newsletter ip registration */
74    public $ip_registration_newsletter;
75
76    /** @var string Newsletter registration date */
77    public $newsletter_date_add;
78
79    /** @var bool Opt-in subscription */
80    public $optin;
81
82    /** @var string WebSite * */
83    public $website;
84
85    /** @var string Company */
86    public $company;
87
88    /** @var string SIRET */
89    public $siret;
90
91    /** @var string APE */
92    public $ape;
93
94    /** @var float Outstanding allow amount (B2B opt) */
95    public $outstanding_allow_amount = 0;
96
97    /** @var int Show public prices (B2B opt) */
98    public $show_public_prices = 0;
99
100    /** @var int Risk ID (B2B opt) */
101    public $id_risk;
102
103    /** @var int Max payment day */
104    public $max_payment_days = 0;
105
106    /** @var string Password */
107    public $passwd;
108
109    /** @var string Datetime Password */
110    public $last_passwd_gen;
111
112    /** @var bool Status */
113    public $active = true;
114
115    /** @var bool Status */
116    public $is_guest = 0;
117
118    /** @var bool True if carrier has been deleted (staying in database as deleted) */
119    public $deleted = 0;
120
121    /** @var string Object creation date */
122    public $date_add;
123
124    /** @var string Object last modification date */
125    public $date_upd;
126
127    public $years;
128    public $days;
129    public $months;
130
131    /** @var int customer id_country as determined by geolocation */
132    public $geoloc_id_country;
133    /** @var int customer id_state as determined by geolocation */
134    public $geoloc_id_state;
135    /** @var string customer postcode as determined by geolocation */
136    public $geoloc_postcode;
137
138    /** @var bool is the customer logged in */
139    public $logged = 0;
140
141    /** @var int id_guest meaning the guest table, not the guest customer */
142    public $id_guest;
143
144    public $groupBox;
145
146    /** @var string Unique token for forgot password feature */
147    public $reset_password_token;
148
149    /** @var string token validity date for forgot password feature */
150    public $reset_password_validity;
151
152    protected $webserviceParameters = [
153        'objectMethods' => [
154            'add' => 'addWs',
155            'update' => 'updateWs',
156        ],
157        'fields' => [
158            'id_default_group' => ['xlink_resource' => 'groups'],
159            'id_lang' => ['xlink_resource' => 'languages'],
160            'newsletter_date_add' => [],
161            'ip_registration_newsletter' => [],
162            'last_passwd_gen' => ['setter' => null],
163            'secure_key' => ['setter' => null],
164            'deleted' => [],
165            'passwd' => ['setter' => 'setWsPasswd'],
166        ],
167        'associations' => [
168            'groups' => ['resource' => 'group'],
169        ],
170    ];
171
172    /**
173     * @see ObjectModel::$definition
174     */
175    public static $definition = [
176        'table' => 'customer',
177        'primary' => 'id_customer',
178        'fields' => [
179            'secure_key' => ['type' => self::TYPE_STRING, 'validate' => 'isMd5', 'copy_post' => false],
180            'lastname' => ['type' => self::TYPE_STRING, 'validate' => 'isCustomerName', 'required' => true, 'size' => 255],
181            'firstname' => ['type' => self::TYPE_STRING, 'validate' => 'isCustomerName', 'required' => true, 'size' => 255],
182            'email' => ['type' => self::TYPE_STRING, 'validate' => 'isEmail', 'required' => true, 'size' => 255],
183            'passwd' => ['type' => self::TYPE_STRING, 'validate' => 'isPasswd', 'required' => true, 'size' => 255],
184            'last_passwd_gen' => ['type' => self::TYPE_STRING, 'copy_post' => false],
185            'id_gender' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
186            'birthday' => ['type' => self::TYPE_DATE, 'validate' => 'isBirthDate'],
187            'newsletter' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
188            'newsletter_date_add' => ['type' => self::TYPE_DATE, 'copy_post' => false],
189            'ip_registration_newsletter' => ['type' => self::TYPE_STRING, 'copy_post' => false],
190            'optin' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
191            'website' => ['type' => self::TYPE_STRING, 'validate' => 'isUrl'],
192            'company' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName'],
193            'siret' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName'],
194            'ape' => ['type' => self::TYPE_STRING, 'validate' => 'isApe'],
195            'outstanding_allow_amount' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat', 'copy_post' => false],
196            'show_public_prices' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'copy_post' => false],
197            'id_risk' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'copy_post' => false],
198            'max_payment_days' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'copy_post' => false],
199            'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'copy_post' => false],
200            'deleted' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'copy_post' => false],
201            'note' => ['type' => self::TYPE_HTML, 'size' => 65000, 'copy_post' => false],
202            'is_guest' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'copy_post' => false],
203            'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'copy_post' => false],
204            'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'copy_post' => false],
205            'id_default_group' => ['type' => self::TYPE_INT, 'copy_post' => false],
206            'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'copy_post' => false],
207            'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
208            'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false],
209            'reset_password_token' => ['type' => self::TYPE_STRING, 'validate' => 'isSha1', 'size' => 40, 'copy_post' => false],
210            'reset_password_validity' => ['type' => self::TYPE_DATE, 'validate' => 'isDateOrNull', 'copy_post' => false],
211        ],
212    ];
213
214    protected static $_defaultGroupId = [];
215    protected static $_customerHasAddress = [];
216    protected static $_customer_groups = [];
217
218    /**
219     * CustomerCore constructor.
220     *
221     * @param int|null $id
222     */
223    public function __construct($id = null)
224    {
225        // It sets default value for customer group even when customer does not exist
226        $this->id_default_group = (int) Configuration::get('PS_CUSTOMER_GROUP');
227        parent::__construct($id);
228    }
229
230    /**
231     * Adds current Customer as a new Object to the database.
232     *
233     * @param bool $autoDate Automatically set `date_upd` and `date_add` columns
234     * @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
235     *
236     * @return bool Indicates whether the Customer has been successfully added
237     *
238     * @throws PrestaShopDatabaseException
239     * @throws PrestaShopException
240     */
241    public function add($autoDate = true, $nullValues = true)
242    {
243        $this->id_shop = ($this->id_shop) ? $this->id_shop : Context::getContext()->shop->id;
244        $this->id_shop_group = ($this->id_shop_group) ? $this->id_shop_group : Context::getContext()->shop->id_shop_group;
245        $this->id_lang = ($this->id_lang) ? $this->id_lang : Context::getContext()->language->id;
246        $this->birthday = (empty($this->years) ? $this->birthday : (int) $this->years . '-' . (int) $this->months . '-' . (int) $this->days);
247        $this->secure_key = md5(uniqid(mt_rand(0, mt_getrandmax()), true));
248        $this->last_passwd_gen = date('Y-m-d H:i:s', strtotime('-' . Configuration::get('PS_PASSWD_TIME_FRONT') . 'minutes'));
249
250        if ($this->newsletter && !Validate::isDate($this->newsletter_date_add)) {
251            $this->newsletter_date_add = date('Y-m-d H:i:s');
252        }
253
254        if ($this->id_default_group == Configuration::get('PS_CUSTOMER_GROUP')) {
255            if ($this->is_guest) {
256                $this->id_default_group = (int) Configuration::get('PS_GUEST_GROUP');
257            } else {
258                $this->id_default_group = (int) Configuration::get('PS_CUSTOMER_GROUP');
259            }
260        }
261
262        /* Can't create a guest customer, if this feature is disabled */
263        if ($this->is_guest && !Configuration::get('PS_GUEST_CHECKOUT_ENABLED')) {
264            return false;
265        }
266        $success = parent::add($autoDate, $nullValues);
267        $this->updateGroup($this->groupBox);
268
269        return $success;
270    }
271
272    /**
273     * Adds current Customer as a new Object to the database.
274     *
275     * @param bool $autoDate Automatically set `date_upd` and `date_add` columns
276     * @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
277     *
278     * @return bool Indicates whether the Customer has been successfully added
279     *
280     * @throws PrestaShopDatabaseException
281     * @throws PrestaShopException
282     */
283    public function addWs($autodate = true, $null_values = false)
284    {
285        if (Customer::customerExists($this->email)) {
286            WebserviceRequest::getInstance()->setError(
287                500,
288                $this->trans(
289                    'The email is already used, please choose another one',
290                     [],
291                    'Admin.Notifications.Error'
292                ),
293                140
294            );
295
296            return false;
297        }
298
299        return $this->add($autodate, $null_values);
300    }
301
302    /**
303     * Updates the current Customer in the database.
304     *
305     * @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
306     *
307     * @return bool Indicates whether the Customer has been successfully updated
308     *
309     * @throws PrestaShopDatabaseException
310     * @throws PrestaShopException
311     */
312    public function update($nullValues = false)
313    {
314        $this->birthday = (empty($this->years) ? $this->birthday : (int) $this->years . '-' . (int) $this->months . '-' . (int) $this->days);
315
316        if ($this->newsletter && !Validate::isDate($this->newsletter_date_add)) {
317            $this->newsletter_date_add = date('Y-m-d H:i:s');
318        }
319        if (isset(Context::getContext()->controller) && Context::getContext()->controller->controller_type == 'admin') {
320            $this->updateGroup($this->groupBox);
321        }
322
323        if ($this->deleted) {
324            $addresses = $this->getAddresses((int) Configuration::get('PS_LANG_DEFAULT'));
325            foreach ($addresses as $address) {
326                $obj = new Address((int) $address['id_address']);
327                $obj->deleted = true;
328                $obj->save();
329            }
330        }
331
332        try {
333            return parent::update(true);
334        } catch (\PrestaShopException $exception) {
335            $message = $exception->getMessage();
336            error_log($message);
337
338            return false;
339        }
340    }
341
342    /**
343     * Updates the current Customer in the database.
344     *
345     * @param bool $nullValues Whether we want to use NULL values instead of empty quotes values
346     *
347     * @return bool Indicates whether the Customer has been successfully updated
348     *
349     * @throws PrestaShopDatabaseException
350     * @throws PrestaShopException
351     */
352    public function updateWs($nullValues = false)
353    {
354        if (Customer::customerExists($this->email)
355            && Customer::customerExists($this->email, true) !== (int) $this->id
356        ) {
357            WebserviceRequest::getInstance()->setError(
358                500,
359                $this->trans(
360                    'The email is already used, please choose another one',
361                    [],
362                    'Admin.Notifications.Error'
363                ),
364                141
365            );
366
367            return false;
368        }
369
370        return $this->update($nullValues = false);
371    }
372
373    /**
374     * Deletes current Customer from the database.
375     *
376     * @return bool True if delete was successful
377     *
378     * @throws PrestaShopException
379     */
380    public function delete()
381    {
382        if (!count(Order::getCustomerOrders((int) $this->id))) {
383            $addresses = $this->getAddresses((int) Configuration::get('PS_LANG_DEFAULT'));
384            foreach ($addresses as $address) {
385                $obj = new Address((int) $address['id_address']);
386                $obj->delete();
387            }
388        }
389        Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customer_group` WHERE `id_customer` = ' . (int) $this->id);
390        Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'message WHERE id_customer=' . (int) $this->id);
391        Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'specific_price WHERE id_customer=' . (int) $this->id);
392
393        $carts = Db::getInstance()->executeS('SELECT id_cart FROM ' . _DB_PREFIX_ . 'cart WHERE id_customer=' . (int) $this->id);
394        if ($carts) {
395            foreach ($carts as $cart) {
396                Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'cart WHERE id_cart=' . (int) $cart['id_cart']);
397                Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'cart_product WHERE id_cart=' . (int) $cart['id_cart']);
398            }
399        }
400
401        $cts = Db::getInstance()->executeS('SELECT id_customer_thread FROM ' . _DB_PREFIX_ . 'customer_thread WHERE id_customer=' . (int) $this->id);
402        if ($cts) {
403            foreach ($cts as $ct) {
404                Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'customer_thread WHERE id_customer_thread=' . (int) $ct['id_customer_thread']);
405                Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'customer_message WHERE id_customer_thread=' . (int) $ct['id_customer_thread']);
406            }
407        }
408
409        CartRule::deleteByIdCustomer((int) $this->id);
410
411        return parent::delete();
412    }
413
414    /**
415     * Return customers list.
416     *
417     * @param bool|null $onlyActive Returns only active customers when `true`
418     *
419     * @return array Customers
420     */
421    public static function getCustomers($onlyActive = null)
422    {
423        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
424            '
425            SELECT `id_customer`, `email`, `firstname`, `lastname`
426            FROM `' . _DB_PREFIX_ . 'customer`
427            WHERE 1 ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) .
428            ($onlyActive ? ' AND `active` = 1' : '') . '
429            ORDER BY `id_customer` ASC'
430        );
431    }
432
433    /**
434     * Return customer instance from its e-mail (optionally check password).
435     *
436     * @param string $email e-mail
437     * @param string $plaintextPassword Password is also checked if specified
438     * @param bool $ignoreGuest
439     *
440     * @return bool|Customer|CustomerCore Customer instance
441     *
442     * @throws \InvalidArgumentException if given input is not valid
443     */
444    public function getByEmail($email, $plaintextPassword = null, $ignoreGuest = true)
445    {
446        if (!Validate::isEmail($email)) {
447            throw new \InvalidArgumentException(sprintf(
448                'Cannot get customer by email as %s is not a valid email',
449                $email
450            ));
451        }
452        if (($plaintextPassword && !Validate::isPlaintextPassword($plaintextPassword))) {
453            throw new \InvalidArgumentException(
454                'Cannot get customer by email as given password is not a valid password'
455            );
456        }
457
458        $shopGroup = Shop::getGroupFromShop(Shop::getContextShopID(), false);
459
460        $sql = new DbQuery();
461        $sql->select('c.`passwd`');
462        $sql->from('customer', 'c');
463        $sql->where('c.`email` = \'' . pSQL($email) . '\'');
464        if (Shop::getContext() == Shop::CONTEXT_SHOP && $shopGroup['share_customer']) {
465            $sql->where('c.`id_shop_group` = ' . (int) Shop::getContextShopGroupID());
466        } else {
467            $sql->where('c.`id_shop` IN (' . implode(', ', Shop::getContextListShopID(Shop::SHARE_CUSTOMER)) . ')');
468        }
469
470        if ($ignoreGuest) {
471            $sql->where('c.`is_guest` = 0');
472        }
473        $sql->where('c.`deleted` = 0');
474
475        $passwordHash = Db::getInstance()->getValue($sql);
476
477        try {
478            /** @var \PrestaShop\PrestaShop\Core\Crypto\Hashing $crypto */
479            $crypto = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Crypto\\Hashing');
480        } catch (CoreException $e) {
481            return false;
482        }
483
484        $shouldCheckPassword = null !== $plaintextPassword;
485        if ($shouldCheckPassword && !$crypto->checkHash($plaintextPassword, $passwordHash)) {
486            return false;
487        }
488
489        $sql = new DbQuery();
490        $sql->select('c.*');
491        $sql->from('customer', 'c');
492        $sql->where('c.`email` = \'' . pSQL($email) . '\'');
493        if (Shop::getContext() == Shop::CONTEXT_SHOP && $shopGroup['share_customer']) {
494            $sql->where('c.`id_shop_group` = ' . (int) Shop::getContextShopGroupID());
495        } else {
496            $sql->where('c.`id_shop` IN (' . implode(', ', Shop::getContextListShopID(Shop::SHARE_CUSTOMER)) . ')');
497        }
498        if ($ignoreGuest) {
499            $sql->where('c.`is_guest` = 0');
500        }
501        $sql->where('c.`deleted` = 0');
502
503        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
504
505        if (!$result) {
506            return false;
507        }
508
509        $this->id = $result['id_customer'];
510        foreach ($result as $key => $value) {
511            if (property_exists($this, $key)) {
512                $this->{$key} = $value;
513            }
514        }
515
516        if ($shouldCheckPassword && !$crypto->isFirstHash($plaintextPassword, $passwordHash)) {
517            $this->passwd = $crypto->hash($plaintextPassword);
518            $this->update();
519        }
520
521        return $this;
522    }
523
524    /**
525     * Retrieve customers by email address.
526     *
527     * @param string $email
528     *
529     * @return array
530     */
531    public static function getCustomersByEmail($email)
532    {
533        $sql = 'SELECT *
534                FROM `' . _DB_PREFIX_ . 'customer`
535                WHERE `email` = \'' . pSQL($email) . '\'
536                    ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER);
537
538        return Db::getInstance()->executeS($sql);
539    }
540
541    /**
542     * Check id the customer is active or not.
543     *
544     * @param int $idCustomer
545     *
546     * @return bool Customer validity
547     */
548    public static function isBanned($idCustomer)
549    {
550        if (!Validate::isUnsignedId($idCustomer)) {
551            return true;
552        }
553        $cacheId = 'Customer::isBanned_' . (int) $idCustomer;
554        if (!Cache::isStored($cacheId)) {
555            $result = (bool) !Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
556            SELECT `id_customer`
557            FROM `' . _DB_PREFIX_ . 'customer`
558            WHERE `id_customer` = \'' . (int) $idCustomer . '\'
559            AND active = 1
560            AND `deleted` = 0');
561            Cache::store($cacheId, $result);
562
563            return $result;
564        }
565
566        return Cache::retrieve($cacheId);
567    }
568
569    /**
570     * Check if e-mail is already registered in database.
571     *
572     * @param string $email e-mail
573     * @param bool $returnId
574     * @param bool $ignoreGuest To exclude guest customer
575     *
576     * @return bool|int Customer ID if found
577     *                  `false` otherwise
578     */
579    public static function customerExists($email, $returnId = false, $ignoreGuest = true)
580    {
581        if (!Validate::isEmail($email)) {
582            return false;
583        }
584
585        $result = Db::getInstance()->getValue('
586        SELECT `id_customer`
587        FROM `' . _DB_PREFIX_ . 'customer`
588        WHERE `email` = \'' . pSQL($email) . '\'
589        ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) . '
590        ' . ($ignoreGuest ? ' AND `is_guest` = 0' : ''), false);
591
592        return $returnId ? (int) $result : (bool) $result;
593    }
594
595    /**
596     * Check if an address is owned by a customer.
597     *
598     * @param int $idCustomer Customer ID
599     * @param int $idAddress Address ID
600     *
601     * @return bool result
602     */
603    public static function customerHasAddress($idCustomer, $idAddress)
604    {
605        $key = (int) $idCustomer . '-' . (int) $idAddress;
606        if (!array_key_exists($key, self::$_customerHasAddress)) {
607            self::$_customerHasAddress[$key] = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
608            SELECT `id_address`
609            FROM `' . _DB_PREFIX_ . 'address`
610            WHERE `id_customer` = ' . (int) $idCustomer . '
611            AND `id_address` = ' . (int) $idAddress . '
612            AND `deleted` = 0');
613        }
614
615        return self::$_customerHasAddress[$key];
616    }
617
618    /**
619     * Reset Address cache.
620     *
621     * @param int $idCustomer Customer ID
622     * @param int $idAddress Address ID
623     */
624    public static function resetAddressCache($idCustomer = null, $idAddress = null)
625    {
626        if ($idCustomer === null || $idAddress === null) {
627            self::$_customerHasAddress = [];
628            self::$_customer_groups = [];
629            self::$_defaultGroupId = [];
630        }
631        $key = (int) $idCustomer . '-' . (int) $idAddress;
632        if (array_key_exists($key, self::$_customerHasAddress)) {
633            unset(self::$_customerHasAddress[$key]);
634        }
635    }
636
637    /**
638     * Return customer addresses.
639     *
640     * @param int $idLang Language ID
641     *
642     * @return array Addresses
643     */
644    public function getAddresses($idLang)
645    {
646        $group = Context::getContext()->shop->getGroup();
647        $shareOrder = isset($group->share_order) ? (bool) $group->share_order : false;
648        $cacheId = 'Customer::getAddresses'
649            . '-' . (int) $this->id
650            . '-' . (int) $idLang
651            . '-' . ($shareOrder ? 1 : 0);
652        if (!Cache::isStored($cacheId)) {
653            $sql = 'SELECT DISTINCT a.*, cl.`name` AS country, s.name AS state, s.iso_code AS state_iso
654                    FROM `' . _DB_PREFIX_ . 'address` a
655                    LEFT JOIN `' . _DB_PREFIX_ . 'country` c ON (a.`id_country` = c.`id_country`)
656                    LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (c.`id_country` = cl.`id_country`)
657                    LEFT JOIN `' . _DB_PREFIX_ . 'state` s ON (s.`id_state` = a.`id_state`)
658                    ' . ($shareOrder ? '' : Shop::addSqlAssociation('country', 'c')) . '
659                    WHERE `id_lang` = ' . (int) $idLang . ' AND `id_customer` = ' . (int) $this->id . ' AND a.`deleted` = 0';
660
661            $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
662            Cache::store($cacheId, $result);
663
664            return $result;
665        }
666
667        return Cache::retrieve($cacheId);
668    }
669
670    /**
671     * Get simplified Addresses arrays.
672     *
673     * @param int|null $idLang Language ID
674     *
675     * @return array
676     */
677    public function getSimpleAddresses($idLang = null)
678    {
679        if (!$this->id) {
680            return [];
681        }
682
683        if (null === $idLang) {
684            $idLang = Context::getContext()->language->id;
685        }
686
687        $sql = $this->getSimpleAddressSql(null, $idLang);
688        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
689        $addresses = [];
690        foreach ($result as $addr) {
691            $addresses[$addr['id']] = $addr;
692        }
693
694        return $addresses;
695    }
696
697    /**
698     * Get Address as array.
699     *
700     * @param int $idAddress Address ID
701     * @param int|null $idLang Language ID
702     *
703     * @return array|false|mysqli_result|PDOStatement|resource|null
704     */
705    public function getSimpleAddress($idAddress, $idLang = null)
706    {
707        if (!$this->id || !(int) $idAddress || !$idAddress) {
708            return [
709                'id' => '',
710                'alias' => '',
711                'firstname' => '',
712                'lastname' => '',
713                'company' => '',
714                'address1' => '',
715                'address2' => '',
716                'postcode' => '',
717                'city' => '',
718                'id_state' => '',
719                'state' => '',
720                'state_iso' => '',
721                'id_country' => '',
722                'country' => '',
723                'country_iso' => '',
724                'other' => '',
725                'phone' => '',
726                'phone_mobile' => '',
727                'vat_number' => '',
728                'dni' => '',
729            ];
730        }
731
732        $sql = $this->getSimpleAddressSql($idAddress, $idLang);
733        $res = Db::getInstance()->executeS($sql);
734        if (count($res) === 1) {
735            return $res[0];
736        } else {
737            return $res;
738        }
739    }
740
741    /**
742     * Get SQL query to retrieve Address in an array.
743     *
744     * @param int|null $idAddress Address ID
745     * @param int|null $idLang Language ID
746     *
747     * @return string
748     */
749    public function getSimpleAddressSql($idAddress = null, $idLang = null)
750    {
751        if (null === $idLang) {
752            $idLang = Context::getContext()->language->id;
753        }
754        $shareOrder = (bool) Context::getContext()->shop->getGroup()->share_order;
755
756        $sql = 'SELECT DISTINCT
757                      a.`id_address` AS `id`,
758                      a.`alias`,
759                      a.`firstname`,
760                      a.`lastname`,
761                      a.`company`,
762                      a.`address1`,
763                      a.`address2`,
764                      a.`postcode`,
765                      a.`city`,
766                      a.`id_state`,
767                      s.name AS state,
768                      s.`iso_code` AS state_iso,
769                      a.`id_country`,
770                      cl.`name` AS country,
771                      co.`iso_code` AS country_iso,
772                      a.`other`,
773                      a.`phone`,
774                      a.`phone_mobile`,
775                      a.`vat_number`,
776                      a.`dni`
777                    FROM `' . _DB_PREFIX_ . 'address` a
778                    LEFT JOIN `' . _DB_PREFIX_ . 'country` co ON (a.`id_country` = co.`id_country`)
779                    LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (co.`id_country` = cl.`id_country`)
780                    LEFT JOIN `' . _DB_PREFIX_ . 'state` s ON (s.`id_state` = a.`id_state`)
781                    ' . ($shareOrder ? '' : Shop::addSqlAssociation('country', 'co')) . '
782                    WHERE
783                        `id_lang` = ' . (int) $idLang . '
784                        AND `id_customer` = ' . (int) $this->id . '
785                        AND a.`deleted` = 0
786                        AND a.`active` = 1';
787
788        if (null !== $idAddress) {
789            $sql .= ' AND a.`id_address` = ' . (int) $idAddress;
790        }
791
792        return $sql;
793    }
794
795    /**
796     * Count the number of addresses for a customer.
797     *
798     * @param int $idCustomer Customer ID
799     *
800     * @return int Number of addresses
801     */
802    public static function getAddressesTotalById($idCustomer)
803    {
804        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
805            '
806            SELECT COUNT(`id_address`)
807            FROM `' . _DB_PREFIX_ . 'address`
808            WHERE `id_customer` = ' . (int) $idCustomer . '
809            AND `deleted` = 0'
810        );
811    }
812
813    /**
814     * Check if customer password is the right one.
815     *
816     * @param int $idCustomer Customer ID
817     * @param string $passwordHash Hashed password
818     *
819     * @return bool result
820     */
821    public static function checkPassword($idCustomer, $passwordHash)
822    {
823        if (!Validate::isUnsignedId($idCustomer)) {
824            die(Tools::displayError());
825        }
826
827        // Check that customers password hasn't changed since last login
828        $context = Context::getContext();
829        if ($passwordHash != $context->cookie->__get('passwd')) {
830            return false;
831        }
832
833        $cacheId = 'Customer::checkPassword' . (int) $idCustomer . '-' . $passwordHash;
834        if (!Cache::isStored($cacheId)) {
835            $sql = new DbQuery();
836            $sql->select('c.`id_customer`');
837            $sql->from('customer', 'c');
838            $sql->where('c.`id_customer` = ' . (int) $idCustomer);
839            $sql->where('c.`passwd` = \'' . pSQL($passwordHash) . '\'');
840
841            $result = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
842
843            Cache::store($cacheId, $result);
844
845            return $result;
846        }
847
848        return Cache::retrieve($cacheId);
849    }
850
851    /**
852     * Light back office search for customers.
853     *
854     * @param string $query Searched string
855     * @param int|null $limit Limit query results
856     *
857     * @return array|false|mysqli_result|PDOStatement|resource|null Corresponding customers
858     *
859     * @throws PrestaShopDatabaseException
860     */
861    public static function searchByName($query, $limit = null)
862    {
863        $sql = 'SELECT *
864                FROM `' . _DB_PREFIX_ . 'customer`
865                WHERE 1';
866        $search_items = explode(' ', $query);
867        $research_fields = ['id_customer', 'firstname', 'lastname', 'email'];
868        if (Configuration::get('PS_B2B_ENABLE')) {
869            $research_fields[] = 'company';
870        }
871
872        $items = [];
873        foreach ($research_fields as $field) {
874            foreach ($search_items as $item) {
875                $items[$item][] = $field . ' LIKE \'%' . pSQL($item) . '%\' ';
876            }
877        }
878
879        foreach ($items as $likes) {
880            $sql .= ' AND (' . implode(' OR ', $likes) . ') ';
881        }
882
883        $sql .= Shop::addSqlRestriction(Shop::SHARE_CUSTOMER);
884
885        if ($limit) {
886            $sql .= ' LIMIT 0, ' . (int) $limit;
887        }
888
889        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
890    }
891
892    /**
893     * Search for customers by ip address.
894     *
895     * @param string $ip Searched string
896     *
897     * @return array|false|mysqli_result|PDOStatement|resource|null
898     */
899    public static function searchByIp($ip)
900    {
901        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
902        SELECT DISTINCT c.*
903        FROM `' . _DB_PREFIX_ . 'customer` c
904        LEFT JOIN `' . _DB_PREFIX_ . 'guest` g ON g.id_customer = c.id_customer
905        LEFT JOIN `' . _DB_PREFIX_ . 'connections` co ON g.id_guest = co.id_guest
906        WHERE co.`ip_address` = \'' . (int) ip2long(trim($ip)) . '\'');
907    }
908
909    /**
910     * Return several useful statistics about customer.
911     *
912     * @return array Stats
913     */
914    public function getStats()
915    {
916        $result = Db::getInstance()->getRow('
917        SELECT COUNT(`id_order`) AS nb_orders, SUM(`total_paid` / o.`conversion_rate`) AS total_orders
918        FROM `' . _DB_PREFIX_ . 'orders` o
919        WHERE o.`id_customer` = ' . (int) $this->id . '
920        AND o.valid = 1');
921
922        $result2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
923        SELECT c.`date_add` AS last_visit
924        FROM `' . _DB_PREFIX_ . 'connections` c
925        LEFT JOIN `' . _DB_PREFIX_ . 'guest` g USING (id_guest)
926        WHERE g.`id_customer` = ' . (int) $this->id . ' ORDER BY c.`date_add` DESC ');
927
928        $result3 = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
929        SELECT (YEAR(CURRENT_DATE)-YEAR(c.`birthday`)) - (RIGHT(CURRENT_DATE, 5)<RIGHT(c.`birthday`, 5)) AS age
930        FROM `' . _DB_PREFIX_ . 'customer` c
931        WHERE c.`id_customer` = ' . (int) $this->id);
932
933        $result['last_visit'] = $result2['last_visit'] ?? null;
934        $result['age'] = (isset($result3['age']) && $result3['age'] != date('Y') ? $result3['age'] : '--');
935
936        return $result;
937    }
938
939    /**
940     * Get last 10 emails sent to the Customer.
941     *
942     * @return array|false|mysqli_result|PDOStatement|resource|null
943     */
944    public function getLastEmails()
945    {
946        if (!$this->id) {
947            return [];
948        }
949
950        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
951        SELECT m.*, l.name as language
952        FROM `' . _DB_PREFIX_ . 'mail` m
953        LEFT JOIN `' . _DB_PREFIX_ . 'lang` l ON m.id_lang = l.id_lang
954        WHERE `recipient` = "' . pSQL($this->email) . '"
955        ORDER BY m.date_add DESC
956        LIMIT 10');
957    }
958
959    /**
960     * Get last 10 Connections of the Customer.
961     *
962     * @return array|false|mysqli_result|PDOStatement|resource|null
963     */
964    public function getLastConnections()
965    {
966        if (!$this->id) {
967            return [];
968        }
969
970        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
971            '
972            SELECT c.id_connections, c.date_add, COUNT(cp.id_page) AS pages, TIMEDIFF(MAX(cp.time_end), c.date_add) as time, http_referer,INET_NTOA(ip_address) as ipaddress
973            FROM `' . _DB_PREFIX_ . 'guest` g
974            LEFT JOIN `' . _DB_PREFIX_ . 'connections` c ON c.id_guest = g.id_guest
975            LEFT JOIN `' . _DB_PREFIX_ . 'connections_page` cp ON c.id_connections = cp.id_connections
976            WHERE g.`id_customer` = ' . (int) $this->id . '
977            GROUP BY c.`id_connections`
978            ORDER BY c.date_add DESC
979            LIMIT 10'
980        );
981    }
982
983    /**
984     * Check if Customer ID exists.
985     *
986     * @param int $idCustomer Customer ID
987     *
988     * @return int|null Customer ID if found
989     */
990    public static function customerIdExistsStatic($idCustomer)
991    {
992        $cacheId = 'Customer::customerIdExistsStatic' . (int) $idCustomer;
993        if (!Cache::isStored($cacheId)) {
994            $result = (int) Db::getInstance()->getValue('
995            SELECT `id_customer`
996            FROM ' . _DB_PREFIX_ . 'customer c
997            WHERE c.`id_customer` = ' . (int) $idCustomer);
998            Cache::store($cacheId, $result);
999
1000            return $result;
1001        }
1002
1003        return Cache::retrieve($cacheId);
1004    }
1005
1006    /**
1007     * Update customer groups associated to the object.
1008     *
1009     * @param array $list groups
1010     */
1011    public function updateGroup($list)
1012    {
1013        Hook::exec('actionCustomerBeforeUpdateGroup', ['id_customer' => $this->id, 'groups' => $list]);
1014        if ($list && !empty($list)) {
1015            $this->cleanGroups();
1016            $this->addGroups($list);
1017        } else {
1018            $this->addGroups([$this->id_default_group]);
1019        }
1020    }
1021
1022    /**
1023     * Remove this Customer ID from Customer Groups.
1024     *
1025     * @return bool Indicates whether the Customer ID has been successfully removed
1026     *              from the Customer Group Db table
1027     */
1028    public function cleanGroups()
1029    {
1030        return Db::getInstance()->delete('customer_group', 'id_customer = ' . (int) $this->id);
1031    }
1032
1033    /**
1034     * Add the Customer to the given Customer Groups.
1035     *
1036     * @param array $groups Customer Group IDs
1037     */
1038    public function addGroups($groups)
1039    {
1040        Hook::exec('actionCustomerAddGroups', ['id_customer' => $this->id, 'groups' => $groups]);
1041        foreach ($groups as $group) {
1042            $row = ['id_customer' => (int) $this->id, 'id_group' => (int) $group];
1043            Db::getInstance()->insert('customer_group', $row, false, true, Db::INSERT_IGNORE);
1044        }
1045    }
1046
1047    /**
1048     * Get Groups that have the given Customer ID.
1049     *
1050     * @param int $idCustomer Customer ID
1051     *
1052     * @return array|mixed
1053     */
1054    public static function getGroupsStatic($idCustomer)
1055    {
1056        if (!Group::isFeatureActive()) {
1057            return [Configuration::get('PS_CUSTOMER_GROUP')];
1058        }
1059
1060        if ($idCustomer == 0) {
1061            self::$_customer_groups[$idCustomer] = [(int) Configuration::get('PS_UNIDENTIFIED_GROUP')];
1062        }
1063
1064        if (!isset(self::$_customer_groups[$idCustomer])) {
1065            self::$_customer_groups[$idCustomer] = [];
1066            $result = Db::getInstance()->executeS('
1067            SELECT cg.`id_group`
1068            FROM ' . _DB_PREFIX_ . 'customer_group cg
1069            WHERE cg.`id_customer` = ' . (int) $idCustomer);
1070            foreach ($result as $group) {
1071                self::$_customer_groups[$idCustomer][] = (int) $group['id_group'];
1072            }
1073        }
1074
1075        return self::$_customer_groups[$idCustomer];
1076    }
1077
1078    public function getGroups()
1079    {
1080        return Customer::getGroupsStatic((int) $this->id);
1081    }
1082
1083    /**
1084     * Get Products bought by this Customer.
1085     *
1086     * @return array|false|mysqli_result|PDOStatement|resource|null
1087     */
1088    public function getBoughtProducts()
1089    {
1090        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
1091        SELECT * FROM `' . _DB_PREFIX_ . 'orders` o
1092        LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order
1093        WHERE o.valid = 1 AND o.`id_customer` = ' . (int) $this->id);
1094    }
1095
1096    /**
1097     * Get Default Customer Group ID.
1098     *
1099     * @param int $idCustomer Customer ID
1100     *
1101     * @return mixed|string|null
1102     */
1103    public static function getDefaultGroupId($idCustomer)
1104    {
1105        if (!Group::isFeatureActive()) {
1106            static $psCustomerGroup = null;
1107            if ($psCustomerGroup === null) {
1108                $psCustomerGroup = Configuration::get('PS_CUSTOMER_GROUP');
1109            }
1110
1111            return $psCustomerGroup;
1112        }
1113
1114        if (!isset(self::$_defaultGroupId[(int) $idCustomer])) {
1115            self::$_defaultGroupId[(int) $idCustomer] = Db::getInstance()->getValue(
1116                '
1117                SELECT `id_default_group`
1118                FROM `' . _DB_PREFIX_ . 'customer`
1119                WHERE `id_customer` = ' . (int) $idCustomer
1120            );
1121        }
1122
1123        return self::$_defaultGroupId[(int) $idCustomer];
1124    }
1125
1126    /**
1127     * Get current Country.
1128     *
1129     * @param int $idCustomer
1130     * @param Cart|null $cart
1131     *
1132     * @return int Country ID
1133     */
1134    public static function getCurrentCountry($idCustomer, Cart $cart = null)
1135    {
1136        if (!$cart) {
1137            $cart = Context::getContext()->cart;
1138        }
1139        if (!$cart || !$cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}) {
1140            $idAddress = (int) Db::getInstance()->getValue(
1141                '
1142                SELECT `id_address`
1143                FROM `' . _DB_PREFIX_ . 'address`
1144                WHERE `id_customer` = ' . (int) $idCustomer . '
1145                AND `deleted` = 0 ORDER BY `id_address`'
1146            );
1147        } else {
1148            $idAddress = $cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
1149        }
1150        $ids = Address::getCountryAndState($idAddress);
1151
1152        return (int) ($ids['id_country'] ?? Configuration::get('PS_COUNTRY_DEFAULT'));
1153    }
1154
1155    /**
1156     * Is the current Customer a Guest?
1157     *
1158     * @return bool Indicates whether the Customer is a Guest
1159     */
1160    public function isGuest()
1161    {
1162        return (bool) $this->is_guest;
1163    }
1164
1165    /**
1166     * Transform the Guest to a Customer.
1167     *
1168     * @param int $idLang Language ID
1169     * @param string|null $password Password
1170     *
1171     * @return bool
1172     */
1173    public function transformToCustomer($idLang, $password = null)
1174    {
1175        if (!$this->isGuest()) {
1176            return false;
1177        }
1178        if (empty($password)) {
1179            $password = Tools::passwdGen(8, 'RANDOM');
1180        }
1181        if (!Validate::isPasswd($password)) {
1182            return false;
1183        }
1184
1185        $language = new Language($idLang);
1186        if (!Validate::isLoadedObject($language)) {
1187            $language = Context::getContext()->language;
1188        }
1189
1190        /** @var \PrestaShop\PrestaShop\Core\Crypto\Hashing $crypto */
1191        $crypto = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Crypto\\Hashing');
1192        $this->is_guest = 0;
1193        $this->passwd = $crypto->hash($password);
1194        $this->cleanGroups();
1195        $this->addGroups([Configuration::get('PS_CUSTOMER_GROUP')]);
1196        $this->id_default_group = Configuration::get('PS_CUSTOMER_GROUP');
1197        $this->stampResetPasswordToken();
1198        if ($this->update()) {
1199            $vars = [
1200                '{firstname}' => $this->firstname,
1201                '{lastname}' => $this->lastname,
1202                '{email}' => $this->email,
1203                '{url}' => Context::getContext()->link->getPageLink(
1204                    'password',
1205                    true,
1206                    null,
1207                    sprintf(
1208                        'token=%s&id_customer=%s&reset_token=%s',
1209                        $this->secure_key,
1210                        (int) $this->id,
1211                        $this->reset_password_token
1212                    )
1213                ),
1214            ];
1215            Mail::Send(
1216                (int) $idLang,
1217                'guest_to_customer',
1218                Context::getContext()->getTranslator()->trans(
1219                    'Your guest account has been transformed into a customer account',
1220                    [],
1221                    'Emails.Subject',
1222                    $language->locale
1223                ),
1224                $vars,
1225                $this->email,
1226                $this->firstname . ' ' . $this->lastname,
1227                null,
1228                null,
1229                null,
1230                null,
1231                _PS_MAIL_DIR_,
1232                false,
1233                (int) $this->id_shop
1234            );
1235
1236            return true;
1237        }
1238
1239        return false;
1240    }
1241
1242    /**
1243     * Set password
1244     * (for webservice).
1245     *
1246     * @param string $passwd Password
1247     *
1248     * @return bool Indictes whether the password has been successfully set
1249     */
1250    public function setWsPasswd($passwd)
1251    {
1252        /** @var \PrestaShop\PrestaShop\Core\Crypto\Hashing $crypto */
1253        $crypto = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Crypto\\Hashing');
1254        if ($this->id == 0 || $this->passwd != $passwd) {
1255            $this->passwd = $crypto->hash($passwd);
1256        }
1257
1258        return true;
1259    }
1260
1261    /**
1262     * Check customer information and return customer validity.
1263     *
1264     * @since 1.5.0
1265     *
1266     * @param bool $withGuest
1267     *
1268     * @return bool customer validity
1269     */
1270    public function isLogged($withGuest = false)
1271    {
1272        if (!$withGuest && $this->is_guest == 1) {
1273            return false;
1274        }
1275
1276        /* Customer is valid only if it can be load and if object password is the same as database one */
1277        return
1278            $this->logged == 1
1279            && $this->id
1280            && Validate::isUnsignedId($this->id)
1281            && Customer::checkPassword($this->id, $this->passwd)
1282            && Context::getContext()->cookie->isSessionAlive()
1283        ;
1284    }
1285
1286    /**
1287     * Logout.
1288     *
1289     * @since 1.5.0
1290     */
1291    public function logout()
1292    {
1293        Hook::exec('actionCustomerLogoutBefore', ['customer' => $this]);
1294
1295        if (isset(Context::getContext()->cookie)) {
1296            Context::getContext()->cookie->logout();
1297        }
1298
1299        $this->logged = 0;
1300
1301        Hook::exec('actionCustomerLogoutAfter', ['customer' => $this]);
1302    }
1303
1304    /**
1305     * Soft logout, delete everything that links to the customer
1306     * but leave there affiliate's information.
1307     *
1308     * @since 1.5.0
1309     */
1310    public function mylogout()
1311    {
1312        Hook::exec('actionCustomerLogoutBefore', ['customer' => $this]);
1313
1314        if (isset(Context::getContext()->cookie)) {
1315            Context::getContext()->cookie->mylogout();
1316        }
1317
1318        $this->logged = 0;
1319
1320        Hook::exec('actionCustomerLogoutAfter', ['customer' => $this]);
1321    }
1322
1323    /**
1324     * Get last empty Cart for this Customer, when last cart is not empty return false.
1325     *
1326     * @param bool|true $withOrder
1327     *
1328     * @return bool|int
1329     */
1330    public function getLastEmptyCart($withOrder = true)
1331    {
1332        $carts = Cart::getCustomerCarts((int) $this->id, $withOrder);
1333        if (!count($carts)) {
1334            return false;
1335        }
1336        $cart = array_shift($carts);
1337        $cart = new Cart((int) $cart['id_cart']);
1338
1339        return $cart->nbProducts() === 0 ? (int) $cart->id : false;
1340    }
1341
1342    /**
1343     * Validate controller.
1344     *
1345     * @param bool $htmlentities
1346     *
1347     * @return array
1348     */
1349    public function validateController($htmlentities = true)
1350    {
1351        $errors = parent::validateController($htmlentities);
1352        /** @var \PrestaShop\PrestaShop\Core\Crypto\Hashing $crypto */
1353        $crypto = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Crypto\\Hashing');
1354        if ($value = Tools::getValue('passwd')) {
1355            $this->passwd = $crypto->hash($value);
1356        }
1357
1358        return $errors;
1359    }
1360
1361    /**
1362     * Get outstanding amount.
1363     *
1364     * @return float Outstanding amount
1365     */
1366    public function getOutstanding()
1367    {
1368        $query = new DbQuery();
1369        $query->select('SUM(oi.total_paid_tax_incl)');
1370        $query->from('order_invoice', 'oi');
1371        $query->leftJoin('orders', 'o', 'oi.id_order = o.id_order');
1372        $query->groupBy('o.id_customer');
1373        $query->where('o.id_customer = ' . (int) $this->id);
1374        $totalPaid = (float) Db::getInstance()->getValue($query->build());
1375
1376        $query = new DbQuery();
1377        $query->select('SUM(op.amount)');
1378        $query->from('order_payment', 'op');
1379        $query->leftJoin('order_invoice_payment', 'oip', 'op.id_order_payment = oip.id_order_payment');
1380        $query->leftJoin('orders', 'o', 'oip.id_order = o.id_order');
1381        $query->groupBy('o.id_customer');
1382        $query->where('o.id_customer = ' . (int) $this->id);
1383        $totalRest = (float) Db::getInstance()->getValue($query->build());
1384
1385        return $totalPaid - $totalRest;
1386    }
1387
1388    /**
1389     * Get Customer Groups
1390     * (for webservice).
1391     *
1392     * @return array|false|mysqli_result|PDOStatement|resource|null
1393     */
1394    public function getWsGroups()
1395    {
1396        return Db::getInstance()->executeS(
1397            '
1398            SELECT cg.`id_group` as id
1399            FROM ' . _DB_PREFIX_ . 'customer_group cg
1400            ' . Shop::addSqlAssociation('group', 'cg') . '
1401            WHERE cg.`id_customer` = ' . (int) $this->id
1402        );
1403    }
1404
1405    /**
1406     * Set Customer Groups
1407     * (for webservice).
1408     *
1409     * @param $result
1410     *
1411     * @return bool
1412     */
1413    public function setWsGroups($result)
1414    {
1415        $groups = [];
1416        foreach ($result as $row) {
1417            $groups[] = $row['id'];
1418        }
1419        $this->cleanGroups();
1420        $this->addGroups($groups);
1421
1422        return true;
1423    }
1424
1425    /**
1426     * @see ObjectModel::getWebserviceObjectList()
1427     */
1428    public function getWebserviceObjectList($sqlJoin, $sqlFilter, $sqlSort, $sqlLimit)
1429    {
1430        $sqlFilter .= Shop::addSqlRestriction(Shop::SHARE_CUSTOMER, 'main');
1431
1432        return parent::getWebserviceObjectList($sqlJoin, $sqlFilter, $sqlSort, $sqlLimit);
1433    }
1434
1435    /**
1436     * Fill Reset password unique token with random sha1 and its validity date. For forgot password feature.
1437     */
1438    public function stampResetPasswordToken()
1439    {
1440        $salt = $this->id . '-' . $this->secure_key;
1441        $this->reset_password_token = sha1(time() . $salt);
1442        $validity = (int) Configuration::get('PS_PASSWD_RESET_VALIDITY') ?: 1440;
1443        $this->reset_password_validity = date('Y-m-d H:i:s', strtotime('+' . $validity . ' minutes'));
1444    }
1445
1446    /**
1447     * Test if a reset password token is present and is recent enough to avoid creating a new one (in case of customer triggering the forgot password link too often).
1448     */
1449    public function hasRecentResetPasswordToken()
1450    {
1451        if (!$this->reset_password_token || $this->reset_password_token == '') {
1452            return false;
1453        }
1454
1455        // TODO maybe use another 'recent' value for this test. For instance, equals password validity value.
1456        if (!$this->reset_password_validity || strtotime($this->reset_password_validity) < time()) {
1457            return false;
1458        }
1459
1460        return true;
1461    }
1462
1463    /**
1464     * Returns the valid reset password token if it validity date is > now().
1465     */
1466    public function getValidResetPasswordToken()
1467    {
1468        if (!$this->reset_password_token || $this->reset_password_token == '') {
1469            return false;
1470        }
1471
1472        if (!$this->reset_password_validity || strtotime($this->reset_password_validity) < time()) {
1473            return false;
1474        }
1475
1476        return $this->reset_password_token;
1477    }
1478
1479    /**
1480     * Delete reset password token data.
1481     */
1482    public function removeResetPasswordToken()
1483    {
1484        $this->reset_password_token = null;
1485        $this->reset_password_validity = null;
1486    }
1487}
1488