1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class AdminCategoriesControllerCore
34 *
35 * @since 1.0.0
36 */
37class AdminCategoriesControllerCore extends AdminController
38{
39    // @codingStandardsIgnoreStart
40    /** @var bool does the product have to be removed during the delete process */
41    public $remove_products = true;
42    /** @var bool does the product have to be disable during the delete process */
43    public $disable_products = false;
44    /**
45     * @var object Category() instance for navigation
46     */
47    protected $_category = null;
48    protected $position_identifier = 'id_category_to_move';
49    protected $original_filter = '';
50    // @codingStandardsIgnoreEnd
51
52    /**
53     * AdminCategoriesControllerCore constructor.
54     *
55     * @since 1.0.0
56     * @throws PrestaShopException
57     */
58    public function __construct()
59    {
60        $this->bootstrap = true;
61        $this->table = 'category';
62        $this->className = 'Category';
63        $this->lang = true;
64        $this->deleted = false;
65        $this->explicitSelect = true;
66        $this->_defaultOrderBy = 'position';
67        $this->allow_export = true;
68
69        $this->context = Context::getContext();
70
71        $this->fieldImageSettings = [
72            'name' => 'image',
73            'dir'  => 'c',
74        ];
75
76        $this->fields_list = [
77            'id_category' => [
78                'title' => $this->l('ID'),
79                'align' => 'center',
80                'class' => 'fixed-width-xs',
81            ],
82            'name'        => [
83                'title' => $this->l('Name'),
84            ],
85            'description' => [
86                'title'    => $this->l('Description'),
87                'callback' => 'getDescriptionClean',
88                'orderby'  => false,
89            ],
90            'position'    => [
91                'title'      => $this->l('Position'),
92                'filter_key' => 'sa!position',
93                'position'   => 'position',
94                'align'      => 'center',
95            ],
96            'active'      => [
97                'title'   => $this->l('Displayed'),
98                'active'  => 'status',
99                'type'    => 'bool',
100                'class'   => 'fixed-width-xs',
101                'align'   => 'center',
102                'ajax'    => true,
103                'orderby' => false,
104            ],
105        ];
106
107        $this->bulk_actions = [
108            'delete' => [
109                'text'    => $this->l('Delete selected'),
110                'icon'    => 'icon-trash',
111                'confirm' => $this->l('Delete selected items?'),
112            ],
113        ];
114        $this->specificConfirmDelete = false;
115
116        parent::__construct();
117    }
118
119    /**
120     * @param $description
121     *
122     * @return string
123     *
124     * @since 1.0.0
125     */
126    public static function getDescriptionClean($description)
127    {
128        return Tools::getDescriptionClean($description);
129    }
130
131    /**
132     * @since 1.0.0
133     *
134     * @throws PrestaShopException
135     */
136    public function init()
137    {
138        parent::init();
139
140        // context->shop is set in the init() function, so we move the _category instanciation after that
141        if (($idCategory = Tools::getvalue('id_category')) && $this->action != 'select_delete') {
142            $this->_category = new Category($idCategory);
143        } else {
144            if (Shop::getContext() == Shop::CONTEXT_SHOP) {
145                $this->_category = new Category($this->context->shop->id_category);
146            } elseif (count(Category::getCategoriesWithoutParent()) > 1 && Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && count(Shop::getShops(true, null, true)) != 1) {
147                $this->_category = Category::getTopCategory();
148            } else {
149                $this->_category = new Category(Configuration::get('PS_HOME_CATEGORY'));
150            }
151        }
152
153        $countCategoriesWithoutParent = count(Category::getCategoriesWithoutParent());
154
155        if (Tools::isSubmit('id_category')) {
156            $idParent = $this->_category->id;
157        } elseif (!Shop::isFeatureActive() && $countCategoriesWithoutParent > 1) {
158            $idParent = (int) Configuration::get('PS_ROOT_CATEGORY');
159        } elseif (Shop::isFeatureActive() && $countCategoriesWithoutParent == 1) {
160            $idParent = (int) Configuration::get('PS_HOME_CATEGORY');
161        } elseif (Shop::isFeatureActive() && $countCategoriesWithoutParent > 1 && Shop::getContext() != Shop::CONTEXT_SHOP) {
162            if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && count(Shop::getShops(true, null, true)) == 1) {
163                $idParent = $this->context->shop->id_category;
164            } else {
165                $idParent = (int) Configuration::get('PS_ROOT_CATEGORY');
166            }
167        } else {
168            $idParent = $this->context->shop->id_category;
169        }
170        $this->_select = 'sa.position position';
171        $this->original_filter = $this->_filter .= ' AND `id_parent` = '.(int) $idParent.' ';
172        $this->_use_found_rows = false;
173
174        if (Shop::getContext() == Shop::CONTEXT_SHOP) {
175            $this->_join .= ' LEFT JOIN `'._DB_PREFIX_.'category_shop` sa ON (a.`id_category` = sa.`id_category` AND sa.id_shop = '.(int) $this->context->shop->id.') ';
176        } else {
177            $this->_join .= ' LEFT JOIN `'._DB_PREFIX_.'category_shop` sa ON (a.`id_category` = sa.`id_category` AND sa.id_shop = a.id_shop_default) ';
178        }
179
180        // we add restriction for shop
181        if (Shop::getContext() == Shop::CONTEXT_SHOP && Shop::isFeatureActive()) {
182            $this->_where = ' AND sa.`id_shop` = '.(int) $this->context->shop->id;
183        }
184
185        // if we are not in a shop context, we remove the position column
186        if (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP) {
187            unset($this->fields_list['position']);
188        }
189        // shop restriction : if category is not available for current shop, we redirect to the list from default category
190        if (Validate::isLoadedObject($this->_category) && !$this->_category->isAssociatedToShop() && Shop::getContext() == Shop::CONTEXT_SHOP) {
191            $this->redirect_after = static::$currentIndex.'&id_category='.(int) $this->context->shop->getCategory().'&token='.$this->token;
192            $this->redirect();
193        }
194    }
195
196    /**
197     * @since 1.0.0
198     */
199    public function initPageHeaderToolbar()
200    {
201        parent::initPageHeaderToolbar();
202
203        if ($this->display != 'edit' && $this->display != 'add') {
204            if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) {
205                $this->page_header_toolbar_btn['new-url'] = [
206                    'href' => static::$currentIndex.'&add'.$this->table.'root&token='.$this->token,
207                    'desc' => $this->l('Add new root category', null, null, false),
208                ];
209            }
210
211            $idCategory = (Tools::isSubmit('id_category')) ? '&id_parent='.(int) Tools::getValue('id_category') : '';
212            $this->page_header_toolbar_btn['new_category'] = [
213                'href' => static::$currentIndex.'&addcategory&token='.$this->token.$idCategory,
214                'desc' => $this->l('Add new category', null, null, false),
215                'icon' => 'process-icon-new',
216            ];
217        }
218    }
219
220    /**
221     * @since 1.0.0
222     */
223    public function initContent()
224    {
225        if ($this->action == 'select_delete') {
226            $this->context->smarty->assign(
227                [
228                    'delete_form' => true,
229                    'url_delete'  => htmlentities($_SERVER['REQUEST_URI']),
230                    'boxes'       => $this->boxes,
231                ]
232            );
233        }
234
235        parent::initContent();
236    }
237
238    /**
239     * @since 1.0.0
240     */
241    public function setMedia()
242    {
243        parent::setMedia();
244        $this->addJqueryUi('ui.widget');
245        $this->addJqueryPlugin('tagify');
246    }
247
248    /**
249     * @param int  $idLang
250     * @param null $orderBy
251     * @param null $orderWay
252     * @param int  $start
253     * @param null $limit
254     * @param bool $idLangShop
255     *
256     * @since 1.0.0
257     */
258    public function getList($idLang, $orderBy = null, $orderWay = null, $start = 0, $limit = null, $idLangShop = false)
259    {
260        parent::getList($idLang, $orderBy, $orderWay, $start, $limit, $this->context->shop->id);
261        // Check each row to see if there are combinations and get the correct action in consequence
262
263        $nbItems = count($this->_list);
264        for ($i = 0; $i < $nbItems; $i++) {
265            $item = &$this->_list[$i];
266            $categoryTree = Category::getChildren((int) $item['id_category'], $this->context->language->id, false);
267            if (!count($categoryTree)) {
268                $this->addRowActionSkipList('view', [$item['id_category']]);
269            }
270        }
271    }
272
273    /**
274     * @return false|string
275     *
276     * @since 1.0.0
277     */
278    public function renderView()
279    {
280        $this->initToolbar();
281
282        return $this->renderList();
283    }
284
285    /**
286     * @since 1.0.0
287     */
288    public function initToolbar()
289    {
290        if (empty($this->display)) {
291            $this->toolbar_btn['new'] = [
292                'href' => static::$currentIndex.'&add'.$this->table.'&token='.$this->token,
293                'desc' => $this->l('Add New'),
294            ];
295
296            if ($this->can_import) {
297                $this->toolbar_btn['import'] = [
298                    'href' => $this->context->link->getAdminLink('AdminImport', true).'&import_type=categories',
299                    'desc' => $this->l('Import'),
300                ];
301            }
302        }
303        // be able to edit the Home category
304        if (count(Category::getCategoriesWithoutParent()) == 1 && !Tools::isSubmit('id_category')
305            && ($this->display == 'view' || empty($this->display))
306        ) {
307            $this->toolbar_btn['edit'] = [
308                'href' => static::$currentIndex.'&update'.$this->table.'&id_category='.(int) $this->_category->id.'&token='.$this->token,
309                'desc' => $this->l('Edit'),
310            ];
311        }
312        if (Tools::getValue('id_category') && !Tools::isSubmit('updatecategory')) {
313            $this->toolbar_btn['edit'] = [
314                'href' => static::$currentIndex.'&update'.$this->table.'&id_category='.(int) Tools::getValue('id_category').'&token='.$this->token,
315                'desc' => $this->l('Edit'),
316            ];
317        }
318
319        if ($this->display == 'view') {
320            $this->toolbar_btn['new'] = [
321                'href' => static::$currentIndex.'&add'.$this->table.'&id_parent='.(int) Tools::getValue('id_category').'&token='.$this->token,
322                'desc' => $this->l('Add New'),
323            ];
324        }
325        parent::initToolbar();
326        if ($this->_category->id == (int) Configuration::get('PS_ROOT_CATEGORY') && isset($this->toolbar_btn['new'])) {
327            unset($this->toolbar_btn['new']);
328        }
329        // after adding a category
330        if (empty($this->display)) {
331            $idCategory = (Tools::isSubmit('id_category')) ? '&id_parent='.(int) Tools::getValue('id_category') : '';
332            $this->toolbar_btn['new'] = [
333                'href' => static::$currentIndex.'&add'.$this->table.'&token='.$this->token.$idCategory,
334                'desc' => $this->l('Add New'),
335            ];
336
337            if (Tools::isSubmit('id_category')) {
338                $back = Tools::safeOutput(Tools::getValue('back', ''));
339                if (empty($back)) {
340                    $back = static::$currentIndex.'&token='.$this->token;
341                }
342                $this->toolbar_btn['back'] = [
343                    'href' => $back,
344                    'desc' => $this->l('Back to list'),
345                ];
346            }
347        }
348        if (!$this->lite_display && isset($this->toolbar_btn['back']['href']) && $this->_category->level_depth > 1
349            && $this->_category->id_parent && $this->_category->id_parent != (int) Configuration::get('PS_ROOT_CATEGORY')
350        ) {
351            $this->toolbar_btn['back']['href'] .= '&id_category='.(int) $this->_category->id_parent;
352        }
353    }
354
355    /**
356     * @return false|string
357     *
358     * @since 1.0.0
359     */
360    public function renderList()
361    {
362        if (isset($this->_filter) && trim($this->_filter) == '') {
363            $this->_filter = $this->original_filter;
364        }
365
366        $this->addRowAction('view');
367        $this->addRowAction('add');
368        $this->addRowAction('edit');
369        $this->addRowAction('delete');
370
371        $countCategoriesWithoutParent = count(Category::getCategoriesWithoutParent());
372        $categoriesTree = $this->_category->getParentsCategories();
373
374        if (empty($categoriesTree)
375            && ($this->_category->id != (int) Configuration::get('PS_ROOT_CATEGORY') || Tools::isSubmit('id_category'))
376            && (Shop::getContext() == Shop::CONTEXT_SHOP && !Shop::isFeatureActive() && $countCategoriesWithoutParent > 1)
377        ) {
378            $categoriesTree = [['name' => $this->_category->name[$this->context->language->id]]];
379        }
380
381        $categoriesTree = array_reverse($categoriesTree);
382
383        $this->tpl_list_vars['categories_tree'] = $categoriesTree;
384        $this->tpl_list_vars['categories_tree_current_id'] = $this->_category->id;
385
386        if (Tools::isSubmit('submitBulkdelete'.$this->table) || Tools::isSubmit('delete'.$this->table)) {
387            $category = new Category(Tools::getValue('id_category'));
388            if ($category->is_root_category) {
389                $this->tpl_list_vars['need_delete_mode'] = false;
390            } else {
391                $this->tpl_list_vars['need_delete_mode'] = true;
392            }
393            $this->tpl_list_vars['delete_category'] = true;
394            $this->tpl_list_vars['REQUEST_URI'] = $_SERVER['REQUEST_URI'];
395            $this->tpl_list_vars['POST'] = $_POST;
396        }
397
398        return parent::renderList();
399    }
400
401    /**
402     * @since 1.0.0
403     */
404    public function initProcess()
405    {
406        if (Tools::isSubmit('add'.$this->table.'root')) {
407            if ($this->tabAccess['add']) {
408                $this->action = 'add'.$this->table.'root';
409                $obj = $this->loadObject(true);
410                if (Validate::isLoadedObject($obj)) {
411                    $this->display = 'edit';
412                } else {
413                    $this->display = 'add';
414                }
415            } else {
416                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
417            }
418        }
419
420        parent::initProcess();
421
422        if ($this->action == 'delete' || $this->action == 'bulkdelete') {
423            if (Tools::getIsset('cancel')) {
424                Tools::redirectAdmin(static::$currentIndex.'&token='.Tools::getAdminTokenLite('AdminCategories'));
425            } elseif (Tools::getValue('deleteMode') == 'link' || Tools::getValue('deleteMode') == 'linkanddisable' || Tools::getValue('deleteMode') == 'delete') {
426                $this->delete_mode = Tools::getValue('deleteMode');
427            } else {
428                $this->action = 'select_delete';
429            }
430        }
431    }
432
433    /**
434     * @return mixed
435     *
436     * @since 1.0.0
437     */
438    public function renderKpis()
439    {
440        $time = time();
441        $kpis = [];
442
443        /* The data generation is located in AdminStatsControllerCore */
444
445        $helper = new HelperKpi();
446        $helper->id = 'box-disabled-categories';
447        $helper->icon = 'icon-off';
448        $helper->color = 'color1';
449        $helper->title = $this->l('Disabled Categories', null, null, false);
450        if (ConfigurationKPI::get('DISABLED_CATEGORIES') !== false) {
451            $helper->value = ConfigurationKPI::get('DISABLED_CATEGORIES');
452        }
453        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=disabled_categories';
454        $helper->refresh = (bool) (ConfigurationKPI::get('DISABLED_CATEGORIES_EXPIRE') < $time);
455        $kpis[] = $helper->generate();
456
457        $helper = new HelperKpi();
458        $helper->id = 'box-empty-categories';
459        $helper->icon = 'icon-bookmark-empty';
460        $helper->color = 'color2';
461        $helper->href = $this->context->link->getAdminLink('AdminTracking');
462        $helper->title = $this->l('Empty Categories', null, null, false);
463        if (ConfigurationKPI::get('EMPTY_CATEGORIES') !== false) {
464            $helper->value = ConfigurationKPI::get('EMPTY_CATEGORIES');
465        }
466        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=empty_categories';
467        $helper->refresh = (bool) (ConfigurationKPI::get('EMPTY_CATEGORIES_EXPIRE') < $time);
468        $kpis[] = $helper->generate();
469
470        $helper = new HelperKpi();
471        $helper->id = 'box-top-category';
472        $helper->icon = 'icon-money';
473        $helper->color = 'color3';
474        $helper->title = $this->l('Top Category', null, null, false);
475        $helper->subtitle = $this->l('30 days', null, null, false);
476        if (ConfigurationKPI::get('TOP_CATEGORY', $this->context->employee->id_lang) !== false) {
477            $helper->value = ConfigurationKPI::get('TOP_CATEGORY', $this->context->employee->id_lang);
478        }
479        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=top_category';
480        $helper->refresh = (bool) (ConfigurationKPI::get('TOP_CATEGORY_EXPIRE', $this->context->employee->id_lang) < $time);
481        $kpis[] = $helper->generate();
482
483        $helper = new HelperKpi();
484        $helper->id = 'box-products-per-category';
485        $helper->icon = 'icon-search';
486        $helper->color = 'color4';
487        $helper->title = $this->l('Average number of products per category', null, null, false);
488        if (ConfigurationKPI::get('PRODUCTS_PER_CATEGORY') !== false) {
489            $helper->value = ConfigurationKPI::get('PRODUCTS_PER_CATEGORY');
490        }
491        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=products_per_category';
492        $helper->refresh = (bool) (ConfigurationKPI::get('PRODUCTS_PER_CATEGORY_EXPIRE') < $time);
493        $kpis[] = $helper->generate();
494
495        $helper = new HelperKpiRow();
496        $helper->kpis = $kpis;
497
498        return $helper->generate();
499    }
500
501    /**
502     * @return string|void
503     *
504     * @since 1.0.0
505     */
506    public function renderForm()
507    {
508        $this->initToolbar();
509
510        /** @var Category $obj */
511        $obj = $this->loadObject(true);
512        $context = $this->context;
513        $idShop = $context->shop->id;
514        $selectedCategories = [(isset($obj->id_parent) && $obj->isParentCategoryAvailable($idShop)) ? (int) $obj->id_parent : (int) Tools::getValue('id_parent', Category::getRootCategory()->id)];
515        $unidentified = new Group(Configuration::get('PS_UNIDENTIFIED_GROUP'));
516        $guest = new Group(Configuration::get('PS_GUEST_GROUP'));
517        $default = new Group(Configuration::get('PS_CUSTOMER_GROUP'));
518
519        $unidentifiedGroupInformation = sprintf($this->l('%s - All people without a valid customer account.'), '<b>'.$unidentified->name[$this->context->language->id].'</b>');
520        $guestGroupInformation = sprintf($this->l('%s - Customer who placed an order with the guest checkout.'), '<b>'.$guest->name[$this->context->language->id].'</b>');
521        $defaultGroupInformation = sprintf($this->l('%s - All people who have created an account on this site.'), '<b>'.$default->name[$this->context->language->id].'</b>');
522
523        if (!($obj = $this->loadObject(true))) {
524            return;
525        }
526
527        $image = _PS_CAT_IMG_DIR_.$obj->id.'.'.$this->imageType;
528        $imageUrl = ImageManager::thumbnail($image, $this->table.'_'.(int) $obj->id.'.'.$this->imageType, 350, $this->imageType, true, true);
529
530        $imageSize = file_exists($image) ? filesize($image) / 1000 : false;
531        $imagesTypes = ImageType::getImagesTypes('categories');
532        $format = [];
533        $thumb = $thumbUrl = '';
534        $formattedCategory = ImageType::getFormatedName('category');
535        $formattedMedium = ImageType::getFormatedName('medium');
536        foreach ($imagesTypes as $k => $imageType) {
537            if ($formattedCategory == $imageType['name']) {
538                $format['category'] = $imageType;
539            } elseif ($formattedMedium == $imageType['name']) {
540                $format['medium'] = $imageType;
541                $thumb = _PS_CAT_IMG_DIR_.$obj->id.'-'.$imageType['name'].'.'.$this->imageType;
542                if (is_file($thumb)) {
543                    $thumbUrl = ImageManager::thumbnail($thumb, $this->table.'_'.(int) $obj->id.'-thumb.'.$this->imageType, (int) $imageType['width'], $this->imageType, true, true);
544                }
545            }
546        }
547
548        if (!is_file($thumb)) {
549            $thumb = $image;
550            $thumbUrl = ImageManager::thumbnail($image, $this->table.'_'.(int) $obj->id.'-thumb.'.$this->imageType, 125, $this->imageType, true, true);
551            ImageManager::resize(_PS_TMP_IMG_DIR_.$this->table.'_'.(int) $obj->id.'-thumb.'.$this->imageType, _PS_TMP_IMG_DIR_.$this->table.'_'.(int) $obj->id.'-thumb.'.$this->imageType, (int) $imageType['width'], (int) $imageType['height']);
552        }
553
554        $thumbSize = file_exists($thumb) ? filesize($thumb) / 1000 : false;
555
556        $this->fields_form = [
557            'tinymce' => true,
558            'legend'  => [
559                'title' => $this->l('Category'),
560                'icon'  => 'icon-tags',
561            ],
562            'input'   => [
563                [
564                    'type'     => 'text',
565                    'label'    => $this->l('Name'),
566                    'name'     => 'name',
567                    'lang'     => true,
568                    'required' => true,
569                    'class'    => 'copy2friendlyUrl',
570                    'hint'     => $this->l('Invalid characters:').' <>;=#{}',
571                ],
572                [
573                    'type'     => 'switch',
574                    'label'    => $this->l('Displayed'),
575                    'name'     => 'active',
576                    'required' => false,
577                    'is_bool'  => true,
578                    'values'   => [
579                        [
580                            'id'    => 'active_on',
581                            'value' => 1,
582                            'label' => $this->l('Enabled'),
583                        ],
584                        [
585                            'id'    => 'active_off',
586                            'value' => 0,
587                            'label' => $this->l('Disabled'),
588                        ],
589                    ],
590                ],
591                [
592                    'type'     => 'switch',
593                    'label'    => $this->l('Display products from subcategories'),
594                    'name'     => 'display_from_sub',
595                    'required' => false,
596                    'is_bool'  => true,
597                    'values'   => [
598                        [
599                            'id'    => 'display_from_sub_on',
600                            'value' => 1,
601                            'label' => $this->l('Yes'),
602                        ],
603                        [
604                            'id'    => 'display_from_sub_off',
605                            'value' => 0,
606                            'label' => $this->l('No'),
607                        ],
608                    ],
609                ],
610                [
611                    'type'  => 'categories',
612                    'label' => $this->l('Parent category'),
613                    'name'  => 'id_parent',
614                    'tree'  => [
615                        'id'                  => 'categories-tree',
616                        'selected_categories' => $selectedCategories,
617                        'disabled_categories' => (!Tools::isSubmit('add'.$this->table) && !Tools::isSubmit('submitAdd'.$this->table)) ? [$this->_category->id] : null,
618                        'root_category'       => $context->shop->getCategory(),
619                    ],
620                ],
621                [
622                    'type'         => 'textarea',
623                    'label'        => $this->l('Description'),
624                    'name'         => 'description',
625                    'autoload_rte' => true,
626                    'lang'         => true,
627                    'hint'         => $this->l('Invalid characters:').' <>;=#{}',
628                ],
629                [
630                    'type'          => 'file',
631                    'label'         => $this->l('Category Cover Image'),
632                    'name'          => 'image',
633                    'display_image' => true,
634                    'image'         => $imageUrl ? $imageUrl : false,
635                    'size'          => $imageSize,
636                    'delete_url'    => static::$currentIndex.'&'.$this->identifier.'='.$this->_category->id.'&token='.$this->token.'&deleteImage=1',
637                    'hint'          => $this->l('This is the main image for your category, displayed in the category page. The category description will overlap this image and appear in its top-left corner.'),
638                    'format'        => $format['category'],
639                ],
640                [
641                    'type'          => 'file',
642                    'label'         => $this->l('Category thumbnail'),
643                    'name'          => 'thumb',
644                    'display_image' => true,
645                    'image'         => $thumbUrl ? $thumbUrl : false,
646                    'size'          => $thumbSize,
647                    'format'        => $format['medium'],
648                ],
649                [
650                    'type'    => 'text',
651                    'label'   => $this->l('Meta title'),
652                    'name'    => 'meta_title',
653                    'maxchar' => 70,
654                    'lang'    => true,
655                    'rows'    => 5,
656                    'cols'    => 100,
657                    'hint'    => $this->l('Forbidden characters:').' <>;=#{}',
658                ],
659                [
660                    'type'    => 'textarea',
661                    'label'   => $this->l('Meta description'),
662                    'name'    => 'meta_description',
663                    'maxchar' => 160,
664                    'lang'    => true,
665                    'rows'    => 5,
666                    'cols'    => 100,
667                    'hint'    => $this->l('Forbidden characters:').' <>;=#{}',
668                ],
669                [
670                    'type'  => 'tags',
671                    'label' => $this->l('Meta keywords'),
672                    'name'  => 'meta_keywords',
673                    'lang'  => true,
674                    'hint'  => $this->l('To add "tags," click in the field, write something, and then press "Enter."').'&nbsp;'.$this->l('Forbidden characters:').' <>;=#{}',
675                ],
676                [
677                    'type'     => 'text',
678                    'label'    => $this->l('Friendly URL'),
679                    'name'     => 'link_rewrite',
680                    'lang'     => true,
681                    'required' => true,
682                    'hint'     => $this->l('Only letters, numbers, underscore (_) and the minus (-) character are allowed.'),
683                ],
684                [
685                    'type'              => 'group',
686                    'label'             => $this->l('Group access'),
687                    'name'              => 'groupBox',
688                    'values'            => Group::getGroups($this->context->language->id, true),
689                    'info_introduction' => $this->l('You now have three default customer groups.'),
690                    'unidentified'      => $unidentifiedGroupInformation,
691                    'guest'             => $guestGroupInformation,
692                    'customer'          => $defaultGroupInformation,
693                    'hint'              => $this->l('Mark all of the customer groups which you would like to have access to this category.'),
694                ],
695            ],
696            'submit'  => [
697                'title' => $this->l('Save'),
698                'name'  => 'submitAdd'.$this->table.($this->_category->is_root_category && !Tools::isSubmit('add'.$this->table) && !Tools::isSubmit('add'.$this->table.'root') ? '' : 'AndBackToParent'),
699            ],
700        ];
701
702        $this->tpl_form_vars['shared_category'] = Validate::isLoadedObject($obj) && $obj->hasMultishopEntries();
703        $this->tpl_form_vars['PS_ALLOW_ACCENTED_CHARS_URL'] = (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL');
704        $this->tpl_form_vars['displayBackOfficeCategory'] = Hook::exec('displayBackOfficeCategory');
705
706        // Display this field only if multistore option is enabled
707        if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && Tools::isSubmit('add'.$this->table.'root')) {
708            $this->fields_form['input'][] = [
709                'type'     => 'switch',
710                'label'    => $this->l('Root Category'),
711                'name'     => 'is_root_category',
712                'required' => false,
713                'is_bool'  => true,
714                'values'   => [
715                    [
716                        'id'    => 'is_root_on',
717                        'value' => 1,
718                        'label' => $this->l('Yes'),
719                    ],
720                    [
721                        'id'    => 'is_root_off',
722                        'value' => 0,
723                        'label' => $this->l('No'),
724                    ],
725                ],
726            ];
727            unset($this->fields_form['input'][2], $this->fields_form['input'][3]);
728        }
729        // Display this field only if multistore option is enabled AND there are several stores configured
730        if (Shop::isFeatureActive()) {
731            $this->fields_form['input'][] = [
732                'type'  => 'shop',
733                'label' => $this->l('Shop association'),
734                'name'  => 'checkBoxShopAsso',
735            ];
736        }
737
738        // remove category tree and radio button "is_root_category" if this category has the root category as parent category to avoid any conflict
739        if ($this->_category->id_parent == (int) Configuration::get('PS_ROOT_CATEGORY') && Tools::isSubmit('updatecategory')) {
740            foreach ($this->fields_form['input'] as $k => $input) {
741                if (in_array($input['name'], ['id_parent', 'is_root_category'])) {
742                    unset($this->fields_form['input'][$k]);
743                }
744            }
745        }
746
747        if (!($obj = $this->loadObject(true))) {
748            return;
749        }
750
751        $image = ImageManager::thumbnail(_PS_CAT_IMG_DIR_.'/'.$obj->id.'.'.$this->imageType, $this->table.'_'.(int) $obj->id.'.'.$this->imageType, 350, $this->imageType, true);
752
753        $this->fields_value = [
754            'image' => $image ? $image : false,
755            'size'  => $image ? filesize(_PS_CAT_IMG_DIR_.'/'.$obj->id.'.'.$this->imageType) / 1000 : false,
756        ];
757
758        // Added values of object Group
759        $categoryGroupsIds = $obj->getGroups();
760
761        $groups = Group::getGroups($this->context->language->id);
762        // if empty $carrier_groups_ids : object creation : we set the default groups
763        if (empty($categoryGroupsIds)) {
764            $preselected = [Configuration::get('PS_UNIDENTIFIED_GROUP'), Configuration::get('PS_GUEST_GROUP'), Configuration::get('PS_CUSTOMER_GROUP')];
765            $categoryGroupsIds = array_merge($categoryGroupsIds, $preselected);
766        }
767        foreach ($groups as $group) {
768            $this->fields_value['groupBox_'.$group['id_group']] = Tools::getValue('groupBox_'.$group['id_group'], (in_array($group['id_group'], $categoryGroupsIds)));
769        }
770
771        $this->fields_value['is_root_category'] = (bool) Tools::isSubmit('add'.$this->table.'root');
772
773        return parent::renderForm();
774    }
775
776    /**
777     * @return bool
778     *
779     * @since 1.0.0
780     */
781    public function postProcess()
782    {
783        if (!in_array($this->display, ['edit', 'add'])) {
784            $this->multishop_context_group = false;
785        }
786        if (Tools::isSubmit('forcedeleteImage') || (isset($_FILES['image']) && $_FILES['image']['size'] > 0) || Tools::getValue('deleteImage')) {
787            $this->processForceDeleteImage();
788            $this->processForceDeleteThumb();
789            if (Tools::isSubmit('forcedeleteImage')) {
790                Tools::redirectAdmin(static::$currentIndex.'&token='.Tools::getAdminTokenLite('AdminCategories').'&conf=7');
791            }
792        }
793
794        return parent::postProcess();
795    }
796
797    /**
798     * @since 1.0.0
799     */
800    public function processForceDeleteImage()
801    {
802        $category = $this->loadObject(true);
803        if (Validate::isLoadedObject($category)) {
804            $category->deleteImage(true);
805        }
806    }
807
808    /**
809     * @return bool
810     *
811     * @since 1.0.0
812     */
813    public function processForceDeleteThumb()
814    {
815        $category = $this->loadObject(true);
816
817        if (Validate::isLoadedObject($category)) {
818            if (file_exists(_PS_TMP_IMG_DIR_.$this->table.'_'.$category->id.'-thumb.'.$this->imageType)
819                && !unlink(_PS_TMP_IMG_DIR_.$this->table.'_'.$category->id.'-thumb.'.$this->imageType)
820            ) {
821                return false;
822            }
823            if (file_exists(_PS_CAT_IMG_DIR_.$category->id.'_thumb.'.$this->imageType)
824                && !unlink(_PS_CAT_IMG_DIR_.$category->id.'_thumb.'.$this->imageType)
825            ) {
826                return false;
827            }
828
829            $imagesTypes = ImageType::getImagesTypes('categories');
830            $formattedMedium = ImageType::getFormatedName('medium');
831            foreach ($imagesTypes as $k => $imageType) {
832                if ($formattedMedium == $imageType['name'] &&
833                    file_exists(_PS_CAT_IMG_DIR_.$category->id.'-'.$imageType['name'].'.'.$this->imageType) &&
834                    !unlink(_PS_CAT_IMG_DIR_.$category->id.'-'.$imageType['name'].'.'.$this->imageType)
835                ) {
836                    return false;
837                }
838            }
839        }
840
841        return true;
842    }
843
844    /**
845     * @return false|ObjectModel
846     *
847     * @since 1.0.0
848     */
849    public function processAdd()
850    {
851        $idCategory = (int) Tools::getValue('id_category');
852        $idParent = (int) Tools::getValue('id_parent');
853
854        // if true, we are in a root category creation
855        if (!$idParent) {
856            $_POST['is_root_category'] = $_POST['level_depth'] = 1;
857            $_POST['id_parent'] = $idParent = (int) Configuration::get('PS_ROOT_CATEGORY');
858        }
859
860        if ($idCategory) {
861            if ($idCategory != $idParent) {
862                if (!Category::checkBeforeMove($idCategory, $idParent)) {
863                    $this->errors[] = Tools::displayError('The category cannot be moved here.');
864                }
865            } else {
866                $this->errors[] = Tools::displayError('The category cannot be a parent of itself.');
867            }
868        }
869        $object = parent::processAdd();
870
871        //if we create a you root category you have to associate to a shop before to add sub categories in. So we redirect to AdminCategories listing
872        if ($object && Tools::getValue('is_root_category')) {
873            Tools::redirectAdmin(static::$currentIndex.'&id_category='.(int) Configuration::get('PS_ROOT_CATEGORY').'&token='.Tools::getAdminTokenLite('AdminCategories').'&conf=3');
874        }
875
876        return $object;
877    }
878
879    /**
880     * @return bool
881     *
882     * @since 1.0.0
883     */
884    public function processDelete()
885    {
886        if ($this->tabAccess['delete'] === '1') {
887            /** @var Category $category */
888            $category = $this->loadObject();
889            if ($category->isRootCategoryForAShop()) {
890                $this->errors[] = Tools::displayError('You cannot remove this category because one of your shops uses it as a root category.');
891            } elseif (parent::processDelete()) {
892                $this->setDeleteMode();
893                $this->processFatherlessProducts((int) $category->id_parent);
894
895                return true;
896            }
897        } else {
898            $this->errors[] = Tools::displayError('You do not have permission to delete this.');
899        }
900
901        return false;
902    }
903
904    /**
905     * @since 1.0.0
906     */
907    public function processPosition()
908    {
909        if ($this->tabAccess['edit'] !== '1') {
910            $this->errors[] = Tools::displayError('You do not have permission to edit this.');
911        } elseif (!Validate::isLoadedObject($object = new Category((int) Tools::getValue($this->identifier, Tools::getValue('id_category_to_move', 1))))) {
912            $this->errors[] = Tools::displayError('An error occurred while updating the status for an object.').' <b>'.$this->table.'</b> '.Tools::displayError('(cannot load object)');
913        }
914        if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) {
915            $this->errors[] = Tools::displayError('Failed to update the position.');
916        } else {
917            $object->regenerateEntireNtree();
918            Tools::redirectAdmin(static::$currentIndex.'&'.$this->table.'Orderby=position&'.$this->table.'Orderway=asc&conf=5'.(($id_category = (int) Tools::getValue($this->identifier, Tools::getValue('id_category_parent', 1))) ? ('&'.$this->identifier.'='.$id_category) : '').'&token='.Tools::getAdminTokenLite('AdminCategories'));
919        }
920    }
921
922    /**
923     * @since 1.0.0
924     */
925    public function ajaxProcessUpdatePositions()
926    {
927        $idCategoryToMove = (int) Tools::getValue('id_category_to_move');
928        $idCategoryParent = (int) Tools::getValue('id_category_parent');
929        $way = (int) Tools::getValue('way');
930        $positions = Tools::getValue('category');
931        $foundFirst = (bool) Tools::getValue('found_first');
932        if (is_array($positions)) {
933            foreach ($positions as $key => $value) {
934                $pos = explode('_', $value);
935                if ((isset($pos[1]) && isset($pos[2])) && ($pos[1] == $idCategoryParent && $pos[2] == $idCategoryToMove)) {
936                    $position = $key;
937                    break;
938                }
939            }
940        }
941
942        $category = new Category($idCategoryToMove);
943        if (Validate::isLoadedObject($category)) {
944            if (isset($position) && $category->updatePosition($way, $position)) {
945                /* Position '0' was not found in given positions so try to reorder parent category*/
946                if (!$foundFirst) {
947                    $category->cleanPositions((int) $category->id_parent);
948                }
949
950                die(true);
951            } else {
952                die('{"hasError" : true, errors : "Cannot update categories position"}');
953            }
954        } else {
955            die('{"hasError" : true, "errors" : "This category cannot be loaded"}');
956        }
957    }
958
959    /**
960     * @since 1.0.0
961     */
962    public function ajaxProcessStatusCategory()
963    {
964        if (!$idCategory = (int) Tools::getValue('id_category')) {
965            $this->ajaxDie(json_encode(['success' => false, 'error' => true, 'text' => $this->l('Failed to update the status')]));
966        } else {
967            $category = new Category((int) $idCategory);
968            if (Validate::isLoadedObject($category)) {
969                $category->active = $category->active == 1 ? 0 : 1;
970                $category->save() ?
971                    $this->ajaxDie(json_encode(['success' => true, 'text' => $this->l('The status has been updated successfully')])) :
972                    $this->ajaxDie(json_encode(['success' => false, 'error' => true, 'text' => $this->l('Failed to update the status')]));
973            }
974        }
975    }
976
977    /**
978     * @return bool
979     *
980     * @since 1.0.0
981     */
982    protected function processBulkDelete()
983    {
984        if ($this->tabAccess['delete'] === '1') {
985            $catsIds = [];
986            foreach (Tools::getValue($this->table.'Box') as $idCategory) {
987                $category = new Category((int) $idCategory);
988                if (!$category->isRootCategoryForAShop()) {
989                    $catsIds[$category->id] = $category->id_parent;
990                }
991            }
992
993            if (parent::processBulkDelete()) {
994                $this->setDeleteMode();
995                foreach ($catsIds as $id => $idParent) {
996                    $this->processFatherlessProducts((int) $idParent);
997                }
998
999                return true;
1000            } else {
1001                return false;
1002            }
1003        } else {
1004            $this->errors[] = Tools::displayError('You do not have permission to delete this.');
1005        }
1006    }
1007
1008    /**
1009     * @since 1.0.0
1010     */
1011    protected function setDeleteMode()
1012    {
1013        if ($this->delete_mode == 'link' || $this->delete_mode == 'linkanddisable') {
1014            $this->remove_products = false;
1015            if ($this->delete_mode == 'linkanddisable') {
1016                $this->disable_products = true;
1017            }
1018        } elseif ($this->delete_mode != 'delete') {
1019            $this->errors[] = Tools::displayError('Unknown delete mode:'.' '.$this->deleted);
1020        }
1021    }
1022
1023    /**
1024     * @param int $idParent
1025     *
1026     * @since 1.0.0
1027     */
1028    public function processFatherlessProducts($idParent)
1029    {
1030        /* Delete or link products which were not in others categories */
1031        $fatherlessProducts = Db::getInstance()->executeS(
1032            '
1033			SELECT p.`id_product` FROM `'._DB_PREFIX_.'product` p
1034			'.Shop::addSqlAssociation('product', 'p').'
1035			WHERE NOT EXISTS (SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp WHERE cp.`id_product` = p.`id_product`)'
1036        );
1037
1038        foreach ($fatherlessProducts as $idPoorProduct) {
1039            $poorProduct = new Product((int) $idPoorProduct['id_product']);
1040            if (Validate::isLoadedObject($poorProduct)) {
1041                if ($this->remove_products || $idParent == 0) {
1042                    $poorProduct->delete();
1043                } else {
1044                    if ($this->disable_products) {
1045                        $poorProduct->active = 0;
1046                    }
1047                    $poorProduct->id_category_default = (int) $idParent;
1048                    $poorProduct->addToCategories((int) $idParent);
1049                    $poorProduct->save();
1050                }
1051            }
1052        }
1053    }
1054
1055    /**
1056     * @param int $id Category ID
1057     *
1058     * @return bool
1059     *
1060     * @since 1.0.0
1061     * @throws PrestaShopException
1062     */
1063    protected function postImage($id)
1064    {
1065        $ret = parent::postImage($id);
1066        if (($idCategory = (int) Tools::getValue('id_category')) && isset($_FILES) && count($_FILES)) {
1067            $name = 'image';
1068            if ($_FILES[$name]['name'] != null && file_exists(_PS_CAT_IMG_DIR_.$idCategory.'.'.$this->imageType)) {
1069                try {
1070                    $imagesTypes = ImageType::getImagesTypes('categories');
1071                } catch (PrestaShopException $e) {
1072                    Logger::addLog("Error while generating category image: {$e->getMessage()}");
1073
1074                    return false;
1075                }
1076                foreach ($imagesTypes as $k => $imageType) {
1077                    $success = ImageManager::resize(
1078                        _PS_CAT_IMG_DIR_.$idCategory.'.'.$this->imageType,
1079                        _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'.'.$this->imageType,
1080                        (int) $imageType['width'],
1081                        (int) $imageType['height']
1082                    );
1083                    if (ImageManager::webpSupport()) {
1084                        $success &= ImageManager::resize(
1085                            _PS_CAT_IMG_DIR_.$idCategory.'.'.$this->imageType,
1086                            _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'.webp',
1087                            (int) $imageType['width'],
1088                            (int) $imageType['height'],
1089                            'webp'
1090                        );
1091                    }
1092                    if (ImageManager::retinaSupport()) {
1093                        $success &= ImageManager::resize(
1094                            _PS_CAT_IMG_DIR_.$idCategory.'.'.$this->imageType,
1095                            _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'2x.'.$this->imageType,
1096                            (int) $imageType['width'] * 2,
1097                            (int) $imageType['height'] * 2
1098                        );
1099                        if (ImageManager::webpSupport()) {
1100                            $success &= ImageManager::resize(
1101                                _PS_CAT_IMG_DIR_.$idCategory.'.'.$this->imageType,
1102                                _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'2x.webp',
1103                                (int) $imageType['width'] * 2,
1104                                (int) $imageType['height'] * 2,
1105                                'webp'
1106                            );
1107                        }
1108                    }
1109
1110                    if (!$success) {
1111                        $this->errors = Tools::displayError('An error occurred while uploading category image.');
1112                    } else {
1113                        if (Configuration::get('TB_IMAGE_LAST_UPD_CATEGORIES') < $idCategory) {
1114                            Configuration::updateValue('TB_IMAGE_LAST_UPD_CATEGORIES', $idCategory);
1115                        }
1116                    }
1117                }
1118            }
1119
1120            $name = 'thumb';
1121            if ($_FILES[$name]['name'] != null) {
1122                if (!isset($imagesTypes)) {
1123                    try {
1124                        $imagesTypes = ImageType::getImagesTypes('categories');
1125                    } catch (PrestaShopException $e) {
1126                        Logger::addLog("Error while generating category image: {$e->getMessage()}");
1127
1128                        return false;
1129                    }
1130                }
1131                try {
1132                    $formattedMedium = ImageType::getFormatedName('medium');
1133                } catch (PrestaShopException $e) {
1134                    Logger::addLog("Error while generating category image: {$e->getMessage()}");
1135
1136                    return false;
1137                }
1138                foreach ($imagesTypes as $k => $imageType) {
1139                    if ($formattedMedium == $imageType['name']) {
1140                        if ($error = ImageManager::validateUpload($_FILES[$name], Tools::getMaxUploadSize())) {
1141                            $this->errors[] = $error;
1142                        } elseif (!($tmpName = tempnam(_PS_TMP_IMG_DIR_, 'PS')) || !move_uploaded_file($_FILES[$name]['tmp_name'], $tmpName)) {
1143                            $ret = false;
1144                        } else {
1145                            $success = ImageManager::resize(
1146                                $tmpName,
1147                                _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'.'.$this->imageType,
1148                                (int) $imageType['width'],
1149                                (int) $imageType['height']
1150                            );
1151
1152                            if (ImageManager::webpSupport()) {
1153                                ImageManager::resize(
1154                                    $tmpName,
1155                                    _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'.webp',
1156                                    (int) $imageType['width'],
1157                                    (int) $imageType['height'],
1158                                    'webp'
1159                                );
1160                            }
1161                            if (ImageManager::retinaSupport()) {
1162                                ImageManager::resize(
1163                                    $tmpName,
1164                                    _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'2x.'.$this->imageType,
1165                                    (int) $imageType['width'] * 2,
1166                                    (int) $imageType['height'] * 2
1167                                );
1168
1169                                if (ImageManager::webpSupport()) {
1170                                    ImageManager::resize(
1171                                        $tmpName,
1172                                        _PS_CAT_IMG_DIR_.$idCategory.'-'.stripslashes($imageType['name']).'2x.webp',
1173                                        (int) $imageType['width'] * 2,
1174                                        (int) $imageType['height'] * 2,
1175                                        'webp'
1176                                    );
1177                                }
1178                            }
1179
1180                            if (Configuration::get('TB_IMAGE_LAST_UPD_CATEGORIES') < $idCategory) {
1181                                Configuration::updateValue('TB_IMAGE_LAST_UPD_CATEGORIES', $idCategory);
1182                            }
1183
1184                            if (!$success)  {
1185                                $this->errors = Tools::displayError('An error occurred while uploading thumbnail image.');
1186                            }
1187
1188                            if (count($this->errors)) {
1189                                $ret = false;
1190                            } else {
1191                                $ret = true;
1192                            }
1193                            unlink($tmpName);
1194                        }
1195                    }
1196                }
1197            }
1198        }
1199
1200        return $ret;
1201    }
1202}
1203