1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
27
28/**
29 * @property Product $object
30 */
31class AdminProductsControllerCore extends AdminController
32{
33    /** @var int Max image size for upload
34     * As of 1.5 it is recommended to not set a limit to max image size
35     */
36    protected $max_file_size = null;
37    protected $max_image_size = null;
38
39    protected $_category;
40    /**
41     * @var string name of the tab to display
42     */
43    protected $tab_display;
44    protected $tab_display_module;
45
46    /**
47     * The order in the array decides the order in the list of tab. If an element's value is a number, it will be preloaded.
48     * The tabs are preloaded from the smallest to the highest number.
49     *
50     * @var array product tabs
51     */
52    protected $available_tabs = [];
53
54    protected $default_tab = 'Informations';
55
56    protected $available_tabs_lang = [];
57
58    protected $position_identifier = 'id_product';
59
60    protected $submitted_tabs;
61
62    protected $id_current_category;
63
64    public function __construct($theme_name = 'default')
65    {
66        $this->bootstrap = true;
67        $this->table = 'product';
68        $this->className = 'Product';
69        parent::__construct('', $theme_name);
70    }
71
72    public function init()
73    {
74        if (Tools::getIsset('id_product')) {
75            if (Tools::getIsset('addproduct') || Tools::getIsset('updateproduct')) {
76                $sfContainer = SymfonyContainer::getInstance();
77                if (null !== $sfContainer) {
78                    $sfRouter = $sfContainer->get('router');
79                    Tools::redirectAdmin($sfRouter->generate(
80                        'admin_product_form',
81                        ['id' => Tools::getValue('id_product')]
82                    ));
83                }
84            }
85        }
86
87        return parent::init();
88    }
89
90    public static function getQuantities($echo, $tr)
91    {
92        if ((int) $tr['is_virtual'] == 1 && $tr['nb_downloadable'] == 0) {
93            return '&infin;';
94        } else {
95            return $echo;
96        }
97    }
98
99    protected function _cleanMetaKeywords($keywords)
100    {
101        if (!empty($keywords) && $keywords != '') {
102            $out = [];
103            $words = explode(',', $keywords);
104            foreach ($words as $word_item) {
105                $word_item = trim($word_item);
106                if (!empty($word_item) && $word_item != '') {
107                    $out[] = $word_item;
108                }
109            }
110
111            return (count($out) > 0) ? implode(',', $out) : '';
112        } else {
113            return '';
114        }
115    }
116
117    /**
118     * @param Product|ObjectModel $object
119     * @param string $table
120     */
121    protected function copyFromPost(&$object, $table)
122    {
123        parent::copyFromPost($object, $table);
124        if (get_class($object) != 'Product') {
125            return;
126        }
127
128        /* Additional fields */
129        foreach (Language::getIDs(false) as $id_lang) {
130            if (isset($_POST['meta_keywords_' . $id_lang])) {
131                $_POST['meta_keywords_' . $id_lang] = $this->_cleanMetaKeywords(Tools::strtolower($_POST['meta_keywords_' . $id_lang]));
132                // preg_replace('/ *,? +,* /', ',', strtolower($_POST['meta_keywords_'.$id_lang]));
133                $object->meta_keywords[$id_lang] = $_POST['meta_keywords_' . $id_lang];
134            }
135        }
136        $_POST['width'] = empty($_POST['width']) ? '0' : str_replace(',', '.', $_POST['width']);
137        $_POST['height'] = empty($_POST['height']) ? '0' : str_replace(',', '.', $_POST['height']);
138        $_POST['depth'] = empty($_POST['depth']) ? '0' : str_replace(',', '.', $_POST['depth']);
139        $_POST['weight'] = empty($_POST['weight']) ? '0' : str_replace(',', '.', $_POST['weight']);
140
141        if (Tools::getIsset('unit_price') != null) {
142            $object->unit_price = str_replace(',', '.', Tools::getValue('unit_price'));
143        }
144        if (Tools::getIsset('ecotax') != null) {
145            $object->ecotax = str_replace(',', '.', Tools::getValue('ecotax'));
146        }
147
148        if ($this->isTabSubmitted('Informations')) {
149            if ($this->checkMultishopBox('available_for_order', $this->context)) {
150                $object->available_for_order = (int) Tools::getValue('available_for_order');
151            }
152
153            if ($this->checkMultishopBox('show_price', $this->context)) {
154                $object->show_price = $object->available_for_order ? 1 : (int) Tools::getValue('show_price');
155            }
156
157            if ($this->checkMultishopBox('online_only', $this->context)) {
158                $object->online_only = (int) Tools::getValue('online_only');
159            }
160
161            if ($this->checkMultishopBox('show_condition', $this->context)) {
162                $object->show_condition = (int) Tools::getValue('show_condition');
163            }
164        }
165        if ($this->isTabSubmitted('Prices')) {
166            $object->on_sale = (int) Tools::getValue('on_sale');
167        }
168    }
169
170    public function checkMultishopBox($field, $context = null)
171    {
172        static $checkbox = null;
173        static $shop_context = null;
174
175        if ($context == null && $shop_context == null) {
176            $context = Context::getContext();
177        }
178
179        if ($shop_context == null) {
180            $shop_context = $context->shop->getContext();
181        }
182
183        if ($checkbox == null) {
184            $checkbox = Tools::getValue('multishop_check', []);
185        }
186
187        if ($shop_context == Shop::CONTEXT_SHOP) {
188            return true;
189        }
190
191        if (isset($checkbox[$field]) && $checkbox[$field] == 1) {
192            return true;
193        }
194
195        return false;
196    }
197
198    /**
199     * @param int $id_lang
200     * @param string $orderBy
201     * @param string $orderWay
202     * @param int $start
203     * @param int $limit
204     * @param null $id_lang_shop
205     *
206     * @throws PrestaShopDatabaseException
207     * @throws PrestaShopException
208     *
209     * @deprecated
210     */
211    public function getList($id_lang, $orderBy = null, $orderWay = null, $start = 0, $limit = null, $id_lang_shop = null)
212    {
213        $orderByPriceFinal = (empty($orderBy) ? ($this->context->cookie->__get($this->table . 'Orderby') ? $this->context->cookie->__get($this->table . 'Orderby') : 'id_' . $this->table) : $orderBy);
214        $orderWayPriceFinal = (empty($orderWay) ? ($this->context->cookie->__get($this->table . 'Orderway') ? $this->context->cookie->__get($this->table . 'Orderby') : 'ASC') : $orderWay);
215        if ($orderByPriceFinal == 'price_final') {
216            $orderBy = 'id_' . $this->table;
217            $orderWay = 'ASC';
218        }
219        parent::getList($id_lang, $orderBy, $orderWay, $start, $limit, $this->context->shop->id);
220
221        /* update product quantity with attributes ...*/
222        $nb = count($this->_list);
223        if ($this->_list) {
224            $context = $this->context->cloneContext();
225            $context->shop = clone $context->shop;
226            /* update product final price */
227            for ($i = 0; $i < $nb; ++$i) {
228                if (Context::getContext()->shop->getContext() != Shop::CONTEXT_SHOP) {
229                    $context->shop = new Shop((int) $this->_list[$i]['id_shop_default']);
230                }
231
232                // convert price with the currency from context
233                $this->_list[$i]['price'] = Tools::convertPrice($this->_list[$i]['price'], $this->context->currency, true, $this->context);
234                $this->_list[$i]['price_tmp'] = Product::getPriceStatic(
235                    $this->_list[$i]['id_product'],
236                    true,
237                    null,
238                    (int) Configuration::get('PS_PRICE_DISPLAY_PRECISION'),
239                    null,
240                    false,
241                    true,
242                    1,
243                    true,
244                    null,
245                    null,
246                    null,
247                    $nothing,
248                    true,
249                    true,
250                    $context
251                );
252            }
253        }
254
255        if ($orderByPriceFinal == 'price_final') {
256            if (strtolower($orderWayPriceFinal) == 'desc') {
257                uasort($this->_list, 'cmpPriceDesc');
258            } else {
259                uasort($this->_list, 'cmpPriceAsc');
260            }
261        }
262        for ($i = 0; $this->_list && $i < $nb; ++$i) {
263            $this->_list[$i]['price_final'] = $this->_list[$i]['price_tmp'];
264            unset($this->_list[$i]['price_tmp']);
265        }
266    }
267
268    protected function loadObject($opt = false)
269    {
270        $result = parent::loadObject($opt);
271        if ($result && Validate::isLoadedObject($this->object)) {
272            if (Shop::getContext() == Shop::CONTEXT_SHOP && Shop::isFeatureActive() && !$this->object->isAssociatedToShop()) {
273                $default_product = new Product((int) $this->object->id, false, null, (int) $this->object->id_shop_default);
274                $def = ObjectModel::getDefinition($this->object);
275                foreach ($def['fields'] as $field_name => $row) {
276                    if (is_array($default_product->$field_name)) {
277                        foreach ($default_product->$field_name as $key => $value) {
278                            $this->object->{$field_name}[$key] = $value;
279                        }
280                    } else {
281                        $this->object->$field_name = $default_product->$field_name;
282                    }
283                }
284            }
285            $this->object->loadStockData();
286        }
287
288        return $result;
289    }
290
291    public function ajaxProcessGetCategoryTree()
292    {
293        $category = Tools::getValue('category', Category::getRootCategory()->id);
294        $full_tree = Tools::getValue('fullTree', 0);
295        $use_check_box = Tools::getValue('useCheckBox', 1);
296        $selected = Tools::getValue('selected', []);
297        $id_tree = Tools::getValue('type');
298        $input_name = str_replace(['[', ']'], '', Tools::getValue('inputName', null));
299
300        $tree = new HelperTreeCategories('subtree_associated_categories');
301        $tree->setTemplate('subtree_associated_categories.tpl')
302            ->setUseCheckBox($use_check_box)
303            ->setUseSearch(true)
304            ->setIdTree($id_tree)
305            ->setSelectedCategories($selected)
306            ->setFullTree($full_tree)
307            ->setChildrenOnly(true)
308            ->setNoJS(true)
309            ->setRootCategory($category);
310
311        if ($input_name) {
312            $tree->setInputName($input_name);
313        }
314
315        die($tree->render());
316    }
317
318    public function ajaxProcessGetCountriesOptions()
319    {
320        if (!$res = Country::getCountriesByIdShop((int) Tools::getValue('id_shop'), (int) $this->context->language->id)) {
321            return;
322        }
323
324        $tpl = $this->createTemplate('specific_prices_shop_update.tpl');
325        $tpl->assign(
326            [
327                'option_list' => $res,
328                'key_id' => 'id_country',
329                'key_value' => 'name',
330            ]
331        );
332
333        $this->content = $tpl->fetch();
334    }
335
336    public function ajaxProcessGetCurrenciesOptions()
337    {
338        if (!$res = Currency::getCurrenciesByIdShop((int) Tools::getValue('id_shop'))) {
339            return;
340        }
341
342        $tpl = $this->createTemplate('specific_prices_shop_update.tpl');
343        $tpl->assign(
344            [
345                'option_list' => $res,
346                'key_id' => 'id_currency',
347                'key_value' => 'name',
348            ]
349        );
350
351        $this->content = $tpl->fetch();
352    }
353
354    public function ajaxProcessGetGroupsOptions()
355    {
356        if (!$res = Group::getGroups((int) $this->context->language->id, (int) Tools::getValue('id_shop'))) {
357            return;
358        }
359
360        $tpl = $this->createTemplate('specific_prices_shop_update.tpl');
361        $tpl->assign(
362            [
363                'option_list' => $res,
364                'key_id' => 'id_group',
365                'key_value' => 'name',
366            ]
367        );
368
369        $this->content = $tpl->fetch();
370    }
371
372    public function processDeleteVirtualProduct()
373    {
374        if (!($id_product_download = ProductDownload::getIdFromIdProduct((int) Tools::getValue('id_product')))) {
375            $this->errors[] = $this->trans('Cannot retrieve file.', [], 'Admin.Notifications.Error');
376        } else {
377            $product_download = new ProductDownload((int) $id_product_download);
378
379            if (!$product_download->deleteFile((int) $id_product_download)) {
380                $this->errors[] = $this->trans('Cannot delete file', [], 'Admin.Notifications.Error');
381            } else {
382                $this->redirect_after = self::$currentIndex . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=VirtualProduct&conf=1&token=' . $this->token;
383            }
384        }
385
386        $this->display = 'edit';
387        $this->tab_display = 'VirtualProduct';
388    }
389
390    public function ajaxProcessAddAttachment()
391    {
392        if (!$this->access('edit')) {
393            return die(json_encode(['error' => 'You do not have the right permission']));
394        }
395        if (isset($_FILES['attachment_file'])) {
396            if ((int) $_FILES['attachment_file']['error'] === 1) {
397                $_FILES['attachment_file']['error'] = [];
398
399                $max_upload = (int) ini_get('upload_max_filesize');
400                $max_post = (int) ini_get('post_max_size');
401                $upload_mb = min($max_upload, $max_post);
402                $_FILES['attachment_file']['error'][] = sprintf(
403                    'File %1$s exceeds the size allowed by the server. The limit is set to %2$d MB.',
404                    '<b>' . $_FILES['attachment_file']['name'] . '</b> ',
405                    '<b>' . $upload_mb . '</b>'
406                );
407            }
408
409            $_FILES['attachment_file']['error'] = [];
410
411            $is_attachment_name_valid = false;
412            $attachment_names = Tools::getValue('attachment_name');
413            $attachment_descriptions = Tools::getValue('attachment_description');
414
415            if (!isset($attachment_names) || !$attachment_names) {
416                $attachment_names = [];
417            }
418
419            if (!isset($attachment_descriptions) || !$attachment_descriptions) {
420                $attachment_descriptions = [];
421            }
422
423            foreach ($attachment_names as $lang => $name) {
424                $language = Language::getLanguage((int) $lang);
425
426                if (Tools::strlen($name) > 0) {
427                    $is_attachment_name_valid = true;
428                }
429
430                if (!Validate::isGenericName($name)) {
431                    $_FILES['attachment_file']['error'][] = $this->trans('Invalid name for %s language', [$language['name']], 'Admin.Notifications.Error');
432                } elseif (Tools::strlen($name) > 32) {
433                    $_FILES['attachment_file']['error'][] = $this->trans('The name for %1s language is too long (%2d chars max).', [$language['name'], 32], 'Admin.Notifications.Error');
434                }
435            }
436
437            foreach ($attachment_descriptions as $lang => $description) {
438                $language = Language::getLanguage((int) $lang);
439
440                if (!Validate::isCleanHtml($description)) {
441                    $_FILES['attachment_file']['error'][] = $this->trans('Invalid description for %s language', [$language['name']], 'Admin.Catalog.Notification');
442                }
443            }
444
445            if (!$is_attachment_name_valid) {
446                $_FILES['attachment_file']['error'][] = $this->trans('An attachment name is required.', [], 'Admin.Catalog.Notification');
447            }
448
449            if (empty($_FILES['attachment_file']['error'])) {
450                if (is_uploaded_file($_FILES['attachment_file']['tmp_name'])) {
451                    if ($_FILES['attachment_file']['size'] > (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024 * 1024)) {
452                        $_FILES['attachment_file']['error'][] = sprintf(
453                            'The file is too large. Maximum size allowed is: %1$d kB. The file you are trying to upload is %2$d kB.',
454                            (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024),
455                            number_format(($_FILES['attachment_file']['size'] / 1024), 2, '.', '')
456                        );
457                    } else {
458                        do {
459                            $uniqid = sha1(microtime());
460                        } while (file_exists(_PS_DOWNLOAD_DIR_ . $uniqid));
461                        if (!copy($_FILES['attachment_file']['tmp_name'], _PS_DOWNLOAD_DIR_ . $uniqid)) {
462                            $_FILES['attachment_file']['error'][] = 'File copy failed';
463                        }
464                        @unlink($_FILES['attachment_file']['tmp_name']);
465                    }
466                } else {
467                    $_FILES['attachment_file']['error'][] = $this->trans('The file is missing.', [], 'Admin.Notifications.Error');
468                }
469
470                if (empty($_FILES['attachment_file']['error']) && isset($uniqid)) {
471                    $attachment = new Attachment();
472
473                    foreach ($attachment_names as $lang => $name) {
474                        $attachment->name[(int) $lang] = $name;
475                    }
476
477                    foreach ($attachment_descriptions as $lang => $description) {
478                        $attachment->description[(int) $lang] = $description;
479                    }
480
481                    $attachment->file = $uniqid;
482                    $attachment->mime = $_FILES['attachment_file']['type'];
483                    $attachment->file_name = $_FILES['attachment_file']['name'];
484
485                    if (empty($attachment->mime) || Tools::strlen($attachment->mime) > 128) {
486                        $_FILES['attachment_file']['error'][] = $this->trans('Invalid file extension', [], 'Admin.Notifications.Error');
487                    }
488                    if (!Validate::isGenericName($attachment->file_name)) {
489                        $_FILES['attachment_file']['error'][] = $this->trans('Invalid file name', [], 'Admin.Notifications.Error');
490                    }
491                    if (Tools::strlen($attachment->file_name) > 128) {
492                        $_FILES['attachment_file']['error'][] = $this->trans('The file name is too long.', [], 'Admin.Notifications.Error');
493                    }
494                    if (empty($this->errors)) {
495                        $res = $attachment->add();
496                        if (!$res) {
497                            $_FILES['attachment_file']['error'][] = $this->trans('This attachment was unable to be loaded into the database.', [], 'Admin.Catalog.Notification');
498                        } else {
499                            $_FILES['attachment_file']['id_attachment'] = $attachment->id;
500                            $_FILES['attachment_file']['filename'] = $attachment->name[$this->context->employee->id_lang];
501                            $id_product = (int) Tools::getValue($this->identifier);
502                            $res = $attachment->attachProduct($id_product);
503                            if (!$res) {
504                                $_FILES['attachment_file']['error'][] = $this->trans('We were unable to associate this attachment to a product.', [], 'Admin.Catalog.Notification');
505                            }
506                        }
507                    } else {
508                        $_FILES['attachment_file']['error'][] = $this->trans('Invalid file', [], 'Admin.Notifications.Error');
509                    }
510                }
511            }
512
513            die(json_encode($_FILES));
514        }
515    }
516
517    /**
518     * Attach an existing attachment to the product.
519     */
520    public function processAttachments()
521    {
522        if ($id = (int) Tools::getValue($this->identifier)) {
523            $attachments = trim(Tools::getValue('arrayAttachments'), ',');
524            $attachments = explode(',', $attachments);
525            if (!Attachment::attachToProduct($id, $attachments)) {
526                $this->errors[] = $this->trans('An error occurred while saving product attachments.', [], 'Admin.Catalog.Notification');
527            }
528        }
529    }
530
531    public function processDuplicate()
532    {
533        if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
534            $id_product_old = $product->id;
535            if (empty($product->price) && Shop::getContext() == Shop::CONTEXT_GROUP) {
536                $shops = ShopGroup::getShopsFromGroup(Shop::getContextShopGroupID());
537                foreach ($shops as $shop) {
538                    if ($product->isAssociatedToShop($shop['id_shop'])) {
539                        $product_price = new Product($id_product_old, false, null, $shop['id_shop']);
540                        $product->price = $product_price->price;
541                    }
542                }
543            }
544            unset(
545                $product->id,
546                $product->id_product
547            );
548
549            $product->indexed = 0;
550            $product->active = 0;
551            if ($product->add()
552            && Category::duplicateProductCategories($id_product_old, $product->id)
553            && Product::duplicateSuppliers($id_product_old, $product->id)
554            && ($combination_images = Product::duplicateAttributes($id_product_old, $product->id)) !== false
555            && GroupReduction::duplicateReduction($id_product_old, $product->id)
556            && Product::duplicateAccessories($id_product_old, $product->id)
557            && Product::duplicateFeatures($id_product_old, $product->id)
558            && Product::duplicateSpecificPrices($id_product_old, $product->id)
559            && Pack::duplicate($id_product_old, $product->id)
560            && Product::duplicateCustomizationFields($id_product_old, $product->id)
561            && Product::duplicateTags($id_product_old, $product->id)
562            && Product::duplicateDownload($id_product_old, $product->id)) {
563                if ($product->hasAttributes()) {
564                    Product::updateDefaultAttribute($product->id);
565                }
566
567                if (!Tools::getValue('noimage') && !Image::duplicateProductImages($id_product_old, $product->id, $combination_images)) {
568                    $this->errors[] = $this->trans('An error occurred while copying the image.', [], 'Admin.Notifications.Error');
569                } else {
570                    Hook::exec('actionProductAdd', ['id_product_old' => $id_product_old, 'id_product' => (int) $product->id, 'product' => $product]);
571                    if (in_array($product->visibility, ['both', 'search']) && Configuration::get('PS_SEARCH_INDEXATION')) {
572                        Search::indexation(false, $product->id);
573                    }
574                    $this->redirect_after = self::$currentIndex . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '') . '&conf=19&token=' . $this->token;
575                }
576            } else {
577                $this->errors[] = $this->trans('An error occurred while creating an object.', [], 'Admin.Notifications.Error');
578            }
579        }
580    }
581
582    public function processDelete()
583    {
584        if (Validate::isLoadedObject($object = $this->loadObject()) && isset($this->fieldImageSettings)) {
585            /** @var Product $object */
586            // check if request at least one object with noZeroObject
587            if (isset($object->noZeroObject) && count($taxes = call_user_func([$this->className, $object->noZeroObject])) <= 1) {
588                $this->errors[] = $this->trans('You need at least one object.', [], 'Admin.Notifications.Error') . ' <b>' . $this->table . '</b><br />' . $this->trans('You cannot delete all of the items.', [], 'Admin.Notifications.Error');
589            } else {
590                /*
591                 * @since 1.5.0
592                 * It is NOT possible to delete a product if there are currently:
593                 * - physical stock for this product
594                 * - supply order(s) for this product
595                 */
596                if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $object->advanced_stock_management) {
597                    $stock_manager = StockManagerFactory::getManager();
598                    $physical_quantity = $stock_manager->getProductPhysicalQuantities($object->id, 0);
599                    $real_quantity = $stock_manager->getProductRealQuantities($object->id, 0);
600                    if ($physical_quantity > 0 || $real_quantity > $physical_quantity) {
601                        $this->errors[] = $this->trans('You cannot delete this product because there is physical stock left.', [], 'Admin.Catalog.Notification');
602                    }
603                }
604
605                if (!count($this->errors)) {
606                    if ($object->delete()) {
607                        $id_category = (int) Tools::getValue('id_category');
608                        $category_url = empty($id_category) ? '' : '&id_category=' . (int) $id_category;
609                        PrestaShopLogger::addLog(sprintf('%s deletion', $this->className), 1, null, $this->className, (int) $object->id, true, (int) $this->context->employee->id);
610                        $this->redirect_after = self::$currentIndex . '&conf=1&token=' . $this->token . $category_url;
611                    } else {
612                        $this->errors[] = $this->trans('An error occurred during deletion.', [], 'Admin.Notifications.Error');
613                    }
614                }
615            }
616        } else {
617            $this->errors[] = $this->trans('An error occurred while deleting the object.', [], 'Admin.Notifications.Error') . ' <b>' . $this->table . '</b> ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error');
618        }
619    }
620
621    public function processImage()
622    {
623        $id_image = (int) Tools::getValue('id_image');
624        $image = new Image((int) $id_image);
625        if (Validate::isLoadedObject($image)) {
626            /* Update product image/legend */
627            // @todo : move in processEditProductImage
628            if (Tools::getIsset('editImage')) {
629                if ($image->cover) {
630                    $_POST['cover'] = 1;
631                }
632
633                $_POST['id_image'] = $image->id;
634            } elseif (Tools::getIsset('coverImage')) {
635                /* Choose product cover image */
636                Image::deleteCover($image->id_product);
637                $image->cover = 1;
638                if (!$image->update()) {
639                    $this->errors[] = $this->trans('You cannot change the product\'s cover image.', [], 'Admin.Catalog.Notification');
640                } else {
641                    $productId = (int) Tools::getValue('id_product');
642                    @unlink(_PS_TMP_IMG_DIR_ . 'product_' . $productId . '.jpg');
643                    @unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . $productId . '_' . $this->context->shop->id . '.jpg');
644                    $this->redirect_after = self::$currentIndex . '&id_product=' . $image->id_product . '&id_category=' . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '') . '&action=Images&addproduct' . '&token=' . $this->token;
645                }
646            } elseif (Tools::getIsset('imgPosition') && Tools::getIsset('imgDirection')) {
647                /* Choose product image position */
648                $image->updatePosition(Tools::getValue('imgDirection'), Tools::getValue('imgPosition'));
649                $this->redirect_after = self::$currentIndex . '&id_product=' . $image->id_product . '&id_category=' . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '') . '&add' . $this->table . '&action=Images&token=' . $this->token;
650            }
651        } else {
652            $this->errors[] = $this->trans('The image could not be found. ', [], 'Admin.Catalog.Notification');
653        }
654    }
655
656    protected function processBulkDelete()
657    {
658        if ($this->access('delete')) {
659            if (is_array($this->boxes) && !empty($this->boxes)) {
660                $object = new $this->className();
661
662                if (isset($object->noZeroObject) &&
663                    // Check if all object will be deleted
664                    (count(call_user_func([$this->className, $object->noZeroObject])) <= 1 || count($_POST[$this->table . 'Box']) == count(call_user_func([$this->className, $object->noZeroObject])))) {
665                    $this->errors[] = $this->trans('You need at least one object.', [], 'Admin.Notifications.Error') . ' <b>' . $this->table . '</b><br />' . $this->trans('You cannot delete all of the items.', [], 'Admin.Notifications.Error');
666                } else {
667                    $success = 1;
668                    $products = Tools::getValue($this->table . 'Box');
669                    if (is_array($products) && ($count = count($products))) {
670                        // Deleting products can be quite long on a cheap server. Let's say 1.5 seconds by product (I've seen it!).
671                        if ((int) (ini_get('max_execution_time')) < round($count * 1.5)) {
672                            ini_set('max_execution_time', round($count * 1.5));
673                        }
674
675                        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) {
676                            $stock_manager = StockManagerFactory::getManager();
677                        }
678
679                        foreach ($products as $id_product) {
680                            $product = new Product((int) $id_product);
681                            /*
682                             * @since 1.5.0
683                             * It is NOT possible to delete a product if there are currently:
684                             * - physical stock for this product
685                             * - supply order(s) for this product
686                             */
687                            if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $product->advanced_stock_management) {
688                                $physical_quantity = $stock_manager->getProductPhysicalQuantities($product->id, 0);
689                                $real_quantity = $stock_manager->getProductRealQuantities($product->id, 0);
690                                if ($physical_quantity > 0 || $real_quantity > $physical_quantity) {
691                                    $this->errors[] = $this->trans('You cannot delete the product #%d because there is physical stock left.', [$product->id], 'Admin.Catalog.Notification');
692                                }
693                            }
694                            if (!count($this->errors)) {
695                                if ($product->delete()) {
696                                    PrestaShopLogger::addLog(sprintf('%s deletion', $this->className), 1, null, $this->className, (int) $product->id, true, (int) $this->context->employee->id);
697                                } else {
698                                    $success = false;
699                                }
700                            } else {
701                                $success = 0;
702                            }
703                        }
704                    }
705
706                    if ($success) {
707                        $id_category = (int) Tools::getValue('id_category');
708                        $category_url = empty($id_category) ? '' : '&id_category=' . (int) $id_category;
709                        $this->redirect_after = self::$currentIndex . '&conf=2&token=' . $this->token . $category_url;
710                    } else {
711                        $this->errors[] = $this->trans('An error occurred while deleting this selection.', [], 'Admin.Notifications.Error');
712                    }
713                }
714            } else {
715                $this->errors[] = $this->trans('You must select at least one element to delete.', [], 'Admin.Notifications.Error');
716            }
717        } else {
718            $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
719        }
720    }
721
722    public function processProductAttribute()
723    {
724        // Don't process if the combination fields have not been submitted
725        if (!Combination::isFeatureActive() || !Tools::getValue('attribute_combination_list')) {
726            return;
727        }
728
729        if (Validate::isLoadedObject($product = $this->object)) {
730            if ($this->isProductFieldUpdated('attribute_price') && (!Tools::getIsset('attribute_price') || Tools::getIsset('attribute_price') == null)) {
731                $this->errors[] = $this->trans('The price attribute is required.', [], 'Admin.Catalog.Notification');
732            }
733            if (!Tools::getIsset('attribute_combination_list') || Tools::isEmpty(Tools::getValue('attribute_combination_list'))) {
734                $this->errors[] = $this->trans('You must add at least one attribute.', [], 'Admin.Catalog.Notification');
735            }
736
737            $array_checks = [
738                'reference' => 'isReference',
739                'supplier_reference' => 'isReference',
740                'location' => 'isReference',
741                'ean13' => 'isEan13',
742                'isbn' => 'isIsbn',
743                'upc' => 'isUpc',
744                'mpn' => 'isMpn',
745                'wholesale_price' => 'isPrice',
746                'price' => 'isPrice',
747                'ecotax' => 'isPrice',
748                'quantity' => 'isInt',
749                'weight' => 'isUnsignedFloat',
750                'unit_price_impact' => 'isPrice',
751                'default_on' => 'isBool',
752                'minimal_quantity' => 'isUnsignedInt',
753                'available_date' => 'isDateFormat',
754            ];
755            foreach ($array_checks as $property => $check) {
756                if (Tools::getValue('attribute_' . $property) !== false && !call_user_func(['Validate', $check], Tools::getValue('attribute_' . $property))) {
757                    $this->errors[] = $this->trans('The %s field is not valid', [$property], 'Admin.Notifications.Error');
758                }
759            }
760
761            if (!count($this->errors)) {
762                if (!isset($_POST['attribute_wholesale_price'])) {
763                    $_POST['attribute_wholesale_price'] = 0;
764                }
765                if (!isset($_POST['attribute_price_impact'])) {
766                    $_POST['attribute_price_impact'] = 0;
767                }
768                if (!isset($_POST['attribute_weight_impact'])) {
769                    $_POST['attribute_weight_impact'] = 0;
770                }
771                if (!isset($_POST['attribute_ecotax'])) {
772                    $_POST['attribute_ecotax'] = 0;
773                }
774                if (Tools::getValue('attribute_default')) {
775                    $product->deleteDefaultAttributes();
776                }
777
778                // Change existing one
779                if (($id_product_attribute = (int) Tools::getValue('id_product_attribute')) || ($id_product_attribute = $product->productAttributeExists(Tools::getValue('attribute_combination_list'), false, null, true, true))) {
780                    if ($this->access('edit')) {
781                        if ($this->isProductFieldUpdated('available_date_attribute') && (Tools::getValue('available_date_attribute') != '' && !Validate::isDateFormat(Tools::getValue('available_date_attribute')))) {
782                            $this->errors[] = $this->trans('Invalid date format.', [], 'Admin.Notifications.Error');
783                        } else {
784                            $product->updateAttribute(
785                                (int) $id_product_attribute,
786                                $this->isProductFieldUpdated('attribute_wholesale_price') ? Tools::getValue('attribute_wholesale_price') : null,
787                                $this->isProductFieldUpdated('attribute_price_impact') ? Tools::getValue('attribute_price') * Tools::getValue('attribute_price_impact') : null,
788                                $this->isProductFieldUpdated('attribute_weight_impact') ? Tools::getValue('attribute_weight') * Tools::getValue('attribute_weight_impact') : null,
789                                $this->isProductFieldUpdated('attribute_unit_impact') ? Tools::getValue('attribute_unity') * Tools::getValue('attribute_unit_impact') : null,
790                                $this->isProductFieldUpdated('attribute_ecotax') ? Tools::getValue('attribute_ecotax') : null,
791                                Tools::getValue('id_image_attr'),
792                                Tools::getValue('attribute_reference'),
793                                Tools::getValue('attribute_ean13'),
794                                $this->isProductFieldUpdated('attribute_default') ? Tools::getValue('attribute_default') : null,
795                                Tools::getValue('attribute_location'),
796                                Tools::getValue('attribute_upc'),
797                                $this->isProductFieldUpdated('attribute_minimal_quantity') ? Tools::getValue('attribute_minimal_quantity') : null,
798                                $this->isProductFieldUpdated('available_date_attribute') ? Tools::getValue('available_date_attribute') : null,
799                                false,
800                                [],
801                                Tools::getValue('attribute_isbn'),
802                                Tools::getValue('attribute_low_stock_threshold'),
803                                Tools::getValue('attribute_low_stock_alert'),
804                                Tools::getValue('attribute_mpn')
805                            );
806                            StockAvailable::setProductDependsOnStock((int) $product->id, $product->depends_on_stock, null, (int) $id_product_attribute);
807                            StockAvailable::setProductOutOfStock((int) $product->id, $product->out_of_stock, null, (int) $id_product_attribute);
808                        }
809                    } else {
810                        $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error');
811                    }
812                } else {
813                    // Add new
814                    if ($this->access('add')) {
815                        if ($product->productAttributeExists(Tools::getValue('attribute_combination_list'))) {
816                            $this->errors[] = $this->trans('This combination already exists.', [], 'Admin.Catalog.Notification');
817                        } else {
818                            $id_product_attribute = $product->addCombinationEntity(
819                                Tools::getValue('attribute_wholesale_price'),
820                                Tools::getValue('attribute_price') * Tools::getValue('attribute_price_impact'),
821                                Tools::getValue('attribute_weight') * Tools::getValue('attribute_weight_impact'),
822                                Tools::getValue('attribute_unity') * Tools::getValue('attribute_unit_impact'),
823                                Tools::getValue('attribute_ecotax'),
824                                0,
825                                Tools::getValue('id_image_attr'),
826                                Tools::getValue('attribute_reference'),
827                                null,
828                                Tools::getValue('attribute_ean13'),
829                                Tools::getValue('attribute_default'),
830                                Tools::getValue('attribute_location'),
831                                Tools::getValue('attribute_upc'),
832                                Tools::getValue('attribute_minimal_quantity'),
833                                [],
834                                Tools::getValue('available_date_attribute'),
835                                Tools::getValue('attribute_isbn'),
836                                Tools::getValue('attribute_low_stock_threshold'),
837                                Tools::getValue('attribute_low_stock_alert'),
838                                Tools::getValue('attribute_mpn')
839                            );
840                            StockAvailable::setProductDependsOnStock((int) $product->id, $product->depends_on_stock, null, (int) $id_product_attribute);
841                            StockAvailable::setProductOutOfStock((int) $product->id, $product->out_of_stock, null, (int) $id_product_attribute);
842                        }
843                    } else {
844                        $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
845                    }
846                }
847                if (!count($this->errors)) {
848                    $combination = new Combination((int) $id_product_attribute);
849                    $combination->setAttributes(Tools::getValue('attribute_combination_list'));
850
851                    // images could be deleted before
852                    $id_images = Tools::getValue('id_image_attr');
853                    if (!empty($id_images)) {
854                        $combination->setImages($id_images);
855                    }
856
857                    $product->checkDefaultAttributes();
858                    if (Tools::getValue('attribute_default')) {
859                        Product::updateDefaultAttribute((int) $product->id);
860                        if (isset($id_product_attribute)) {
861                            $product->cache_default_attribute = (int) $id_product_attribute;
862                        }
863
864                        if ($available_date = Tools::getValue('available_date_attribute')) {
865                            $product->setAvailableDate($available_date);
866                        } else {
867                            $product->setAvailableDate();
868                        }
869                    }
870                }
871            }
872        }
873    }
874
875    public function processFeatures($id_product = null)
876    {
877        if (!Feature::isFeatureActive()) {
878            return;
879        }
880
881        $id_product = (int) $id_product ? $id_product : (int) Tools::getValue('id_product');
882
883        if (Validate::isLoadedObject($product = new Product($id_product))) {
884            // delete all objects
885            $product->deleteFeatures();
886
887            // add new objects
888            $languages = Language::getLanguages(false);
889            $form = Tools::getValue('form', false);
890            if (false !== $form) {
891                $features = isset($form['step1']['features']) ? $form['step1']['features'] : [];
892                if (is_array($features)) {
893                    foreach ($features as $feature) {
894                        if (!empty($feature['value'])) {
895                            $product->addFeaturesToDB($feature['feature'], $feature['value']);
896                        } elseif ($defaultValue = $this->checkFeatures($languages, $feature)) {
897                            $idValue = $product->addFeaturesToDB($feature['feature'], 0, 1);
898                            foreach ($languages as $language) {
899                                $valueToAdd = (isset($feature['custom_value'][$language['id_lang']]))
900                                    ? $feature['custom_value'][$language['id_lang']]
901                                    : $defaultValue;
902
903                                $product->addFeaturesCustomToDB($idValue, (int) $language['id_lang'], $valueToAdd);
904                            }
905                        }
906                    }
907                }
908            }
909        } else {
910            $this->errors[] = $this->trans('A product must be created before adding features.', [], 'Admin.Catalog.Notification');
911        }
912    }
913
914    /**
915     * This function is never called at the moment (specific prices cannot be edited).
916     */
917    public function processPricesModification()
918    {
919        $id_specific_prices = Tools::getValue('spm_id_specific_price');
920        $id_combinations = Tools::getValue('spm_id_product_attribute');
921        $id_shops = Tools::getValue('spm_id_shop');
922        $id_currencies = Tools::getValue('spm_id_currency');
923        $id_countries = Tools::getValue('spm_id_country');
924        $id_groups = Tools::getValue('spm_id_group');
925        $id_customers = Tools::getValue('spm_id_customer');
926        $prices = Tools::getValue('spm_price');
927        $from_quantities = Tools::getValue('spm_from_quantity');
928        $reductions = Tools::getValue('spm_reduction');
929        $reduction_types = Tools::getValue('spm_reduction_type');
930        $froms = Tools::getValue('spm_from');
931        $tos = Tools::getValue('spm_to');
932
933        foreach ($id_specific_prices as $key => $id_specific_price) {
934            if ($reduction_types[$key] == 'percentage' && ((float) $reductions[$key] <= 0 || (float) $reductions[$key] > 100)) {
935                $this->errors[] = $this->trans('Submitted reduction value (0-100) is out-of-range', [], 'Admin.Catalog.Notification');
936            } elseif ($this->_validateSpecificPrice($id_shops[$key], $id_currencies[$key], $id_countries[$key], $id_groups[$key], $id_customers[$key], $prices[$key], $from_quantities[$key], $reductions[$key], $reduction_types[$key], $froms[$key], $tos[$key], $id_combinations[$key])) {
937                $specific_price = new SpecificPrice((int) ($id_specific_price));
938                $specific_price->id_shop = (int) $id_shops[$key];
939                $specific_price->id_product_attribute = (int) $id_combinations[$key];
940                $specific_price->id_currency = (int) ($id_currencies[$key]);
941                $specific_price->id_country = (int) ($id_countries[$key]);
942                $specific_price->id_group = (int) ($id_groups[$key]);
943                $specific_price->id_customer = (int) $id_customers[$key];
944                $specific_price->price = (float) ($prices[$key]);
945                $specific_price->from_quantity = (int) ($from_quantities[$key]);
946                $specific_price->reduction = (float) ($reduction_types[$key] == 'percentage' ? ($reductions[$key] / 100) : $reductions[$key]);
947                $specific_price->reduction_type = !$reductions[$key] ? 'amount' : $reduction_types[$key];
948                $specific_price->from = !$froms[$key] ? '0000-00-00 00:00:00' : $froms[$key];
949                $specific_price->to = !$tos[$key] ? '0000-00-00 00:00:00' : $tos[$key];
950                if (!$specific_price->update()) {
951                    $this->errors[] = $this->trans('An error occurred while updating the specific price.', [], 'Admin.Catalog.Notification');
952                }
953            }
954        }
955        if (!count($this->errors)) {
956            $this->redirect_after = self::$currentIndex . '&id_product=' . (int) (Tools::getValue('id_product')) . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '') . '&update' . $this->table . '&action=Prices&token=' . $this->token;
957        }
958    }
959
960    public function processPriceAddition()
961    {
962        // Check if a specific price has been submitted
963        if (!Tools::getIsset('submitPriceAddition')) {
964            return;
965        }
966
967        $id_product = Tools::getValue('id_product');
968        $id_product_attribute = Tools::getValue('sp_id_product_attribute');
969        $id_shop = Tools::getValue('sp_id_shop');
970        $id_currency = Tools::getValue('sp_id_currency');
971        $id_country = Tools::getValue('sp_id_country');
972        $id_group = Tools::getValue('sp_id_group');
973        $id_customer = Tools::getValue('sp_id_customer');
974        $price = Tools::getValue('leave_bprice') ? '-1' : Tools::getValue('sp_price');
975        $from_quantity = Tools::getValue('sp_from_quantity');
976        $reduction = (float) (Tools::getValue('sp_reduction'));
977        $reduction_tax = Tools::getValue('sp_reduction_tax');
978        $reduction_type = !$reduction ? 'amount' : Tools::getValue('sp_reduction_type');
979        $reduction_type = $reduction_type == '-' ? 'amount' : $reduction_type;
980        $from = Tools::getValue('sp_from');
981        if (!$from) {
982            $from = '0000-00-00 00:00:00';
983        }
984        $to = Tools::getValue('sp_to');
985        if (!$to) {
986            $to = '0000-00-00 00:00:00';
987        }
988
989        if (($price == '-1') && ((float) $reduction == '0')) {
990            $this->errors[] = $this->trans('No reduction value has been submitted', [], 'Admin.Catalog.Notification');
991        } elseif ($to != '0000-00-00 00:00:00' && strtotime($to) < strtotime($from)) {
992            $this->errors[] = $this->trans('Invalid date range', [], 'Admin.Notifications.Error');
993        } elseif ($reduction_type == 'percentage' && ((float) $reduction <= 0 || (float) $reduction > 100)) {
994            $this->errors[] = $this->trans('Submitted reduction value (0-100) is out-of-range', [], 'Admin.Catalog.Notification');
995        } elseif ($this->_validateSpecificPrice($id_shop, $id_currency, $id_country, $id_group, $id_customer, $price, $from_quantity, $reduction, $reduction_type, $from, $to, $id_product_attribute)) {
996            $specificPrice = new SpecificPrice();
997            $specificPrice->id_product = (int) $id_product;
998            $specificPrice->id_product_attribute = (int) $id_product_attribute;
999            $specificPrice->id_shop = (int) $id_shop;
1000            $specificPrice->id_currency = (int) ($id_currency);
1001            $specificPrice->id_country = (int) ($id_country);
1002            $specificPrice->id_group = (int) ($id_group);
1003            $specificPrice->id_customer = (int) $id_customer;
1004            $specificPrice->price = (float) ($price);
1005            $specificPrice->from_quantity = (int) ($from_quantity);
1006            $specificPrice->reduction = (float) ($reduction_type == 'percentage' ? $reduction / 100 : $reduction);
1007            $specificPrice->reduction_tax = $reduction_tax;
1008            $specificPrice->reduction_type = $reduction_type;
1009            $specificPrice->from = $from;
1010            $specificPrice->to = $to;
1011            if (!$specificPrice->add()) {
1012                $this->errors[] = $this->trans('An error occurred while updating the specific price.', [], 'Admin.Catalog.Notification');
1013            }
1014        }
1015    }
1016
1017    public function ajaxProcessDeleteSpecificPrice()
1018    {
1019        if ($this->access('delete')) {
1020            $id_specific_price = (int) Tools::getValue('id_specific_price');
1021            if (!$id_specific_price || !Validate::isUnsignedId($id_specific_price)) {
1022                $error = $this->trans('The specific price ID is invalid.', [], 'Admin.Catalog.Notification');
1023            } else {
1024                $specificPrice = new SpecificPrice((int) $id_specific_price);
1025                if (!$specificPrice->delete()) {
1026                    $error = $this->trans('An error occurred while attempting to delete the specific price.', [], 'Admin.Catalog.Notification');
1027                }
1028            }
1029        } else {
1030            $error = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
1031        }
1032
1033        if (isset($error)) {
1034            $json = [
1035                'status' => 'error',
1036                'message' => $error,
1037            ];
1038        } else {
1039            $json = [
1040                'status' => 'ok',
1041                'message' => $this->_conf[1],
1042            ];
1043        }
1044
1045        die(json_encode($json));
1046    }
1047
1048    public function processSpecificPricePriorities()
1049    {
1050        if (!($obj = $this->loadObject())) {
1051            return;
1052        }
1053        if (!$priorities = Tools::getValue('specificPricePriority')) {
1054            $this->errors[] = $this->trans('Please specify priorities.', [], 'Admin.Catalog.Notification');
1055        } elseif (Tools::isSubmit('specificPricePriorityToAll') && Tools::getValue('specificPricePriorityToAll')) {
1056            if (!SpecificPrice::setPriorities($priorities)) {
1057                $this->errors[] = $this->trans('An error occurred while updating priorities.', [], 'Admin.Catalog.Notification');
1058            } else {
1059                $this->confirmations[] = 'The price rule has successfully updated';
1060            }
1061        } elseif (!SpecificPrice::setSpecificPriority((int) $obj->id, $priorities)) {
1062            $this->errors[] = $this->trans('An error occurred while setting priorities.', [], 'Admin.Catalog.Notification');
1063        }
1064    }
1065
1066    public function processCustomizationConfiguration()
1067    {
1068        $product = $this->object;
1069        // Get the number of existing customization fields ($product->text_fields is the updated value, not the existing value)
1070        $current_customization = $product->getCustomizationFieldIds();
1071        $files_count = 0;
1072        $text_count = 0;
1073        if (is_array($current_customization)) {
1074            foreach ($current_customization as $field) {
1075                if ($field['type'] == Product::CUSTOMIZE_TEXTFIELD) {
1076                    ++$text_count;
1077                } else {
1078                    ++$files_count;
1079                }
1080            }
1081        }
1082
1083        if (!$product->createLabels((int) $product->uploadable_files - $files_count, (int) $product->text_fields - $text_count)) {
1084            $this->errors[] = $this->trans('An error occurred while creating customization fields.', [], 'Admin.Catalog.Notification');
1085        }
1086        if (!count($this->errors) && !$product->updateLabels()) {
1087            $this->errors[] = $this->trans('An error occurred while updating customization fields.', [], 'Admin.Catalog.Notification');
1088        }
1089        $product->customizable = ($product->uploadable_files > 0 || $product->text_fields > 0) ? 1 : 0;
1090        if (($product->uploadable_files != $files_count || $product->text_fields != $text_count) && !count($this->errors) && !$product->update()) {
1091            $this->errors[] = $this->trans('An error occurred while updating the custom configuration.', [], 'Admin.Catalog.Notification');
1092        }
1093    }
1094
1095    public function processProductCustomization()
1096    {
1097        if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
1098            foreach ($_POST as $field => $value) {
1099                if (strncmp($field, 'label_', 6) == 0 && !Validate::isLabel($value)) {
1100                    $this->errors[] = $this->trans('The label fields defined are invalid.', [], 'Admin.Catalog.Notification');
1101                }
1102            }
1103            if (empty($this->errors) && !$product->updateLabels()) {
1104                $this->errors[] = $this->trans('An error occurred while updating customization fields.', [], 'Admin.Catalog.Notification');
1105            }
1106            if (empty($this->errors)) {
1107                $this->confirmations[] = 'Update successful';
1108            }
1109        } else {
1110            $this->errors[] = $this->trans('A product must be created before adding customization.', [], 'Admin.Catalog.Notification');
1111        }
1112    }
1113
1114    /**
1115     * Overrides parent for custom redirect link.
1116     */
1117    public function processPosition()
1118    {
1119        /** @var Product $object */
1120        if (!Validate::isLoadedObject($object = $this->loadObject())) {
1121            $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') .
1122                ' <b>' . $this->table . '</b> ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error');
1123        } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) {
1124            $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error');
1125        } else {
1126            $category = new Category((int) Tools::getValue('id_category'));
1127            if (Validate::isLoadedObject($category)) {
1128                Hook::exec('actionCategoryUpdate', ['category' => $category]);
1129            }
1130            $this->redirect_after = self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&action=Customization&conf=5' . (($id_category = (Tools::getIsset('id_category') ? (int) Tools::getValue('id_category') : '')) ? ('&id_category=' . $id_category) : '') . '&token=' . Tools::getAdminTokenLite('AdminProducts');
1131        }
1132    }
1133
1134    public function initProcess()
1135    {
1136        if (Tools::isSubmit('submitAddproductAndStay') || Tools::isSubmit('submitAddproduct')) {
1137            $this->id_object = (int) Tools::getValue('id_product');
1138            $this->object = new Product($this->id_object);
1139
1140            if ($this->isTabSubmitted('Informations') && $this->object->is_virtual && (int) Tools::getValue('type_product') != 2) {
1141                if ($id_product_download = (int) ProductDownload::getIdFromIdProduct($this->id_object)) {
1142                    $product_download = new ProductDownload($id_product_download);
1143                    if (!$product_download->deleteFile($id_product_download)) {
1144                        $this->errors[] = $this->trans('Cannot delete file', [], 'Admin.Notifications.Error');
1145                    }
1146                }
1147            }
1148        }
1149
1150        // Delete a product in the download folder
1151        if (Tools::getValue('deleteVirtualProduct')) {
1152            if ($this->access('delete')) {
1153                $this->action = 'deleteVirtualProduct';
1154            } else {
1155                $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
1156            }
1157        } elseif (Tools::isSubmit('submitAddProductAndPreview')) {
1158            // Product preview
1159            $this->display = 'edit';
1160            $this->action = 'save';
1161            if (Tools::getValue('id_product')) {
1162                $this->id_object = Tools::getValue('id_product');
1163                $this->object = new Product((int) Tools::getValue('id_product'));
1164            }
1165        } elseif (Tools::isSubmit('submitAttachments')) {
1166            if ($this->access('edit')) {
1167                $this->action = 'attachments';
1168                $this->tab_display = 'attachments';
1169            } else {
1170                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1171            }
1172        } elseif (Tools::getIsset('duplicate' . $this->table)) {
1173            // Product duplication
1174            if ($this->access('add')) {
1175                $this->action = 'duplicate';
1176            } else {
1177                $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error');
1178            }
1179        } elseif (Tools::getValue('id_image') && Tools::getValue('ajax')) {
1180            // Product images management
1181            if ($this->access('edit')) {
1182                $this->action = 'image';
1183            } else {
1184                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1185            }
1186        } elseif (Tools::isSubmit('submitProductAttribute')) {
1187            // Product attributes management
1188            if ($this->access('edit')) {
1189                $this->action = 'productAttribute';
1190            } else {
1191                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1192            }
1193        } elseif (Tools::isSubmit('submitFeatures') || Tools::isSubmit('submitFeaturesAndStay')) {
1194            // Product features management
1195            if ($this->access('edit')) {
1196                $this->action = 'features';
1197            } else {
1198                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1199            }
1200        } elseif (Tools::isSubmit('submitPricesModification')) {
1201            // Product specific prices management NEVER USED
1202            if ($this->access('add')) {
1203                $this->action = 'pricesModification';
1204            } else {
1205                $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error');
1206            }
1207        } elseif (Tools::isSubmit('deleteSpecificPrice')) {
1208            if ($this->access('delete')) {
1209                $this->action = 'deleteSpecificPrice';
1210            } else {
1211                $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
1212            }
1213        } elseif (Tools::isSubmit('submitSpecificPricePriorities')) {
1214            if ($this->access('edit')) {
1215                $this->action = 'specificPricePriorities';
1216                $this->tab_display = 'prices';
1217            } else {
1218                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1219            }
1220        } elseif (Tools::isSubmit('submitCustomizationConfiguration')) {
1221            // Customization management
1222            if ($this->access('edit')) {
1223                $this->action = 'customizationConfiguration';
1224                $this->tab_display = 'customization';
1225                $this->display = 'edit';
1226            } else {
1227                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1228            }
1229        } elseif (Tools::isSubmit('submitProductCustomization')) {
1230            if ($this->access('edit')) {
1231                $this->action = 'productCustomization';
1232                $this->tab_display = 'customization';
1233                $this->display = 'edit';
1234            } else {
1235                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
1236            }
1237        } elseif (Tools::isSubmit('id_product')) {
1238            $post_max_size = Tools::getMaxUploadSize(Configuration::get('PS_LIMIT_UPLOAD_FILE_VALUE') * 1024 * 1024);
1239            if ($post_max_size && isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] && $_SERVER['CONTENT_LENGTH'] > $post_max_size) {
1240                $this->errors[] = $this->trans(
1241                    'The uploaded file exceeds the "Maximum size for a downloadable product" set in preferences (%1$dMB) or the post_max_size/ directive in php.ini (%2$dMB).',
1242                    [
1243                        number_format((Configuration::get('PS_LIMIT_UPLOAD_FILE_VALUE'))),
1244                        ($post_max_size / 1024 / 1024),
1245                    ],
1246                    'Admin.Catalog.Notification'
1247                );
1248            }
1249        }
1250
1251        if (!$this->action) {
1252            parent::initProcess();
1253        } else {
1254            $this->id_object = (int) Tools::getValue($this->identifier);
1255        }
1256
1257        if (isset($this->available_tabs[Tools::getValue('key_tab')])) {
1258            $this->tab_display = Tools::getValue('key_tab');
1259        }
1260
1261        // Set tab to display if not decided already
1262        if (!$this->tab_display && $this->action) {
1263            if (in_array($this->action, array_keys($this->available_tabs))) {
1264                $this->tab_display = $this->action;
1265            }
1266        }
1267
1268        // And if still not set, use default
1269        if (!$this->tab_display) {
1270            if (in_array($this->default_tab, $this->available_tabs)) {
1271                $this->tab_display = $this->default_tab;
1272            } else {
1273                $this->tab_display = key($this->available_tabs);
1274            }
1275        }
1276    }
1277
1278    /**
1279     * postProcess for new form archi (need object return).
1280     *
1281     * @return ObjectModel|false
1282     */
1283    public function postCoreProcess()
1284    {
1285        return parent::postProcess();
1286    }
1287
1288    /**
1289     * postProcess handle every checks before saving products information.
1290     */
1291    public function postProcess()
1292    {
1293        if (!$this->redirect_after) {
1294            parent::postProcess();
1295        }
1296
1297        if ($this->display == 'edit' || $this->display == 'add') {
1298            $this->addJqueryUI([
1299                'ui.core',
1300                'ui.widget',
1301            ]);
1302
1303            $this->addjQueryPlugin([
1304                'autocomplete',
1305                'tablednd',
1306                'thickbox',
1307                'ajaxfileupload',
1308                'date',
1309                'tagify',
1310                'select2',
1311                'validate',
1312            ]);
1313
1314            $this->addJS([
1315                _PS_JS_DIR_ . 'admin/products.js',
1316                _PS_JS_DIR_ . 'admin/attributes.js',
1317                _PS_JS_DIR_ . 'admin/price.js',
1318                _PS_JS_DIR_ . 'tiny_mce/tiny_mce.js',
1319                _PS_JS_DIR_ . 'admin/tinymce.inc.js',
1320                _PS_JS_DIR_ . 'admin/dnd.js',
1321                _PS_JS_DIR_ . 'jquery/ui/jquery.ui.progressbar.min.js',
1322                _PS_JS_DIR_ . 'vendor/spin.js',
1323                _PS_JS_DIR_ . 'vendor/ladda.js',
1324            ]);
1325
1326            $this->addJS(_PS_JS_DIR_ . 'jquery/plugins/select2/select2_locale_' . $this->context->language->iso_code . '.js');
1327            $this->addJS(_PS_JS_DIR_ . 'jquery/plugins/validate/localization/messages_' . $this->context->language->iso_code . '.js');
1328
1329            $this->addCSS([
1330                _PS_JS_DIR_ . 'jquery/plugins/timepicker/jquery-ui-timepicker-addon.css',
1331            ]);
1332        }
1333    }
1334
1335    public function ajaxProcessDeleteProductAttribute()
1336    {
1337        if (!Combination::isFeatureActive()) {
1338            return;
1339        }
1340
1341        if ($this->access('delete')) {
1342            $id_product = (int) Tools::getValue('id_product');
1343            $id_product_attribute = (int) Tools::getValue('id_product_attribute');
1344
1345            if ($id_product && Validate::isUnsignedId($id_product) && Validate::isLoadedObject($product = new Product($id_product))) {
1346                if (($depends_on_stock = StockAvailable::dependsOnStock($id_product)) && StockAvailable::getQuantityAvailableByProduct($id_product, $id_product_attribute)) {
1347                    $json = [
1348                        'status' => 'error',
1349                        'message' => 'It is not possible to delete a combination while it still has some quantities in the Advanced Stock Management. You must delete its stock first.',
1350                    ];
1351                } else {
1352                    $product->deleteAttributeCombination((int) $id_product_attribute);
1353                    $product->checkDefaultAttributes();
1354                    Tools::clearColorListCache((int) $product->id);
1355                    if (!$product->hasAttributes()) {
1356                        $product->cache_default_attribute = 0;
1357                        $product->update();
1358                    } else {
1359                        Product::updateDefaultAttribute($id_product);
1360                    }
1361
1362                    if ($depends_on_stock && !Stock::deleteStockByIds($id_product, $id_product_attribute)) {
1363                        $json = [
1364                            'status' => 'error',
1365                            'message' => 'Error while deleting the stock',
1366                        ];
1367                    } else {
1368                        $json = [
1369                            'status' => 'ok',
1370                            'message' => $this->_conf[1],
1371                            'id_product_attribute' => (int) $id_product_attribute,
1372                        ];
1373                    }
1374                }
1375            } else {
1376                $json = [
1377                    'status' => 'error',
1378                    'message' => 'You cannot delete this attribute.',
1379                ];
1380            }
1381        } else {
1382            $json = [
1383                'status' => 'error',
1384                'message' => 'You do not have permission to delete this.',
1385            ];
1386        }
1387
1388        die(json_encode($json));
1389    }
1390
1391    public function ajaxProcessDefaultProductAttribute()
1392    {
1393        if ($this->access('edit')) {
1394            if (!Combination::isFeatureActive()) {
1395                return;
1396            }
1397
1398            if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
1399                $product->deleteDefaultAttributes();
1400                $product->setDefaultAttribute((int) Tools::getValue('id_product_attribute'));
1401                $json = [
1402                    'status' => 'ok',
1403                    'message' => $this->_conf[4],
1404                ];
1405            } else {
1406                $json = [
1407                    'status' => 'error',
1408                    'message' => 'You cannot make this the default attribute.',
1409                ];
1410            }
1411
1412            die(json_encode($json));
1413        }
1414    }
1415
1416    public function ajaxProcessEditProductAttribute()
1417    {
1418        if ($this->access('edit')) {
1419            $id_product = (int) Tools::getValue('id_product');
1420            $id_product_attribute = (int) Tools::getValue('id_product_attribute');
1421            if ($id_product && Validate::isUnsignedId($id_product) && Validate::isLoadedObject($product = new Product((int) $id_product))) {
1422                $combinations = $product->getAttributeCombinationsById($id_product_attribute, $this->context->language->id);
1423                foreach ($combinations as $key => $combination) {
1424                    $combinations[$key]['attributes'][] = [$combination['group_name'], $combination['attribute_name'], $combination['id_attribute']];
1425                }
1426
1427                die(json_encode($combinations));
1428            }
1429        }
1430    }
1431
1432    public function ajaxPreProcess()
1433    {
1434        if (Tools::getIsset('update' . $this->table) && Tools::getIsset('id_' . $this->table)) {
1435            $this->display = 'edit';
1436            $this->action = Tools::getValue('action');
1437        }
1438    }
1439
1440    public function ajaxProcessUpdateProductImageShopAsso()
1441    {
1442        $id_product = Tools::getValue('id_product');
1443        if (($id_image = Tools::getValue('id_image')) && ($id_shop = (int) Tools::getValue('id_shop'))) {
1444            if (Tools::getValue('active') == 'true') {
1445                $res = Db::getInstance()->execute('INSERT INTO ' . _DB_PREFIX_ . 'image_shop (`id_product`, `id_image`, `id_shop`, `cover`) VALUES(' . (int) $id_product . ', ' . (int) $id_image . ', ' . (int) $id_shop . ', NULL)');
1446            } else {
1447                $res = Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'image_shop WHERE `id_image` = ' . (int) $id_image . ' AND `id_shop` = ' . (int) $id_shop);
1448            }
1449        }
1450
1451        // Clean covers in image table
1452        $count_cover_image = Db::getInstance()->getValue('
1453			SELECT COUNT(*) FROM ' . _DB_PREFIX_ . 'image i
1454			INNER JOIN ' . _DB_PREFIX_ . 'image_shop ish ON (i.id_image = ish.id_image AND ish.id_shop = ' . (int) $id_shop . ')
1455			WHERE i.cover = 1 AND i.`id_product` = ' . (int) $id_product);
1456
1457        if (!$id_image) {
1458            $id_image = Db::getInstance()->getValue('
1459                SELECT i.`id_image` FROM ' . _DB_PREFIX_ . 'image i
1460                INNER JOIN ' . _DB_PREFIX_ . 'image_shop ish ON (i.id_image = ish.id_image AND ish.id_shop = ' . (int) $id_shop . ')
1461                WHERE i.`id_product` = ' . (int) $id_product);
1462        }
1463
1464        if ($count_cover_image < 1) {
1465            Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'image i SET i.cover = 1 WHERE i.id_image = ' . (int) $id_image . ' AND i.`id_product` = ' . (int) $id_product . ' LIMIT 1');
1466        }
1467
1468        // Clean covers in image_shop table
1469        $count_cover_image_shop = Db::getInstance()->getValue('
1470			SELECT COUNT(*)
1471			FROM ' . _DB_PREFIX_ . 'image_shop ish
1472			WHERE ish.`id_product` = ' . (int) $id_product . ' AND ish.id_shop = ' . (int) $id_shop . ' AND ish.cover = 1');
1473
1474        if ($count_cover_image_shop < 1) {
1475            Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'image_shop ish SET ish.cover = 1 WHERE ish.id_image = ' . (int) $id_image . ' AND ish.`id_product` = ' . (int) $id_product . ' AND ish.id_shop =  ' . (int) $id_shop . ' LIMIT 1');
1476        }
1477
1478        if ($res) {
1479            $this->jsonConfirmation($this->_conf[27]);
1480        } else {
1481            $this->jsonError($this->trans('An error occurred while attempting to associate this image with your shop. ', [], 'Admin.Catalog.Notification'));
1482        }
1483    }
1484
1485    public function ajaxProcessUpdateImagePosition()
1486    {
1487        if (!$this->access('edit')) {
1488            return die(json_encode(['error' => 'You do not have the right permission']));
1489        }
1490        $res = false;
1491        if ($json = Tools::getValue('json')) {
1492            $res = true;
1493            $json = stripslashes($json);
1494            $images = json_decode($json, true);
1495            foreach ($images as $id => $position) {
1496                $img = new Image((int) $id);
1497                $img->position = (int) $position;
1498                $res &= $img->update();
1499            }
1500        }
1501        if ($res) {
1502            $this->jsonConfirmation($this->_conf[25]);
1503        } else {
1504            $this->jsonError($this->trans('An error occurred while attempting to move this picture.', [], 'Admin.Catalog.Notification'));
1505        }
1506    }
1507
1508    public function ajaxProcessUpdateCover()
1509    {
1510        if (!$this->access('edit')) {
1511            return die(json_encode(['error' => 'You do not have the right permission']));
1512        }
1513        Image::deleteCover((int) Tools::getValue('id_product'));
1514        $img = new Image((int) Tools::getValue('id_image'));
1515        $img->cover = 1;
1516
1517        @unlink(_PS_TMP_IMG_DIR_ . 'product_' . (int) $img->id_product . '.jpg');
1518        @unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int) $img->id_product . '_' . $this->context->shop->id . '.jpg');
1519
1520        if ($img->update()) {
1521            $this->jsonConfirmation($this->_conf[26]);
1522        } else {
1523            $this->jsonError($this->trans('An error occurred while attempting to update the cover picture.', [], 'Admin.Catalog.Notification'));
1524        }
1525    }
1526
1527    public function ajaxProcessDeleteProductImage($id_image = null)
1528    {
1529        $this->display = 'content';
1530        $res = true;
1531        /* Delete product image */
1532        $id_image = $id_image ? $id_image : (int) Tools::getValue('id_image');
1533
1534        $image = new Image($id_image);
1535        $this->content['id'] = $image->id;
1536        $res &= $image->delete();
1537        // if deleted image was the cover, change it to the first one
1538        if (!Image::getCover($image->id_product)) {
1539            $res &= Db::getInstance()->execute('
1540			UPDATE `' . _DB_PREFIX_ . 'image_shop` image_shop
1541			SET image_shop.`cover` = 1
1542			WHERE image_shop.`id_product` = ' . (int) $image->id_product . '
1543			AND id_shop=' . (int) $this->context->shop->id . ' LIMIT 1');
1544        }
1545
1546        if (!Image::getGlobalCover($image->id_product)) {
1547            $res &= Db::getInstance()->execute('
1548			UPDATE `' . _DB_PREFIX_ . 'image` i
1549			SET i.`cover` = 1
1550			WHERE i.`id_product` = ' . (int) $image->id_product . ' LIMIT 1');
1551        }
1552
1553        if (file_exists(_PS_TMP_IMG_DIR_ . 'product_' . $image->id_product . '.jpg')) {
1554            $res &= @unlink(_PS_TMP_IMG_DIR_ . 'product_' . $image->id_product . '.jpg');
1555        }
1556        if (file_exists(_PS_TMP_IMG_DIR_ . 'product_mini_' . $image->id_product . '_' . $this->context->shop->id . '.jpg')) {
1557            $res &= @unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . $image->id_product . '_' . $this->context->shop->id . '.jpg');
1558        }
1559
1560        if ($res) {
1561            $this->jsonConfirmation($this->_conf[7]);
1562        } else {
1563            $this->jsonError($this->trans('An error occurred while attempting to delete the product image.', [], 'Admin.Catalog.Notification'));
1564        }
1565    }
1566
1567    protected function _validateSpecificPrice($id_shop, $id_currency, $id_country, $id_group, $id_customer, $price, $from_quantity, $reduction, $reduction_type, $from, $to, $id_combination = 0)
1568    {
1569        if (!Validate::isUnsignedId($id_shop) || !Validate::isUnsignedId($id_currency) || !Validate::isUnsignedId($id_country) || !Validate::isUnsignedId($id_group) || !Validate::isUnsignedId($id_customer)) {
1570            $this->errors[] = $this->trans('Wrong IDs', [], 'Admin.Catalog.Notification');
1571        } elseif ((!isset($price) && !isset($reduction)) || (isset($price) && !Validate::isNegativePrice($price)) || (isset($reduction) && !Validate::isPrice($reduction))) {
1572            $this->errors[] = $this->trans('Invalid price/discount amount', [], 'Admin.Catalog.Notification');
1573        } elseif (!Validate::isUnsignedInt($from_quantity)) {
1574            $this->errors[] = $this->trans('Invalid quantity', [], 'Admin.Catalog.Notification');
1575        } elseif ($reduction && !Validate::isReductionType($reduction_type)) {
1576            $this->errors[] = $this->trans('Please select a discount type (amount or percentage).', [], 'Admin.Catalog.Notification');
1577        } elseif ($from && $to && (!Validate::isDateFormat($from) || !Validate::isDateFormat($to))) {
1578            $this->errors[] = $this->trans('The from/to date is invalid.', [], 'Admin.Catalog.Notification');
1579        } elseif (SpecificPrice::exists((int) $this->object->id, $id_combination, $id_shop, $id_group, $id_country, $id_currency, $id_customer, $from_quantity, $from, $to, false)) {
1580            $this->errors[] = $this->trans('A specific price already exists for these parameters.', [], 'Admin.Catalog.Notification');
1581        } else {
1582            return true;
1583        }
1584
1585        return false;
1586    }
1587
1588    /**
1589     * Checking customs feature.
1590     *
1591     * @param array $languages
1592     * @param array $featureInfo
1593     *
1594     * @return int|string
1595     */
1596    protected function checkFeatures($languages, $featureInfo)
1597    {
1598        $rules = call_user_func(['FeatureValue', 'getValidationRules'], 'FeatureValue');
1599        $feature = Feature::getFeature((int) Configuration::get('PS_LANG_DEFAULT'), $featureInfo['feature']);
1600
1601        foreach ($languages as $language) {
1602            if (isset($featureInfo['custom_value'][$language['id_lang']])) {
1603                $val = $featureInfo['custom_value'][$language['id_lang']];
1604                $current_language = new Language($language['id_lang']);
1605                if (Tools::strlen($val) > $rules['sizeLang']['value']) {
1606                    $this->errors[] = $this->trans(
1607                        'The name for feature %1$s is too long in %2$s.',
1608                        [
1609                            ' <b>' . $feature['name'] . '</b>',
1610                            $current_language->name,
1611                        ],
1612                        'Admin.Catalog.Notification'
1613                    );
1614                } elseif (!call_user_func(['Validate', $rules['validateLang']['value']], $val)) {
1615                    $this->errors[] = $this->trans(
1616                        'A valid name required for feature. %1$s in %2$s.',
1617                        [
1618                            ' <b>' . $feature['name'] . '</b>',
1619                            $current_language->name,
1620                        ],
1621                        'Admin.Catalog.Notification'
1622                    );
1623                }
1624                if (count($this->errors)) {
1625                    return 0;
1626                }
1627                // Getting default language
1628                if ($language['id_lang'] == Configuration::get('PS_LANG_DEFAULT')) {
1629                    return $val;
1630                }
1631            }
1632        }
1633
1634        return 0;
1635    }
1636
1637    /**
1638     * Add or update a product image.
1639     *
1640     * @param Product $product Product object to add image
1641     * @param string $method
1642     *
1643     * @return int|false
1644     */
1645    public function addProductImage($product, $method = 'auto')
1646    {
1647        /* Updating an existing product image */
1648        if ($id_image = (int) Tools::getValue('id_image')) {
1649            $image = new Image((int) $id_image);
1650            if (!Validate::isLoadedObject($image)) {
1651                $this->errors[] = $this->trans('An error occurred while uploading the image.', [], 'Admin.Notifications.Error');
1652            } else {
1653                if (($cover = Tools::getValue('cover')) == 1) {
1654                    Image::deleteCover($product->id);
1655                }
1656                $image->cover = $cover;
1657                $this->validateRules('Image');
1658                $this->copyFromPost($image, 'image');
1659                if (count($this->errors) || !$image->update()) {
1660                    $this->errors[] = $this->trans('An error occurred while updating the image.', [], 'Admin.Notifications.Error');
1661                } elseif (isset($_FILES['image_product']['tmp_name']) && $_FILES['image_product']['tmp_name'] != null) {
1662                    $this->copyImage($product->id, $image->id, $method);
1663                }
1664            }
1665        }
1666        if (isset($image) && Validate::isLoadedObject($image) && !file_exists(_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.' . $image->image_format)) {
1667            $image->delete();
1668        }
1669        if (count($this->errors)) {
1670            return false;
1671        }
1672        @unlink(_PS_TMP_IMG_DIR_ . 'product_' . $product->id . '.jpg');
1673        @unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . $product->id . '_' . $this->context->shop->id . '.jpg');
1674
1675        return (isset($id_image) && is_int($id_image) && $id_image) ? $id_image : false;
1676    }
1677
1678    /**
1679     * Copy a product image.
1680     *
1681     * @param int $id_product Product Id for product image filename
1682     * @param int $id_image Image Id for product image filename
1683     * @param string $method
1684     *
1685     * @return void|false
1686     *
1687     * @throws PrestaShopException
1688     */
1689    public function copyImage($id_product, $id_image, $method = 'auto')
1690    {
1691        if (!isset($_FILES['image_product']['tmp_name'])) {
1692            return false;
1693        }
1694        if ($error = ImageManager::validateUpload($_FILES['image_product'])) {
1695            $this->errors[] = $error;
1696        } else {
1697            $image = new Image($id_image);
1698
1699            if (!$new_path = $image->getPathForCreation()) {
1700                $this->errors[] = $this->trans('An error occurred while attempting to create a new folder.', [], 'Admin.Notifications.Error');
1701            }
1702            if (!($tmpName = tempnam(_PS_TMP_IMG_DIR_, 'PS')) || !move_uploaded_file($_FILES['image_product']['tmp_name'], $tmpName)) {
1703                $this->errors[] = $this->trans('An error occurred while uploading the image.', [], 'Admin.Notifications.Error');
1704            } elseif (!ImageManager::resize($tmpName, $new_path . '.' . $image->image_format)) {
1705                $this->errors[] = $this->trans('An error occurred while copying the image.', [], 'Admin.Notifications.Error');
1706            } elseif ($method == 'auto') {
1707                $imagesTypes = ImageType::getImagesTypes('products');
1708                foreach ($imagesTypes as $k => $image_type) {
1709                    if (!ImageManager::resize($tmpName, $new_path . '-' . stripslashes($image_type['name']) . '.' . $image->image_format, $image_type['width'], $image_type['height'], $image->image_format)) {
1710                        $this->errors[] = $this->trans('An error occurred while copying this image: %s', [stripslashes($image_type['name'])], 'Admin.Notifications.Error');
1711                    }
1712                }
1713            }
1714
1715            @unlink($tmpName);
1716            Hook::exec('actionWatermark', ['id_image' => $id_image, 'id_product' => $id_product]);
1717        }
1718    }
1719
1720    protected function updateAssoShop($id_object)
1721    {
1722        //override AdminController::updateAssoShop() specifically for products because shop association is set with the context in ObjectModel
1723    }
1724
1725    public function processAdd()
1726    {
1727        $this->checkProduct();
1728
1729        if (!empty($this->errors)) {
1730            $this->display = 'add';
1731
1732            return false;
1733        }
1734
1735        $this->object = new $this->className();
1736        $this->_removeTaxFromEcotax();
1737        $this->copyFromPost($this->object, $this->table);
1738        if ($this->object->add()) {
1739            PrestaShopLogger::addLog(sprintf('%s addition', $this->className), 1, null, $this->className, (int) $this->object->id, true, (int) $this->context->employee->id);
1740            $this->addCarriers($this->object);
1741            $this->updateAccessories($this->object);
1742            $this->updatePackItems($this->object);
1743            $this->updateDownloadProduct($this->object);
1744
1745            if (Configuration::get('PS_FORCE_ASM_NEW_PRODUCT') && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $this->object->getType() != Product::PTYPE_VIRTUAL) {
1746                $this->object->advanced_stock_management = 1;
1747                $this->object->save();
1748                $id_shops = Shop::getContextListShopID();
1749                foreach ($id_shops as $id_shop) {
1750                    StockAvailable::setProductDependsOnStock($this->object->id, true, (int) $id_shop, 0);
1751                }
1752            }
1753
1754            if (empty($this->errors)) {
1755                $languages = Language::getLanguages(false);
1756                if ($this->isProductFieldUpdated('category_box') && !$this->object->updateCategories(Tools::getValue('categoryBox'))) {
1757                    $this->errors[] = $this->trans(
1758                        'An error occurred while linking the object %table_name% to categories.',
1759                        [
1760                            '%table_name%' => ' <b>' . $this->table . '</b> ',
1761                        ],
1762                        'Admin.Notifications.Error'
1763                    );
1764                } elseif (!$this->updateTags($languages, $this->object)) {
1765                    $this->errors[] = $this->trans('An error occurred while adding tags.', [], 'Admin.Catalog.Notification');
1766                } else {
1767                    Hook::exec('actionProductAdd', ['id_product_old' => null, 'id_product' => (int) $this->object->id, 'product' => $this->object]);
1768                    if (in_array($this->object->visibility, ['both', 'search']) && Configuration::get('PS_SEARCH_INDEXATION')) {
1769                        Search::indexation(false, $this->object->id);
1770                    }
1771                }
1772
1773                if (Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT') != 0 && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) {
1774                    $warehouse_location_entity = new WarehouseProductLocation();
1775                    $warehouse_location_entity->id_product = $this->object->id;
1776                    $warehouse_location_entity->id_product_attribute = 0;
1777                    $warehouse_location_entity->id_warehouse = Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT');
1778                    $warehouse_location_entity->location = pSQL('');
1779                    $warehouse_location_entity->save();
1780                }
1781
1782                // Apply groups reductions
1783                $this->object->setGroupReduction();
1784
1785                // Save and preview
1786                if (Tools::isSubmit('submitAddProductAndPreview')) {
1787                    $this->redirect_after = $this->getPreviewUrl($this->object);
1788                }
1789
1790                // Save and stay on same form
1791                if ($this->display == 'edit') {
1792                    $this->redirect_after = self::$currentIndex . '&id_product=' . (int) $this->object->id
1793                        . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '')
1794                        . '&updateproduct&conf=3&key_tab=' . Tools::safeOutput(Tools::getValue('key_tab')) . '&token=' . $this->token;
1795                } else {
1796                    // Default behavior (save and back)
1797                    $this->redirect_after = self::$currentIndex
1798                        . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '')
1799                        . '&conf=3&token=' . $this->token;
1800                }
1801            } else {
1802                $this->object->delete();
1803                // if errors : stay on edit page
1804                $this->display = 'edit';
1805            }
1806        } else {
1807            $this->errors[] = $this->trans('An error occurred while creating an object.', [], 'Admin.Notifications.Error') . ' <b>' . $this->table . '</b>';
1808        }
1809
1810        return $this->object;
1811    }
1812
1813    protected function isTabSubmitted($tab_name)
1814    {
1815        if (!is_array($this->submitted_tabs)) {
1816            $this->submitted_tabs = Tools::getValue('submitted_tabs');
1817        }
1818
1819        if (is_array($this->submitted_tabs) && in_array($tab_name, $this->submitted_tabs)) {
1820            return true;
1821        }
1822
1823        return false;
1824    }
1825
1826    public function processStatus()
1827    {
1828        $this->loadObject(true);
1829        if (!Validate::isLoadedObject($this->object)) {
1830            return false;
1831        }
1832        if (($error = $this->object->validateFields(false, true)) !== true) {
1833            $this->errors[] = $error;
1834        }
1835        if (($error = $this->object->validateFieldsLang(false, true)) !== true) {
1836            $this->errors[] = $error;
1837        }
1838
1839        if (count($this->errors)) {
1840            return false;
1841        }
1842
1843        $res = parent::processStatus();
1844
1845        $query = trim(Tools::getValue('bo_query'));
1846        $searchType = (int) Tools::getValue('bo_search_type');
1847
1848        if ($query) {
1849            $this->redirect_after = preg_replace('/[\?|&](bo_query|bo_search_type)=([^&]*)/i', '', $this->redirect_after);
1850            $this->redirect_after .= '&bo_query=' . $query . '&bo_search_type=' . $searchType;
1851        }
1852
1853        return $res;
1854    }
1855
1856    public function processUpdate()
1857    {
1858        $existing_product = $this->object;
1859
1860        $this->checkProduct();
1861
1862        if (!empty($this->errors)) {
1863            $this->display = 'edit';
1864
1865            return false;
1866        }
1867
1868        $id = (int) Tools::getValue('id_' . $this->table);
1869        /* Update an existing product */
1870        if (isset($id) && !empty($id)) {
1871            /** @var Product $object */
1872            $object = new $this->className((int) $id);
1873            $this->object = $object;
1874
1875            if (Validate::isLoadedObject($object)) {
1876                $this->_removeTaxFromEcotax();
1877                $product_type_before = $object->getType();
1878                $this->copyFromPost($object, $this->table);
1879                $object->indexed = 0;
1880
1881                if (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP) {
1882                    $object->setFieldsToUpdate((array) Tools::getValue('multishop_check', []));
1883                }
1884
1885                // Duplicate combinations if not associated to shop
1886                if ($this->context->shop->getContext() == Shop::CONTEXT_SHOP && !$object->isAssociatedToShop()) {
1887                    $is_associated_to_shop = false;
1888                    $combinations = Product::getProductAttributesIds($object->id);
1889                    if ($combinations) {
1890                        foreach ($combinations as $id_combination) {
1891                            $combination = new Combination((int) $id_combination['id_product_attribute']);
1892                            $default_combination = new Combination((int) $id_combination['id_product_attribute'], null, (int) $this->object->id_shop_default);
1893
1894                            $def = ObjectModel::getDefinition($default_combination);
1895                            foreach ($def['fields'] as $field_name => $row) {
1896                                $combination->$field_name = ObjectModel::formatValue($default_combination->$field_name, $def['fields'][$field_name]['type']);
1897                            }
1898
1899                            $combination->save();
1900                        }
1901                    }
1902                } else {
1903                    $is_associated_to_shop = true;
1904                }
1905
1906                if ($object->update()) {
1907                    // If the product doesn't exist in the current shop but exists in another shop
1908                    if (Shop::getContext() == Shop::CONTEXT_SHOP && !$existing_product->isAssociatedToShop($this->context->shop->id)) {
1909                        $out_of_stock = StockAvailable::outOfStock($existing_product->id, $existing_product->id_shop_default);
1910                        $depends_on_stock = StockAvailable::dependsOnStock($existing_product->id, $existing_product->id_shop_default);
1911                        StockAvailable::setProductOutOfStock((int) $this->object->id, $out_of_stock, $this->context->shop->id);
1912                        StockAvailable::setProductDependsOnStock((int) $this->object->id, $depends_on_stock, $this->context->shop->id);
1913                    }
1914
1915                    PrestaShopLogger::addLog(sprintf('%s modification', $this->className), 1, null, $this->className, (int) $this->object->id, true, (int) $this->context->employee->id);
1916                    if (in_array($this->context->shop->getContext(), [Shop::CONTEXT_SHOP, Shop::CONTEXT_ALL])) {
1917                        if ($this->isTabSubmitted('Shipping')) {
1918                            $this->addCarriers();
1919                        }
1920                        if ($this->isTabSubmitted('Associations')) {
1921                            $this->updateAccessories($object);
1922                        }
1923                        if ($this->isTabSubmitted('Suppliers')) {
1924                            $this->processSuppliers();
1925                        }
1926                        if ($this->isTabSubmitted('Features')) {
1927                            $this->processFeatures();
1928                        }
1929                        if ($this->isTabSubmitted('Combinations')) {
1930                            $this->processProductAttribute();
1931                        }
1932                        if ($this->isTabSubmitted('Prices')) {
1933                            $this->processPriceAddition();
1934                            $this->processSpecificPricePriorities();
1935                        }
1936                        if ($this->isTabSubmitted('Customization')) {
1937                            $this->processCustomizationConfiguration();
1938                        }
1939                        if ($this->isTabSubmitted('Attachments')) {
1940                            $this->processAttachments();
1941                        }
1942                        if ($this->isTabSubmitted('Images')) {
1943                            $this->processImageLegends();
1944                        }
1945
1946                        $this->updatePackItems($object);
1947                        // Disallow avanced stock management if the product become a pack
1948                        if ($product_type_before == Product::PTYPE_SIMPLE && $object->getType() == Product::PTYPE_PACK) {
1949                            StockAvailable::setProductDependsOnStock((int) $object->id, false);
1950                        }
1951                        $this->updateDownloadProduct($object, 1);
1952                        $this->updateTags(Language::getLanguages(false), $object);
1953
1954                        if ($this->isProductFieldUpdated('category_box') && !$object->updateCategories(Tools::getValue('categoryBox'))) {
1955                            $this->errors[] = $this->trans(
1956                                'An error occurred while linking the object %table_name% to categories.',
1957                                [
1958                                    '%table_name%' => ' <b>' . $this->table . '</b> ',
1959                                ],
1960                                'Admin.Notifications.Error'
1961                            );
1962                        }
1963                    }
1964
1965                    if ($this->isTabSubmitted('Warehouses')) {
1966                        $this->processWarehouses();
1967                    }
1968                    if (empty($this->errors)) {
1969                        if (in_array($object->visibility, ['both', 'search']) && Configuration::get('PS_SEARCH_INDEXATION')) {
1970                            Search::indexation(false, $object->id);
1971                        }
1972
1973                        // Save and preview
1974                        if (Tools::isSubmit('submitAddProductAndPreview')) {
1975                            $this->redirect_after = $this->getPreviewUrl($object);
1976                        } else {
1977                            $page = (int) Tools::getValue('page');
1978                            // Save and stay on same form
1979                            if ($this->display == 'edit') {
1980                                $this->confirmations[] = 'Update successful';
1981                                $this->redirect_after = self::$currentIndex . '&id_product=' . (int) $this->object->id
1982                                    . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '')
1983                                    . '&updateproduct&conf=4&key_tab=' . Tools::safeOutput(Tools::getValue('key_tab')) . ($page > 1 ? '&page=' . (int) $page : '') . '&token=' . $this->token;
1984                            } else {
1985                                // Default behavior (save and back)
1986                                $this->redirect_after = self::$currentIndex . (Tools::getIsset('id_category') ? '&id_category=' . (int) Tools::getValue('id_category') : '') . '&conf=4' . ($page > 1 ? '&submitFilterproduct=' . (int) $page : '') . '&token=' . $this->token;
1987                            }
1988                        }
1989                    } else {
1990                        // if errors: stay on edit page
1991                        $this->display = 'edit';
1992                    }
1993                } else {
1994                    if (!$is_associated_to_shop && $combinations) {
1995                        foreach ($combinations as $id_combination) {
1996                            $combination = new Combination((int) $id_combination['id_product_attribute']);
1997                            $combination->delete();
1998                        }
1999                    }
2000                    $this->errors[] = $this->trans('An error occurred while updating an object.', [], 'Admin.Notifications.Error') . ' <b>' . $this->table . '</b> (' . Db::getInstance()->getMsgError() . ')';
2001                }
2002            } else {
2003                $this->errors[] = $this->trans('An error occurred while updating an object.', [], 'Admin.Notifications.Error') . ' <b>' . $this->table . '</b> (' . $this->trans('The object cannot be loaded. ', [], 'Admin.Notifications.Error') . ')';
2004            }
2005
2006            return $object;
2007        }
2008    }
2009
2010    /**
2011     * Check that a saved product is valid.
2012     */
2013    public function checkProduct()
2014    {
2015        /** @todo : the call_user_func seems to contains only statics values (className = 'Product') */
2016        $rules = call_user_func([$this->className, 'getValidationRules'], $this->className);
2017        $default_language = new Language((int) Configuration::get('PS_LANG_DEFAULT'));
2018        $languages = Language::getLanguages(false);
2019
2020        // Check required fields
2021        foreach ($rules['required'] as $field) {
2022            if (!$this->isProductFieldUpdated($field)) {
2023                continue;
2024            }
2025
2026            if (($value = Tools::getValue($field)) == false && $value != '0') {
2027                if (Tools::getValue('id_' . $this->table) && $field == 'passwd') {
2028                    continue;
2029                }
2030                $this->errors[] = $this->trans('The %name% field is required.', ['%name%' => call_user_func([$this->className, 'displayFieldName'], $field, $this->className)], 'Admin.Notifications.Error');
2031            }
2032        }
2033
2034        // Check multilingual required fields
2035        foreach ($rules['requiredLang'] as $fieldLang) {
2036            if ($this->isProductFieldUpdated($fieldLang, $default_language->id) && !Tools::getValue($fieldLang . '_' . $default_language->id)) {
2037                $this->errors[] = $this->trans(
2038                    'This %1$s field is required at least in %2$s',
2039                    [
2040                        call_user_func([$this->className, 'displayFieldName'], $fieldLang, $this->className),
2041                        $default_language->name,
2042                    ],
2043                    'Admin.Catalog.Notification'
2044                );
2045            }
2046        }
2047
2048        // Check fields sizes
2049        foreach ($rules['size'] as $field => $maxLength) {
2050            if ($this->isProductFieldUpdated($field) && ($value = Tools::getValue($field)) && Tools::strlen($value) > $maxLength) {
2051                $this->errors[] = $this->trans(
2052                    'The %1$s field is too long (%2$d chars max).',
2053                    [
2054                        call_user_func([$this->className, 'displayFieldName'], $field, $this->className),
2055                        $maxLength,
2056                    ],
2057                    'Admin.Catalog.Notification'
2058                );
2059            }
2060        }
2061
2062        if (Tools::getIsset('description_short') && $this->isProductFieldUpdated('description_short')) {
2063            $saveShort = Tools::getValue('description_short');
2064            $_POST['description_short'] = strip_tags(Tools::getValue('description_short'));
2065        }
2066
2067        // Check description short size without html
2068        $limit = (int) Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT');
2069        if ($limit <= 0) {
2070            $limit = 400;
2071        }
2072        foreach ($languages as $language) {
2073            if ($this->isProductFieldUpdated('description_short', $language['id_lang']) && ($value = Tools::getValue('description_short_' . $language['id_lang']))) {
2074                // This validation computation actually comes from TinyMceMaxLengthValidator if you modify it here you
2075                // should keep the validator in sync (along with other parts of the code, more info in the
2076                // TinyMceMaxLengthValidator comments).
2077                $replaceArray = [
2078                    "\n",
2079                    "\r",
2080                    "\n\r",
2081                    "\r\n",
2082                ];
2083                $str = str_replace($replaceArray, [''], strip_tags($value));
2084                $shortDescriptionLength = iconv_strlen($str);
2085                if ($shortDescriptionLength > $limit) {
2086                    $this->errors[] = $this->trans(
2087                        'This %1$s field (%2$s) is too long: %3$d chars max (current count %4$d).',
2088                        [
2089                            call_user_func([$this->className, 'displayFieldName'], 'description_short'),
2090                            $language['name'],
2091                            $limit,
2092                            Tools::strlen(strip_tags($value)),
2093                        ],
2094                        'Admin.Catalog.Notification'
2095                    );
2096                }
2097            }
2098        }
2099
2100        // Check multilingual fields sizes
2101        foreach ($rules['sizeLang'] as $fieldLang => $maxLength) {
2102            foreach ($languages as $language) {
2103                $value = Tools::getValue($fieldLang . '_' . $language['id_lang']);
2104                if ($value && Tools::strlen($value) > $maxLength) {
2105                    $this->errors[] = $this->trans(
2106                        'The %1$s field is too long (%2$d chars max).',
2107                        [
2108                            call_user_func([$this->className, 'displayFieldName'], $fieldLang, $this->className),
2109                            $maxLength,
2110                        ],
2111                        'Admin.Catalog.Notification'
2112                    );
2113                }
2114            }
2115        }
2116
2117        if ($this->isProductFieldUpdated('description_short') && isset($_POST['description_short'])) {
2118            $_POST['description_short'] = $saveShort;
2119        }
2120
2121        // Check fields validity
2122        foreach ($rules['validate'] as $field => $function) {
2123            if ($this->isProductFieldUpdated($field) && ($value = Tools::getValue($field))) {
2124                $res = true;
2125                if (Tools::strtolower($function) == 'iscleanhtml') {
2126                    if (!Validate::$function($value, (int) Configuration::get('PS_ALLOW_HTML_IFRAME'))) {
2127                        $res = false;
2128                    }
2129                } elseif (!Validate::$function($value)) {
2130                    $res = false;
2131                }
2132
2133                if (!$res) {
2134                    $this->errors[] = $this->trans(
2135                        'The %s field is invalid.',
2136                        [
2137                            call_user_func([$this->className, 'displayFieldName'], $field, $this->className),
2138                        ],
2139                        'Admin.Notifications.Error'
2140                    );
2141                }
2142            }
2143        }
2144        // Check multilingual fields validity
2145        foreach ($rules['validateLang'] as $fieldLang => $function) {
2146            foreach ($languages as $language) {
2147                if ($this->isProductFieldUpdated($fieldLang, $language['id_lang']) && ($value = Tools::getValue($fieldLang . '_' . $language['id_lang']))) {
2148                    if (!Validate::$function($value, (int) Configuration::get('PS_ALLOW_HTML_IFRAME'))) {
2149                        $this->errors[] = $this->trans(
2150                            'The %1$s field (%2$s) is invalid.',
2151                            [
2152                                call_user_func([$this->className, 'displayFieldName'], $fieldLang, $this->className),
2153                                $language['name'],
2154                            ],
2155                            'Admin.Notifications.Error'
2156                        );
2157                    }
2158                }
2159            }
2160        }
2161
2162        // Categories
2163        if ($this->isProductFieldUpdated('id_category_default') && (!Tools::isSubmit('categoryBox') || !count(Tools::getValue('categoryBox')))) {
2164            $this->errors[] = 'Products must be in at least one category.';
2165        }
2166
2167        if ($this->isProductFieldUpdated('id_category_default') && (!is_array(Tools::getValue('categoryBox')) || !in_array(Tools::getValue('id_category_default'), Tools::getValue('categoryBox')))) {
2168            $this->errors[] = 'This product must be in the default category.';
2169        }
2170
2171        // Tags
2172        foreach ($languages as $language) {
2173            if ($value = Tools::getValue('tags_' . $language['id_lang'])) {
2174                if (!Validate::isTagsList($value)) {
2175                    $this->errors[] = $this->trans(
2176                        'The tags list (%s) is invalid.',
2177                        [
2178                            $language['name'],
2179                        ],
2180                        'Admin.Notifications.Error'
2181                    );
2182                }
2183            }
2184        }
2185    }
2186
2187    /**
2188     * Check if a field is edited (if the checkbox is checked)
2189     * This method will do something only for multishop with a context all / group.
2190     *
2191     * @param string $field Name of field
2192     * @param int $id_lang
2193     *
2194     * @return bool
2195     */
2196    protected function isProductFieldUpdated($field, $id_lang = null)
2197    {
2198        // Cache this condition to improve performances
2199        static $is_activated = null;
2200        if (null === $is_activated) {
2201            $is_activated = Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP && $this->id_object;
2202        }
2203
2204        if (!$is_activated) {
2205            return true;
2206        }
2207
2208        $def = ObjectModel::getDefinition($this->object);
2209        if (!$this->object->isMultiShopField($field) && null === $id_lang && isset($def['fields'][$field])) {
2210            return true;
2211        }
2212
2213        if (null === $id_lang) {
2214            return !empty($_POST['multishop_check'][$field]);
2215        } else {
2216            return !empty($_POST['multishop_check'][$field][$id_lang]);
2217        }
2218    }
2219
2220    protected function _removeTaxFromEcotax()
2221    {
2222        if ($ecotax = Tools::getValue('ecotax')) {
2223            $_POST['ecotax'] = Tools::ps_round($ecotax / (1 + Tax::getProductEcotaxRate() / 100), 6);
2224        }
2225    }
2226
2227    protected function _applyTaxToEcotax($product)
2228    {
2229        if ($product->ecotax) {
2230            $product->ecotax = Tools::ps_round($product->ecotax * (1 + Tax::getProductEcotaxRate() / 100), 2);
2231        }
2232    }
2233
2234    /**
2235     * Update product download.
2236     *
2237     * @param Product $product
2238     * @param int $edit
2239     *
2240     * @return bool
2241     */
2242    public function updateDownloadProduct($product, $edit = 0)
2243    {
2244        //legacy/sf2 form workaround
2245        //if is_virtual_file parameter was not send (SF2 form case), don't process virtual file
2246        if (Tools::getValue('is_virtual_file') === false) {
2247            return false;
2248        }
2249
2250        if ((int) Tools::getValue('is_virtual_file') == 1) {
2251            if (isset($_FILES['virtual_product_file_uploader']) && $_FILES['virtual_product_file_uploader']['size'] > 0) {
2252                $virtual_product_filename = ProductDownload::getNewFilename();
2253                $helper = new HelperUploader('virtual_product_file_uploader');
2254                $helper->setPostMaxSize(Tools::getOctets(ini_get('upload_max_filesize')))
2255                    ->setSavePath(_PS_DOWNLOAD_DIR_)->upload($_FILES['virtual_product_file_uploader'], $virtual_product_filename);
2256            } else {
2257                $virtual_product_filename = Tools::getValue('virtual_product_filename', ProductDownload::getNewFilename());
2258            }
2259
2260            $product->setDefaultAttribute(0); //reset cache_default_attribute
2261            if (Tools::getValue('virtual_product_expiration_date') && !Validate::isDate(Tools::getValue('virtual_product_expiration_date'))) {
2262                if (!Tools::getValue('virtual_product_expiration_date')) {
2263                    $this->errors[] = $this->trans('The expiration-date attribute is required.', [], 'Admin.Catalog.Notification');
2264
2265                    return false;
2266                }
2267            }
2268
2269            // Trick's
2270            if ($edit == 1) {
2271                $id_product_download = (int) ProductDownload::getIdFromIdProduct((int) $product->id, false);
2272                if (!$id_product_download) {
2273                    $id_product_download = (int) Tools::getValue('virtual_product_id');
2274                }
2275            } else {
2276                $id_product_download = Tools::getValue('virtual_product_id');
2277            }
2278
2279            $is_shareable = Tools::getValue('virtual_product_is_shareable');
2280            $virtual_product_name = Tools::getValue('virtual_product_name');
2281            $virtual_product_nb_days = Tools::getValue('virtual_product_nb_days');
2282            $virtual_product_nb_downloable = Tools::getValue('virtual_product_nb_downloable');
2283            $virtual_product_expiration_date = Tools::getValue('virtual_product_expiration_date');
2284
2285            $download = new ProductDownload((int) $id_product_download);
2286            $download->id_product = (int) $product->id;
2287            $download->display_filename = $virtual_product_name;
2288            $download->filename = $virtual_product_filename;
2289            $download->date_add = date('Y-m-d H:i:s');
2290            $download->date_expiration = $virtual_product_expiration_date ? $virtual_product_expiration_date . ' 23:59:59' : '';
2291            $download->nb_days_accessible = (int) $virtual_product_nb_days;
2292            $download->nb_downloadable = (int) $virtual_product_nb_downloable;
2293            $download->active = 1;
2294            $download->is_shareable = (int) $is_shareable;
2295            if ($download->save()) {
2296                return true;
2297            }
2298        } else {
2299            /* unactive download product if checkbox not checked */
2300            if ($edit == 1) {
2301                $id_product_download = (int) ProductDownload::getIdFromIdProduct((int) $product->id);
2302                if (!$id_product_download) {
2303                    $id_product_download = (int) Tools::getValue('virtual_product_id');
2304                }
2305            } else {
2306                $id_product_download = ProductDownload::getIdFromIdProduct($product->id);
2307            }
2308
2309            if (!empty($id_product_download)) {
2310                $product_download = new ProductDownload((int) $id_product_download);
2311                $product_download->date_expiration = date('Y-m-d H:i:s', time() - 1);
2312                $product_download->active = 0;
2313
2314                return $product_download->save();
2315            }
2316        }
2317
2318        return false;
2319    }
2320
2321    /**
2322     * Update product accessories.
2323     *
2324     * @param object $product Product
2325     */
2326    public function updateAccessories($product)
2327    {
2328        $product->deleteAccessories();
2329        if ($accessories = Tools::getValue('inputAccessories')) {
2330            $accessories_id = array_unique(explode('-', $accessories));
2331            if (count($accessories_id)) {
2332                array_pop($accessories_id);
2333                $product->changeAccessories($accessories_id);
2334            }
2335        }
2336    }
2337
2338    /**
2339     * Update product tags.
2340     *
2341     * @param array $languages Array languages
2342     * @param object $product Product
2343     *
2344     * @return bool Update result
2345     */
2346    public function updateTags($languages, $product)
2347    {
2348        $tag_success = true;
2349        /* Reset all tags for THIS product */
2350        if (!Tag::deleteTagsForProduct((int) $product->id)) {
2351            $this->errors[] = $this->trans('An error occurred while attempting to delete previous tags.', [], 'Admin.Catalog.Notification');
2352        }
2353        /* Assign tags to this product */
2354        foreach ($languages as $language) {
2355            if ($value = Tools::getValue('tags_' . $language['id_lang'])) {
2356                $tag_success &= Tag::addTags($language['id_lang'], (int) $product->id, $value);
2357            }
2358        }
2359
2360        if (!$tag_success) {
2361            $this->errors[] = $this->trans('An error occurred while adding tags.', [], 'Admin.Catalog.Notification');
2362        }
2363
2364        return $tag_success;
2365    }
2366
2367    public function ajaxProcessProductManufacturers()
2368    {
2369        $manufacturers = Manufacturer::getManufacturers(false, 0, true, false, false, false, true);
2370        $jsonArray = [];
2371
2372        if ($manufacturers) {
2373            foreach ($manufacturers as $manufacturer) {
2374                $tmp = ['optionValue' => $manufacturer['id_manufacturer'], 'optionDisplay' => htmlspecialchars(trim($manufacturer['name']))];
2375                $jsonArray[] = json_encode($tmp);
2376            }
2377        }
2378
2379        die('[' . implode(',', $jsonArray) . ']');
2380    }
2381
2382    /**
2383     * Build a categories tree.
2384     *
2385     * @param $id_obj
2386     * @param array $indexedCategories Array with categories where product is indexed (in order to check checkbox)
2387     * @param array $categories Categories to list
2388     * @param $current
2389     * @param null $id_category Current category ID
2390     * @param null $id_category_default
2391     * @param array $has_suite
2392     *
2393     * @return string
2394     */
2395    public static function recurseCategoryForInclude($id_obj, $indexedCategories, $categories, $current, $id_category = null, $id_category_default = null, $has_suite = [])
2396    {
2397        @trigger_error('This function is deprecated since 1.7.0.', E_USER_DEPRECATED);
2398    }
2399
2400    public function getPreviewUrl(Product $product)
2401    {
2402        $id_lang = Configuration::get('PS_LANG_DEFAULT', null, null, Context::getContext()->shop->id);
2403
2404        if (!ShopUrl::getMainShopDomain()) {
2405            return false;
2406        }
2407
2408        $is_rewrite_active = (bool) Configuration::get('PS_REWRITING_SETTINGS');
2409        $preview_url = $this->context->link->getProductLink(
2410            $product,
2411            $this->getFieldValue($product, 'link_rewrite', $this->context->language->id),
2412            Category::getLinkRewrite($this->getFieldValue($product, 'id_category_default'), $this->context->language->id),
2413            null,
2414            $id_lang,
2415            (int) Context::getContext()->shop->id,
2416            0,
2417            $is_rewrite_active
2418        );
2419
2420        if (!$product->active) {
2421            $admin_dir = dirname($_SERVER['PHP_SELF']);
2422            $admin_dir = substr($admin_dir, strrpos($admin_dir, '/') + 1);
2423            $preview_url .= ((strpos($preview_url, '?') === false) ? '?' : '&') . 'adtoken=' . $this->token . '&ad=' . $admin_dir . '&id_employee=' . (int) $this->context->employee->id;
2424        }
2425
2426        return $preview_url;
2427    }
2428
2429    /**
2430     * Post treatment for suppliers.
2431     *
2432     * @param int|null $id_product
2433     */
2434    public function processSuppliers($id_product = null)
2435    {
2436        $id_product = (int) $id_product ? $id_product : (int) Tools::getValue('id_product');
2437
2438        if ((int) Tools::getValue('supplier_loaded') === 1 && Validate::isLoadedObject($product = new Product($id_product))) {
2439            // Get all id_product_attribute
2440            $attributes = $product->getAttributesResume($this->context->language->id);
2441            if (empty($attributes)) {
2442                $attributes[] = [
2443                    'id_product_attribute' => 0,
2444                    'attribute_designation' => '',
2445                ];
2446            }
2447
2448            // Get all available suppliers
2449            $suppliers = Supplier::getSuppliers();
2450
2451            // Get already associated suppliers
2452            $associated_suppliers = ProductSupplier::getSupplierCollection($product->id);
2453
2454            $suppliers_to_associate = [];
2455            $new_default_supplier = 0;
2456            $defaultWholeslePrice = (float) 0;
2457            $defaultReference = '';
2458
2459            if (Tools::isSubmit('default_supplier')) {
2460                $new_default_supplier = (int) Tools::getValue('default_supplier');
2461            }
2462
2463            // Get new associations
2464            foreach ($suppliers as $supplier) {
2465                if (Tools::isSubmit('check_supplier_' . $supplier['id_supplier'])) {
2466                    $suppliers_to_associate[] = $supplier['id_supplier'];
2467                }
2468            }
2469
2470            // Delete already associated suppliers if needed
2471            foreach ($associated_suppliers as $key => $associated_supplier) {
2472                /** @var ProductSupplier $associated_supplier */
2473                if (!in_array($associated_supplier->id_supplier, $suppliers_to_associate)) {
2474                    $associated_supplier->delete();
2475                    unset($associated_suppliers[$key]);
2476                }
2477            }
2478
2479            // Associate suppliers
2480            foreach ($suppliers_to_associate as $id) {
2481                $to_add = true;
2482                foreach ($associated_suppliers as $as) {
2483                    /** @var ProductSupplier $as */
2484                    if ($id == $as->id_supplier) {
2485                        $to_add = false;
2486                    }
2487                }
2488
2489                if ($to_add) {
2490                    $product_supplier = new ProductSupplier();
2491                    $product_supplier->id_product = $product->id;
2492                    $product_supplier->id_product_attribute = 0;
2493                    $product_supplier->id_supplier = $id;
2494                    if ($this->context->currency->id) {
2495                        $product_supplier->id_currency = (int) $this->context->currency->id;
2496                    } else {
2497                        $product_supplier->id_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT');
2498                    }
2499                    $product_supplier->save();
2500
2501                    $associated_suppliers[] = $product_supplier;
2502                    foreach ($attributes as $attribute) {
2503                        if ((int) $attribute['id_product_attribute'] > 0) {
2504                            $product_supplier = new ProductSupplier();
2505                            $product_supplier->id_product = $product->id;
2506                            $product_supplier->id_product_attribute = (int) $attribute['id_product_attribute'];
2507                            $product_supplier->id_supplier = $id;
2508                            $product_supplier->save();
2509                        }
2510                    }
2511                }
2512            }
2513
2514            // Manage references and prices
2515            foreach ($attributes as $attribute) {
2516                foreach ($associated_suppliers as $supplier) {
2517                    /** @var ProductSupplier $supplier */
2518                    if (Tools::isSubmit('supplier_reference_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier) ||
2519                        (Tools::isSubmit('product_price_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier) &&
2520                         Tools::isSubmit('product_price_currency_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier))) {
2521                        $reference = pSQL(
2522                            Tools::getValue(
2523                                'supplier_reference_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier,
2524                                ''
2525                            )
2526                        );
2527
2528                        $price = (float) str_replace(
2529                            [' ', ','],
2530                            ['', '.'],
2531                            Tools::getValue(
2532                                'product_price_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier,
2533                                0
2534                            )
2535                        );
2536
2537                        $price = Tools::ps_round($price, 6);
2538
2539                        $id_currency = (int) Tools::getValue(
2540                            'product_price_currency_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier,
2541                            0
2542                        );
2543
2544                        if ($id_currency <= 0 || (!($result = Currency::getCurrency($id_currency)) || empty($result))) {
2545                            $this->errors[] = $this->trans('The selected currency is not valid', [], 'Admin.Catalog.Notification');
2546                        }
2547
2548                        // Save product-supplier data
2549                        $product_supplier_id = (int) ProductSupplier::getIdByProductAndSupplier($product->id, $attribute['id_product_attribute'], $supplier->id_supplier);
2550
2551                        if (!$product_supplier_id) {
2552                            $product->addSupplierReference($supplier->id_supplier, (int) $attribute['id_product_attribute'], $reference, (float) $price, (int) $id_currency);
2553                        } else {
2554                            $product_supplier = new ProductSupplier($product_supplier_id);
2555                            $product_supplier->id_currency = (int) $id_currency;
2556                            $product_supplier->product_supplier_price_te = (float) $price;
2557                            $product_supplier->product_supplier_reference = pSQL($reference);
2558                            $product_supplier->update();
2559                        }
2560
2561                        if ($new_default_supplier == $supplier->id_supplier) {
2562                            if ((int) $attribute['id_product_attribute'] > 0) {
2563                                $data = [
2564                                    'supplier_reference' => pSQL($reference),
2565                                    'wholesale_price' => (float) Tools::convertPrice($price, $id_currency),
2566                                ];
2567                                $where = '
2568                                    a.id_product = ' . (int) $product->id . '
2569                                    AND a.id_product_attribute = ' . (int) $attribute['id_product_attribute'];
2570                                ObjectModel::updateMultishopTable('Combination', $data, $where);
2571                            } else {
2572                                // @deprecated 1.7.7.0 This condition block will be remove in the next major, use ProductSupplier instead
2573                                $defaultWholeslePrice = (float) Tools::convertPrice($price, $id_currency);
2574                                $defaultReference = $reference;
2575                            }
2576                        }
2577                    } elseif (Tools::isSubmit('supplier_reference_' . $product->id . '_' . $attribute['id_product_attribute'] . '_' . $supplier->id_supplier)) {
2578                        //int attribute with default values if possible
2579                        if ((int) $attribute['id_product_attribute'] > 0) {
2580                            $product_supplier = new ProductSupplier();
2581                            $product_supplier->id_product = $product->id;
2582                            $product_supplier->id_product_attribute = (int) $attribute['id_product_attribute'];
2583                            $product_supplier->id_supplier = $supplier->id_supplier;
2584                            $product_supplier->save();
2585                        }
2586                    }
2587                }
2588            }
2589
2590            if ($this->object) {
2591                $product->updateDefaultSupplierData(
2592                    $new_default_supplier,
2593                    $defaultReference,
2594                    $defaultWholeslePrice
2595                );
2596            }
2597        }
2598    }
2599
2600    /**
2601     * Post treatment for warehouses.
2602     */
2603    public function processWarehouses()
2604    {
2605        if ((int) Tools::getValue('warehouse_loaded') === 1 && Validate::isLoadedObject($product = new Product((int) $id_product = Tools::getValue('id_product')))) {
2606            // Get all id_product_attribute
2607            $attributes = $product->getAttributesResume($this->context->language->id);
2608            if (empty($attributes)) {
2609                $attributes[] = [
2610                    'id_product_attribute' => 0,
2611                    'attribute_designation' => '',
2612                ];
2613            }
2614
2615            // Get all available warehouses
2616            $warehouses = Warehouse::getWarehouses(true);
2617
2618            // Get already associated warehouses
2619            $associated_warehouses_collection = WarehouseProductLocation::getCollection($product->id);
2620
2621            $elements_to_manage = [];
2622
2623            // get form information
2624            foreach ($attributes as $attribute) {
2625                foreach ($warehouses as $warehouse) {
2626                    $key = $warehouse['id_warehouse'] . '_' . $product->id . '_' . $attribute['id_product_attribute'];
2627
2628                    // get elements to manage
2629                    if (Tools::isSubmit('check_warehouse_' . $key)) {
2630                        $location = Tools::getValue('location_warehouse_' . $key, '');
2631                        $elements_to_manage[$key] = $location;
2632                    }
2633                }
2634            }
2635
2636            // Delete entry if necessary
2637            foreach ($associated_warehouses_collection as $awc) {
2638                /** @var WarehouseProductLocation $awc */
2639                if (!array_key_exists($awc->id_warehouse . '_' . $awc->id_product . '_' . $awc->id_product_attribute, $elements_to_manage)) {
2640                    $awc->delete();
2641                }
2642            }
2643
2644            // Manage locations
2645            foreach ($elements_to_manage as $key => $location) {
2646                $params = explode('_', $key);
2647
2648                $wpl_id = (int) WarehouseProductLocation::getIdByProductAndWarehouse((int) $params[1], (int) $params[2], (int) $params[0]);
2649
2650                if (empty($wpl_id)) {
2651                    //create new record
2652                    $warehouse_location_entity = new WarehouseProductLocation();
2653                    $warehouse_location_entity->id_product = (int) $params[1];
2654                    $warehouse_location_entity->id_product_attribute = (int) $params[2];
2655                    $warehouse_location_entity->id_warehouse = (int) $params[0];
2656                    $warehouse_location_entity->location = pSQL($location);
2657                    $warehouse_location_entity->save();
2658                } else {
2659                    $warehouse_location_entity = new WarehouseProductLocation((int) $wpl_id);
2660
2661                    $location = pSQL($location);
2662
2663                    if ($location != $warehouse_location_entity->location) {
2664                        $warehouse_location_entity->location = pSQL($location);
2665                        $warehouse_location_entity->update();
2666                    }
2667                }
2668            }
2669            StockAvailable::synchronize((int) $id_product);
2670        }
2671    }
2672
2673    /**
2674     * Get an array of pack items for display from the product object if specified, else from POST/GET values.
2675     *
2676     * @param Product $product
2677     *
2678     * @return array of pack items
2679     */
2680    public function getPackItems($product = null)
2681    {
2682        $pack_items = [];
2683
2684        if (!$product) {
2685            $names_input = Tools::getValue('namePackItems');
2686            $ids_input = Tools::getValue('inputPackItems');
2687            if (!$names_input || !$ids_input) {
2688                return [];
2689            }
2690            // ids is an array of string with format : QTYxID
2691            $ids = array_unique(explode('-', $ids_input));
2692            $names = array_unique(explode('¤', $names_input));
2693
2694            if (!empty($ids)) {
2695                $length = count($ids);
2696                for ($i = 0; $i < $length; ++$i) {
2697                    if (!empty($ids[$i]) && !empty($names[$i])) {
2698                        list($pack_items[$i]['pack_quantity'], $pack_items[$i]['id']) = explode('x', $ids[$i]);
2699                        $exploded_name = explode('x', $names[$i]);
2700                        $pack_items[$i]['name'] = $exploded_name[1];
2701                    }
2702                }
2703            }
2704        } else {
2705            $i = 0;
2706            foreach ($product->packItems as $pack_item) {
2707                $pack_items[$i]['id'] = $pack_item->id;
2708                $pack_items[$i]['pack_quantity'] = $pack_item->pack_quantity;
2709                $pack_items[$i]['name'] = $pack_item->name;
2710                $pack_items[$i]['reference'] = $pack_item->reference;
2711                $pack_items[$i]['id_product_attribute'] = isset($pack_item->id_pack_product_attribute) && $pack_item->id_pack_product_attribute ? $pack_item->id_pack_product_attribute : 0;
2712                $cover = $pack_item->id_pack_product_attribute ? Product::getCombinationImageById($pack_item->id_pack_product_attribute, Context::getContext()->language->id) : Product::getCover($pack_item->id);
2713                $pack_items[$i]['image'] = Context::getContext()->link->getImageLink($pack_item->link_rewrite, $cover['id_image'], 'home_default');
2714                // @todo: don't rely on 'home_default'
2715                //$path_to_image = _PS_IMG_DIR_.'p/'.Image::getImgFolderStatic($cover['id_image']).(int)$cover['id_image'].'.jpg';
2716                //$pack_items[$i]['image'] = ImageManager::thumbnail($path_to_image, 'pack_mini_'.$pack_item->id.'_'.$this->context->shop->id.'.jpg', 120);
2717                ++$i;
2718            }
2719        }
2720
2721        return $pack_items;
2722    }
2723
2724    protected function _getFinalPrice($specific_price, $product_price, $tax_rate)
2725    {
2726        return $this->object->getPrice(false, $specific_price['id_product_attribute'], 2);
2727    }
2728
2729    protected function _getCustomizationFieldIds($labels, $alreadyGenerated, $obj)
2730    {
2731        $customizableFieldIds = [];
2732        if (isset($labels[Product::CUSTOMIZE_FILE])) {
2733            foreach ($labels[Product::CUSTOMIZE_FILE] as $id_customization_field => $label) {
2734                $customizableFieldIds[] = 'label_' . Product::CUSTOMIZE_FILE . '_' . (int) ($id_customization_field);
2735            }
2736        }
2737        if (isset($labels[Product::CUSTOMIZE_TEXTFIELD])) {
2738            foreach ($labels[Product::CUSTOMIZE_TEXTFIELD] as $id_customization_field => $label) {
2739                $customizableFieldIds[] = 'label_' . Product::CUSTOMIZE_TEXTFIELD . '_' . (int) ($id_customization_field);
2740            }
2741        }
2742        $j = 0;
2743        for ($i = $alreadyGenerated[Product::CUSTOMIZE_FILE]; $i < (int) ($this->getFieldValue($obj, 'uploadable_files')); ++$i) {
2744            $customizableFieldIds[] = 'newLabel_' . Product::CUSTOMIZE_FILE . '_' . $j++;
2745        }
2746        $j = 0;
2747        for ($i = $alreadyGenerated[Product::CUSTOMIZE_TEXTFIELD]; $i < (int) ($this->getFieldValue($obj, 'text_fields')); ++$i) {
2748            $customizableFieldIds[] = 'newLabel_' . Product::CUSTOMIZE_TEXTFIELD . '_' . $j++;
2749        }
2750
2751        return implode('¤', $customizableFieldIds);
2752    }
2753
2754    protected function getCarrierList()
2755    {
2756        $carrier_list = Carrier::getCarriers($this->context->language->id, false, false, false, null, Carrier::ALL_CARRIERS);
2757
2758        if ($product = $this->loadObject(true)) {
2759            /** @var Product $product */
2760            $carrier_selected_list = $product->getCarriers();
2761            foreach ($carrier_list as &$carrier) {
2762                foreach ($carrier_selected_list as $carrier_selected) {
2763                    if ($carrier_selected['id_reference'] == $carrier['id_reference']) {
2764                        $carrier['selected'] = true;
2765
2766                        continue;
2767                    }
2768                }
2769            }
2770        }
2771
2772        return $carrier_list;
2773    }
2774
2775    protected function addCarriers($product = null)
2776    {
2777        if (!isset($product)) {
2778            $product = new Product((int) Tools::getValue('id_product'));
2779        }
2780
2781        if (Validate::isLoadedObject($product)) {
2782            $carriers = [];
2783
2784            if (Tools::getValue('selectedCarriers')) {
2785                $carriers = Tools::getValue('selectedCarriers');
2786            }
2787
2788            $product->setCarriers($carriers);
2789        }
2790    }
2791
2792    /**
2793     * Ajax process upload images.
2794     *
2795     * @param int|null $idProduct
2796     * @param string $inputFileName
2797     * @param bool $die If method must die or return values
2798     *
2799     * @return array
2800     */
2801    public function ajaxProcessaddProductImage($idProduct = null, $inputFileName = 'file', $die = true)
2802    {
2803        $idProduct = $idProduct ? $idProduct : Tools::getValue('id_product');
2804
2805        self::$currentIndex = 'index.php?tab=AdminProducts';
2806        $product = new Product((int) $idProduct);
2807        $legends = Tools::getValue('legend');
2808
2809        if (!is_array($legends)) {
2810            $legends = (array) $legends;
2811        }
2812
2813        if (!Validate::isLoadedObject($product)) {
2814            $files = [];
2815            $files[0]['error'] = $this->trans('Cannot add image because product creation failed.', [], 'Admin.Catalog.Notification');
2816        }
2817
2818        $image_uploader = new HelperImageUploader($inputFileName);
2819        $image_uploader->setAcceptTypes(['jpeg', 'gif', 'png', 'jpg'])->setMaxSize($this->max_image_size);
2820        $files = $image_uploader->process();
2821
2822        foreach ($files as &$file) {
2823            $image = new Image();
2824            $image->id_product = (int) ($product->id);
2825            $image->position = Image::getHighestPosition($product->id) + 1;
2826
2827            foreach ($legends as $key => $legend) {
2828                if (!empty($legend)) {
2829                    $image->legend[(int) $key] = $legend;
2830                }
2831            }
2832
2833            if (!Image::getCover($image->id_product)) {
2834                $image->cover = 1;
2835            } else {
2836                $image->cover = 0;
2837            }
2838
2839            if (($validate = $image->validateFieldsLang(false, true)) !== true) {
2840                $file['error'] = $validate;
2841            }
2842
2843            if (isset($file['error']) && (!is_numeric($file['error']) || $file['error'] != 0)) {
2844                continue;
2845            }
2846
2847            if (!$image->add()) {
2848                $file['error'] = $this->trans('Error while creating additional image', [], 'Admin.Catalog.Notification');
2849            } else {
2850                if (!$new_path = $image->getPathForCreation()) {
2851                    $file['error'] = $this->trans('An error occurred while attempting to create a new folder.', [], 'Admin.Notifications.Error');
2852
2853                    continue;
2854                }
2855
2856                $error = 0;
2857
2858                if (!ImageManager::resize($file['save_path'], $new_path . '.' . $image->image_format, null, null, 'jpg', false, $error)) {
2859                    switch ($error) {
2860                        case ImageManager::ERROR_FILE_NOT_EXIST:
2861                            $file['error'] = $this->trans('An error occurred while copying image, the file does not exist anymore.', [], 'Admin.Catalog.Notification');
2862
2863                            break;
2864
2865                        case ImageManager::ERROR_FILE_WIDTH:
2866                            $file['error'] = $this->trans('An error occurred while copying image, the file width is 0px.', [], 'Admin.Catalog.Notification');
2867
2868                            break;
2869
2870                        case ImageManager::ERROR_MEMORY_LIMIT:
2871                            $file['error'] = $this->trans('An error occurred while copying image, check your memory limit.', [], 'Admin.Catalog.Notification');
2872
2873                            break;
2874
2875                        default:
2876                            $file['error'] = $this->trans('An error occurred while copying the image.', [], 'Admin.Catalog.Notification');
2877
2878                            break;
2879                    }
2880
2881                    continue;
2882                } else {
2883                    $imagesTypes = ImageType::getImagesTypes('products');
2884                    $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI');
2885
2886                    foreach ($imagesTypes as $imageType) {
2887                        if (!ImageManager::resize($file['save_path'], $new_path . '-' . stripslashes($imageType['name']) . '.' . $image->image_format, $imageType['width'], $imageType['height'], $image->image_format)) {
2888                            $file['error'] = $this->trans('An error occurred while copying this image:', [], 'Admin.Notifications.Error') . ' ' . stripslashes($imageType['name']);
2889
2890                            continue;
2891                        }
2892
2893                        if ($generate_hight_dpi_images) {
2894                            if (!ImageManager::resize($file['save_path'], $new_path . '-' . stripslashes($imageType['name']) . '2x.' . $image->image_format, (int) $imageType['width'] * 2, (int) $imageType['height'] * 2, $image->image_format)) {
2895                                $file['error'] = $this->trans('An error occurred while copying this image:', [], 'Admin.Notifications.Error') . ' ' . stripslashes($imageType['name']);
2896
2897                                continue;
2898                            }
2899                        }
2900                    }
2901                }
2902
2903                unlink($file['save_path']);
2904                //Necesary to prevent hacking
2905                unset($file['save_path']);
2906                Hook::exec('actionWatermark', ['id_image' => $image->id, 'id_product' => $product->id]);
2907
2908                if (!$image->update()) {
2909                    $file['error'] = $this->trans('Error while updating the status.', [], 'Admin.Notifications.Error');
2910
2911                    continue;
2912                }
2913
2914                // Associate image to shop from context
2915                $shops = Shop::getContextListShopID();
2916                $image->associateTo($shops);
2917                $json_shops = [];
2918
2919                foreach ($shops as $id_shop) {
2920                    $json_shops[$id_shop] = true;
2921                }
2922
2923                $file['status'] = 'ok';
2924                $file['id'] = $image->id;
2925                $file['position'] = $image->position;
2926                $file['cover'] = $image->cover;
2927                $file['legend'] = $image->legend;
2928                $file['path'] = $image->getExistingImgPath();
2929                $file['shops'] = $json_shops;
2930
2931                @unlink(_PS_TMP_IMG_DIR_ . 'product_' . (int) $product->id . '.jpg');
2932                @unlink(_PS_TMP_IMG_DIR_ . 'product_mini_' . (int) $product->id . '_' . $this->context->shop->id . '.jpg');
2933            }
2934        }
2935
2936        if ($die) {
2937            die(json_encode([$image_uploader->getName() => $files]));
2938        } else {
2939            return $files;
2940        }
2941    }
2942
2943    public function ajaxProcessProductQuantity()
2944    {
2945        if (!$this->access('edit')) {
2946            return die(json_encode(['error' => 'You do not have the right permission']));
2947        }
2948        if (!Tools::getValue('actionQty')) {
2949            return json_encode(['error' => 'Undefined action']);
2950        }
2951
2952        $product = new Product((int) Tools::getValue('id_product'), true);
2953        switch (Tools::getValue('actionQty')) {
2954            case 'depends_on_stock':
2955                if (Tools::getValue('value') === false) {
2956                    die(json_encode(['error' => 'Undefined value']));
2957                }
2958                if ((int) Tools::getValue('value') != 0 && (int) Tools::getValue('value') != 1) {
2959                    die(json_encode(['error' => 'Incorrect value']));
2960                }
2961                if (!$product->advanced_stock_management && (int) Tools::getValue('value') == 1) {
2962                    die(json_encode(['error' => 'Not possible if advanced stock management is disabled.']));
2963                }
2964                if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')
2965                    && (int) Tools::getValue('value') == 1
2966                    && (
2967                        Pack::isPack($product->id)
2968                        && !Pack::allUsesAdvancedStockManagement($product->id)
2969                        && (
2970                            $product->pack_stock_type == Pack::STOCK_TYPE_PACK_BOTH
2971                            || $product->pack_stock_type == Pack::STOCK_TYPE_PRODUCTS_ONLY
2972                            || (
2973                                $product->pack_stock_type == Pack::STOCK_TYPE_DEFAULT
2974                                && (Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PRODUCTS_ONLY
2975                                    || Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_BOTH)
2976                            )
2977                        )
2978                    )
2979                ) {
2980                    die(json_encode(['error' => 'You cannot use advanced stock management for this pack because' . '<br />' .
2981                        '- advanced stock management is not enabled for these products' . '<br />' .
2982                        '- you have chosen to decrement products quantities.', ]));
2983                }
2984
2985                StockAvailable::setProductDependsOnStock($product->id, (int) Tools::getValue('value'));
2986
2987                break;
2988
2989            case 'pack_stock_type':
2990                $value = Tools::getValue('value');
2991                if ($value === false) {
2992                    die(json_encode(['error' => 'Undefined value']));
2993                }
2994                if ((int) $value != 0 && (int) $value != 1
2995                    && (int) $value != 2 && (int) $value != 3) {
2996                    die(json_encode(['error' => 'Incorrect value']));
2997                }
2998                if ($product->depends_on_stock
2999                    && !Pack::allUsesAdvancedStockManagement($product->id)
3000                    && (
3001                        (int) $value == 1
3002                        || (int) $value == 2
3003                        || (
3004                            (int) $value == 3
3005                            && (Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PRODUCTS_ONLY
3006                                || Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_BOTH)
3007                        )
3008                    )
3009                ) {
3010                    die(json_encode(['error' => 'You cannot use this stock management option because:' . '<br />' .
3011                        '- advanced stock management is not enabled for these products' . '<br />' .
3012                        '- advanced stock management is enabled for the pack', ]));
3013                }
3014
3015                Product::setPackStockType($product->id, $value);
3016
3017                break;
3018
3019            case 'out_of_stock':
3020                if (Tools::getValue('value') === false) {
3021                    die(json_encode(['error' => 'Undefined value']));
3022                }
3023                if (!in_array((int) Tools::getValue('value'), [0, 1, 2])) {
3024                    die(json_encode(['error' => 'Incorrect value']));
3025                }
3026
3027                StockAvailable::setProductOutOfStock($product->id, (int) Tools::getValue('value'));
3028
3029                break;
3030
3031            case 'set_qty':
3032                if (Tools::getValue('value') === false || (!is_numeric(trim(Tools::getValue('value'))))) {
3033                    die(json_encode(['error' => 'Undefined value']));
3034                }
3035                if (Tools::getValue('id_product_attribute') === false) {
3036                    die(json_encode(['error' => 'Undefined id product attribute']));
3037                }
3038
3039                StockAvailable::setQuantity($product->id, (int) Tools::getValue('id_product_attribute'), (int) Tools::getValue('value'));
3040                Hook::exec('actionProductUpdate', ['id_product' => (int) $product->id, 'product' => $product]);
3041
3042                // Catch potential echo from modules
3043                // This echoed error is kept for legacy controllers, but is dropped during sf refactoring of the hook.
3044                $error = ob_get_contents();
3045                if (!empty($error)) {
3046                    ob_end_clean();
3047                    die(json_encode(['error' => $error]));
3048                }
3049
3050                break;
3051            case 'advanced_stock_management':
3052                if (Tools::getValue('value') === false) {
3053                    die(json_encode(['error' => 'Undefined value']));
3054                }
3055                if ((int) Tools::getValue('value') != 1 && (int) Tools::getValue('value') != 0) {
3056                    die(json_encode(['error' => 'Incorrect value']));
3057                }
3058                if (!Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && (int) Tools::getValue('value') == 1) {
3059                    die(json_encode(['error' => 'Not possible if advanced stock management is disabled. ']));
3060                }
3061
3062                $product->setAdvancedStockManagement((int) Tools::getValue('value'));
3063                if (StockAvailable::dependsOnStock($product->id) == 1 && (int) Tools::getValue('value') == 0) {
3064                    StockAvailable::setProductDependsOnStock($product->id, 0);
3065                }
3066
3067                break;
3068        }
3069        die(json_encode(['error' => false]));
3070    }
3071
3072    public function getCombinationImagesJS()
3073    {
3074        /** @var Product $obj */
3075        if (!($obj = $this->loadObject(true))) {
3076            return;
3077        }
3078
3079        $content = 'var combination_images = new Array();';
3080        if (!$allCombinationImages = $obj->getCombinationImages($this->context->language->id)) {
3081            return $content;
3082        }
3083        foreach ($allCombinationImages as $id_product_attribute => $combination_images) {
3084            $i = 0;
3085            $content .= 'combination_images[' . (int) $id_product_attribute . '] = new Array();';
3086            foreach ($combination_images as $combination_image) {
3087                $content .= 'combination_images[' . (int) $id_product_attribute . '][' . $i++ . '] = ' . (int) $combination_image['id_image'] . ';';
3088            }
3089        }
3090
3091        return $content;
3092    }
3093
3094    public function haveThisAccessory($accessory_id, $accessories)
3095    {
3096        foreach ($accessories as $accessory) {
3097            if ((int) $accessory['id_product'] == (int) $accessory_id) {
3098                return true;
3099            }
3100        }
3101
3102        return false;
3103    }
3104
3105    protected function initPack(Product $product)
3106    {
3107        $this->tpl_form_vars['is_pack'] = ($product->id && Pack::isPack($product->id)) || Tools::getValue('type_product') == Product::PTYPE_PACK;
3108        $product->packItems = Pack::getItems($product->id, $this->context->language->id);
3109
3110        $input_pack_items = '';
3111        if (Tools::getValue('inputPackItems')) {
3112            $input_pack_items = Tools::getValue('inputPackItems');
3113        } else {
3114            foreach ($product->packItems as $pack_item) {
3115                $input_pack_items .= $pack_item->pack_quantity . 'x' . $pack_item->id . '-';
3116            }
3117        }
3118        $this->tpl_form_vars['input_pack_items'] = $input_pack_items;
3119
3120        $input_namepack_items = '';
3121        if (Tools::getValue('namePackItems')) {
3122            $input_namepack_items = Tools::getValue('namePackItems');
3123        } else {
3124            foreach ($product->packItems as $pack_item) {
3125                $input_namepack_items .= $pack_item->pack_quantity . ' x ' . $pack_item->name . '¤';
3126            }
3127        }
3128        $this->tpl_form_vars['input_namepack_items'] = $input_namepack_items;
3129    }
3130
3131    /**
3132     * delete all items in pack, then check if type_product value is 2.
3133     * if yes, add the pack items from input "inputPackItems".
3134     *
3135     * @param Product $product
3136     *
3137     * @return bool
3138     */
3139    public function updatePackItems($product)
3140    {
3141        Pack::deleteItems($product->id);
3142        // lines format: QTY x ID-QTY x ID
3143        if (Tools::getValue('type_product') == Product::PTYPE_PACK) {
3144            $product->setDefaultAttribute(0); //reset cache_default_attribute
3145            $items = Tools::getValue('inputPackItems');
3146            $lines = array_unique(explode('-', $items));
3147
3148            // lines is an array of string with format : QTYxIDxID_PRODUCT_ATTRIBUTE
3149            if (count($lines)) {
3150                foreach ($lines as $line) {
3151                    if (!empty($line)) {
3152                        $item_id_attribute = 0;
3153                        count($array = explode('x', $line)) == 3 ? list($qty, $item_id, $item_id_attribute) = $array : list($qty, $item_id) = $array;
3154                        if ($qty > 0 && isset($item_id)) {
3155                            if (Pack::isPack((int) $item_id)) {
3156                                $this->errors[] = $this->trans('You can\'t add product packs into a pack', [], 'Admin.Catalog.Notification');
3157                            } elseif (!Pack::addItem((int) $product->id, (int) $item_id, (int) $qty, (int) $item_id_attribute)) {
3158                                $this->errors[] = $this->trans('An error occurred while attempting to add products to the pack.', [], 'Admin.Catalog.Notification');
3159                            }
3160                        }
3161                    }
3162                }
3163            }
3164        }
3165    }
3166
3167    public function ajaxProcessCheckProductName()
3168    {
3169        if ($this->access('view')) {
3170            $search = Tools::getValue('q');
3171            $id_lang = Tools::getValue('id_lang');
3172            $limit = Tools::getValue('limit');
3173            if (Context::getContext()->shop->getContext() != Shop::CONTEXT_SHOP) {
3174                $result = false;
3175            } else {
3176                $result = Db::getInstance()->executeS('
3177					SELECT DISTINCT pl.`name`, p.`id_product`, pl.`id_shop`
3178					FROM `' . _DB_PREFIX_ . 'product` p
3179					LEFT JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON (ps.id_product = p.id_product AND ps.id_shop =' . (int) Context::getContext()->shop->id . ')
3180					LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
3181						ON (pl.`id_product` = p.`id_product` AND pl.`id_lang` = ' . (int) $id_lang . ')
3182					WHERE pl.`name` LIKE "%' . pSQL($search) . '%" AND ps.id_product IS NULL
3183					GROUP BY pl.`id_product`
3184					LIMIT ' . (int) $limit);
3185            }
3186            die(json_encode($result));
3187        }
3188    }
3189
3190    public function ajaxProcessUpdatePositions()
3191    {
3192        if ($this->access('edit')) {
3193            $way = (int) (Tools::getValue('way'));
3194            $id_product = (int) Tools::getValue('id_product');
3195            $id_category = (int) Tools::getValue('id_category');
3196            $positions = Tools::getValue('product');
3197            $page = (int) Tools::getValue('page');
3198            $selected_pagination = (int) Tools::getValue('selected_pagination');
3199
3200            if (is_array($positions)) {
3201                foreach ($positions as $position => $value) {
3202                    $pos = explode('_', $value);
3203
3204                    if ((isset($pos[1], $pos[2])) && ($pos[1] == $id_category && (int) $pos[2] === $id_product)) {
3205                        if ($page > 1) {
3206                            $position = $position + (($page - 1) * $selected_pagination);
3207                        }
3208
3209                        if ($product = new Product((int) $pos[2])) {
3210                            if (isset($position) && $product->updatePosition($way, $position)) {
3211                                $category = new Category((int) $id_category);
3212                                if (Validate::isLoadedObject($category)) {
3213                                    hook::Exec('categoryUpdate', ['category' => $category]);
3214                                }
3215                                echo 'ok position ' . (int) $position . ' for product ' . (int) $pos[2] . "\r\n";
3216                            } else {
3217                                echo '{"hasError" : true, "errors" : "Can not update product ' . (int) $id_product . ' to position ' . (int) $position . ' "}';
3218                            }
3219                        } else {
3220                            echo '{"hasError" : true, "errors" : "This product (' . (int) $id_product . ') can t be loaded"}';
3221                        }
3222
3223                        break;
3224                    }
3225                }
3226            }
3227        }
3228    }
3229
3230    public function ajaxProcessPublishProduct()
3231    {
3232        if ($this->access('edit')) {
3233            if ($id_product = (int) Tools::getValue('id_product')) {
3234                $bo_product_url = dirname($_SERVER['PHP_SELF']) . '/index.php?tab=AdminProducts&id_product=' . $id_product . '&updateproduct&token=' . $this->token;
3235
3236                if (Tools::getValue('redirect')) {
3237                    die($bo_product_url);
3238                }
3239
3240                $product = new Product((int) $id_product);
3241                if (!Validate::isLoadedObject($product)) {
3242                    die('error: invalid id');
3243                }
3244
3245                $product->active = 1;
3246
3247                if ($product->save()) {
3248                    die($bo_product_url);
3249                } else {
3250                    die('error: saving');
3251                }
3252            }
3253        }
3254    }
3255
3256    public function processImageLegends()
3257    {
3258        if (Tools::getValue('key_tab') == 'Images' && Tools::getValue('submitAddproductAndStay') == 'update_legends' && Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
3259            $id_image = (int) Tools::getValue('id_caption');
3260            $language_ids = Language::getIDs(false);
3261            foreach ($_POST as $key => $val) {
3262                if (preg_match('/^legend_([0-9]+)/i', $key, $match)) {
3263                    foreach ($language_ids as $id_lang) {
3264                        if ($val && $id_lang == $match[1]) {
3265                            Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'image_lang SET legend = "' . pSQL($val) . '" WHERE ' . ($id_image ? 'id_image = ' . (int) $id_image : 'EXISTS (SELECT 1 FROM ' . _DB_PREFIX_ . 'image WHERE ' . _DB_PREFIX_ . 'image.id_image = ' . _DB_PREFIX_ . 'image_lang.id_image AND id_product = ' . (int) $product->id . ')') . ' AND id_lang = ' . (int) $id_lang);
3266                        }
3267                    }
3268                }
3269            }
3270        }
3271    }
3272
3273    /**
3274     * Returns in an homemade JSON with the content of a products pack.
3275     */
3276    public function displayAjaxProductPackItems()
3277    {
3278        $jsonArray = [];
3279        $products = Db::getInstance()->executeS('
3280            SELECT p.`id_product`, pl.`name`
3281            FROM `' . _DB_PREFIX_ . 'product` p
3282            NATURAL LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
3283            WHERE pl.`id_lang` = ' . (int) (Tools::getValue('id_lang')) . '
3284            ' . Shop::addSqlRestrictionOnLang('pl') . '
3285            AND NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'pack` WHERE `id_product_pack` = p.`id_product`)
3286            AND p.`id_product` != ' . (int) (Tools::getValue('id_product')));
3287
3288        foreach ($products as $packItem) {
3289            $jsonArray[] = '{"value": "' . (int) ($packItem['id_product']) . '-' . addslashes($packItem['name'])
3290                . '", "text":"' . (int) ($packItem['id_product']) . ' - ' . addslashes($packItem['name']) . '"}';
3291        }
3292        $this->ajaxRender('[' . implode(',', $jsonArray) . ']');
3293    }
3294
3295    /**
3296     * Displays a list of products when their name matches a given query
3297     * Optional parameters allow products to be excluded from the results.
3298     */
3299    public function displayAjaxProductsList()
3300    {
3301        $query = Tools::getValue('q', false);
3302        if (empty($query)) {
3303            return;
3304        }
3305
3306        /*
3307         * In the SQL request the "q" param is used entirely to match result in database.
3308         * In this way if string:"(ref : #ref_pattern#)" is displayed on the return list,
3309         * they are no return values just because string:"(ref : #ref_pattern#)"
3310         * is not write in the name field of the product.
3311         * So the ref pattern will be cut for the search request.
3312         */
3313        if ($pos = strpos($query, ' (ref:')) {
3314            $query = substr($query, 0, $pos);
3315        }
3316
3317        $excludeIds = Tools::getValue('excludeIds', false);
3318        if ($excludeIds && $excludeIds != 'NaN') {
3319            $excludeIds = implode(',', array_map('intval', explode(',', $excludeIds)));
3320        } else {
3321            $excludeIds = '';
3322        }
3323
3324        // Excluding downloadable products from packs because download from pack is not supported
3325        $forceJson = Tools::getValue('forceJson', false);
3326        $disableCombination = Tools::getValue('disableCombination', false);
3327        $excludeVirtuals = (bool) Tools::getValue('excludeVirtuals', true);
3328        $exclude_packs = (bool) Tools::getValue('exclude_packs', true);
3329
3330        $context = Context::getContext();
3331
3332        $sql = 'SELECT p.`id_product`, pl.`link_rewrite`, p.`reference`, pl.`name`, image_shop.`id_image` id_image, il.`legend`, p.`cache_default_attribute`
3333                FROM `' . _DB_PREFIX_ . 'product` p
3334                ' . Shop::addSqlAssociation('product', 'p') . '
3335                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (pl.id_product = p.id_product AND pl.id_lang = ' . (int) $context->language->id . Shop::addSqlRestrictionOnLang('pl') . ')
3336                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
3337                    ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ')
3338                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $context->language->id . ')
3339                WHERE (pl.name LIKE \'%' . pSQL($query) . '%\' OR p.reference LIKE \'%' . pSQL($query) . '%\')' .
3340                (!empty($excludeIds) ? ' AND p.id_product NOT IN (' . $excludeIds . ') ' : ' ') .
3341                ($excludeVirtuals ? 'AND NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'product_download` pd WHERE (pd.id_product = p.id_product))' : '') .
3342                ($exclude_packs ? 'AND (p.cache_is_pack IS NULL OR p.cache_is_pack = 0)' : '') .
3343                ' GROUP BY p.id_product';
3344
3345        $items = Db::getInstance()->executeS($sql);
3346
3347        if ($items && ($disableCombination || $excludeIds)) {
3348            $results = [];
3349            foreach ($items as $item) {
3350                if (!$forceJson) {
3351                    $item['name'] = str_replace('|', '&#124;', $item['name']);
3352                    $results[] = trim($item['name']) . (!empty($item['reference']) ? ' (ref: ' . $item['reference'] . ')' : '') . '|' . (int) ($item['id_product']);
3353                } else {
3354                    $results[] = [
3355                        'id' => $item['id_product'],
3356                        'name' => $item['name'] . (!empty($item['reference']) ? ' (ref: ' . $item['reference'] . ')' : ''),
3357                        'ref' => (!empty($item['reference']) ? $item['reference'] : ''),
3358                        'image' => str_replace('http://', Tools::getShopProtocol(), $context->link->getImageLink($item['link_rewrite'], $item['id_image'], 'home_default')),
3359                    ];
3360                }
3361            }
3362
3363            if (!$forceJson) {
3364                return $this->ajaxRender(implode(PHP_EOL, $results));
3365            }
3366
3367            return $this->ajaxRender(json_encode($results));
3368        }
3369        if ($items) {
3370            // packs
3371            $results = [];
3372            foreach ($items as $item) {
3373                // check if product have combination
3374                if (Combination::isFeatureActive() && $item['cache_default_attribute']) {
3375                    $sql = 'SELECT pa.`id_product_attribute`, pa.`reference`, ag.`id_attribute_group`, pai.`id_image`, agl.`name` AS group_name, al.`name` AS attribute_name,
3376                                a.`id_attribute`
3377                            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
3378                            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
3379                            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute`
3380                            LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute`
3381                            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group`
3382                            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $context->language->id . ')
3383                            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $context->language->id . ')
3384                            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_image` pai ON pai.`id_product_attribute` = pa.`id_product_attribute`
3385                            WHERE pa.`id_product` = ' . (int) $item['id_product'] . '
3386                            GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group`
3387                            ORDER BY pa.`id_product_attribute`';
3388
3389                    $combinations = Db::getInstance()->executeS($sql);
3390                    if (!empty($combinations)) {
3391                        foreach ($combinations as $k => $combination) {
3392                            $results[$combination['id_product_attribute']]['id'] = $item['id_product'];
3393                            $results[$combination['id_product_attribute']]['id_product_attribute'] = $combination['id_product_attribute'];
3394                            !empty($results[$combination['id_product_attribute']]['name']) ? $results[$combination['id_product_attribute']]['name'] .= ' ' . $combination['group_name'] . '-' . $combination['attribute_name']
3395                            : $results[$combination['id_product_attribute']]['name'] = $item['name'] . ' ' . $combination['group_name'] . '-' . $combination['attribute_name'];
3396                            if (!empty($combination['reference'])) {
3397                                $results[$combination['id_product_attribute']]['ref'] = $combination['reference'];
3398                            } else {
3399                                $results[$combination['id_product_attribute']]['ref'] = !empty($item['reference']) ? $item['reference'] : '';
3400                            }
3401                            if (empty($results[$combination['id_product_attribute']]['image'])) {
3402                                $results[$combination['id_product_attribute']]['image'] = str_replace('http://', Tools::getShopProtocol(), $context->link->getImageLink($item['link_rewrite'], $combination['id_image'], 'home_default'));
3403                            }
3404                        }
3405                    } else {
3406                        $results[] = [
3407                            'id' => $item['id_product'],
3408                            'name' => $item['name'],
3409                            'ref' => (!empty($item['reference']) ? $item['reference'] : ''),
3410                            'image' => str_replace('http://', Tools::getShopProtocol(), $context->link->getImageLink($item['link_rewrite'], $item['id_image'], 'home_default')),
3411                        ];
3412                    }
3413                } else {
3414                    $results[] = [
3415                        'id' => $item['id_product'],
3416                        'name' => $item['name'],
3417                        'ref' => (!empty($item['reference']) ? $item['reference'] : ''),
3418                        'image' => str_replace('http://', Tools::getShopProtocol(), $context->link->getImageLink($item['link_rewrite'], $item['id_image'], 'home_default')),
3419                    ];
3420                }
3421            }
3422
3423            return $this->ajaxRender(json_encode(array_values($results)));
3424        }
3425
3426        return $this->ajaxRender(json_encode([]));
3427    }
3428}
3429