1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 *  @author    thirty bees <contact@thirtybees.com>
25 *  @author    PrestaShop SA <contact@prestashop.com>
26 *  @copyright 2017-2018 thirty bees
27 *  @copyright 2007-2016 PrestaShop SA
28 *  @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class ObjectModelCore
34 *
35 * @since 1.0.0
36 */
37abstract class ObjectModelCore implements Core_Foundation_Database_EntityInterface
38{
39    /**
40     * List of field types
41     */
42    const TYPE_INT     = 1;
43    const TYPE_BOOL    = 2;
44    const TYPE_STRING  = 3;
45    const TYPE_FLOAT   = 4;
46    const TYPE_DATE    = 5;
47    const TYPE_HTML    = 6;
48    const TYPE_NOTHING = 7;
49    const TYPE_SQL     = 8;
50    const TYPE_PRICE   = 9;
51
52    /**
53     * List of data to format
54     */
55    const FORMAT_COMMON = 1;
56    const FORMAT_LANG   = 2;
57    const FORMAT_SHOP   = 3;
58
59    /**
60     * List of association types
61     */
62    const HAS_ONE  = 1;
63    const HAS_MANY = 2;
64    const BELONGS_TO_MANY = 3;
65
66    // @codingStandardsIgnoreStart
67    /** @var int Object ID */
68    public $id;
69
70    /** @var int Language ID */
71    public $id_lang = null;
72
73    /** @var int Shop ID */
74    public $id_shop = null;
75
76    /** @var array|null List of shop IDs */
77    public $id_shop_list = null;
78
79    /** @var bool */
80    protected $get_shop_from_context = true;
81
82    /** @var array|null Holds required fields for each ObjectModel class */
83    protected static $fieldsRequiredDatabase = null;
84
85    /**
86     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
87     * @var string
88     */
89    protected $table;
90
91    /**
92     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
93     * @var string
94     */
95    protected $identifier;
96
97    /**
98     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
99     * @var array
100     */
101    protected $fieldsRequired = [];
102
103    /**
104     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
105     * @var array
106     */
107    protected $fieldsSize = [];
108
109    /**
110     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
111     * @var array
112     */
113    protected $fieldsValidate = [];
114
115    /**
116     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
117     * @var array
118     */
119    protected $fieldsRequiredLang = [];
120
121    /**
122     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
123     * @var array
124     */
125    protected $fieldsSizeLang = [];
126
127    /**
128     * @deprecated 1.0.0 Define property using $definition['table'] property instead.
129     * @var array
130     */
131    protected $fieldsValidateLang = [];
132
133    /**
134     * @deprecated 1.0.0
135     * @var array
136     */
137    protected $tables = [];
138
139    /** @var array Tables */
140    protected $webserviceParameters = [];
141
142    /** @var string Path to image directory. Used for image deletion. */
143    protected $image_dir = null;
144
145    /** @var String file type of image files. */
146    protected $image_format = 'jpg';
147
148    /**
149     * @var array Contains object definition
150     * @since 1.5.0.1
151     */
152    public static $definition = [];
153
154    /**
155     * Holds compiled definitions of each ObjectModel class.
156     * Values are assigned during object initialization.
157     *
158     * @var array
159     */
160    protected static $loaded_classes = [];
161
162    /** @var array Contains current object definition. */
163    protected $def;
164
165    /** @var array|null List of specific fields to update (all fields if null). */
166    protected $update_fields = null;
167
168    /** @var Db An instance of the db in order to avoid calling Db::getInstance() thousands of times. */
169    protected static $db = false;
170
171    /** @var bool Enables to define an ID before adding object. */
172    public $force_id = false;
173
174    /**
175     * @var bool If true, objects are cached in memory.
176     */
177    protected static $cache_objects = true;
178    // @codingStandardsIgnoreEnd
179
180    /**
181     * @return null
182     *
183     * @since   1.0.0
184     * @version 1.0.0 Initial version
185     */
186    public static function getRepositoryClassName()
187    {
188        return null;
189    }
190
191    /**
192     * Returns object validation rules (fields validity)
193     *
194     * @param  string $class Child class name for static use (optional)
195     *
196     * @return array Validation rules (fields validity)
197     *
198     * @since   1.0.0
199     * @version 1.0.0 Initial version
200     */
201    public static function getValidationRules($class = __CLASS__)
202    {
203        $object = new $class();
204
205        return [
206            'required'     => $object->fieldsRequired,
207            'size'         => $object->fieldsSize,
208            'validate'     => $object->fieldsValidate,
209            'requiredLang' => $object->fieldsRequiredLang,
210            'sizeLang'     => $object->fieldsSizeLang,
211            'validateLang' => $object->fieldsValidateLang,
212        ];
213    }
214
215    /**
216     * Builds the object
217     *
218     * @param int|null $id     If specified, loads and existing object from DB (optional).
219     * @param int|null $idLang Required if object is multilingual (optional).
220     * @param int|null $idShop ID shop for objects with multishop tables.
221     *
222     * @throws PrestaShopDatabaseException
223     * @throws PrestaShopException
224     *
225     * @since   1.0.0
226     * @version 1.0.0 Initial version
227     * @throws Adapter_Exception
228     */
229    public function __construct($id = null, $idLang = null, $idShop = null)
230    {
231        $className = get_class($this);
232        if (!isset(ObjectModel::$loaded_classes[$className])) {
233            $this->def = ObjectModel::getDefinition($className);
234            $this->setDefinitionRetrocompatibility();
235            if (!Validate::isTableOrIdentifier($this->def['primary']) || !Validate::isTableOrIdentifier($this->def['table'])) {
236                throw new PrestaShopException('Identifier or table format not valid for class '.$className);
237            }
238
239            ObjectModel::$loaded_classes[$className] = get_object_vars($this);
240        } else {
241            foreach (ObjectModel::$loaded_classes[$className] as $key => $value) {
242                $this->{$key} = $value;
243            }
244        }
245
246        if ($idLang !== null) {
247            $this->id_lang = (Language::getLanguage($idLang) !== false) ? $idLang : Configuration::get('PS_LANG_DEFAULT');
248        }
249
250        if ($idShop && $this->isMultishop()) {
251            $this->id_shop = (int) $idShop;
252            $this->get_shop_from_context = false;
253        }
254
255        if ($this->isMultishop() && !$this->id_shop) {
256            $this->id_shop = Context::getContext()->shop->id;
257        }
258
259        if ($id) {
260            $entityMapper = Adapter_ServiceLocator::get("Adapter_EntityMapper");
261            $entityMapper->load($id, $idLang, $this, $this->def, $this->id_shop, static::$cache_objects);
262        }
263    }
264
265    /**
266     * thirty bees' new coding style dictates that camelCase should be used
267     * rather than snake_case
268     * These magic methods provide backwards compatibility for modules/themes/whatevers
269     * that still access properties via their snake_case names
270     *
271     * @param string $property Property name
272     *
273     * @return mixed
274     *
275     * @since 1.0.1
276     */
277    public function &__get($property)
278    {
279        // Property to camelCase for backwards compatibility
280        $camelCaseProperty = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $property))));
281        if (property_exists($this, $camelCaseProperty)) {
282            return $this->$camelCaseProperty;
283        }
284
285        return $this->$property;
286    }
287
288    /**
289     * thirty bees' new coding style dictates that camelCase should be used
290     * rather than snake_case
291     * These magic methods provide backwards compatibility for modules/themes/whatevers
292     * that still access properties via their snake_case names
293     *
294     * @param string $property
295     * @param mixed  $value
296     *
297     * @return void
298     *
299     * @since 1.0.1
300     */
301    public function __set($property, $value)
302    {
303        // Property to camelCase for backwards compatibility
304        $snakeCaseProperty = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $property))));
305        if (property_exists($this, $snakeCaseProperty)) {
306            $this->$snakeCaseProperty = $value;
307        } else {
308            $this->$property = $value;
309        }
310    }
311
312    /**
313     * Prepare fields for ObjectModel class (add, update)
314     * All fields are verified (pSQL, intval, ...)
315     *
316     * @return array All object fields
317     * @throws PrestaShopException
318     *
319     * @since   1.0.0
320     * @version 1.0.0 Initial version
321     */
322    public function getFields()
323    {
324        $this->validateFields();
325        $fields = $this->formatFields(static::FORMAT_COMMON);
326
327        // For retro compatibility
328        if (Shop::isTableAssociated($this->def['table'])) {
329            $fields = array_merge($fields, $this->getFieldsShop());
330        }
331
332        // Ensure that we get something to insert
333        if (!$fields && isset($this->id) && Validate::isUnsignedId($this->id)) {
334            $fields[$this->def['primary']] = $this->id;
335        }
336
337        return $fields;
338    }
339
340    /**
341     * Prepare fields for multishop
342     * Fields are not validated here, we consider they are already validated in getFields() method,
343     * this is not the best solution but this is the only one possible for retro compatibility.
344     *
345     * @return array All object fields
346     *
347     * @since   1.0.0
348     * @version 1.0.0 Initial version
349     * @throws PrestaShopException
350     */
351    public function getFieldsShop()
352    {
353        $fields = $this->formatFields(static::FORMAT_SHOP);
354        if (!$fields && isset($this->id) && Validate::isUnsignedId($this->id)) {
355            $fields[$this->def['primary']] = $this->id;
356        }
357
358        return $fields;
359    }
360
361    /**
362     * Prepare multilang fields
363     *
364     * @return array
365     * @throws PrestaShopException
366     *
367     * @since   1.0.0
368     * @version 1.0.0 Initial version
369     */
370    public function getFieldsLang()
371    {
372        // Backward compatibility
373        if (method_exists($this, 'getTranslationsFieldsChild')) {
374            return $this->getTranslationsFieldsChild();
375        }
376
377        $this->validateFieldsLang();
378        $isLangMultishop = $this->isLangMultishop();
379
380        $fields = [];
381        if ($this->id_lang === null) {
382            foreach (Language::getIDs(false) as $idLang) {
383                $fields[$idLang] = $this->formatFields(static::FORMAT_LANG, $idLang);
384                $fields[$idLang]['id_lang'] = $idLang;
385                if ($this->id_shop && $isLangMultishop) {
386                    $fields[$idLang]['id_shop'] = (int) $this->id_shop;
387                }
388            }
389        } else {
390            $fields = [$this->id_lang => $this->formatFields(static::FORMAT_LANG, $this->id_lang)];
391            $fields[$this->id_lang]['id_lang'] = $this->id_lang;
392            if ($this->id_shop && $isLangMultishop) {
393                $fields[$this->id_lang]['id_shop'] = (int) $this->id_shop;
394            }
395        }
396
397        return $fields;
398    }
399
400    /**
401     * Formats values of each fields.
402     *
403     * @param int $type   FORMAT_COMMON or FORMAT_LANG or FORMAT_SHOP
404     * @param int $idLang If this parameter is given, only take lang fields
405     *
406     * @return array
407     *
408     * @since   1.0.0
409     * @version 1.0.0 Initial version
410     */
411    protected function formatFields($type, $idLang = null)
412    {
413        $fields = [];
414
415        // Set primary key in fields
416        if (isset($this->id)) {
417            $fields[$this->def['primary']] = $this->id;
418        }
419
420        foreach ($this->def['fields'] as $field => $data) {
421            // Only get fields we need for the type
422            // E.g. if only lang fields are filtered, ignore fields without lang => true
423            if (($type == static::FORMAT_LANG && empty($data['lang']))
424                || ($type == static::FORMAT_SHOP && empty($data['shop']))
425                || ($type == static::FORMAT_COMMON && ((!empty($data['shop']) && $data['shop'] != 'both') || !empty($data['lang'])))) {
426                continue;
427            }
428
429            if (is_array($this->update_fields)) {
430                if ((!empty($data['lang']) || (!empty($data['shop']) && $data['shop'] != 'both')) && (empty($this->update_fields[$field]) || ($type == static::FORMAT_LANG && empty($this->update_fields[$field][$idLang])))) {
431                    continue;
432                }
433            }
434
435            // Get field value, if value is multilang and field is empty, use value from default lang
436            $value = $this->$field;
437            if ($type == static::FORMAT_LANG && $idLang && is_array($value)) {
438                if (!empty($value[$idLang])) {
439                    $value = $value[$idLang];
440                } elseif (!empty($data['required'])) {
441                    $value = $value[Configuration::get('PS_LANG_DEFAULT')];
442                } else {
443                    $value = '';
444                }
445            }
446
447            $purify = (isset($data['validate']) && mb_strtolower($data['validate']) == 'iscleanhtml') ? true : false;
448            // Format field value
449            $fields[$field] = ObjectModel::formatValue($value, $data['type'], false, $purify, !empty($data['allow_null']));
450        }
451
452        return $fields;
453    }
454
455    /**
456     * Formats a value
457     *
458     * @param mixed $value
459     * @param int   $type
460     * @param bool  $withQuotes
461     * @param bool  $purify
462     * @param bool  $allowNull
463     *
464     * @return mixed
465     *
466     * @since   1.0.0
467     * @version 1.0.0 Initial version
468     * @throws PrestaShopException
469     */
470    public static function formatValue($value, $type, $withQuotes = false, $purify = true, $allowNull = false)
471    {
472        if ($allowNull && $value === null) {
473            return ['type' => 'sql', 'value' => 'NULL'];
474        }
475
476        switch ($type) {
477            case self::TYPE_INT:
478                return (int) $value;
479
480            case self::TYPE_BOOL:
481                return (int) $value;
482
483            case self::TYPE_FLOAT:
484                return (float) str_replace(',', '.', $value);
485
486            case self::TYPE_PRICE:
487                return round($value, _TB_PRICE_DATABASE_PRECISION_);
488
489            case self::TYPE_DATE:
490                if (!$value) {
491                    return '0000-00-00';
492                }
493
494                if ($withQuotes) {
495                    return '\''.pSQL($value).'\'';
496                }
497                return pSQL($value);
498
499            case self::TYPE_HTML:
500                if ($purify) {
501                    $value = Tools::purifyHTML($value);
502                }
503                if ($withQuotes) {
504                    return '\''.pSQL($value, true).'\'';
505                }
506                return pSQL($value, true);
507
508            case self::TYPE_SQL:
509                if ($withQuotes) {
510                    return '\''.pSQL($value, true).'\'';
511                }
512                return pSQL($value, true);
513
514            case self::TYPE_NOTHING:
515                return $value;
516
517            case self::TYPE_STRING:
518            default :
519                if ($withQuotes) {
520                    return '\''.pSQL($value).'\'';
521                }
522                return pSQL($value);
523        }
524    }
525
526    /**
527     * Saves current object to database (add or update)
528     *
529     * @param bool $nullValues
530     * @param bool $autoDate
531     *
532     * @return bool Insertion result
533     * @throws PrestaShopException
534     *
535     * @since   1.0.0
536     * @version 1.0.0 Initial version
537     */
538    public function save($nullValues = false, $autoDate = true)
539    {
540        return (int) $this->id > 0 ? $this->update($nullValues) : $this->add($autoDate, $nullValues);
541    }
542
543    /**
544     * Adds current object to the database
545     *
546     * @param bool $autoDate
547     * @param bool $nullValues
548     *
549     * @return bool Insertion result
550     * @throws PrestaShopDatabaseException
551     * @throws PrestaShopException
552     *
553     * @since   1.0.0
554     * @version 1.0.0 Initial version
555     */
556    public function add($autoDate = true, $nullValues = false)
557    {
558        if (isset($this->id) && !$this->force_id) {
559            unset($this->id);
560        }
561
562        // @hook actionObject*AddBefore
563        Hook::exec('actionObjectAddBefore', ['object' => $this]);
564        Hook::exec('actionObject'.get_class($this).'AddBefore', ['object' => $this]);
565
566        // Automatically fill dates
567        if ($autoDate && property_exists($this, 'date_add')) {
568            $this->date_add = date('Y-m-d H:i:s');
569        }
570        if ($autoDate && property_exists($this, 'date_upd')) {
571            $this->date_upd = date('Y-m-d H:i:s');
572        }
573
574        if (Shop::isTableAssociated($this->def['table'])) {
575            if (is_array($this->id_shop_list) && count($this->id_shop_list)) {
576                $idShopList = $this->id_shop_list;
577            } else {
578                $idShopList = Shop::getContextListShopID();
579            }
580        }
581
582        // Database insertion
583        if (Shop::checkIdShopDefault($this->def['table'])) {
584            $this->id_shop_default = (in_array(Configuration::get('PS_SHOP_DEFAULT'), $idShopList) == true) ? Configuration::get('PS_SHOP_DEFAULT') : min($idShopList);
585        }
586        $fields = $this->getFields();
587        if (!$result = Db::getInstance()->insert($this->def['table'], $fields, $nullValues)) {
588            return false;
589        }
590
591        // Get object id in database
592        $this->id = Db::getInstance()->Insert_ID();
593
594        // Database insertion for multishop fields related to the object
595        if (Shop::isTableAssociated($this->def['table'])) {
596            $fields = $this->getFieldsShop();
597            $fields[$this->def['primary']] = (int) $this->id;
598
599            foreach ($idShopList as $idShop) {
600                $fields['id_shop'] = (int) $idShop;
601                $result &= Db::getInstance()->insert($this->def['table'].'_shop', $fields, $nullValues);
602            }
603        }
604
605        if (!$result) {
606            return false;
607        }
608
609        // Database insertion for multilingual fields related to the object
610        if (!empty($this->def['multilang'])) {
611            $fields = $this->getFieldsLang();
612            if ($fields && is_array($fields)) {
613                $shops = Shop::getCompleteListOfShopsID();
614                $asso = Shop::getAssoTable($this->def['table'].'_lang');
615                foreach ($fields as $field) {
616                    foreach (array_keys($field) as $key) {
617                        if (!Validate::isTableOrIdentifier($key)) {
618                            throw new PrestaShopException('key '.$key.' is not table or identifier');
619                        }
620                    }
621                    $field[$this->def['primary']] = (int) $this->id;
622
623                    if ($asso !== false && $asso['type'] == 'fk_shop') {
624                        foreach ($shops as $idShop) {
625                            $field['id_shop'] = (int) $idShop;
626                            $result &= Db::getInstance()->insert($this->def['table'].'_lang', $field);
627                        }
628                    } else {
629                        $result &= Db::getInstance()->insert($this->def['table'].'_lang', $field);
630                    }
631                }
632            }
633        }
634
635        // @hook actionObject*AddAfter
636        Hook::exec('actionObjectAddAfter', ['object' => $this]);
637        Hook::exec('actionObject'.get_class($this).'AddAfter', ['object' => $this]);
638
639        return $result;
640    }
641
642    /**
643     * Takes current object ID, gets its values from database,
644     * saves them in a new row and loads newly saved values as a new object.
645     *
646     * @return ObjectModel|false
647     * @throws PrestaShopDatabaseException
648     *
649     * @since   1.0.0
650     * @version 1.0.0 Initial version
651     * @throws PrestaShopException
652     */
653    public function duplicateObject()
654    {
655        $definition = ObjectModel::getDefinition($this);
656
657        $res = Db::getInstance()->getRow('
658					SELECT *
659					FROM `'._DB_PREFIX_.bqSQL($definition['table']).'`
660					WHERE `'.bqSQL($definition['primary']).'` = '.(int) $this->id
661                );
662        if (!$res) {
663            return false;
664        }
665
666        unset($res[$definition['primary']]);
667        foreach ($res as $field => &$value) {
668            if (isset($definition['fields'][$field])) {
669                $value = ObjectModel::formatValue($value, $definition['fields'][$field]['type'], false, true, !empty($definition['fields'][$field]['allow_null']));
670            }
671        }
672
673        if (!Db::getInstance()->insert($definition['table'], $res)) {
674            return false;
675        }
676
677        $objectId = Db::getInstance()->Insert_ID();
678
679        if (isset($definition['multilang']) && $definition['multilang']) {
680            $result = Db::getInstance()->executeS('
681			SELECT *
682			FROM `'._DB_PREFIX_.bqSQL($definition['table']).'_lang`
683			WHERE `'.bqSQL($definition['primary']).'` = '.(int) $this->id);
684            if (!$result) {
685                return false;
686            }
687
688            foreach ($result as &$row) {
689                foreach ($row as $field => &$value) {
690                    if (isset($definition['fields'][$field])) {
691                        $value = ObjectModel::formatValue($value, $definition['fields'][$field]['type'], false, true, !empty($definition['fields'][$field]['allow_null']));
692                    }
693                }
694            }
695
696            // Keep $row2, you cannot use $row because there is an unexplicated conflict with the previous usage of this variable
697            foreach ($result as $row2) {
698                $row2[$definition['primary']] = (int) $objectId;
699                if (!Db::getInstance()->insert($definition['table'].'_lang', $row2)) {
700                    return false;
701                }
702            }
703        }
704
705        /** @var ObjectModel $objectDuplicated */
706        $objectDuplicated = new $definition['classname']((int) $objectId);
707        $objectDuplicated->duplicateShops((int) $this->id);
708
709        return $objectDuplicated;
710    }
711
712    /**
713     * Updates the current object in the database
714     *
715     * @param bool $nullValues
716     *
717     * @return bool
718     * @throws PrestaShopDatabaseException
719     * @throws PrestaShopException
720     */
721    public function update($nullValues = false)
722    {
723        // @hook actionObject*UpdateBefore
724        Hook::exec('actionObjectUpdateBefore', ['object' => $this]);
725        Hook::exec('actionObject'.get_class($this).'UpdateBefore', ['object' => $this]);
726
727        $this->clearCache();
728
729        // Automatically fill dates
730        if (property_exists($this, 'date_upd')) {
731            $this->date_upd = date('Y-m-d H:i:s');
732            if (isset($this->update_fields) && is_array($this->update_fields) && count($this->update_fields)) {
733                $this->update_fields['date_upd'] = true;
734            }
735        }
736
737        // Automatically fill dates
738        if (property_exists($this, 'date_add') && $this->date_add == null) {
739            $this->date_add = date('Y-m-d H:i:s');
740            if (isset($this->update_fields) && is_array($this->update_fields) && count($this->update_fields)) {
741                $this->update_fields['date_add'] = true;
742            }
743        }
744
745        if (is_array($this->id_shop_list) && count($this->id_shop_list)) {
746            $idShopList = $this->id_shop_list;
747        } else {
748            $idShopList = Shop::getContextListShopID();
749        }
750
751        if (Shop::checkIdShopDefault($this->def['table']) && !$this->id_shop_default) {
752            $this->id_shop_default = (in_array(Configuration::get('PS_SHOP_DEFAULT'), $idShopList) == true) ? Configuration::get('PS_SHOP_DEFAULT') : min($idShopList);
753        }
754        // Database update
755        if (!$result = Db::getInstance()->update($this->def['table'], $this->getFields(), '`'.pSQL($this->def['primary']).'` = '.(int) $this->id, 0, $nullValues)) {
756            return false;
757        }
758
759        // Database insertion for multishop fields related to the object
760        if (Shop::isTableAssociated($this->def['table'])) {
761
762            // for insert operation we need all multishop fields
763            $insertFields = $this->getFieldsShop();
764            $insertFields[$this->def['primary']] = (int) $this->id;
765
766            // by default update all fields except primary key
767            $updateFields = $insertFields;
768            unset($updateFields[$this->def['primary']]);
769            unset($updateFields['id_shop']);
770
771            // if property $update_fields exists, we have to use it to restrict update fields
772            if (is_array($this->update_fields)) {
773                foreach ($updateFields as $key => $val) {
774                    if (!array_key_exists($key, $this->update_fields)) {
775                        unset($updateFields[$key]);
776                    }
777                }
778            }
779
780            // update or create multishop entries
781            foreach ($idShopList as $idShop) {
782                $where = $this->def['primary'].' = '.(int) $this->id.' AND id_shop = '.(int) $idShop;
783
784                $shopEntryExists = Db::getInstance()->getValue('SELECT '.$this->def['primary'].' FROM '._DB_PREFIX_.$this->def['table'].'_shop WHERE '.$where);
785                if ($shopEntryExists) {
786                    // if multishop db entry exists, we use $updateFields array to update it
787                    $result &= Db::getInstance()->update($this->def['table'].'_shop', $updateFields, $where, 0, $nullValues);
788                } elseif (Shop::getContext() == Shop::CONTEXT_SHOP) {
789                    // if multishop db entry doesnt exist yet, we use $insertFields array to create it
790                    $insertFields['id_shop'] = (int) $idShop;
791                    $result &= Db::getInstance()->insert($this->def['table'].'_shop', $insertFields, $nullValues);
792                }
793            }
794        }
795
796        // Database update for multilingual fields related to the object
797        if (isset($this->def['multilang']) && $this->def['multilang']) {
798            $fields = $this->getFieldsLang();
799            if (is_array($fields)) {
800                foreach ($fields as $field) {
801                    foreach (array_keys($field) as $key) {
802                        if (!Validate::isTableOrIdentifier($key)) {
803                            throw new PrestaShopException('key '.$key.' is not a valid table or identifier');
804                        }
805                    }
806
807                    // If this table is linked to multishop system, update / insert for all shops from context
808                    if ($this->isLangMultishop()) {
809                        if (is_array($this->id_shop_list)
810                            && count($this->id_shop_list)) {
811                            $idShopList = $this->id_shop_list;
812                        } else {
813                            $idShopList = Shop::getContextListShopID();
814                        }
815                        foreach ($idShopList as $idShop) {
816                            $field['id_shop'] = (int) $idShop;
817                            $where = pSQL($this->def['primary']).' = '.(int) $this->id.' AND id_lang = '.(int) $field['id_lang'].' AND id_shop = '.(int) $idShop;
818
819                            if (Db::getInstance()->getValue('SELECT COUNT(*) FROM '.pSQL(_DB_PREFIX_.$this->def['table']).'_lang WHERE '.$where)) {
820                                $result &= Db::getInstance()->update($this->def['table'].'_lang', $field, $where);
821                            } else {
822                                $result &= Db::getInstance()->insert($this->def['table'].'_lang', $field);
823                            }
824                        }
825                    } else {
826                        // If this table is not linked to multishop system ...
827                        $where = pSQL($this->def['primary']).' = '.(int) $this->id.' AND id_lang = '.(int) $field['id_lang'];
828                        if (Db::getInstance()->getValue('SELECT COUNT(*) FROM '.pSQL(_DB_PREFIX_.$this->def['table']).'_lang WHERE '.$where)) {
829                            $result &= Db::getInstance()->update($this->def['table'].'_lang', $field, $where);
830                        } else {
831                            $result &= Db::getInstance()->insert($this->def['table'].'_lang', $field, $nullValues);
832                        }
833                    }
834                }
835            }
836        }
837
838        // @hook actionObject*UpdateAfter
839        Hook::exec('actionObjectUpdateAfter', ['object' => $this]);
840        Hook::exec('actionObject'.get_class($this).'UpdateAfter', ['object' => $this]);
841
842        return $result;
843    }
844
845    /**
846     * Deletes current object from database
847     *
848     * @return bool True if delete was successful
849     * @throws PrestaShopException
850     *
851     * @since   1.0.0
852     * @version 1.0.0 Initial version
853     */
854    public function delete()
855    {
856        // @hook actionObject*DeleteBefore
857        Hook::exec('actionObjectDeleteBefore', ['object' => $this]);
858        Hook::exec('actionObject'.get_class($this).'DeleteBefore', ['object' => $this]);
859
860        $this->clearCache();
861        $result = true;
862        // Remove association to multishop table
863        if (Shop::isTableAssociated($this->def['table'])) {
864            if (is_array($this->id_shop_list) && count($this->id_shop_list)) {
865                $idShopList = $this->id_shop_list;
866            } else {
867                $idShopList = Shop::getContextListShopID();
868            }
869
870            $result &= Db::getInstance()->delete($this->def['table'].'_shop', '`'.$this->def['primary'].'`='.(int) $this->id.' AND id_shop IN ('.implode(', ', $idShopList).')');
871        }
872
873        // Database deletion
874        $hasMultishopEntries = $this->hasMultishopEntries();
875        if ($result && !$hasMultishopEntries) {
876            $result &= Db::getInstance()->delete($this->def['table'], '`'.bqSQL($this->def['primary']).'` = '.(int) $this->id);
877        }
878
879        if (!$result) {
880            return false;
881        }
882
883        // Database deletion for multilingual fields related to the object
884        if (!empty($this->def['multilang']) && !$hasMultishopEntries) {
885            $result &= Db::getInstance()->delete($this->def['table'].'_lang', '`'.bqSQL($this->def['primary']).'` = '.(int) $this->id);
886        }
887
888        // @hook actionObject*DeleteAfter
889        Hook::exec('actionObjectDeleteAfter', ['object' => $this]);
890        Hook::exec('actionObject'.get_class($this).'DeleteAfter', ['object' => $this]);
891
892        return $result;
893    }
894
895    /**
896     * Deletes multiple objects from the database at once
897     *
898     * @param array $ids Array of objects IDs.
899     *
900     * @return bool
901     *
902     * @since   1.0.0
903     * @version 1.0.0 Initial version
904     * @throws PrestaShopException
905     */
906    public function deleteSelection($ids)
907    {
908        $result = true;
909        foreach ($ids as $id) {
910            $this->id = (int) $id;
911            $result = $result && $this->delete();
912        }
913
914        return $result;
915    }
916
917    /**
918     * Toggles object status in database
919     *
920     * @return bool Update result
921     * @throws PrestaShopException
922     *
923     * @since   1.0.0
924     * @version 1.0.0 Initial version
925     */
926    public function toggleStatus()
927    {
928        // Object must have a variable called 'active'
929        if (!property_exists($this, 'active')) {
930            throw new PrestaShopException('property "active" is missing in object '.get_class($this));
931        }
932
933        // Update only active field
934        $this->setFieldsToUpdate(['active' => true]);
935
936        // Update active status on object
937        $this->active = !(int) $this->active;
938
939        // Change status to active/inactive
940        return $this->update(false);
941    }
942
943    /**
944     * @deprecated 1.0.0 (use getFieldsLang())
945     *
946     * @param array $fieldsArray
947     *
948     * @return array
949     * @throws PrestaShopException
950     *
951     * @since   1.0.0
952     * @version 1.0.0 Initial version
953     */
954    protected function getTranslationsFields($fieldsArray)
955    {
956        $fields = [];
957
958        if ($this->id_lang == null) {
959            foreach (Language::getIDs(false) as $id_lang) {
960                $this->makeTranslationFields($fields, $fieldsArray, $id_lang);
961            }
962        } else {
963            $this->makeTranslationFields($fields, $fieldsArray, $this->id_lang);
964        }
965
966        return $fields;
967    }
968
969    /**
970     * @deprecated 1.0.0
971     *
972     * @param array       $fields
973     * @param array $fieldsArray
974     * @param int   $idLanguage
975     *
976     * @throws PrestaShopException
977     */
978    protected function makeTranslationFields(&$fields, &$fieldsArray, $idLanguage)
979    {
980        $fields[$idLanguage]['id_lang'] = $idLanguage;
981        $fields[$idLanguage][$this->def['primary']] = (int) $this->id;
982        if ($this->id_shop && $this->isLangMultishop()) {
983            $fields[$idLanguage]['id_shop'] = (int) $this->id_shop;
984        }
985        foreach ($fieldsArray as $k => $field) {
986            $html = false;
987            $fieldName = $field;
988            if (is_array($field)) {
989                $fieldName = $k;
990                $html = (isset($field['html'])) ? $field['html'] : false;
991            }
992
993            /* Check fields validity */
994            if (!Validate::isTableOrIdentifier($fieldName)) {
995                throw new PrestaShopException('identifier is not table or identifier : '.$fieldName);
996            }
997
998            // Copy the field, or the default language field if it's both required and empty
999            if ((!$this->id_lang && isset($this->{$fieldName}[$idLanguage]) && !empty($this->{$fieldName}[$idLanguage]))
1000            || ($this->id_lang && isset($this->$fieldName) && !empty($this->$fieldName))) {
1001                $fields[$idLanguage][$fieldName] = $this->id_lang ? pSQL($this->$fieldName, $html) : pSQL($this->{$fieldName}[$idLanguage], $html);
1002            } elseif (in_array($fieldName, $this->fieldsRequiredLang)) {
1003                $fields[$idLanguage][$fieldName] = pSQL($this->id_lang ? $this->$fieldName : $this->{$fieldName}[Configuration::get('PS_LANG_DEFAULT')], $html);
1004            } else {
1005                $fields[$idLanguage][$fieldName] = '';
1006            }
1007        }
1008    }
1009
1010    /**
1011     * Checks if object field values are valid before database interaction
1012     *
1013     * @param bool $die
1014     * @param bool $errorReturn
1015     *
1016     * @return bool|string True, false or error message.
1017     * @throws PrestaShopException
1018     *
1019     * @since   1.0.0
1020     * @version 1.0.0 Initial version
1021     */
1022    public function validateFields($die = true, $errorReturn = false)
1023    {
1024        foreach ($this->def['fields'] as $field => $data) {
1025            if (!empty($data['lang'])) {
1026                continue;
1027            }
1028
1029            if (is_array($this->update_fields) && empty($this->update_fields[$field]) && isset($this->def['fields'][$field]['shop']) && $this->def['fields'][$field]['shop']) {
1030                continue;
1031            }
1032
1033            $message = $this->validateField($field, $this->$field);
1034            if ($message !== true) {
1035                if ($die) {
1036                    throw new PrestaShopException($message);
1037                }
1038
1039                return $errorReturn ? $message : false;
1040            }
1041        }
1042
1043        return true;
1044    }
1045
1046    /**
1047     * Checks if multilingual object field values are valid before database interaction.
1048     *
1049     * @param bool $die
1050     * @param bool $errorReturn
1051     *
1052     * @return bool|string True, false or error message.
1053     * @throws PrestaShopException
1054     *
1055     * @since   1.0.0
1056     * @version 1.0.0 Initial version
1057     */
1058    public function validateFieldsLang($die = true, $errorReturn = false)
1059    {
1060        $idLangDefault = Configuration::get('PS_LANG_DEFAULT');
1061
1062        foreach ($this->def['fields'] as $field => $data) {
1063            if (empty($data['lang'])) {
1064                continue;
1065            }
1066
1067            $values = $this->$field;
1068
1069            // If the object has not been loaded in multilanguage, then the value is the one for the current language of the object
1070            if (!is_array($values)) {
1071                $values = [$this->id_lang => $values];
1072            }
1073
1074            // The value for the default must always be set, so we put an empty string if it does not exists
1075            if (!isset($values[$idLangDefault])) {
1076                $values[$idLangDefault] = '';
1077            }
1078
1079            foreach ($values as $idLang => $value) {
1080                if (is_array($this->update_fields) && empty($this->update_fields[$field][$idLang])) {
1081                    continue;
1082                }
1083
1084                $message = $this->validateField($field, $value, $idLang);
1085                if ($message !== true) {
1086                    if ($die) {
1087                        throw new PrestaShopException($message);
1088                    }
1089
1090                    return $errorReturn ? $message : false;
1091                }
1092            }
1093        }
1094
1095        return true;
1096    }
1097
1098    /**
1099     * Validate a single field
1100     *
1101     * @param string   $field       Field name
1102     * @param mixed    $value       Field value
1103     * @param int|null $idLang      Language ID
1104     * @param array    $skip        Array of fields to skip.
1105     * @param bool     $humanErrors If true, uses more descriptive, translatable error strings.
1106     *
1107     * @return true|string True or error message string.
1108     * @throws PrestaShopException
1109     *
1110     * @since   1.0.0
1111     * @version 1.0.0 Initial version
1112     */
1113    public function validateField($field, $value, $idLang = null, $skip = [], $humanErrors = false)
1114    {
1115        static $psLangDefault = null;
1116        static $psAllowHtmlIframe = null;
1117
1118        if ($psLangDefault === null) {
1119            $psLangDefault = Configuration::get('PS_LANG_DEFAULT');
1120        }
1121
1122        if ($psAllowHtmlIframe === null) {
1123            $psAllowHtmlIframe = (int) Configuration::get('PS_ALLOW_HTML_IFRAME');
1124        }
1125
1126
1127        $this->cacheFieldsRequiredDatabase();
1128        $data = $this->def['fields'][$field];
1129
1130
1131
1132        // Check if field is required
1133        $requiredFields = (isset(static::$fieldsRequiredDatabase[get_class($this)])) ? static::$fieldsRequiredDatabase[get_class($this)] : [];
1134        if (!$idLang || $idLang == $psLangDefault) {
1135            if (!in_array('required', $skip) && (!empty($data['required']) || in_array($field, $requiredFields))) {
1136                if (Tools::isEmpty($value)) {
1137                    if ($humanErrors) {
1138                        return sprintf(Tools::displayError('The %s field is required.'), $this->displayFieldName($field, get_class($this)));
1139                    } else {
1140                        return 'Property '.get_class($this).'->'.$field.' is empty';
1141                    }
1142                }
1143            }
1144        }
1145
1146        // Default value
1147        if (!$value && !empty($data['default'])) {
1148            $value = $data['default'];
1149            $this->$field = $value;
1150        }
1151
1152        // Check field values
1153        if (!in_array('values', $skip) && !empty($data['values']) && is_array($data['values']) && !in_array($value, $data['values'])) {
1154            return 'Property '.get_class($this).'->'.$field.' has bad value (allowed values are: '.implode(', ', $data['values']).')';
1155        }
1156
1157        // Check field size
1158        if (!in_array('size', $skip) && !empty($data['size'])) {
1159            $size = $data['size'];
1160            if (!is_array($data['size'])) {
1161                $size = ['min' => 0, 'max' => $data['size']];
1162            }
1163
1164            $length = mb_strlen($value);
1165            if ($length < $size['min'] || $length > $size['max']) {
1166                if ($humanErrors) {
1167                    if (isset($data['lang']) && $data['lang']) {
1168                        $language = new Language((int) $idLang);
1169
1170                        return sprintf(Tools::displayError('The field %1$s (%2$s) is too long (%3$d chars max, html chars including).'), $this->displayFieldName($field, get_class($this)), $language->name, $size['max']);
1171                    } else {
1172                        return sprintf(Tools::displayError('The %1$s field is too long (%2$d chars max).'), $this->displayFieldName($field, get_class($this)), $size['max']);
1173                    }
1174                } else {
1175                    return 'Property '.get_class($this).'->'.$field.' length ('.$length.') must be between '.$size['min'].' and '.$size['max'];
1176                }
1177            }
1178        }
1179
1180        // Check field validator
1181        if (!in_array('validate', $skip) && !empty($data['validate'])) {
1182            if (!method_exists('Validate', $data['validate'])) {
1183                throw new PrestaShopException('Validation function not found. '.$data['validate']);
1184            }
1185
1186            if (!empty($value)) {
1187                $res = true;
1188                if (mb_strtolower($data['validate']) == 'iscleanhtml') {
1189                    if (!call_user_func(['Validate', $data['validate']], $value, $psAllowHtmlIframe)) {
1190                        $res = false;
1191                    }
1192                } else {
1193                    if (!call_user_func(['Validate', $data['validate']], $value)) {
1194                        $res = false;
1195                    }
1196                }
1197                if (!$res) {
1198                    if ($humanErrors) {
1199                        return sprintf(Tools::displayError('The %s field is invalid.'), $this->displayFieldName($field, get_class($this)));
1200                    } else {
1201                        return 'Property '.get_class($this).'->'.$field.' is not valid';
1202                    }
1203                }
1204            }
1205        }
1206
1207        return true;
1208    }
1209
1210    /**
1211     * Returns field name translation
1212     *
1213     * @param string       $field        Field name
1214     * @param string       $class        ObjectModel class name
1215     * @param bool         $htmlentities If true, applies htmlentities() to result string
1216     * @param Context|null $context      Context object
1217     *
1218     * @return string
1219     *
1220     * @since   1.0.0
1221     * @version 1.0.0 Initial version
1222     */
1223    public static function displayFieldName($field, $class = __CLASS__, $htmlentities = true, Context $context = null)
1224    {
1225        global $_FIELDS;
1226
1227        if (!isset($context)) {
1228            $context = Context::getContext();
1229        }
1230
1231        if ($_FIELDS === null && file_exists(_PS_TRANSLATIONS_DIR_.$context->language->iso_code.'/fields.php')) {
1232            include_once(_PS_TRANSLATIONS_DIR_.$context->language->iso_code.'/fields.php');
1233        }
1234
1235        $key = $class.'_'.md5($field);
1236
1237        if (is_array($_FIELDS) && array_key_exists($key, $_FIELDS) && $_FIELDS[$key] !== '') {
1238            $str = $_FIELDS[$key];
1239            return $htmlentities ? htmlentities($str, ENT_QUOTES, 'utf-8') : $str;
1240        }
1241
1242        return $field;
1243    }
1244
1245    /**
1246     * @deprecated 1.0.0 Use validateController() instead
1247     *
1248     * @param bool $htmlentities
1249     *
1250     * @return array
1251     * @throws PrestaShopDatabaseException
1252     */
1253    public function validateControler($htmlentities = true)
1254    {
1255        Tools::displayAsDeprecated();
1256
1257        return $this->validateController($htmlentities);
1258    }
1259
1260    /**
1261     * Validates submitted values and returns an array of errors, if any.
1262     *
1263     * @param bool $htmlentities If true, uses htmlentities() for field name translations in errors.
1264     *
1265     * @return array
1266     *
1267     * @since   1.0.0
1268     * @version 1.0.0 Initial version
1269     * @throws PrestaShopDatabaseException
1270     */
1271    public function validateController($htmlentities = true)
1272    {
1273        $this->cacheFieldsRequiredDatabase();
1274        $errors = [];
1275        $requiredFieldsDatabase = (isset(static::$fieldsRequiredDatabase[get_class($this)])) ? static::$fieldsRequiredDatabase[get_class($this)] : [];
1276        foreach ($this->def['fields'] as $field => $data) {
1277            $value = Tools::getValue($field, $this->{$field});
1278            // Check if field is required by user
1279            if (in_array($field, $requiredFieldsDatabase)) {
1280                $data['required'] = true;
1281            }
1282
1283            // Checking for required fields
1284            if (isset($data['required']) && $data['required'] && empty($value) && $value !== '0') {
1285                if (!$this->id || $field != 'passwd') {
1286                    $errors[$field] = '<b>'.static::displayFieldName($field, get_class($this), $htmlentities).'</b> '.Tools::displayError('is required.');
1287                }
1288            }
1289
1290            // Checking for maximum fields sizes
1291            if (isset($data['size']) && !empty($value) && mb_strlen($value) > $data['size']) {
1292                $errors[$field] = sprintf(
1293                    Tools::displayError('%1$s is too long. Maximum length: %2$d'),
1294                    static::displayFieldName($field, get_class($this), $htmlentities),
1295                    $data['size']
1296                );
1297            }
1298
1299            // Checking for fields validity
1300            // Hack for postcode required for country which does not have postcodes
1301            if (!empty($value) || $value === '0' || ($field == 'postcode' && $value == '0')) {
1302                $validationError = false;
1303                if (isset($data['validate'])) {
1304                    $dataValidate = $data['validate'];
1305                    if (!Validate::$dataValidate($value) && (!empty($value) || $data['required'])) {
1306                        $errors[$field] = '<b>'.static::displayFieldName($field, get_class($this), $htmlentities).
1307                            '</b> '.Tools::displayError('is invalid.');
1308                        $validationError = true;
1309                    }
1310                }
1311
1312                if (!$validationError) {
1313                    if (isset($data['copy_post']) && !$data['copy_post']) {
1314                        continue;
1315                    }
1316                    if ($field == 'passwd') {
1317                        if ($value = Tools::getValue($field)) {
1318                            $this->{$field} = Tools::hash($value);
1319                        }
1320                    } else {
1321                        $this->{$field} = $value;
1322                    }
1323                }
1324            }
1325        }
1326
1327        return $errors;
1328    }
1329
1330    /**
1331     * Returns webservice parameters of this object.
1332     *
1333     * @param string|null $wsParamsAttributeName
1334     *
1335     * @return array
1336     *
1337     * @since   1.0.0
1338     * @version 1.0.0 Initial version
1339     * @throws PrestaShopDatabaseException
1340     */
1341    public function getWebserviceParameters($wsParamsAttributeName = null)
1342    {
1343        $this->cacheFieldsRequiredDatabase();
1344        $defaultResourceParameters = [
1345            'objectSqlId' => $this->def['primary'],
1346            'retrieveData' => [
1347                'className' => get_class($this),
1348                'retrieveMethod' => 'getWebserviceObjectList',
1349                'params' => [],
1350                'table' => $this->def['table'],
1351            ],
1352            'fields' => [
1353                'id' => ['sqlId' => $this->def['primary'], 'i18n' => false],
1354            ],
1355        ];
1356
1357        if ($wsParamsAttributeName === null) {
1358            $wsParamsAttributeName = 'webserviceParameters';
1359        }
1360
1361        if (!isset($this->{$wsParamsAttributeName}['objectNodeName'])) {
1362            $defaultResourceParameters['objectNodeName'] = $this->def['table'];
1363        }
1364        if (!isset($this->{$wsParamsAttributeName}['objectsNodeName'])) {
1365            $defaultResourceParameters['objectsNodeName'] = $this->def['table'].'s';
1366        }
1367
1368        if (isset($this->{$wsParamsAttributeName}['associations'])) {
1369            foreach ($this->{$wsParamsAttributeName}['associations'] as $assocName => &$association) {
1370                if (!array_key_exists('setter', $association) || (isset($association['setter']) && !$association['setter'])) {
1371                    $association['setter'] = Tools::toCamelCase('set_ws_'.$assocName);
1372                }
1373                if (!array_key_exists('getter', $association)) {
1374                    $association['getter'] = Tools::toCamelCase('get_ws_'.$assocName);
1375                }
1376            }
1377        }
1378
1379        if (isset($this->{$wsParamsAttributeName}['retrieveData']) && isset($this->{$wsParamsAttributeName}['retrieveData']['retrieveMethod'])) {
1380            unset($defaultResourceParameters['retrieveData']['retrieveMethod']);
1381        }
1382
1383        $resourceParameters = array_merge_recursive($defaultResourceParameters, $this->{$wsParamsAttributeName});
1384
1385        $requiredFields = (isset(static::$fieldsRequiredDatabase[get_class($this)]) ? static::$fieldsRequiredDatabase[get_class($this)] : []);
1386        foreach ($this->def['fields'] as $fieldName => $details) {
1387            if (!isset($resourceParameters['fields'][$fieldName])) {
1388                $resourceParameters['fields'][$fieldName] = [];
1389            }
1390            $currentField = [];
1391            $currentField['sqlId'] = $fieldName;
1392            if (isset($details['size'])) {
1393                $currentField['maxSize'] = $details['size'];
1394            }
1395            if (isset($details['lang'])) {
1396                $currentField['i18n'] = $details['lang'];
1397            } else {
1398                $currentField['i18n'] = false;
1399            }
1400            if ((isset($details['required']) && $details['required'] === true) || in_array($fieldName, $requiredFields)) {
1401                $currentField['required'] = true;
1402            } else {
1403                $currentField['required'] = false;
1404            }
1405            if (isset($details['validate'])) {
1406                $currentField['validateMethod'] = (
1407                                array_key_exists('validateMethod', $resourceParameters['fields'][$fieldName]) ?
1408                                array_merge($resourceParameters['fields'][$fieldName]['validateMethod'], [$details['validate']]) :
1409                                [$details['validate']]
1410                            );
1411            }
1412            $resourceParameters['fields'][$fieldName] = array_merge($resourceParameters['fields'][$fieldName], $currentField);
1413
1414            if (isset($details['ws_modifier'])) {
1415                $resourceParameters['fields'][$fieldName]['modifier'] = $details['ws_modifier'];
1416            }
1417        }
1418        if (isset($this->date_add)) {
1419            $resourceParameters['fields']['date_add']['setter'] = false;
1420        }
1421        if (isset($this->date_upd)) {
1422            $resourceParameters['fields']['date_upd']['setter'] = false;
1423        }
1424        foreach ($resourceParameters['fields'] as $key => $resourceParametersField) {
1425            if (!isset($resourceParametersField['sqlId'])) {
1426                $resourceParameters['fields'][$key]['sqlId'] = $key;
1427            }
1428        }
1429
1430        return $resourceParameters;
1431    }
1432
1433    /**
1434     * Returns webservice object list.
1435     *
1436     * @param string $sqlJoin
1437     * @param string $sqlFilter
1438     * @param string $sqlSort
1439     * @param string $sqlLimit
1440     *
1441     * @return array|null
1442     * @throws PrestaShopDatabaseException
1443     *
1444     * @since   1.0.0
1445     * @version 1.0.0 Initial version
1446     * @throws PrestaShopException
1447     */
1448    public function getWebserviceObjectList($sqlJoin, $sqlFilter, $sqlSort, $sqlLimit)
1449    {
1450        $assoc = Shop::getAssoTable($this->def['table']);
1451        // @codingStandardsIgnoreStart
1452        $className = WebserviceRequest::$ws_current_classname;
1453        // @codingStandardsIgnoreEnd
1454        $vars = get_class_vars($className);
1455        if ($assoc !== false) {
1456            if ($assoc['type'] !== 'fk_shop') {
1457                $multiShopJoin = ' LEFT JOIN `'._DB_PREFIX_.bqSQL($this->def['table']).'_'.bqSQL($assoc['type']).'`
1458										AS `multi_shop_'.bqSQL($this->def['table']).'`
1459										ON (main.`'.bqSQL($this->def['primary']).'` = `multi_shop_'.bqSQL($this->def['table']).'`.`'.bqSQL($this->def['primary']).'`)';
1460                $sqlFilter = 'AND `multi_shop_'.bqSQL($this->def['table']).'`.id_shop = '.Context::getContext()->shop->id.' '.$sqlFilter;
1461                $sqlJoin = $multiShopJoin.' '.$sqlJoin;
1462            } else {
1463                $vars = get_class_vars($className);
1464                foreach ($vars['shopIDs'] as $idShop) {
1465                    $or[] = '(main.id_shop = '.(int) $idShop.(isset($this->def['fields']['id_shop_group']) ? ' OR (id_shop = 0 AND id_shop_group='.(int) Shop::getGroupFromShop((int) $idShop).')' : '').')';
1466                }
1467
1468                $prepend = '';
1469                if (isset($or) && count($or)) {
1470                    $prepend = 'AND ('.implode('OR', $or).')';
1471                }
1472                $sqlFilter = $prepend.' '.$sqlFilter;
1473            }
1474        }
1475        $query = '
1476		SELECT DISTINCT main.`'.bqSQL($this->def['primary']).'` FROM `'._DB_PREFIX_.bqSQL($this->def['table']).'` AS main
1477		'.$sqlJoin.'
1478		WHERE 1 '.$sqlFilter.'
1479		'.($sqlSort != '' ? $sqlSort : '').'
1480		'.($sqlLimit != '' ? $sqlLimit : '');
1481
1482        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
1483    }
1484
1485    /**
1486     * Validate required fields.
1487     *
1488     * @param bool $htmlentities
1489     *
1490     * @return array
1491     * @throws PrestaShopException
1492     *
1493     * @since   1.0.0
1494     * @version 1.0.0 Initial version
1495     */
1496    public function validateFieldsRequiredDatabase($htmlentities = true)
1497    {
1498        $this->cacheFieldsRequiredDatabase();
1499        $errors = [];
1500        $requiredFields = (isset(static::$fieldsRequiredDatabase[get_class($this)])) ? static::$fieldsRequiredDatabase[get_class($this)] : [];
1501
1502        foreach ($this->def['fields'] as $field => $data) {
1503            if (!in_array($field, $requiredFields)) {
1504                continue;
1505            }
1506
1507            if (!method_exists('Validate', $data['validate'])) {
1508                throw new PrestaShopException('Validation function not found. '.$data['validate']);
1509            }
1510
1511            $value = Tools::getValue($field);
1512
1513            if (empty($value)) {
1514                $errors[$field] = sprintf(Tools::displayError('The field %s is required.'), static::displayFieldName($field, get_class($this), $htmlentities));
1515            }
1516        }
1517
1518        return $errors;
1519    }
1520
1521    /**
1522     * Returns an array of required fields
1523     *
1524     * @param bool $all If true, returns required fields of all object classes.
1525     *
1526     * @return array|null
1527     * @throws PrestaShopDatabaseException
1528     *
1529     * @since   1.0.0
1530     * @version 1.0.0 Initial version
1531     * @throws PrestaShopException
1532     */
1533    public function getFieldsRequiredDatabase($all = false)
1534    {
1535        return Db::getInstance()->executeS('
1536		SELECT id_required_field, object_name, field_name
1537		FROM '._DB_PREFIX_.'required_field
1538		'.(!$all ? 'WHERE object_name = \''.pSQL(get_class($this)).'\'' : ''));
1539    }
1540
1541    /**
1542     * Caches data about required objects fields in memory
1543     *
1544     * @param bool $all If true, caches required fields of all object classes.
1545     *
1546     * @since   1.0.0
1547     * @version 1.0.0 Initial version
1548     * @throws PrestaShopDatabaseException
1549     * @throws PrestaShopException
1550     */
1551    public function cacheFieldsRequiredDatabase($all = true)
1552    {
1553        if (!is_array(static::$fieldsRequiredDatabase)) {
1554            $fields = $this->getfieldsRequiredDatabase((bool) $all);
1555            if ($fields) {
1556                foreach ($fields as $row) {
1557                    static::$fieldsRequiredDatabase[$row['object_name']][(int) $row['id_required_field']] = pSQL($row['field_name']);
1558                }
1559            } else {
1560                static::$fieldsRequiredDatabase = [];
1561            }
1562        }
1563    }
1564
1565    /**
1566     * Sets required field for this class in the database.
1567     *
1568     * @param array $fields
1569     *
1570     * @return bool
1571     * @throws PrestaShopDatabaseException
1572     *
1573     * @since   1.0.0
1574     * @version 1.0.0 Initial version
1575     * @throws PrestaShopException
1576     * @throws PrestaShopException
1577     */
1578    public function addFieldsRequiredDatabase($fields)
1579    {
1580        if (!is_array($fields)) {
1581            return false;
1582        }
1583
1584        if (!Db::getInstance()->execute('DELETE FROM '._DB_PREFIX_.'required_field WHERE object_name = \''.get_class($this).'\'')) {
1585            return false;
1586        }
1587
1588        foreach ($fields as $field) {
1589            if (!Db::getInstance()->insert('required_field', ['object_name' => get_class($this), 'field_name' => pSQL($field)])) {
1590                return false;
1591            }
1592        }
1593
1594        return true;
1595    }
1596
1597    /**
1598     * Clears cache entries that have this object's ID.
1599     *
1600     * @param bool $all If true, clears cache for all objects
1601     *
1602     *                  @since   1.0.0
1603     * @version 1.0.0 Initial version
1604     */
1605    public function clearCache($all = false)
1606    {
1607        if ($all) {
1608            Cache::clean('objectmodel_'.$this->def['classname'].'_*');
1609        } elseif ($this->id) {
1610            Cache::clean('objectmodel_'.$this->def['classname'].'_'.(int) $this->id.'_*');
1611        }
1612    }
1613
1614    /**
1615     * Checks if current object is associated to a shop.
1616     *
1617     * @param int|null $idShop
1618     *
1619     * @return bool
1620     *
1621     * @since   1.0.0
1622     * @version 1.0.0 Initial version
1623     * @throws PrestaShopException
1624     */
1625    public function isAssociatedToShop($idShop = null)
1626    {
1627        if ($idShop === null) {
1628            $idShop = Context::getContext()->shop->id;
1629        }
1630
1631        $cacheId = 'objectmodel_shop_'.$this->def['classname'].'_'.(int) $this->id.'-'.(int) $idShop;
1632        if (!ObjectModel::$cache_objects || !Cache::isStored($cacheId)) {
1633            $associated = (bool)Db::getInstance()->getValue('
1634				SELECT id_shop
1635				FROM `'.pSQL(_DB_PREFIX_.$this->def['table']).'_shop`
1636				WHERE `'.$this->def['primary'].'` = '.(int) $this->id.'
1637				AND id_shop = '.(int) $idShop
1638            );
1639
1640            if (!ObjectModel::$cache_objects) {
1641                return $associated;
1642            }
1643
1644            Cache::store($cacheId, $associated);
1645
1646            return $associated;
1647        }
1648
1649        return Cache::retrieve($cacheId);
1650    }
1651
1652    /**
1653     * This function associate an item to its context
1654     *
1655     * @param int|array $idShops
1656     *
1657     * @return bool
1658     * @throws PrestaShopDatabaseException
1659     *
1660     * @since   1.0.0
1661     * @version 1.0.0 Initial version
1662     * @throws PrestaShopException
1663     */
1664    public function associateTo($idShops)
1665    {
1666        if (!$this->id) {
1667            return false;
1668        }
1669
1670        if (!is_array($idShops)) {
1671            $idShops = [$idShops];
1672        }
1673
1674        $data = [];
1675        foreach ($idShops as $idShop) {
1676            if (!$this->isAssociatedToShop($idShop)) {
1677                $data[] = [
1678                    $this->def['primary'] => (int) $this->id,
1679                    'id_shop'             => (int) $idShop,
1680                ];
1681            }
1682        }
1683
1684        if ($data) {
1685            return Db::getInstance()->insert($this->def['table'].'_shop', $data);
1686        }
1687
1688        return true;
1689    }
1690
1691    /**
1692     * Gets the list of associated shop IDs
1693     *
1694     * @return array
1695     * @throws PrestaShopDatabaseException
1696     *
1697     * @since   1.0.0
1698     * @version 1.0.0 Initial version
1699     * @throws PrestaShopException
1700     */
1701    public function getAssociatedShops()
1702    {
1703        if (!Shop::isTableAssociated($this->def['table'])) {
1704            return [];
1705        }
1706
1707        $list = [];
1708        $sql = 'SELECT id_shop FROM `'._DB_PREFIX_.$this->def['table'].'_shop` WHERE `'.$this->def['primary'].'` = '.(int) $this->id;
1709        foreach (Db::getInstance()->executeS($sql) as $row) {
1710            $list[] = $row['id_shop'];
1711        }
1712
1713        return $list;
1714    }
1715
1716    /**
1717     * Copies shop association data from object with specified ID.
1718     *
1719     * @param int $id
1720     *
1721     * @return bool|void
1722     * @throws PrestaShopDatabaseException
1723     *
1724     * @since   1.0.0
1725     * @version 1.0.0 Initial version
1726     * @throws PrestaShopException
1727     */
1728    public function duplicateShops($id)
1729    {
1730        if (!Shop::isTableAssociated($this->def['table'])) {
1731            return false;
1732        }
1733
1734        $sql = 'SELECT id_shop
1735				FROM '._DB_PREFIX_.$this->def['table'].'_shop
1736				WHERE '.$this->def['primary'].' = '.(int) $id;
1737        if ($results = Db::getInstance()->executeS($sql)) {
1738            $ids = [];
1739            foreach ($results as $row) {
1740                $ids[] = $row['id_shop'];
1741            }
1742
1743            return $this->associateTo($ids);
1744        }
1745
1746        return false;
1747    }
1748
1749    /**
1750     * Checks if there is more than one entry in associated shop table for current object.
1751     *
1752     * @return bool
1753     *
1754     * @since   1.0.0
1755     * @version 1.0.0 Initial version
1756     * @throws PrestaShopException
1757     */
1758    public function hasMultishopEntries()
1759    {
1760        if (!Shop::isTableAssociated($this->def['table']) || !Shop::isFeatureActive()) {
1761            return false;
1762        }
1763
1764        return (bool) Db::getInstance()->getValue('SELECT COUNT(*) FROM `'._DB_PREFIX_.$this->def['table'].'_shop` WHERE `'.$this->def['primary'].'` = '.(int) $this->id);
1765    }
1766
1767    /**
1768     * Checks if object is multi-shop object.
1769     *
1770     * @return bool
1771     *
1772     * @since   1.0.0
1773     * @version 1.0.0 Initial version
1774     */
1775    public function isMultishop()
1776    {
1777        return Shop::isTableAssociated($this->def['table']) || !empty($this->def['multilang_shop']);
1778    }
1779
1780    /**
1781     * Checks if a field is a multi-shop field.
1782     *
1783     * @param string $field
1784     *
1785     * @return bool
1786     *
1787     * @since   1.0.0
1788     * @version 1.0.0 Initial version
1789     */
1790    public function isMultiShopField($field)
1791    {
1792        return (isset($this->def['fields'][$field]) && isset($this->def['fields'][$field]['shop']) && $this->def['fields'][$field]['shop']);
1793    }
1794
1795    /**
1796     * Checks if the object is both multi-language and multi-shop.
1797     *
1798     * @return bool
1799     *
1800     * @since   1.0.0
1801     * @version 1.0.0 Initial version
1802     */
1803    public function isLangMultishop()
1804    {
1805        return !empty($this->def['multilang']) && !empty($this->def['multilang_shop']);
1806    }
1807
1808    /**
1809     * Updates a table and splits the common datas and the shop datas.
1810     *
1811     * @param string $className
1812     * @param array  $data
1813     * @param string $where
1814     * @param string $specific_where Only executed for common table
1815     *
1816     * @return bool
1817     *
1818     * @since   1.0.0
1819     * @version 1.0.0 Initial version
1820     * @throws PrestaShopException
1821     */
1822    public static function updateMultishopTable($className, $data, $where = '', $specificWhere = '')
1823    {
1824        $def = ObjectModel::getDefinition($className);
1825        $updateData = [];
1826        foreach ($data as $field => $value) {
1827            if (!isset($def['fields'][$field])) {
1828                continue;
1829            }
1830
1831            if (!empty($def['fields'][$field]['shop'])) {
1832                if ($value === null && !empty($def['fields'][$field]['allow_null'])) {
1833                    $updateData[] = "a.$field = NULL";
1834                    $updateData[] = "{$def['table']}_shop.$field = NULL";
1835                } else {
1836                    $updateData[] = "a.$field = '$value'";
1837                    $updateData[] = "{$def['table']}_shop.$field = '$value'";
1838                }
1839            } else {
1840                if ($value === null && !empty($def['fields'][$field]['allow_null'])) {
1841                    $updateData[] = "a.$field = NULL";
1842                } else {
1843                    $updateData[] = "a.$field = '$value'";
1844                }
1845            }
1846        }
1847
1848        $sql = 'UPDATE '._DB_PREFIX_.$def['table'].' a
1849				'.Shop::addSqlAssociation($def['table'], 'a', true, null, true).'
1850				SET '.implode(', ', $updateData).
1851                (!empty($where) ? ' WHERE '.$where : '');
1852
1853        return Db::getInstance()->execute($sql);
1854    }
1855
1856    /**
1857     * Delete images associated with the object
1858     *
1859     * @param bool $forceDelete
1860     *
1861     * @return bool
1862     *
1863     * @since   1.0.0
1864     * @version 1.0.0 Initial version
1865     * @throws PrestaShopDatabaseException
1866     * @throws PrestaShopException
1867     */
1868    public function deleteImage($forceDelete = false)
1869    {
1870        if (!$this->id) {
1871            return false;
1872        }
1873
1874        if ($forceDelete || !$this->hasMultishopEntries()) {
1875            /* Deleting object images and thumbnails (cache) */
1876            if ($this->image_dir) {
1877                if (file_exists($this->image_dir.$this->id.'.'.$this->image_format)
1878                    && !unlink($this->image_dir.$this->id.'.'.$this->image_format)) {
1879                    return false;
1880                }
1881            }
1882            if (file_exists(_PS_TMP_IMG_DIR_.$this->def['table'].'_'.$this->id.'.'.$this->image_format)
1883                && !unlink(_PS_TMP_IMG_DIR_.$this->def['table'].'_'.$this->id.'.'.$this->image_format)) {
1884                return false;
1885            }
1886            if (file_exists(_PS_TMP_IMG_DIR_.$this->def['table'].'_mini_'.$this->id.'.'.$this->image_format)
1887                && !unlink(_PS_TMP_IMG_DIR_.$this->def['table'].'_mini_'.$this->id.'.'.$this->image_format)) {
1888                return false;
1889            }
1890
1891            $types = ImageType::getImagesTypes();
1892            foreach ($types as $imageType) {
1893                if (file_exists($this->image_dir.$this->id.'-'.stripslashes($imageType['name']).'.'.$this->image_format)
1894                && !unlink($this->image_dir.$this->id.'-'.stripslashes($imageType['name']).'.'.$this->image_format)) {
1895                    return false;
1896                }
1897            }
1898        }
1899
1900        return true;
1901    }
1902
1903    /**
1904     * Checks if an object exists in database.
1905     *
1906     * @param int    $idEntity
1907     * @param string $table
1908     *
1909     * @return bool
1910     *
1911     * @throws PrestaShopDatabaseException
1912     * @throws PrestaShopException
1913     * @since   1.0.0
1914     * @version 1.0.0 Initial version
1915     */
1916    public static function existsInDatabase($idEntity, $table)
1917    {
1918        $row = Db::getInstance()->getRow('
1919			SELECT `id_'.bqSQL($table).'` as id
1920			FROM `'._DB_PREFIX_.bqSQL($table).'` e
1921			WHERE e.`id_'.bqSQL($table).'` = '.(int) $idEntity, false
1922        );
1923
1924        return isset($row['id']);
1925    }
1926
1927    /**
1928     * Checks if an object type exists in the database.
1929     *
1930     * @param string|null $table           Name of table linked to entity
1931     * @param bool        $hasActiveColumn True if the table has an active column
1932     *
1933     * @return bool
1934     *
1935     * @since   1.0.0
1936     * @version 1.0.0 Initial version
1937     * @throws PrestaShopException
1938     */
1939    public static function isCurrentlyUsed($table = null, $hasActiveColumn = false)
1940    {
1941        if ($table === null) {
1942            $table = static::$definition['table'];
1943        }
1944
1945        $query = new DbQuery();
1946        $query->select('`id_'.bqSQL($table).'`');
1947        $query->from($table);
1948        if ($hasActiveColumn) {
1949            $query->where('`active` = 1');
1950        }
1951
1952        return (bool) Db::getInstance()->getValue($query);
1953    }
1954
1955    /**
1956     * Fill an object with given data. Data must be an array with this syntax:
1957     * array(objProperty => value, objProperty2 => value, etc.)
1958     *
1959     * @param array    $data
1960     * @param int|null $idLang
1961     *
1962     * @since 1.0.0
1963     */
1964    public function hydrate(array $data, $idLang = null)
1965    {
1966        $this->id_lang = $idLang;
1967        if (isset($data[$this->def['primary']])) {
1968            $this->id = $data[$this->def['primary']];
1969        }
1970
1971        foreach ($data as $key => $value) {
1972            if (array_key_exists($key, $this)) {
1973                $this->$key = $value;
1974            }
1975        }
1976    }
1977
1978    /**
1979     * Fill an object with given data. Data must be an array with this syntax:
1980     * array(
1981     *   array(id_lang => 1, objProperty => value, objProperty2 => value, etc.),
1982     *   array(id_lang => 2, objProperty => value, objProperty2 => value, etc.),
1983     * );
1984     *
1985     * @param array    $data
1986     *
1987     * @since 1.0.4
1988     */
1989    public function hydrateMultilang(array $data)
1990    {
1991        foreach ($data as $row) {
1992            if (isset($row[$this->def['primary']])) {
1993                $this->id = $row[$this->def['primary']];
1994            }
1995
1996            foreach ($row as $key => $value) {
1997                if (array_key_exists($key, $this)) {
1998                    if (!empty($this->def['fields'][$key]['lang']) && !empty($row['id_lang'])) {
1999                        // Multilang
2000                        if (!is_array($this->$key)) {
2001                            $this->$key = [];
2002                        }
2003                        $this->$key[(int) $row['id_lang']] = $value;
2004                    } else {
2005                        // Normal
2006                        if (array_key_exists($key, $this)) {
2007                            $this->$key = $value;
2008                        }
2009                    }
2010                }
2011            }
2012        }
2013    }
2014
2015    /**
2016     * Fill (hydrate) a list of objects in order to get a collection of these objects
2017     *
2018     * @param string   $class   Class of objects to hydrate
2019     * @param array    $datas   List of data (multi-dimensional array)
2020     * @param int|null $idLang
2021     *
2022     * @return array
2023     * @throws PrestaShopException
2024     *
2025     * @since   1.0.0
2026     * @version 1.0.0 Initial version
2027     */
2028    public static function hydrateCollection($class, array $datas, $idLang = null)
2029    {
2030        if (!class_exists($class)) {
2031            throw new PrestaShopException("Class '$class' not found");
2032        }
2033
2034        $collection = [];
2035        $rows = [];
2036        if ($datas) {
2037            $definition = ObjectModel::getDefinition($class);
2038            if (!array_key_exists($definition['primary'], $datas[0])) {
2039                throw new PrestaShopException("Identifier '{$definition['primary']}' not found for class '$class'");
2040            }
2041
2042            foreach ($datas as $row) {
2043                // Get object common properties
2044                $id = $row[$definition['primary']];
2045                if (!isset($rows[$id])) {
2046                    $rows[$id] = $row;
2047                }
2048
2049                // Get object lang properties
2050                if (isset($row['id_lang']) && !$idLang) {
2051                    foreach ($definition['fields'] as $field => $data) {
2052                        if (!empty($data['lang'])) {
2053                            if (!is_array($rows[$id][$field])) {
2054                                $rows[$id][$field] = [];
2055                            }
2056                            $rows[$id][$field][$row['id_lang']] = $row[$field];
2057                        }
2058                    }
2059                }
2060            }
2061        }
2062
2063        // Hydrate objects
2064        foreach ($rows as $row) {
2065            /** @var ObjectModel $obj */
2066            $obj = new $class();
2067            $obj->hydrate($row, $idLang);
2068            $collection[] = $obj;
2069        }
2070
2071        return $collection;
2072    }
2073
2074    /**
2075     * Returns object definition
2076     *
2077     * @param string      $class Name of object
2078     * @param string|null $field Name of field if we want the definition of one field only
2079     *
2080     * @return array
2081     *
2082     * @since   1.0.0
2083     * @version 1.0.0 Initial version
2084     */
2085    public static function getDefinition($class, $field = null)
2086    {
2087        if (is_object($class)) {
2088            $class = get_class($class);
2089        }
2090
2091        if ($field === null) {
2092            $cacheId = 'objectmodel_def_'.$class;
2093        }
2094
2095        if ($field !== null || !Cache::isStored($cacheId)) {
2096            $reflection = new ReflectionClass($class);
2097
2098            if (!$reflection->hasProperty('definition')) {
2099                return false;
2100            }
2101
2102            $definition = $reflection->getStaticPropertyValue('definition');
2103
2104            $definition['classname'] = $class;
2105
2106            if (!empty($definition['multilang'])) {
2107                $definition['associations'][PrestaShopCollection::LANG_ALIAS] = [
2108                    'type' => static::HAS_MANY,
2109                    'field' => $definition['primary'],
2110                    'foreign_field' => $definition['primary'],
2111                ];
2112            }
2113
2114            if ($field) {
2115                return isset($definition['fields'][$field]) ? $definition['fields'][$field] : null;
2116            }
2117
2118            Cache::store($cacheId, $definition);
2119
2120            return $definition;
2121        }
2122
2123        return Cache::retrieve($cacheId);
2124    }
2125
2126    /**
2127     * Retrocompatibility for classes without $definition static
2128     *
2129     * @deprecated 2.0.0
2130     */
2131    protected function setDefinitionRetrocompatibility()
2132    {
2133        // Retrocompatibility with $table property ($definition['table'])
2134        if (isset($this->def['table'])) {
2135            $this->table = $this->def['table'];
2136        } else {
2137            $this->def['table'] = $this->table;
2138        }
2139
2140        // Retrocompatibility with $identifier property ($definition['primary'])
2141        if (isset($this->def['primary'])) {
2142            $this->identifier = $this->def['primary'];
2143        } else {
2144            $this->def['primary'] = $this->identifier;
2145        }
2146
2147        // Check multilang retrocompatibility
2148        if (method_exists($this, 'getTranslationsFieldsChild')) {
2149            $this->def['multilang'] = true;
2150        }
2151
2152        // Retrocompatibility with $fieldsValidate, $fieldsRequired and $fieldsSize properties ($definition['fields'])
2153        if (isset($this->def['fields'])) {
2154            foreach ($this->def['fields'] as $field => $data) {
2155                $suffix = (isset($data['lang']) && $data['lang']) ? 'Lang' : '';
2156                if (isset($data['validate'])) {
2157                    $this->{'fieldsValidate'.$suffix}[$field] = $data['validate'];
2158                }
2159                if (isset($data['required']) && $data['required']) {
2160                    $this->{'fieldsRequired'.$suffix}[] = $field;
2161                }
2162                if (isset($data['size'])) {
2163                    $this->{'fieldsSize'.$suffix}[$field] = $data['size'];
2164                }
2165            }
2166        } else {
2167            $this->def['fields'] = [];
2168            $suffixs = ['', 'Lang'];
2169            foreach ($suffixs as $suffix) {
2170                foreach ($this->{'fieldsValidate'.$suffix} as $field => $validate) {
2171                    $this->def['fields'][$field]['validate'] = $validate;
2172                    if ($suffix == 'Lang') {
2173                        $this->def['fields'][$field]['lang'] = true;
2174                    }
2175                }
2176                foreach ($this->{'fieldsRequired'.$suffix} as $field) {
2177                    $this->def['fields'][$field]['required'] = true;
2178                    if ($suffix == 'Lang') {
2179                        $this->def['fields'][$field]['lang'] = true;
2180                    }
2181                }
2182                foreach ($this->{'fieldsSize'.$suffix} as $field => $size) {
2183                    $this->def['fields'][$field]['size'] = $size;
2184                    if ($suffix == 'Lang') {
2185                        $this->def['fields'][$field]['lang'] = true;
2186                    }
2187                }
2188            }
2189        }
2190    }
2191
2192    /**
2193     * Return the field value for the specified language if the field is multilang,
2194     * else the field value.
2195     *
2196     * @param string   $fieldName
2197     * @param int|null $idLang
2198     *
2199     * @return mixed
2200     * @throws PrestaShopException
2201     *
2202     * @since   1.0.0
2203     * @version 1.0.0 Initial version
2204     */
2205    public function getFieldByLang($fieldName, $idLang = null)
2206    {
2207        $definition = ObjectModel::getDefinition($this);
2208        // Is field in definition?
2209        if ($definition && isset($definition['fields'][$fieldName])) {
2210            $field = $definition['fields'][$fieldName];
2211            // Is field multilang?
2212            if (isset($field['lang']) && $field['lang']) {
2213                if (is_array($this->{$fieldName})) {
2214                    return $this->{$fieldName}[$idLang ?: Context::getContext()->language->id];
2215                }
2216            }
2217
2218            return $this->{$fieldName};
2219        } else {
2220            throw new PrestaShopException('Could not load field from definition.');
2221        }
2222    }
2223
2224    /**
2225     * Set a list of specific fields to update
2226     * array(field1 => true, field2 => false,
2227     * langfield1 => array(1 => true, 2 => false))
2228     *
2229     * @param array $fields
2230     *
2231     * @since   1.0.0
2232     * @version 1.0.0 Initial version
2233     */
2234    public function setFieldsToUpdate(array $fields)
2235    {
2236        $this->update_fields = $fields;
2237    }
2238
2239    /**
2240     * Enables object caching
2241     *
2242     * @since   1.0.0
2243     * @version 1.0.0 Initial version
2244     */
2245    public static function enableCache()
2246    {
2247        ObjectModel::$cache_objects = true;
2248    }
2249
2250    /**
2251     * Disables object caching
2252     *
2253     * @since   1.0.0
2254     * @version 1.0.0 Initial version
2255     */
2256    public static function disableCache()
2257    {
2258        ObjectModel::$cache_objects = false;
2259    }
2260
2261    /**
2262     *  Create the database table with its columns. Similar to the createColumn() method.
2263     *
2264     * @param string|null $className Class name
2265     *
2266     * @return bool Indicates whether the database was successfully added
2267     *
2268     * @since   1.0.0
2269     * @version 1.0.0 Initial version
2270     * @throws PrestaShopException
2271     * @throws PrestaShopException
2272     * @throws PrestaShopException
2273     */
2274    public static function createDatabase($className = null)
2275    {
2276        $success = true;
2277
2278        if (empty($className)) {
2279            $className = get_called_class();
2280        }
2281
2282        $definition = static::getDefinition($className);
2283        $sql = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.bqSQL($definition['table']).'` (';
2284        $sql .= '`'.$definition['primary'].'` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,';
2285        foreach ($definition['fields'] as $fieldName => $field) {
2286            if ($fieldName === $definition['primary']) {
2287                continue;
2288            }
2289            if (isset($field['lang']) && $field['lang'] || isset($field['shop']) && $field['shop']) {
2290                continue;
2291            }
2292
2293            if (empty($field['db_type'])) {
2294                switch ($field['type']) {
2295                    case '1':
2296                        $field['db_type'] = 'INT(11) UNSIGNED';
2297                        break;
2298                    case '2':
2299                        $field['db_type'] .= 'TINYINT(1)';
2300                        break;
2301                    case '3':
2302                        (isset($field['size']) && $field['size'] > 256)
2303                            ? $field['db_type'] = 'VARCHAR(256)'
2304                            : $field['db_type'] = 'VARCHAR(512)';
2305                        break;
2306                    case '4':
2307                        $field['db_type'] = 'DECIMAL(20,6)';
2308                        break;
2309                    case '5':
2310                        $field['db_type'] = 'DATETIME';
2311                        break;
2312                    case '6':
2313                        $field['db_type'] = 'TEXT';
2314                        break;
2315                }
2316            }
2317            $sql .= '`'.$fieldName.'` '.$field['db_type'];
2318
2319            if (isset($field['required'])) {
2320                $sql .= ' NOT NULL';
2321            }
2322            if (isset($field['default'])) {
2323                $sql .= ' DEFAULT \''.$field['default'].'\'';
2324            }
2325            $sql .= ',';
2326        }
2327        $sql = trim($sql, ',');
2328        $sql .= ')';
2329
2330        try {
2331            $success &= Db::getInstance()->execute($sql);
2332        } catch (\PrestaShopDatabaseException $exception) {
2333            static::dropDatabase($className);
2334
2335            return false;
2336        }
2337
2338        if (isset($definition['multilang']) && $definition['multilang']
2339            || isset($definition['multilang_shop']) && $definition['multilang_shop']) {
2340            $sql = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.bqSQL($definition['table']).'_lang` (';
2341            $sql .= '`'.$definition['primary'].'` INT(11) UNSIGNED NOT NULL,';
2342            foreach ($definition['fields'] as $fieldName => $field) {
2343                if ($fieldName === $definition['primary'] || !(isset($field['lang']) && $field['lang'])) {
2344                    continue;
2345                }
2346                $sql .= '`'.$fieldName.'` '.$field['db_type'];
2347                if (isset($field['required'])) {
2348                    $sql .= ' NOT NULL';
2349                }
2350                if (isset($field['default'])) {
2351                    $sql .= ' DEFAULT \''.$field['default'].'\'';
2352                }
2353                $sql .= ',';
2354            }
2355
2356            // Lang field
2357            $sql .= '`id_lang` INT(11) NOT NULL,';
2358
2359            if (isset($definition['multilang_shop']) && $definition['multilang_shop']) {
2360                $sql .= '`id_shop` INT(11) NOT NULL,';
2361            }
2362
2363            // Primary key
2364            $sql .= 'PRIMARY KEY (`'.bqSQL($definition['primary']).'`, `id_lang`)';
2365
2366            $sql .= ')';
2367
2368            try {
2369                $success &= Db::getInstance()->execute($sql);
2370            } catch (\PrestaShopDatabaseException $exception) {
2371                static::dropDatabase($className);
2372
2373                return false;
2374	        }
2375        }
2376
2377        if (isset($definition['multishop']) && $definition['multishop']
2378            || isset($definition['multilang_shop']) && $definition['multilang_shop']) {
2379            $sql = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.bqSQL($definition['table']).'_shop` (';
2380            $sql .= '`'.$definition['primary'].'` INT(11) UNSIGNED NOT NULL,';
2381            foreach ($definition['fields'] as $fieldName => $field) {
2382                if ($fieldName === $definition['primary'] || !(isset($field['shop']) && $field['shop'])) {
2383                    continue;
2384                }
2385                $sql .= '`'.$fieldName.'` '.$field['db_type'];
2386                if (isset($field['required'])) {
2387                    $sql .= ' NOT NULL';
2388                }
2389                if (isset($field['default'])) {
2390                    $sql .= ' DEFAULT \''.$field['default'].'\'';
2391                }
2392                $sql .= ',';
2393            }
2394
2395            // Shop field
2396            $sql .= '`id_shop` INT(11) NOT NULL,';
2397
2398            // Primary key
2399            $sql .= 'PRIMARY KEY (`'.bqSQL($definition['primary']).'`, `id_shop`)';
2400
2401            $sql .= ')';
2402
2403            try {
2404                $success &= Db::getInstance()->execute($sql);
2405            } catch (\PrestaShopDatabaseException $exception) {
2406                static::dropDatabase($className);
2407
2408                return false;
2409            }
2410        }
2411
2412        return $success;
2413    }
2414
2415    /**
2416     * Drop the database for this ObjectModel
2417     *
2418     * @param string|null $className Class name
2419     *
2420     * @return bool Indicates whether the database was successfully dropped
2421     *
2422     * @since   1.0.0
2423     * @version 1.0.0 Initial version
2424     * @throws PrestaShopException
2425     */
2426    public static function dropDatabase($className = null)
2427    {
2428        $success = true;
2429        if (empty($className)) {
2430            $className = get_called_class();
2431        }
2432
2433        $definition = \ObjectModel::getDefinition($className);
2434
2435        $success &= Db::getInstance()->execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.bqSQL($definition['table']).'`');
2436
2437        if (isset($definition['multilang']) && $definition['multilang']
2438            || isset($definition['multilang_shop']) && $definition['multilang_shop']) {
2439            $success &= Db::getInstance()->execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.bqSQL($definition['table']).'_lang`');
2440        }
2441
2442        if (isset($definition['multishop']) && $definition['multishop']
2443            || isset($definition['multilang_shop']) && $definition['multilang_shop']) {
2444            $success &= Db::getInstance()->execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.bqSQL($definition['table']).'_shop`');
2445        }
2446
2447        return $success;
2448    }
2449
2450    /**
2451     * Get columns in database
2452     *
2453     * @param string|null $className Class name
2454     *
2455     * @return array|false|\mysqli_result|null|\PDOStatement|resource
2456     *
2457     * @throws PrestaShopDatabaseException
2458     * @throws PrestaShopException
2459     * @since   1.0.0
2460     * @version 1.0.0 Initial version
2461     */
2462    public static function getDatabaseColumns($className = null)
2463    {
2464        if (empty($className)) {
2465            $className = get_called_class();
2466        }
2467
2468        $definition = \ObjectModel::getDefinition($className);
2469
2470        $sql = 'SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=\''._DB_NAME_.'\' AND TABLE_NAME=\''._DB_PREFIX_.pSQL($definition['table']).'\'';
2471
2472        return Db::getInstance()->executeS($sql);
2473    }
2474
2475    /**
2476     * Add a column in the table relative to the ObjectModel.
2477     * This method uses the $definition property of the ObjectModel,
2478     * with some extra properties.
2479     *
2480     * Example:
2481     * 'table'        => 'tablename',
2482     * 'primary'      => 'id',
2483     * 'fields'       => array(
2484     *     'id'     => array('type' => self::TYPE_INT, 'validate' => 'isInt'),
2485     *     'number' => array(
2486     *         'type'     => self::TYPE_STRING,
2487     *         'db_type'  => 'varchar(20)',
2488     *         'required' => true,
2489     *         'default'  => '25'
2490     *     ),
2491     * ),
2492     *
2493     * The primary column is created automatically as INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT. The other columns
2494     * require an extra parameter, with the type of the column in the database.
2495     *
2496     * @param string      $name             Column name
2497     * @param array       $columnDefinition Column type definition
2498     * @param string|null $className        Class name
2499     *
2500     * @return bool Indicates whether the column was successfully created
2501     *
2502     * @since   1.0.0
2503     * @version 1.0.0 Initial version
2504     * @throws PrestaShopException
2505     */
2506    public static function createColumn($name, $columnDefinition, $className = null)
2507    {
2508        if (empty($className)) {
2509            $className = get_called_class();
2510        }
2511
2512        $definition = static::getDefinition($className);
2513        $sql = 'ALTER TABLE `'._DB_PREFIX_.bqSQL($definition['table']).'`';
2514        $sql .= ' ADD COLUMN `'.bqSQL($name).'` '.bqSQL($columnDefinition['db_type']).'';
2515        if ($name === $definition['primary']) {
2516            $sql .= ' INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT';
2517        } else {
2518            if (isset($columnDefinition['required']) && $columnDefinition['required']) {
2519                $sql .= ' NOT NULL';
2520            }
2521            if (isset($columnDefinition['default'])) {
2522                $sql .= ' DEFAULT "'.pSQL($columnDefinition['default']).'"';
2523            }
2524        }
2525
2526        return (bool) Db::getInstance()->execute($sql);
2527    }
2528
2529    /**
2530     *  Create in the database every column detailed in the $definition property that are
2531     *  missing in the database.
2532     *
2533     * @param string|null $className Class name
2534     *
2535     * @return bool Indicates whether the missing columns were successfully created
2536     *
2537     * @throws PrestaShopDatabaseException
2538     * @throws PrestaShopException
2539     * @since   1.0.0
2540     * @version 1.0.0 Initial version
2541     *
2542     * @todo    : Support multishop and multilang
2543     */
2544    public static function createMissingColumns($className = null)
2545    {
2546        if (empty($className)) {
2547            $className = get_called_class();
2548        }
2549
2550        $success = true;
2551
2552        $definition = static::getDefinition($className);
2553        $columns = static::getDatabaseColumns();
2554        foreach ($definition['fields'] as $columnName => $columnDefinition) {
2555            //column exists in database
2556            $exists = false;
2557            foreach ($columns as $column) {
2558                if ($column['COLUMN_NAME'] === $columnName) {
2559                    $exists = true;
2560                    break;
2561                }
2562            }
2563            if (!$exists) {
2564                $success &= static::createColumn($columnName, $columnDefinition);
2565            }
2566        }
2567
2568        return $success;
2569    }
2570}
2571