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