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."').' '.$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