From edab0cabb1fccfd55529ce08f87f44f3dac76558 Mon Sep 17 00:00:00 2001 From: Roman Pyrih Date: Fri, 20 Jun 2025 15:49:56 +0200 Subject: [PATCH] Download controllers --- controllers/.htaccess | 10 + controllers/admin/AdminAccessController.php | 235 + .../admin/AdminAddressesController.php | 571 ++ .../admin/AdminAttachmentsController.php | 264 + .../AdminAttributeGeneratorController.php | 273 + .../admin/AdminAttributesGroupsController.php | 974 ++++ .../admin/AdminCarrierWizardController.php | 960 ++++ controllers/admin/AdminCarriersController.php | 737 +++ .../admin/AdminCartRulesController.php | 761 +++ controllers/admin/AdminCartsController.php | 962 ++++ .../admin/AdminCmsCategoriesController.php | 320 ++ .../admin/AdminCmsContentController.php | 314 ++ controllers/admin/AdminCmsController.php | 468 ++ .../admin/AdminCountriesController.php | 527 ++ .../admin/AdminCustomerThreadsController.php | 1205 ++++ .../admin/AdminDashboardController.php | 514 ++ controllers/admin/AdminFeaturesController.php | 656 +++ controllers/admin/AdminGendersController.php | 232 + controllers/admin/AdminGroupsController.php | 656 +++ controllers/admin/AdminImagesController.php | 755 +++ controllers/admin/AdminImportController.php | 4954 +++++++++++++++++ .../admin/AdminLegacyLayoutController.php | 195 + controllers/admin/AdminLoginController.php | 475 ++ .../admin/AdminManufacturersController.php | 878 +++ controllers/admin/AdminModulesController.php | 1561 ++++++ .../admin/AdminModulesPositionsController.php | 644 +++ controllers/admin/AdminNotFoundController.php | 51 + .../admin/AdminOrderMessageController.php | 112 + controllers/admin/AdminOrdersController.php | 3129 +++++++++++ .../admin/AdminOutstandingController.php | 181 + controllers/admin/AdminPatternsController.php | 687 +++ controllers/admin/AdminPdfController.php | 211 + .../admin/AdminPreferencesController.php | 246 + controllers/admin/AdminProductsController.php | 3417 ++++++++++++ .../admin/AdminQuickAccessesController.php | 241 + .../admin/AdminReferrersController.php | 525 ++ .../admin/AdminRequestSqlController.php | 555 ++ controllers/admin/AdminReturnController.php | 296 + .../admin/AdminSearchConfController.php | 492 ++ controllers/admin/AdminSearchController.php | 525 ++ .../admin/AdminSearchEnginesController.php | 102 + controllers/admin/AdminShopController.php | 864 +++ .../admin/AdminShopGroupController.php | 364 ++ controllers/admin/AdminShopUrlController.php | 574 ++ controllers/admin/AdminSlipController.php | 202 + .../AdminSpecificPriceRuleController.php | 384 ++ controllers/admin/AdminStatesController.php | 299 + controllers/admin/AdminStatsController.php | 1115 ++++ controllers/admin/AdminStatsTabController.php | 306 + controllers/admin/AdminStatusesController.php | 734 +++ controllers/admin/AdminStoresController.php | 604 ++ .../admin/AdminSuppliersController.php | 592 ++ controllers/admin/AdminTabsController.php | 384 ++ controllers/admin/AdminTagsController.php | 168 + .../admin/AdminTaxRulesGroupController.php | 578 ++ controllers/admin/AdminTrackingController.php | 482 ++ .../admin/AdminTranslationsController.php | 3320 +++++++++++ controllers/admin/AdminZonesController.php | 160 + controllers/admin/BoOrder.php | 35 + controllers/admin/index.php | 34 + controllers/front/AddressController.php | 165 + controllers/front/AddressesController.php | 76 + controllers/front/AttachmentController.php | 81 + controllers/front/AuthController.php | 128 + controllers/front/CartController.php | 682 +++ .../front/ChangeCurrencyController.php | 44 + controllers/front/CmsController.php | 218 + controllers/front/ContactController.php | 54 + controllers/front/DiscountController.php | 212 + controllers/front/GetFileController.php | 349 ++ controllers/front/GuestTrackingController.php | 166 + controllers/front/HistoryController.php | 118 + controllers/front/IdentityController.php | 92 + controllers/front/IndexController.php | 43 + controllers/front/MyAccountController.php | 56 + .../front/OrderConfirmationController.php | 166 + controllers/front/OrderController.php | 359 ++ controllers/front/OrderDetailController.php | 225 + controllers/front/OrderFollowController.php | 145 + controllers/front/OrderReturnController.php | 183 + controllers/front/OrderSlipController.php | 90 + controllers/front/PageNotFoundController.php | 63 + controllers/front/PasswordController.php | 291 + controllers/front/PdfInvoiceController.php | 75 + .../front/PdfOrderReturnController.php | 58 + controllers/front/PdfOrderSlipController.php | 56 + controllers/front/ProductController.php | 1359 +++++ controllers/front/SitemapController.php | 189 + controllers/front/StatisticsController.php | 93 + controllers/front/StoresController.php | 236 + controllers/front/index.php | 34 + .../front/listing/BestSalesController.php | 93 + .../front/listing/CategoryController.php | 275 + .../front/listing/ManufacturerController.php | 217 + .../front/listing/NewProductsController.php | 81 + .../front/listing/PricesDropController.php | 81 + .../front/listing/SearchController.php | 118 + .../front/listing/SupplierController.php | 221 + controllers/front/listing/index.php | 34 + controllers/index.php | 34 + 100 files changed, 49330 insertions(+) create mode 100644 controllers/.htaccess create mode 100644 controllers/admin/AdminAccessController.php create mode 100644 controllers/admin/AdminAddressesController.php create mode 100644 controllers/admin/AdminAttachmentsController.php create mode 100644 controllers/admin/AdminAttributeGeneratorController.php create mode 100644 controllers/admin/AdminAttributesGroupsController.php create mode 100644 controllers/admin/AdminCarrierWizardController.php create mode 100644 controllers/admin/AdminCarriersController.php create mode 100644 controllers/admin/AdminCartRulesController.php create mode 100644 controllers/admin/AdminCartsController.php create mode 100644 controllers/admin/AdminCmsCategoriesController.php create mode 100644 controllers/admin/AdminCmsContentController.php create mode 100644 controllers/admin/AdminCmsController.php create mode 100644 controllers/admin/AdminCountriesController.php create mode 100644 controllers/admin/AdminCustomerThreadsController.php create mode 100644 controllers/admin/AdminDashboardController.php create mode 100644 controllers/admin/AdminFeaturesController.php create mode 100644 controllers/admin/AdminGendersController.php create mode 100644 controllers/admin/AdminGroupsController.php create mode 100644 controllers/admin/AdminImagesController.php create mode 100644 controllers/admin/AdminImportController.php create mode 100644 controllers/admin/AdminLegacyLayoutController.php create mode 100644 controllers/admin/AdminLoginController.php create mode 100644 controllers/admin/AdminManufacturersController.php create mode 100644 controllers/admin/AdminModulesController.php create mode 100644 controllers/admin/AdminModulesPositionsController.php create mode 100644 controllers/admin/AdminNotFoundController.php create mode 100644 controllers/admin/AdminOrderMessageController.php create mode 100644 controllers/admin/AdminOrdersController.php create mode 100644 controllers/admin/AdminOutstandingController.php create mode 100644 controllers/admin/AdminPatternsController.php create mode 100644 controllers/admin/AdminPdfController.php create mode 100644 controllers/admin/AdminPreferencesController.php create mode 100644 controllers/admin/AdminProductsController.php create mode 100644 controllers/admin/AdminQuickAccessesController.php create mode 100644 controllers/admin/AdminReferrersController.php create mode 100644 controllers/admin/AdminRequestSqlController.php create mode 100644 controllers/admin/AdminReturnController.php create mode 100644 controllers/admin/AdminSearchConfController.php create mode 100644 controllers/admin/AdminSearchController.php create mode 100644 controllers/admin/AdminSearchEnginesController.php create mode 100644 controllers/admin/AdminShopController.php create mode 100644 controllers/admin/AdminShopGroupController.php create mode 100644 controllers/admin/AdminShopUrlController.php create mode 100644 controllers/admin/AdminSlipController.php create mode 100644 controllers/admin/AdminSpecificPriceRuleController.php create mode 100644 controllers/admin/AdminStatesController.php create mode 100644 controllers/admin/AdminStatsController.php create mode 100644 controllers/admin/AdminStatsTabController.php create mode 100644 controllers/admin/AdminStatusesController.php create mode 100644 controllers/admin/AdminStoresController.php create mode 100644 controllers/admin/AdminSuppliersController.php create mode 100644 controllers/admin/AdminTabsController.php create mode 100644 controllers/admin/AdminTagsController.php create mode 100644 controllers/admin/AdminTaxRulesGroupController.php create mode 100644 controllers/admin/AdminTrackingController.php create mode 100644 controllers/admin/AdminTranslationsController.php create mode 100644 controllers/admin/AdminZonesController.php create mode 100644 controllers/admin/BoOrder.php create mode 100644 controllers/admin/index.php create mode 100644 controllers/front/AddressController.php create mode 100644 controllers/front/AddressesController.php create mode 100644 controllers/front/AttachmentController.php create mode 100644 controllers/front/AuthController.php create mode 100644 controllers/front/CartController.php create mode 100644 controllers/front/ChangeCurrencyController.php create mode 100644 controllers/front/CmsController.php create mode 100644 controllers/front/ContactController.php create mode 100644 controllers/front/DiscountController.php create mode 100644 controllers/front/GetFileController.php create mode 100644 controllers/front/GuestTrackingController.php create mode 100644 controllers/front/HistoryController.php create mode 100644 controllers/front/IdentityController.php create mode 100644 controllers/front/IndexController.php create mode 100644 controllers/front/MyAccountController.php create mode 100644 controllers/front/OrderConfirmationController.php create mode 100644 controllers/front/OrderController.php create mode 100644 controllers/front/OrderDetailController.php create mode 100644 controllers/front/OrderFollowController.php create mode 100644 controllers/front/OrderReturnController.php create mode 100644 controllers/front/OrderSlipController.php create mode 100644 controllers/front/PageNotFoundController.php create mode 100644 controllers/front/PasswordController.php create mode 100644 controllers/front/PdfInvoiceController.php create mode 100644 controllers/front/PdfOrderReturnController.php create mode 100644 controllers/front/PdfOrderSlipController.php create mode 100644 controllers/front/ProductController.php create mode 100644 controllers/front/SitemapController.php create mode 100644 controllers/front/StatisticsController.php create mode 100644 controllers/front/StoresController.php create mode 100644 controllers/front/index.php create mode 100644 controllers/front/listing/BestSalesController.php create mode 100644 controllers/front/listing/CategoryController.php create mode 100644 controllers/front/listing/ManufacturerController.php create mode 100644 controllers/front/listing/NewProductsController.php create mode 100644 controllers/front/listing/PricesDropController.php create mode 100644 controllers/front/listing/SearchController.php create mode 100644 controllers/front/listing/SupplierController.php create mode 100644 controllers/front/listing/index.php create mode 100644 controllers/index.php diff --git a/controllers/.htaccess b/controllers/.htaccess new file mode 100644 index 00000000..3de9e400 --- /dev/null +++ b/controllers/.htaccess @@ -0,0 +1,10 @@ +# Apache 2.2 + + Order deny,allow + Deny from all + + +# Apache 2.4 + + Require all denied + diff --git a/controllers/admin/AdminAccessController.php b/controllers/admin/AdminAccessController.php new file mode 100644 index 00000000..3bb1226b --- /dev/null +++ b/controllers/admin/AdminAccessController.php @@ -0,0 +1,235 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Profile $object + */ +class AdminAccessControllerCore extends AdminController +{ + /** @var array : Black list of id_tab that do not have access */ + public $accesses_black_list = []; + + public function __construct() + { + $this->bootstrap = true; + $this->show_toolbar = false; + $this->table = 'access'; + $this->className = 'Profile'; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->lang = false; + $this->context = Context::getContext(); + + // Blacklist AdminLogin + $this->accesses_black_list[] = Tab::getIdFromClassName('AdminLogin'); + + parent::__construct(); + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $current_profile = (int) $this->getCurrentProfileId(); + $profiles = Profile::getProfiles($this->context->language->id); + $tabs = Tab::getTabs($this->context->language->id); + + $accesses = []; + foreach ($profiles as $profile) { + $accesses[$profile['id_profile']] = Profile::getProfileAccesses($profile['id_profile']); + } + + // Deleted id_tab that do not have access + foreach ($tabs as $key => $tab) { + // Don't allow permissions for unnamed tabs (ie. AdminLogin) + if (empty($tab['name'])) { + unset($tabs[$key]); + } + + foreach ($this->accesses_black_list as $id_tab) { + if ($tab['id_tab'] == (int) $id_tab) { + unset($tabs[$key]); + } + } + } + + $modules = []; + foreach ($profiles as $profile) { + $modules[$profile['id_profile']] = Module::getModulesAccessesByIdProfile($profile['id_profile']); + uasort($modules[$profile['id_profile']], [$this, 'sortModuleByName']); + } + + $this->fields_form = ['']; + $this->tpl_form_vars = [ + 'profiles' => $profiles, + 'accesses' => $accesses, + 'id_tab_parentmodule' => (int) Tab::getIdFromClassName('AdminParentModules'), + 'id_tab_module' => (int) Tab::getIdFromClassName('AdminModules'), + 'tabs' => $this->displayTabs($tabs), + 'current_profile' => (int) $current_profile, + 'admin_profile' => (int) _PS_ADMIN_PROFILE_, + 'access_edit' => $this->access('edit'), + 'perms' => ['view', 'add', 'edit', 'delete'], + 'id_perms' => ['view' => 0, 'add' => 1, 'edit' => 2, 'delete' => 3, 'all' => 4], + 'modules' => $modules, + 'link' => $this->context->link, + 'employee_profile_id' => (int) $this->context->employee->id_profile, + ]; + + return parent::renderForm(); + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + $this->display = 'edit'; + + if (!$this->loadObject(true)) { + return; + } + + $this->content .= $this->renderForm(); + + $this->context->smarty->assign([ + 'content' => $this->content, + ]); + } + + public function initToolbarTitle() + { + $this->toolbar_title = array_unique($this->breadcrumbs); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + unset($this->page_header_toolbar_btn['cancel']); + } + + public function ajaxProcessUpdateAccess() + { + if (_PS_MODE_DEMO_) { + throw new PrestaShopException($this->trans('This functionality has been disabled.', [], 'Admin.Notifications.Error')); + } + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('submitAddAccess')) { + $access = new Access(); + $perm = Tools::getValue('perm'); + if (!in_array($perm, ['view', 'add', 'edit', 'delete', 'all'])) { + throw new PrestaShopException('permission does not exist'); + } + + $enabled = (int) Tools::getValue('enabled'); + $id_tab = (int) Tools::getValue('id_tab'); + $id_profile = (int) Tools::getValue('id_profile'); + $addFromParent = (int) Tools::getValue('addFromParent'); + + die($access->updateLgcAccess((int) $id_profile, $id_tab, $perm, $enabled, $addFromParent)); + } + } + + public function ajaxProcessUpdateModuleAccess() + { + if (_PS_MODE_DEMO_) { + throw new PrestaShopException($this->trans('This functionality has been disabled.', [], 'Admin.Notifications.Error')); + } + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('changeModuleAccess')) { + $access = new Access(); + $perm = Tools::getValue('perm'); + $enabled = (int) Tools::getValue('enabled'); + $id_module = (int) Tools::getValue('id_module'); + $id_profile = (int) Tools::getValue('id_profile'); + + if (!in_array($perm, ['view', 'configure', 'uninstall'])) { + throw new PrestaShopException('permission does not exist'); + } + + die($access->updateLgcModuleAccess((int) $id_profile, $id_module, $perm, $enabled)); + } + } + + /** + * Get the current profile id. + * + * @return int the $_GET['profile'] if valid, else 1 (the first profile id) + */ + public function getCurrentProfileId() + { + return (isset($_GET['id_profile']) && !empty($_GET['id_profile']) && is_numeric($_GET['id_profile'])) ? (int) $_GET['id_profile'] : 1; + } + + /** + * @param array $a module data + * @param array $b module data + * + * @return int + */ + protected function sortModuleByName($a, $b) + { + $moduleAName = isset($a['name']) ? $a['name'] : null; + $moduleBName = isset($b['name']) ? $b['name'] : null; + + return strnatcmp($moduleAName, $moduleBName); + } + + /** + * return human readable Tabs hierarchy for display. + */ + protected function displayTabs(array $tabs) + { + $tabsTree = $this->getChildrenTab($tabs); + + return $tabsTree; + } + + protected function getChildrenTab(array &$tabs, $id_parent = 0) + { + $children = []; + foreach ($tabs as &$tab) { + $id = $tab['id_tab']; + + if ($tab['id_parent'] == $id_parent) { + $children[$id] = $tab; + $children[$id]['children'] = $this->getChildrenTab($tabs, $id); + } + } + + return $children; + } +} diff --git a/controllers/admin/AdminAddressesController.php b/controllers/admin/AdminAddressesController.php new file mode 100644 index 00000000..28872bca --- /dev/null +++ b/controllers/admin/AdminAddressesController.php @@ -0,0 +1,571 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Address $object + */ +class AdminAddressesControllerCore extends AdminController +{ + /** @var array countries list */ + protected $countries_array = array(); + + public function __construct() + { + $this->bootstrap = true; + $this->required_database = true; + $this->required_fields = array('company', 'address2', 'postcode', 'other', 'phone', 'phone_mobile', 'vat_number', 'dni'); + $this->table = 'address'; + $this->className = 'CustomerAddress'; + $this->lang = false; + $this->addressType = 'customer'; + $this->explicitSelect = true; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + ), + ); + + $this->allow_export = true; + + if (!Tools::getValue('realedit')) { + $this->deleted = true; + } + + $countries = Country::getCountries($this->context->language->id); + foreach ($countries as $country) { + $this->countries_array[$country['id_country']] = $country['name']; + } + + $this->fields_list = array( + 'id_address' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'firstname' => array( + 'title' => $this->trans('First Name', array(), 'Admin.Global'), + 'filter_key' => 'a!firstname', + 'maxlength' => 30, + ), + 'lastname' => array( + 'title' => $this->trans('Last Name', array(), 'Admin.Global'), + 'filter_key' => 'a!lastname', + 'maxlength' => 30, + ), + 'address1' => array( + 'title' => $this->trans('Address', array(), 'Admin.Global'), + ), + 'postcode' => array( + 'title' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'align' => 'right', + ), + 'city' => array( + 'title' => $this->trans('City', array(), 'Admin.Global'), + ), + 'country' => array( + 'title' => $this->trans('Country', array(), 'Admin.Global'), + 'type' => 'select', + 'list' => $this->countries_array, + 'filter_key' => 'cl!id_country', + ), + ); + + $this->_select = 'cl.`name` as country'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (cl.`id_country` = a.`id_country` AND cl.`id_lang` = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON a.id_customer = c.id_customer + '; + $this->_where = 'AND a.id_customer != 0 ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER, 'c'); + $this->_use_found_rows = false; + } + + public function initToolbar() + { + parent::initToolbar(); + + if (!$this->display && $this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true, array(), array('import_type' => 'addresses')), + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_address'] = array( + 'href' => $this->context->link->getAdminLink('AdminAddresses', true, array(), array('addaddress' => 1)), + 'desc' => $this->trans('Add new address', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Addresses', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'icon-envelope-alt', + ), + 'input' => array( + array( + 'type' => 'text_customer', + 'label' => $this->trans('Customer', array(), 'Admin.Global'), + 'name' => 'id_customer', + 'required' => false, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Identification number', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'dni', + 'required' => false, + 'col' => '4', + 'hint' => $this->trans('The national ID card number of this person, or a unique tax identification number.', array(), 'Admin.Orderscustomers.Feature'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address alias', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'alias', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Other', array(), 'Admin.Global'), + 'name' => 'other', + 'required' => false, + 'cols' => 15, + 'rows' => 3, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'hidden', + 'name' => 'id_order', + ), + array( + 'type' => 'hidden', + 'name' => 'address_type', + ), + array( + 'type' => 'hidden', + 'name' => 'back', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + $this->fields_value['address_type'] = (int) Tools::getValue('address_type', 1); + + $id_customer = (int) Tools::getValue('id_customer'); + if (!$id_customer && Validate::isLoadedObject($this->object)) { + $id_customer = $this->object->id_customer; + } + if ($id_customer) { + $customer = new Customer((int) $id_customer); + } + + $this->tpl_form_vars = array( + 'customer' => isset($customer) ? $customer : null, + 'customer_view_url' => $this->context->link->getAdminLink('AdminCustomers', true, [], [ + 'viewcustomer' => 1, + 'id_customer' => $id_customer, + ]), + 'back_url' => urldecode(Tools::getValue('back')), + ); + + // Order address fields depending on country format + $addresses_fields = $this->processAddressFormat(); + // we use delivery address + $addresses_fields = $addresses_fields['dlv_all_fields']; + + // get required field + $required_fields = AddressFormat::getFieldsRequired(); + + // Merge with field required + $addresses_fields = array_unique(array_merge($addresses_fields, $required_fields)); + + $temp_fields = array(); + + foreach ($addresses_fields as $addr_field_item) { + if ($addr_field_item == 'company') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Company', array(), 'Admin.Global'), + 'name' => 'company', + 'required' => in_array('company', $required_fields), + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ); + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('VAT number', array(), 'Admin.Orderscustomers.Feature'), + 'col' => '2', + 'name' => 'vat_number', + 'required' => in_array('vat_number', $required_fields), + ); + } elseif ($addr_field_item == 'lastname') { + if (isset($customer) && + !Tools::isSubmit('submit' . strtoupper($this->table)) && + Validate::isLoadedObject($customer) && + !Validate::isLoadedObject($this->object)) { + $default_value = $customer->lastname; + } else { + $default_value = ''; + } + + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Last Name', array(), 'Admin.Global'), + 'name' => 'lastname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', + 'default_value' => $default_value, + ); + } elseif ($addr_field_item == 'firstname') { + if (isset($customer) && + !Tools::isSubmit('submit' . strtoupper($this->table)) && + Validate::isLoadedObject($customer) && + !Validate::isLoadedObject($this->object)) { + $default_value = $customer->firstname; + } else { + $default_value = ''; + } + + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('First Name', array(), 'Admin.Global'), + 'name' => 'firstname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', + 'default_value' => $default_value, + ); + } elseif ($addr_field_item == 'address1') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global'), + 'name' => 'address1', + 'col' => '6', + 'required' => true, + ); + } elseif ($addr_field_item == 'address2') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global') . ' (2)', + 'name' => 'address2', + 'col' => '6', + 'required' => in_array('address2', $required_fields), + ); + } elseif ($addr_field_item == 'postcode') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'name' => 'postcode', + 'col' => '2', + 'required' => true, + ); + } elseif ($addr_field_item == 'city') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('City', array(), 'Admin.Global'), + 'name' => 'city', + 'col' => '4', + 'required' => true, + ); + } elseif ($addr_field_item == 'country' || $addr_field_item == 'Country:name') { + $temp_fields[] = array( + 'type' => 'select', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'id_country', + 'required' => in_array('Country:name', $required_fields) || in_array('country', $required_fields), + 'col' => '4', + 'default_value' => (int) $this->context->country->id, + 'options' => array( + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + ), + ); + $temp_fields[] = array( + 'type' => 'select', + 'label' => $this->trans('State', array(), 'Admin.Global'), + 'name' => 'id_state', + 'required' => false, + 'col' => '4', + 'options' => array( + 'query' => array(), + 'id' => 'id_state', + 'name' => 'name', + ), + ); + } elseif ($addr_field_item == 'phone') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Home phone', array(), 'Admin.Global'), + 'name' => 'phone', + 'required' => in_array('phone', $required_fields), + 'col' => '4', + ); + } elseif ($addr_field_item == 'phone_mobile') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Mobile phone', array(), 'Admin.Global'), + 'name' => 'phone_mobile', + 'required' => in_array('phone_mobile', $required_fields), + 'col' => '4', + ); + } + } + + // merge address format with the rest of the form + array_splice($this->fields_form['input'], 3, 0, $temp_fields); + + return parent::renderForm(); + } + + public function processSave() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + // Transform e-mail in id_customer for parent processing + if (Validate::isEmail(Tools::getValue('email'))) { + $customer = new Customer(); + $customer->getByEmail(Tools::getValue('email'), null, false); + if (Validate::isLoadedObject($customer)) { + $_POST['id_customer'] = $customer->id; + } else { + $this->errors[] = $this->trans('This email address is not registered.', array(), 'Admin.Orderscustomers.Notification'); + } + } elseif ($id_customer = Tools::getValue('id_customer')) { + $customer = new Customer((int) $id_customer); + if (Validate::isLoadedObject($customer)) { + $_POST['id_customer'] = $customer->id; + } else { + $this->errors[] = $this->trans('This customer ID is not recognized.', array(), 'Admin.Orderscustomers.Notification'); + } + } else { + $this->errors[] = $this->trans('This email address is not valid. Please use an address like bob@example.com.', array(), 'Admin.Orderscustomers.Notification'); + } + if (Country::isNeedDniByCountryId(Tools::getValue('id_country')) && !Tools::getValue('dni')) { + $this->errors[] = $this->trans('The identification number is incorrect or has already been used.', array(), 'Admin.Orderscustomers.Notification'); + } + + /* If the selected country does not contain states */ + $id_state = (int) Tools::getValue('id_state'); + $id_country = (int) Tools::getValue('id_country'); + $country = new Country((int) $id_country); + if ($country && !(int) $country->contains_states && $id_state) { + $this->errors[] = $this->trans('You have selected a state for a country that does not contain states.', array(), 'Admin.Orderscustomers.Notification'); + } + + /* If the selected country contains states, then a state have to be selected */ + if ((int) $country->contains_states && !$id_state) { + $this->errors[] = $this->trans('An address located in a country containing states must have a state selected.', array(), 'Admin.Orderscustomers.Notification'); + } + + $postcode = Tools::getValue('postcode'); + /* Check zip code format */ + if ($country->zip_code_format && !$country->checkZipCode($postcode)) { + $this->errors[] = $this->trans('Your Zip/postal code is incorrect.', array(), 'Admin.Notifications.Error') . '
' . $this->trans('It must be entered as follows:', array(), 'Admin.Notifications.Error') . ' ' . str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))); + } elseif (empty($postcode) && $country->need_zip_code) { + $this->errors[] = $this->trans('A Zip/postal code is required.', array(), 'Admin.Notifications.Error'); + } elseif ($postcode && !Validate::isPostCode($postcode)) { + $this->errors[] = $this->trans('The Zip/postal code is invalid.', array(), 'Admin.Notifications.Error'); + } + + /* If this address come from order's edition and is the same as the other one (invoice or delivery one) + ** we delete its id_address to force the creation of a new one */ + if ((int) Tools::getValue('id_order')) { + $this->_redirect = false; + if (isset($_POST['address_type'])) { + $_POST['id_address'] = ''; + $this->id_object = null; + } + } + + // Check the requires fields which are settings in the BO + $address = new Address(); + $this->errors = array_merge($this->errors, $address->validateFieldsRequiredDatabase()); + + $return = false; + if (empty($this->errors)) { + $return = parent::processSave(); + } else { + // if we have errors, we stay on the form instead of going back to the list + $this->display = 'edit'; + } + + /* Reassignation of the order's new (invoice or delivery) address */ + $address_type = (int) Tools::getValue('address_type') == 2 ? 'invoice' : 'delivery'; + + if ($this->action == 'save' && ($id_order = (int) Tools::getValue('id_order')) && !count($this->errors) && !empty($address_type)) { + if (!Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'orders SET `id_address_' . bqSQL($address_type) . '` = ' . (int) $this->object->id . ' WHERE `id_order` = ' . (int) $id_order)) { + $this->errors[] = $this->trans('An error occurred while linking this address to its order.', array(), 'Admin.Orderscustomers.Notification'); + } else { + //update order shipping cost + $order = new Order($id_order); + $order->refreshShippingCost(); + + // update cart + $cart = Cart::getCartByOrderId($id_order); + if (Validate::isLoadedObject($cart)) { + if ($address_type == 'invoice') { + $cart->id_address_invoice = (int) $this->object->id; + } else { + $cart->id_address_delivery = (int) $this->object->id; + } + $cart->update(); + } + // redirect + Tools::redirectAdmin(urldecode(Tools::getValue('back')) . '&conf=4'); + } + } + + return $return; + } + + public function processAdd() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + return parent::processAdd(); + } + + /** + * Get Address formats used by the country where the address id retrieved from POST/GET is. + * + * @return array address formats + */ + protected function processAddressFormat() + { + $tmp_addr = new CustomerAddress((int) Tools::getValue('id_address')); + + $selected_country = ($tmp_addr && $tmp_addr->id_country) ? $tmp_addr->id_country : (int) Configuration::get('PS_COUNTRY_DEFAULT'); + $adr_fields = AddressFormat::getOrderedAddressFields($selected_country, false, true); + + $all_fields = array(); + $out = array(); + + foreach ($adr_fields as $fields_line) { + foreach (explode(' ', $fields_line) as $field_item) { + $all_fields[] = trim($field_item); + } + } + + foreach (array('inv', 'dlv') as $adr_type) { + $out[$adr_type . '_adr_fields'] = $adr_fields; + $out[$adr_type . '_all_fields'] = $all_fields; + } + + return $out; + } + + /** + * Method called when an ajax request is made. + * + * @see AdminController::postProcess() + */ + public function ajaxProcess() + { + if (Tools::isSubmit('email')) { + $email = pSQL(Tools::getValue('email')); + $customer = Customer::searchByName($email); + if (!empty($customer)) { + $customer = $customer['0']; + echo json_encode(array('infos' => pSQL($customer['firstname']) . '_' . pSQL($customer['lastname']) . '_' . pSQL($customer['company']))); + } + } + + if (Tools::isSubmit('dni_required')) { + echo json_encode(['dni_required' => Address::dniRequired((int) Tools::getValue('id_country'))]); + } + + die; + } + + /** + * Object Delete. + */ + public function processDelete() + { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var Address $object */ + if (!$object->isUsed()) { + $this->deleted = false; + } + } + + $res = parent::processDelete(); + + if ($back = Tools::getValue('back')) { + $this->redirect_after = urldecode($back) . '&conf=1'; + } + + return $res; + } + + /** + * Delete multiple items. + * + * @return bool true if succcess + */ + protected function processBulkDelete() + { + if (is_array($this->boxes) && !empty($this->boxes)) { + $deleted = false; + foreach ($this->boxes as $id) { + $to_delete = new Address((int) $id); + if ($to_delete->isUsed()) { + $deleted = true; + + break; + } + } + $this->deleted = $deleted; + } + + return parent::processBulkDelete(); + } +} diff --git a/controllers/admin/AdminAttachmentsController.php b/controllers/admin/AdminAttachmentsController.php new file mode 100644 index 00000000..26b5c1a5 --- /dev/null +++ b/controllers/admin/AdminAttachmentsController.php @@ -0,0 +1,264 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Attachment $object + */ +class AdminAttachmentsControllerCore extends AdminController +{ + public $bootstrap = true; + + protected $product_attachements = array(); + + public function __construct() + { + $this->table = 'attachment'; + $this->className = 'Attachment'; + $this->lang = true; + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + + $this->_select = 'IFNULL(virtual_product_attachment.products, 0) as products'; + $this->_join = 'LEFT JOIN (SELECT id_attachment, COUNT(*) as products FROM ' . _DB_PREFIX_ . 'product_attachment GROUP BY id_attachment) virtual_product_attachment ON a.id_attachment = virtual_product_attachment.id_attachment'; + $this->_use_found_rows = false; + + parent::__construct(); + + $this->fields_list = array( + 'id_attachment' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'file' => array( + 'title' => $this->trans('File', array(), 'Admin.Global'), + 'orderby' => false, + 'search' => false, + ), + 'file_size' => array( + 'title' => $this->trans('Size', array(), 'Admin.Global'), + 'callback' => 'displayHumanReadableSize', + ), + 'products' => array( + 'title' => $this->trans('Associated with', array(), 'Admin.Catalog.Feature'), + 'suffix' => $this->trans('product(s)', array(), 'Admin.Catalog.Feature'), + 'filter_key' => 'virtual_product_attachment!products', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + ), + ); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJs(_PS_JS_DIR_ . '/admin/attachments.js'); + Media::addJsDefL('confirm_text', $this->trans('This file is associated with the following products, do you really want to delete it?', array(), 'Admin.Catalog.Notification')); + } + + public static function displayHumanReadableSize($size) + { + return Tools::formatBytes($size); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_attachment'] = array( + 'href' => self::$currentIndex . '&addattachment&token=' . $this->token, + 'desc' => $this->trans('Add new file', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderView() + { + if (($obj = $this->loadObject(true)) && Validate::isLoadedObject($obj)) { + $link = $this->context->link->getPageLink('attachment', true, null, 'id_attachment=' . $obj->id); + Tools::redirectLink($link); + } + + return $this->displayWarning($this->trans('File not found', array(), 'Admin.Catalog.Notification')); + } + + public function renderForm() + { + if (($obj = $this->loadObject(true)) && Validate::isLoadedObject($obj)) { + /** @var Attachment $obj */ + $link = $this->context->link->getPageLink('attachment', true, null, 'id_attachment=' . $obj->id); + + if (file_exists(_PS_DOWNLOAD_DIR_ . $obj->file)) { + $size = round(filesize(_PS_DOWNLOAD_DIR_ . $obj->file) / 1024); + } + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Add new file', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-paper-clip', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Filename', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'col' => 4, + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'col' => 6, + ), + array( + 'type' => 'file', + 'file' => isset($link) ? $link : null, + 'size' => isset($size) ? $size : null, + 'label' => $this->trans('File', array(), 'Admin.Global'), + 'name' => 'file', + 'required' => true, + 'col' => 6, + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + return parent::renderForm(); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList((int) $id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if (count($this->_list)) { + $this->product_attachements = Attachment::getProductAttached((int) $id_lang, $this->_list); + + $list_product_list = array(); + foreach ($this->_list as $list) { + $product_list = ''; + + if (isset($this->product_attachements[$list['id_attachment']])) { + foreach ($this->product_attachements[$list['id_attachment']] as $product) { + $product_list .= $product . ', '; + } + + $product_list = rtrim($product_list, ', '); + } + + $list_product_list[$list['id_attachment']] = $product_list; + } + + // Assign array in list_action_delete.tpl + $this->tpl_delete_link_vars = array( + 'product_list' => $list_product_list, + 'product_attachements' => $this->product_attachements, + ); + } + } + + public function postProcess() + { + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error'); + + return; + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + $id = (int) Tools::getValue('id_attachment'); + if ($id && $a = new Attachment($id)) { + $_POST['file'] = $a->file; + $_POST['mime'] = $a->mime; + } + if (!count($this->errors)) { + if (isset($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) { + if ($_FILES['file']['size'] > (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024 * 1024)) { + $this->errors[] = $this->trans( + 'The file is too large. Maximum size allowed is: %1$d kB. The file you are trying to upload is %2$d kB.', + array( + '%1$d' => (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024), + '%2$d' => number_format(($_FILES['file']['size'] / 1024), 2, '.', ''), + ), + 'Admin.Notifications.Error' + ); + } else { + do { + $uniqid = sha1(microtime()); + } while (file_exists(_PS_DOWNLOAD_DIR_ . $uniqid)); + if (!move_uploaded_file($_FILES['file']['tmp_name'], _PS_DOWNLOAD_DIR_ . $uniqid)) { + $this->errors[] = $this->trans('Failed to copy the file.', array(), 'Admin.Catalog.Notification'); + } + $_POST['file_name'] = $_FILES['file']['name']; + @unlink($_FILES['file']['tmp_name']); + if (!count($this->errors) && isset($a) && file_exists(_PS_DOWNLOAD_DIR_ . $a->file)) { + unlink(_PS_DOWNLOAD_DIR_ . $a->file); + } + $_POST['file'] = $uniqid; + $_POST['mime'] = $_FILES['file']['type']; + } + } elseif (array_key_exists('file', $_FILES) && (int) $_FILES['file']['error'] === 1) { + $max_upload = (int) ini_get('upload_max_filesize'); + $max_post = (int) ini_get('post_max_size'); + $upload_mb = min($max_upload, $max_post); + $this->errors[] = $this->trans( + 'The file %file% exceeds the size allowed by the server. The limit is set to %size% MB.', + array('%file%' => '' . $_FILES['file']['name'] . ' ', '%size%' => '' . $upload_mb . ''), + 'Admin.Catalog.Notification' + ); + } elseif (!isset($a) || (isset($a) && !file_exists(_PS_DOWNLOAD_DIR_ . $a->file))) { + $this->errors[] = $this->trans('Upload error. Please check your server configurations for the maximum upload size allowed.', array(), 'Admin.Catalog.Notification'); + } + } + $this->validateRules(); + } + $return = parent::postProcess(); + if (!$return && isset($uniqid) && file_exists(_PS_DOWNLOAD_DIR_ . $uniqid)) { + unlink(_PS_DOWNLOAD_DIR_ . $uniqid); + } + + return $return; + } +} diff --git a/controllers/admin/AdminAttributeGeneratorController.php b/controllers/admin/AdminAttributeGeneratorController.php new file mode 100644 index 00000000..8ec6ac96 --- /dev/null +++ b/controllers/admin/AdminAttributeGeneratorController.php @@ -0,0 +1,273 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +@ini_set('max_execution_time', 3600); + +/** + * @property Product $object + */ +class AdminAttributeGeneratorControllerCore extends AdminController +{ + protected $combinations = []; + + /** @var Product */ + protected $product; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'product_attribute'; + $this->className = 'Product'; + $this->multishop_context_group = false; + + parent::__construct(); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJS(_PS_JS_DIR_ . 'admin/attributes.js'); + } + + protected function addAttribute($attributes, $price = 0, $weight = 0) + { + foreach ($attributes as $attribute) { + $price += (float) preg_replace('/[^0-9.-]/', '', str_replace(',', '.', Tools::getValue('price_impact_' . (int) $attribute))); + $weight += (float) preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('weight_impact_' . (int) $attribute))); + } + if ($this->product->id) { + return [ + 'id_product' => (int) $this->product->id, + 'price' => (float) $price, + 'weight' => (float) $weight, + 'ecotax' => 0, + 'quantity' => (int) Tools::getValue('quantity'), + 'reference' => pSQL($_POST['reference']), + 'default_on' => 0, + 'available_date' => '0000-00-00', + ]; + } + + return []; + } + + public static function createCombinations($list) + { + if (count($list) <= 1) { + return count($list) ? array_map(function ($v) { return [$v]; }, $list[0]) : $list; + } + $res = []; + $first = array_pop($list); + foreach ($first as $attribute) { + $tab = AdminAttributeGeneratorController::createCombinations($list); + foreach ($tab as $to_add) { + $res[] = is_array($to_add) ? array_merge($to_add, [$attribute]) : [$to_add, $attribute]; + } + } + + return $res; + } + + public function initProcess() + { + if (!defined('PS_MASS_PRODUCT_CREATION')) { + define('PS_MASS_PRODUCT_CREATION', true); + } + + if (Tools::isSubmit('generate')) { + if ($this->access('edit')) { + $this->action = 'generate'; + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } + parent::initProcess(); + } + + public function postProcess() + { + $this->product = new Product((int) Tools::getValue('id_product')); + $this->product->loadStockData(); + parent::postProcess(); + } + + public function processGenerate() + { + if (!is_array(Tools::getValue('options'))) { + $this->errors[] = $this->trans('Please select at least one attribute.', [], 'Admin.Catalog.Notification'); + } else { + $tab = array_values(Tools::getValue('options')); + if (count($tab) && Validate::isLoadedObject($this->product)) { + AdminAttributeGeneratorController::setAttributesImpacts($this->product->id, $tab); + $this->combinations = array_values(AdminAttributeGeneratorController::createCombinations($tab)); + $values = array_values(array_map([$this, 'addAttribute'], $this->combinations)); + + // @since 1.5.0 + if ($this->product->depends_on_stock == 0) { + $attributes = Product::getProductAttributesIds($this->product->id, true); + foreach ($attributes as $attribute) { + StockAvailable::removeProductFromStockAvailable($this->product->id, $attribute['id_product_attribute'], Context::getContext()->shop); + } + } + + SpecificPriceRule::disableAnyApplication(); + + $this->product->deleteProductAttributes(); + $this->product->generateMultipleCombinations($values, $this->combinations); + + // Reset cached default attribute for the product and get a new one + Product::getDefaultAttribute($this->product->id, 0, true); + Product::updateDefaultAttribute($this->product->id); + + // @since 1.5.0 + if ($this->product->depends_on_stock == 0) { + $attributes = Product::getProductAttributesIds($this->product->id, true); + $quantity = (int) Tools::getValue('quantity'); + foreach ($attributes as $attribute) { + if (Shop::getContext() == Shop::CONTEXT_ALL) { + $shops_list = Shop::getShops(); + if (is_array($shops_list)) { + foreach ($shops_list as $current_shop) { + if (isset($current_shop['id_shop']) && (int) $current_shop['id_shop'] > 0) { + StockAvailable::setQuantity($this->product->id, (int) $attribute['id_product_attribute'], $quantity, (int) $current_shop['id_shop']); + } + } + } + } else { + StockAvailable::setQuantity($this->product->id, (int) $attribute['id_product_attribute'], $quantity); + } + } + } else { + StockAvailable::synchronize($this->product->id); + } + + SpecificPriceRule::enableAnyApplication(); + SpecificPriceRule::applyAllRules([(int) $this->product->id]); + + Tools::redirectAdmin($this->context->link->getAdminLink('AdminProducts') . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=Combinations&conf=4'); + } else { + $this->errors[] = $this->trans('Unable to initialize these parameters. A combination is missing or an object cannot be loaded.', [], 'Admin.Catalog.Notification'); + } + } + } + + protected static function setAttributesImpacts($id_product, $tab) + { + $attributes = []; + foreach ($tab as $group) { + foreach ($group as $attribute) { + $price = preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('price_impact_' . (int) $attribute))); + $weight = preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('weight_impact_' . (int) $attribute))); + $attributes[] = '(' . (int) $id_product . ', ' . (int) $attribute . ', ' . (float) $price . ', ' . (float) $weight . ')'; + } + } + + return Db::getInstance()->execute(' + INSERT INTO `' . _DB_PREFIX_ . 'attribute_impact` (`id_product`, `id_attribute`, `price`, `weight`) + VALUES ' . implode(',', $attributes) . ' + ON DUPLICATE KEY UPDATE `price` = VALUES(price), `weight` = VALUES(weight)'); + } + + public function initGroupTable() + { + $combinations_groups = $this->product->getAttributesGroups($this->context->language->id); + $attributes = []; + $impacts = Product::getAttributesImpacts($this->product->id); + foreach ($combinations_groups as &$combination) { + $target = &$attributes[$combination['id_attribute_group']][$combination['id_attribute']]; + $target = $combination; + if (isset($impacts[$combination['id_attribute']])) { + $target['price'] = $impacts[$combination['id_attribute']]['price']; + $target['weight'] = $impacts[$combination['id_attribute']]['weight']; + } + } + $this->context->smarty->assign([ + 'currency_sign' => $this->context->currency->sign, + 'weight_unit' => Configuration::get('PS_WEIGHT_UNIT'), + 'attributes' => $attributes, + ]); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + $this->page_header_toolbar_title = $this->trans('Attributes generator', [], 'Admin.Catalog.Feature'); + $this->page_header_toolbar_btn['back'] = [ + 'href' => $this->context->link->getAdminLink('AdminProducts') . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=Combinations', + 'desc' => $this->trans('Back to the product', [], 'Admin.Catalog.Feature'), + ]; + } + + public function initBreadcrumbs($tab_id = null, $tabs = null) + { + $this->display = 'generator'; + + return parent::initBreadcrumbs(); + } + + public function initContent() + { + if (!Combination::isFeatureActive()) { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + + $url = '' . + $this->trans('Performance', [], 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %link%.', ['%link%' => $url], 'Admin.Catalog.Notification')); + + return; + } + + // Init toolbar + $this->initPageHeaderToolbar(); + $this->initGroupTable(); + + $attributes = Attribute::getAttributes(Context::getContext()->language->id, true); + $attribute_js = []; + + foreach ($attributes as $k => $attribute) { + $attribute_js[$attribute['id_attribute_group']][$attribute['id_attribute']] = $attribute['name']; + } + + $attribute_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + $this->product = new Product((int) Tools::getValue('id_product')); + + $this->context->smarty->assign([ + 'tax_rates' => $this->product->getTaxesRate(), + 'generate' => isset($_POST['generate']) && !count($this->errors), + 'combinations_size' => count($this->combinations), + 'product_name' => $this->product->name[$this->context->language->id], + 'product_reference' => $this->product->reference, + 'url_generator' => self::$currentIndex . '&id_product=' . (int) Tools::getValue('id_product') . '&attributegenerator&token=' . Tools::getValue('token'), + 'attribute_groups' => $attribute_groups, + 'attribute_js' => $attribute_js, + 'toolbar_btn' => $this->toolbar_btn, + 'toolbar_scroll' => true, + 'show_page_header_toolbar' => $this->show_page_header_toolbar, + 'page_header_toolbar_title' => $this->page_header_toolbar_title, + 'page_header_toolbar_btn' => $this->page_header_toolbar_btn, + ]); + } +} diff --git a/controllers/admin/AdminAttributesGroupsController.php b/controllers/admin/AdminAttributesGroupsController.php new file mode 100644 index 00000000..78644687 --- /dev/null +++ b/controllers/admin/AdminAttributesGroupsController.php @@ -0,0 +1,974 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property AttributeGroup $object + */ +class AdminAttributesGroupsControllerCore extends AdminController +{ + public $bootstrap = true; + protected $id_attribute; + protected $position_identifier = 'id_attribute_group'; + protected $attribute_name; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'attribute_group'; + $this->list_id = 'attribute_group'; + $this->identifier = 'id_attribute_group'; + $this->className = 'AttributeGroup'; + $this->lang = true; + $this->_defaultOrderBy = 'position'; + + parent::__construct(); + + $this->fields_list = [ + 'id_attribute_group' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'filter_key' => 'b!name', + 'align' => 'left', + ], + 'count_values' => [ + 'title' => $this->trans('Values', [], 'Admin.Catalog.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'orderby' => false, + 'search' => false, + ], + 'position' => [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Info'), + ], + ]; + $this->fieldImageSettings = ['name' => 'texture', 'dir' => 'co']; + + $this->image_dir = 'co'; + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + public function renderView() + { + if (($id = (int) Tools::getValue('id_attribute_group'))) { + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + $this->position_identifier = 'id_attribute'; + $this->position_group_identifier = 'id_attribute_group'; + $this->list_id = 'attribute_values'; + $this->lang = true; + + $this->context->smarty->assign([ + 'current' => self::$currentIndex . '&id_attribute_group=' . (int) $id . '&viewattribute_group', + ]); + + if (!Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Catalog.Notification') . + ' ' . $this->table . ' ' . + $this->trans('(cannot load object)', [], 'Admin.Catalog.Notification'); + + return; + } + + $this->attribute_name = $obj->name; + $this->fields_list = [ + 'id_attribute' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Value', [], 'Admin.Catalog.Feature'), + 'width' => 'auto', + 'filter_key' => 'b!name', + ], + ]; + + if ($obj->group_type == 'color') { + $this->fields_list['color'] = [ + 'title' => $this->trans('Color', [], 'Admin.Catalog.Feature'), + 'filter_key' => 'a!color', + ]; + } + + $this->fields_list['position'] = [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'class' => 'fixed-width-md', + ]; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_where = 'AND a.`id_attribute_group` = ' . (int) $id; + $this->_orderBy = 'position'; + + self::$currentIndex = self::$currentIndex . '&id_attribute_group=' . (int) $id . '&viewattribute_group'; + $this->processFilter(); + + return parent::renderList(); + } + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $this->table = 'attribute_group'; + $this->identifier = 'id_attribute_group'; + + $group_type = [ + [ + 'id' => 'select', + 'name' => $this->trans('Drop-down list', [], 'Admin.Global'), + ], + [ + 'id' => 'radio', + 'name' => $this->trans('Radio buttons', [], 'Admin.Global'), + ], + [ + 'id' => 'color', + 'name' => $this->trans('Color or texture', [], 'Admin.Catalog.Feature'), + ], + ]; + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Attributes', [], 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Your internal name for this attribute.', [], 'Admin.Catalog.Help') . ' ' . $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Public name', [], 'Admin.Catalog.Feature'), + 'name' => 'public_name', + 'lang' => true, + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('The public name for this attribute, displayed to the customers.', [], 'Admin.Catalog.Help') . ' ' . $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'select', + 'label' => $this->trans('Attribute type', [], 'Admin.Catalog.Feature'), + 'name' => 'group_type', + 'required' => true, + 'options' => [ + 'query' => $group_type, + 'id' => 'id', + 'name' => 'name', + ], + 'col' => '2', + 'hint' => $this->trans('The way the attribute\'s values will be presented to the customers in the product\'s page.', [], 'Admin.Catalog.Help'), + ], + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if (!($obj = $this->loadObject(true))) { + return; + } + + return parent::renderForm(); + } + + public function renderFormAttributes() + { + $attributes_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + + $this->table = 'attribute'; + $this->identifier = 'id_attribute'; + + $this->show_form_cancel_button = true; + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Values', [], 'Admin.Global'), + 'icon' => 'icon-info-sign', + ], + 'input' => [ + [ + 'type' => 'select', + 'label' => $this->trans('Attribute group', [], 'Admin.Catalog.Feature'), + 'name' => 'id_attribute_group', + 'required' => true, + 'options' => [ + 'query' => $attributes_groups, + 'id' => 'id_attribute_group', + 'name' => 'name', + ], + 'hint' => $this->trans('Choose the attribute group for this value.', [], 'Admin.Catalog.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Value', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + ], + ]; + + if (Shop::isFeatureActive()) { + // We get all associated shops for all attribute groups, because we will disable group shops + // for attributes that the selected attribute group don't support + $sql = 'SELECT id_attribute_group, id_shop FROM ' . _DB_PREFIX_ . 'attribute_group_shop'; + $associations = []; + foreach (Db::getInstance()->executeS($sql) as $row) { + $associations[$row['id_attribute_group']][] = $row['id_shop']; + } + + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + 'values' => Shop::getTree(), + ]; + } else { + $associations = []; + } + + $this->fields_form['shop_associations'] = json_encode($associations); + + $this->fields_form['input'][] = [ + 'type' => 'color', + 'label' => $this->trans('Color', [], 'Admin.Catalog.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Choose a color with the color picker, or enter an HTML color (e.g. "lightblue", "#CC6600").', [], 'Admin.Catalog.Help'), + ]; + + $this->fields_form['input'][] = [ + 'type' => 'file', + 'label' => $this->trans('Texture', [], 'Admin.Catalog.Feature'), + 'name' => 'texture', + 'hint' => [ + $this->trans('Upload an image file containing the color texture from your computer.', [], 'Admin.Catalog.Help'), + $this->trans('This will override the HTML color!', [], 'Admin.Catalog.Help'), + ], + ]; + + $this->fields_form['input'][] = [ + 'type' => 'current_texture', + 'label' => $this->trans('Current texture', [], 'Admin.Catalog.Feature'), + 'name' => 'current_texture', + ]; + + $this->fields_form['input'][] = [ + 'type' => 'closediv', + 'name' => '', + ]; + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + $this->fields_form['buttons'] = [ + 'save-and-stay' => [ + 'title' => $this->trans('Save then add another value', [], 'Admin.Catalog.Feature'), + 'name' => 'submitAdd' . $this->table . 'AndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ], + ]; + + $this->fields_value['id_attribute_group'] = (int) Tools::getValue('id_attribute_group'); + + // Override var of Controller + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + $this->lang = true; + $this->tpl_folder = 'attributes/'; + + // Create object Attribute + if (!$obj = new Attribute((int) Tools::getValue($this->identifier))) { + return; + } + + $str_attributes_groups = ''; + foreach ($attributes_groups as $attribute_group) { + $str_attributes_groups .= '"' . $attribute_group['id_attribute_group'] . '" : ' . ($attribute_group['group_type'] == 'color' ? '1' : '0') . ', '; + } + + $image = '../img/' . $this->fieldImageSettings['dir'] . '/' . (int) $obj->id . '.jpg'; + + $this->tpl_form_vars = [ + 'strAttributesGroups' => $str_attributes_groups, + 'colorAttributeProperties' => Validate::isLoadedObject($obj) && $obj->isColorAttribute(), + 'imageTextureExists' => file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) $obj->id . '.jpg'), + 'imageTexture' => $image, + 'imageTextureUrl' => Tools::safeOutput($_SERVER['REQUEST_URI']) . '&deleteImage=1', + ]; + + return parent::renderForm(); + } + + /** + * AdminController::init() override. + * + * @see AdminController::init() + */ + public function init() + { + if (Tools::isSubmit('updateattribute')) { + $this->display = 'editAttributes'; + } elseif (Tools::isSubmit('submitAddattribute')) { + $this->display = 'editAttributes'; + } elseif (Tools::isSubmit('submitAddattribute_group')) { + $this->display = 'add'; + } + + parent::init(); + } + + /** + * Override processAdd to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processAdd() + { + if ($this->table == 'attribute') { + /** @var AttributeGroup $object */ + $object = new $this->className(); + foreach (Language::getLanguages(false) as $language) { + if ($object->isAttribute( + (int) Tools::getValue('id_attribute_group'), + Tools::getValue('name_' . $language['id_lang']), + $language['id_lang'] + )) { + $this->errors['name_' . $language['id_lang']] = $this->trans( + 'The attribute value "%1$s" already exist for %2$s language', + [ + Tools::getValue('name_' . $language['id_lang']), + $language['name'], + ], + 'Admin.Catalog.Notification' + ); + } + } + + if (!empty($this->errors)) { + return $object; + } + } + + $object = parent::processAdd(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->display == 'add') { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + } + + if (count($this->errors)) { + $this->setTypeAttribute(); + } + + return $object; + } + + /** + * Override processUpdate to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processUpdate() + { + $object = parent::processUpdate(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->display == 'add') { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + } + + if (count($this->errors)) { + $this->setTypeAttribute(); + } + + if (Tools::isSubmit('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute') || Tools::isSubmit('submitBulkdeleteattribute')) { + Tools::clearColorListCache(); + } + + return $object; + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (Combination::isFeatureActive()) { + if ($this->display == 'edit' || $this->display == 'add') { + if (!($this->object = $this->loadObject(true))) { + return; + } + $this->content .= $this->renderForm(); + } elseif ($this->display == 'editAttributes') { + if (!$this->object = new Attribute((int) Tools::getValue('id_attribute'))) { + return; + } + + $this->content .= $this->renderFormAttributes(); + } elseif ($this->display != 'view' && !$this->ajax) { + $this->content .= $this->renderList(); + $this->content .= $this->renderOptions(); + /* reset all attributes filter */ + if (!Tools::getValue('submitFilterattribute_group', 0) && !Tools::getIsset('id_attribute_group')) { + $this->processResetFilters('attribute_values'); + } + } elseif ($this->display == 'view' && !$this->ajax) { + $this->content = $this->renderView(); + } + } else { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + $url = '' . $this->trans('Performance', [], 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %link%.', ['%link%' => $url], 'Admin.Catalog.Notification')); + } + + $this->context->smarty->assign([ + 'table' => $this->table, + 'current' => self::$currentIndex, + 'token' => $this->token, + 'content' => $this->content, + ]); + } + + public function initPageHeaderToolbar() + { + if (Combination::isFeatureActive()) { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_attribute_group'] = [ + 'href' => self::$currentIndex . '&addattribute_group&token=' . $this->token, + 'desc' => $this->trans('Add new attribute', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + $this->page_header_toolbar_btn['new_value'] = [ + 'href' => self::$currentIndex . '&updateattribute&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&token=' . $this->token, + 'desc' => $this->trans('Add new value', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + } + + if ($this->display == 'view') { + $this->page_header_toolbar_btn['new_value'] = [ + 'href' => self::$currentIndex . '&updateattribute&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&token=' . $this->token, + 'desc' => $this->trans('Add new value', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function initToolbar() + { + switch ($this->display) { + // @todo defining default buttons + case 'add': + case 'edit': + case 'editAttributes': + // Default save button - action dynamically handled in javascript + $this->toolbar_btn['save'] = [ + 'href' => '#', + 'desc' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if ($this->display == 'editAttributes' && !$this->id_attribute) { + $this->toolbar_btn['save-and-stay'] = [ + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save then add another value', [], 'Admin.Catalog.Help'), + 'force_desc' => true, + ]; + } + + $this->toolbar_btn['back'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups'), + 'desc' => $this->trans('Back to list', [], 'Admin.Actions'), + ]; + + break; + case 'view': + $this->toolbar_btn['newAttributes'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups', true, [], ['updateattribute' => 1, 'id_attribute_group' => (int) Tools::getValue('id_attribute_group')]), + 'desc' => $this->trans('Add New Values', [], 'Admin.Catalog.Feature'), + 'class' => 'toolbar-new', + ]; + + $this->toolbar_btn['back'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups'), + 'desc' => $this->trans('Back to list', [], 'Admin.Actions'), + ]; + + break; + default: // list + $this->toolbar_btn['new'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups', true, [], ['add' . $this->table => 1]), + 'desc' => $this->trans('Add New Attributes', [], 'Admin.Catalog.Feature'), + ]; + if ($this->can_import) { + $this->toolbar_btn['import'] = [ + 'href' => $this->context->link->getAdminLink('AdminImport', true, [], ['import_type' => 'combinations']), + 'desc' => $this->trans('Import', [], 'Admin.Actions'), + ]; + } + } + } + + public function initToolbarTitle() + { + $bread_extended = $this->breadcrumbs; + + switch ($this->display) { + case 'edit': + $bread_extended[] = $this->trans('Edit New Attribute', [], 'Admin.Catalog.Feature'); + + break; + + case 'add': + $bread_extended[] = $this->trans('Add New Attribute', [], 'Admin.Catalog.Feature'); + + break; + + case 'view': + if (Tools::getIsset('viewattribute_group')) { + if (($id = (int) Tools::getValue('id_attribute_group'))) { + if (Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $bread_extended[] = $obj->name[$this->context->employee->id_lang]; + } + } + } else { + $bread_extended[] = $this->attribute_name[$this->context->employee->id_lang]; + } + + break; + + case 'editAttributes': + if ($this->id_attribute) { + if (($id = (int) Tools::getValue('id_attribute_group'))) { + if (Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $bread_extended[] = '' . $obj->name[$this->context->employee->id_lang] . ''; + } + if (Validate::isLoadedObject($obj = new Attribute((int) $this->id_attribute))) { + $bread_extended[] = $this->trans( + 'Edit: %value%', + [ + '%value%' => $obj->name[$this->context->employee->id_lang], + ], + 'Admin.Catalog.Feature' + ); + } + } else { + $bread_extended[] = $this->trans('Edit Value', [], 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Add New Value', [], 'Admin.Catalog.Feature'); + } + + break; + } + + if (count($bread_extended) > 0) { + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + } + + $this->toolbar_title = $bread_extended; + } + + public function initProcess() + { + $this->setTypeAttribute(); + + if (Tools::getIsset('viewattribute_group')) { + $this->list_id = 'attribute_values'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'attribute_group'; + } + + parent::initProcess(); + + if ($this->table == 'attribute') { + $this->display = 'editAttributes'; + $this->id_attribute = (int) Tools::getValue('id_attribute'); + } + } + + protected function setTypeAttribute() + { + if (Tools::isSubmit('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute') || Tools::isSubmit('submitBulkdeleteattribute')) { + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + + if ($this->display == 'edit') { + $this->display = 'editAttributes'; + } + } + } + + public function processPosition() + { + if (Tools::getIsset('viewattribute_group')) { + $object = new Attribute((int) Tools::getValue('id_attribute')); + self::$currentIndex = self::$currentIndex . '&viewattribute_group'; + } else { + $object = new AttributeGroup((int) Tools::getValue('id_attribute_group')); + } + + if (!Validate::isLoadedObject($object)) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error'); + } else { + $id_identifier_str = ($id_identifier = (int) Tools::getValue($this->identifier)) ? '&' . $this->identifier . '=' . $id_identifier : ''; + $redirect = self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . $id_identifier_str . '&token=' . $this->token; + $this->redirect_after = $redirect; + } + + return $object; + } + + /** + * Call the right method for creating or updating object. + * + * @return mixed + */ + public function processSave() + { + if ($this->display == 'add' || $this->display == 'edit') { + $this->identifier = 'id_attribute_group'; + } + + if (!$this->id_object) { + return $this->processAdd(); + } else { + return $this->processUpdate(); + } + } + + public function postProcess() + { + if (!Combination::isFeatureActive()) { + return; + } + + if (!Tools::getValue($this->identifier) && (int) Tools::getValue('id_attribute') && !Tools::getValue('attributeOrderby')) { + // Override var of Controller + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + } + + /* set location with current index */ + if (Tools::getIsset('id_attribute_group') && Tools::getIsset('viewattribute_group')) { + self::$currentIndex = self::$currentIndex . '&id_attribute_group=' . (int) Tools::getValue('id_attribute_group', 0) . '&viewattribute_group'; + } + + // If it's an attribute, load object Attribute() + if (Tools::getValue('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute')) { + if (true !== $this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + + return; + } elseif (!$object = new Attribute((int) Tools::getValue($this->identifier))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . + $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + + return; + } + + if (Tools::getValue('position') !== false && Tools::getValue('id_attribute')) { + $_POST['id_attribute_group'] = $object->id_attribute_group; + if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&token=' . Tools::getAdminTokenLite('AdminAttributesGroups') . '#details_details_' . $object->id_attribute_group); + } + } elseif (Tools::isSubmit('deleteattribute') && Tools::getValue('id_attribute')) { + if (!$object->delete()) { + $this->errors[] = $this->trans('Failed to delete the attribute.', [], 'Admin.Catalog.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getAdminTokenLite('AdminAttributesGroups')); + } + } elseif (Tools::isSubmit('submitAddattribute')) { + Hook::exec('actionObjectAttributeAddBefore'); + $this->action = 'save'; + $id_attribute = (int) Tools::getValue('id_attribute'); + // Adding last position to the attribute if not exist + if ($id_attribute <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'attribute` + WHERE `id_attribute_group` = ' . (int) Tools::getValue('id_attribute_group') . ' + ORDER BY position DESC'; + // set the position of the new group attribute in $_POST for postProcess() method + $_POST['position'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + $_POST['id_parent'] = 0; + $this->processSave($this->token); + } + } else { + if (Tools::isSubmit('submitBulkdelete' . $this->table)) { + if ($this->access('delete')) { + if (isset($_POST[$this->list_id . 'Box'])) { + /** @var AttributeGroup $object */ + $object = new $this->className(); + if ($object->deleteSelection($_POST[$this->list_id . 'Box'])) { + AttributeGroup::cleanPositions(); + Tools::redirectAdmin(self::$currentIndex . '&conf=2' . '&token=' . $this->token); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', [], 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error'); + } + // clean position after delete + AttributeGroup::cleanPositions(); + } elseif (Tools::isSubmit('submitAdd' . $this->table)) { + Hook::exec('actionObjectAttributeGroupAddBefore'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + // Adding last position to the attribute if not exist + if ($id_attribute_group <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'attribute_group` + ORDER BY position DESC'; + // set the position of the new group attribute in $_POST for postProcess() method + $_POST['position'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + // clean \n\r characters + foreach ($_POST as $key => $value) { + if (preg_match('/^name_/Ui', $key)) { + $_POST[$key] = str_replace('\n', '', str_replace('\r', '', $value)); + } + } + parent::postProcess(); + } else { + parent::postProcess(); + if (Tools::isSubmit('delete' . $this->table)) { + AttributeGroup::cleanPositions(); + } + } + } + } + + /** + * AdminController::getList() override. + * + * @see AdminController::getList() + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if ($this->display == 'view') { + foreach ($this->_list as &$list) { + if (file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) $list['id_attribute'] . '.jpg')) { + if (!isset($list['color']) || !is_array($list['color'])) { + $list['color'] = []; + } + $list['color']['texture'] = '../img/' . $this->fieldImageSettings['dir'] . '/' . (int) $list['id_attribute'] . '.jpg'; + } + } + } else { + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + $item = &$this->_list[$i]; + + $query = new DbQuery(); + $query->select('COUNT(a.id_attribute) as count_values'); + $query->from('attribute', 'a'); + $query->join(Shop::addSqlAssociation('attribute', 'a')); + $query->where('a.id_attribute_group =' . (int) $item['id_attribute_group']); + $query->groupBy('attribute_shop.id_shop'); + $query->orderBy('count_values DESC'); + $item['count_values'] = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); + unset($query); + } + } + } + + /** + * Overrides parent to delete items from sublist. + * + * @return mixed + */ + public function processBulkDelete() + { + // If we are deleting attributes instead of attribute_groups + if (Tools::getIsset('attributeBox')) { + $this->className = 'Attribute'; + $this->table = 'attribute'; + $this->boxes = Tools::getValue($this->table . 'Box'); + } + + $result = parent::processBulkDelete(); + // Restore vars + $this->className = 'AttributeGroup'; + $this->table = 'attribute_group'; + + return $result; + } + + /* Modify group attribute position */ + public function ajaxProcessUpdateGroupsPositions() + { + $way = (int) Tools::getValue('way'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + $positions = Tools::getValue('attribute_group'); + + $new_positions = []; + foreach ($positions as $v) { + if (count(explode('_', $v)) == 4) { + $new_positions[] = $v; + } + } + + foreach ($new_positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_attribute_group) { + if ($group_attribute = new AttributeGroup((int) $pos[2])) { + if (isset($position) && $group_attribute->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for attribute group ' . (int) $pos[2] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update the ' . (int) $id_attribute_group . ' attribute group to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "The (' . (int) $id_attribute_group . ') attribute group cannot be loaded."}'; + } + + break; + } + } + } + + /* Modify attribute position */ + public function ajaxProcessUpdateAttributesPositions() + { + $way = (int) Tools::getValue('way'); + $id_attribute = (int) Tools::getValue('id_attribute'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + $positions = Tools::getValue('attribute'); + + if (is_array($positions)) { + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if ((isset($pos[1], $pos[2])) && (int) $pos[2] === $id_attribute) { + if ($attribute = new Attribute((int) $pos[2])) { + if (isset($position) && $attribute->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for attribute ' . (int) $pos[2] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update the ' . (int) $id_attribute . ' attribute to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "The (' . (int) $id_attribute . ') attribute cannot be loaded"}'; + } + + break; + } + } + } + } +} diff --git a/controllers/admin/AdminCarrierWizardController.php b/controllers/admin/AdminCarrierWizardController.php new file mode 100644 index 00000000..6e25e5c4 --- /dev/null +++ b/controllers/admin/AdminCarrierWizardController.php @@ -0,0 +1,960 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Carrier $object + */ +class AdminCarrierWizardControllerCore extends AdminController +{ + protected $wizard_access; + + public function __construct() + { + $this->bootstrap = true; + $this->display = 'view'; + $this->table = 'carrier'; + $this->identifier = 'id_carrier'; + $this->className = 'Carrier'; + $this->lang = false; + $this->deleted = true; + $this->step_number = 0; + $this->type_context = Shop::getContext(); + $this->old_context = Context::getContext(); + $this->multishop_context = Shop::CONTEXT_ALL; + $this->context = Context::getContext(); + + $this->fieldImageSettings = [ + 'name' => 'logo', + 'dir' => 's', + ]; + + parent::__construct(); + + $this->tabAccess = Profile::getProfileAccess($this->context->employee->id_profile, Tab::getIdFromClassName('AdminCarriers')); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin('smartWizard'); + $this->addJqueryPlugin('typewatch'); + $this->addJs(_PS_JS_DIR_ . 'admin/carrier_wizard.js'); + } + + public function initWizard() + { + $this->wizard_steps = [ + 'name' => 'carrier_wizard', + 'steps' => [ + [ + 'title' => $this->trans('General settings', [], 'Admin.Shipping.Feature'), + ], + [ + 'title' => $this->trans('Shipping locations and costs', [], 'Admin.Shipping.Feature'), + ], + [ + 'title' => $this->trans('Size, weight, and group access', [], 'Admin.Shipping.Feature'), + ], + [ + 'title' => $this->trans('Summary', [], 'Admin.Global'), + ], ], + ]; + + if (Shop::isFeatureActive()) { + $multistore_step = [ + [ + 'title' => $this->trans('MultiStore', [], 'Admin.Global'), + ], + ]; + array_splice($this->wizard_steps['steps'], 1, 0, $multistore_step); + } + } + + public function renderView() + { + $this->initWizard(); + + if (Tools::getValue('id_carrier') && $this->access('edit')) { + $carrier = $this->loadObject(); + } elseif ($this->access('add')) { + $carrier = new Carrier(); + } + + if ((!$this->access('edit') && Tools::getValue('id_carrier')) || (!$this->access('add') && !Tools::getValue('id_carrier'))) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', [], 'Admin.Shipping.Notification'); + + return; + } + + $currency = $this->getActualCurrency(); + + $this->tpl_view_vars = [ + 'currency_sign' => $currency->sign, + 'PS_WEIGHT_UNIT' => Configuration::get('PS_WEIGHT_UNIT'), + 'enableAllSteps' => Validate::isLoadedObject($carrier), + 'wizard_steps' => $this->wizard_steps, + 'validate_url' => $this->context->link->getAdminLink('AdminCarrierWizard'), + 'carrierlist_url' => $this->context->link->getAdminLink('AdminCarriers') . '&conf=' . ((int) Validate::isLoadedObject($carrier) ? 4 : 3), + 'multistore_enable' => Shop::isFeatureActive(), + 'wizard_contents' => [ + 'contents' => [ + 0 => $this->renderStepOne($carrier), + 1 => $this->renderStepThree($carrier), + 2 => $this->renderStepFour($carrier), + 3 => $this->renderStepFive($carrier), + ], + ], + 'labels' => [ + 'next' => $this->trans('Next', [], 'Admin.Global'), + 'previous' => $this->trans('Previous', [], 'Admin.Global'), + 'finish' => $this->trans('Finish', [], 'Admin.Actions'), ], + ]; + + if (Shop::isFeatureActive()) { + array_splice($this->tpl_view_vars['wizard_contents']['contents'], 1, 0, [0 => $this->renderStepTwo($carrier)]); + } + + $this->context->smarty->assign([ + 'carrier_logo' => (Validate::isLoadedObject($carrier) && file_exists(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg') ? _THEME_SHIP_DIR_ . $carrier->id . '.jpg' : false), + ]); + + $this->context->smarty->assign([ + 'logo_content' => $this->createTemplate('logo.tpl')->fetch(), + ]); + + $this->addjQueryPlugin(['ajaxfileupload']); + + return parent::renderView(); + } + + public function initBreadcrumbs($tab_id = null, $tabs = null) + { + if (Tools::getValue('id_carrier')) { + $this->display = 'edit'; + } else { + $this->display = 'add'; + } + + parent::initBreadcrumbs((int) Tab::getIdFromClassName('AdminCarriers')); + + $this->display = 'view'; + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + $this->page_header_toolbar_btn['cancel'] = [ + 'href' => $this->context->link->getAdminLink('AdminCarriers'), + 'desc' => $this->trans('Cancel', [], 'Admin.Actions'), + ]; + } + + public function renderStepOne($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_general', + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Carrier name', [], 'Admin.Shipping.Feature'), + 'name' => 'name', + 'required' => true, + 'hint' => [ + $this->trans('Allowed characters: letters, spaces and "%special_chars%".', ['%special_chars%' => '().-'], 'Admin.Shipping.Help'), + $this->trans('The carrier\'s name will be displayed during checkout.', [], 'Admin.Shipping.Help'), + $this->trans('For in-store pickup, enter 0 to replace the carrier name with your shop name.', [], 'Admin.Shipping.Help'), + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Transit time', [], 'Admin.Shipping.Feature'), + 'name' => 'delay', + 'lang' => true, + 'required' => true, + 'maxlength' => 512, + 'hint' => $this->trans('The delivery time will be displayed during checkout.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Speed grade', [], 'Admin.Shipping.Feature'), + 'name' => 'grade', + 'required' => false, + 'size' => 1, + 'hint' => $this->trans('Enter "0" for a longest shipping delay, or "9" for the shortest shipping delay.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'logo', + 'label' => $this->trans('Logo', [], 'Admin.Global'), + 'name' => 'logo', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Tracking URL', [], 'Admin.Shipping.Feature'), + 'name' => 'url', + 'hint' => $this->trans('Delivery tracking URL: Type \'@\' where the tracking number should appear. It will be automatically replaced by the tracking number.', [], 'Admin.Shipping.Help'), + 'desc' => $this->trans('For example: \'http://example.com/track.php?num=@\' with \'@\' where the tracking number should appear.', [], 'Admin.Shipping.Help'), + ], + ], + ], + ]; + + $tpl_vars = ['max_image_size' => (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE') / 1024 / 1024]; + $fields_value = $this->getStepOneFieldsValues($carrier); + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value, $tpl_vars); + } + + public function renderStepTwo($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_shops', + 'force' => true, + 'input' => [ + [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ], + ], + ], + ]; + $fields_value = $this->getStepTwoFieldsValues($carrier); + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value); + } + + public function renderStepThree($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_ranges', + 'input' => [ + 'shipping_handling' => [ + 'type' => 'switch', + 'label' => $this->trans('Add handling costs', [], 'Admin.Shipping.Feature'), + 'name' => 'shipping_handling', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'shipping_handling_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'shipping_handling_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Include the handling costs (as set in Shipping > Preferences) in the final carrier price.', [], 'Admin.Shipping.Help'), + ], + 'is_free' => [ + 'type' => 'switch', + 'label' => $this->trans('Free shipping', [], 'Admin.Shipping.Feature'), + 'name' => 'is_free', + 'required' => false, + 'class' => 't', + 'values' => [ + [ + 'id' => 'is_free_on', + 'value' => 1, + 'label' => '' . $this->trans('No', [], 'Admin.Global') . '', + ], + [ + 'id' => 'is_free_off', + 'value' => 0, + 'label' => '' . $this->trans('Yes', [], 'Admin.Global') . '', + ], + ], + ], + 'shipping_method' => [ + 'type' => 'radio', + 'label' => $this->trans('Billing', [], 'Admin.Shipping.Feature'), + 'name' => 'shipping_method', + 'required' => false, + 'class' => 't', + 'br' => true, + 'values' => [ + [ + 'id' => 'billing_price', + 'value' => Carrier::SHIPPING_METHOD_PRICE, + 'label' => $this->trans('According to total price.', [], 'Admin.Shipping.Feature'), + ], + [ + 'id' => 'billing_weight', + 'value' => Carrier::SHIPPING_METHOD_WEIGHT, + 'label' => $this->trans('According to total weight.', [], 'Admin.Shipping.Feature'), + ], + ], + ], + 'id_tax_rules_group' => [ + 'type' => 'select', + 'label' => $this->trans('Tax', [], 'Admin.Global'), + 'name' => 'id_tax_rules_group', + 'options' => [ + 'query' => TaxRulesGroup::getTaxRulesGroups(true), + 'id' => 'id_tax_rules_group', + 'name' => 'name', + 'default' => [ + 'label' => $this->trans('No tax', [], 'Admin.Global'), + 'value' => 0, + ], + ], + ], + 'range_behavior' => [ + 'type' => 'select', + 'label' => $this->trans('Out-of-range behavior', [], 'Admin.Shipping.Feature'), + 'name' => 'range_behavior', + 'options' => [ + 'query' => [ + [ + 'id' => 0, + 'name' => $this->trans('Apply the cost of the highest defined range', [], 'Admin.Shipping.Feature'), + ], + [ + 'id' => 1, + 'name' => $this->trans('Disable carrier', [], 'Admin.Shipping.Feature'), + ], + ], + 'id' => 'id', + 'name' => 'name', + ], + 'hint' => $this->trans('Out-of-range behavior occurs when no defined range matches the customer\'s cart (e.g. when the weight of the cart is greater than the highest weight limit defined by the weight ranges).', [], 'Admin.Shipping.Help'), + ], + 'zones' => [ + 'type' => 'zone', + 'name' => 'zones', + ], + ], + ], + ]; + + if (Configuration::get('PS_ATCP_SHIPWRAP')) { + unset($this->fields_form['form']['input']['id_tax_rules_group']); + } + + $tpl_vars = []; + $tpl_vars['PS_WEIGHT_UNIT'] = Configuration::get('PS_WEIGHT_UNIT'); + + $currency = $this->getActualCurrency(); + + $tpl_vars['currency_sign'] = $currency->sign; + + $fields_value = $this->getStepThreeFieldsValues($carrier); + + $this->getTplRangesVarsAndValues($carrier, $tpl_vars, $fields_value); + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value, $tpl_vars); + } + + /** + * @param Carrier $carrier + * + * @return string + */ + public function renderStepFour($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_conf', + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package width (%s)', ['%s' => Configuration::get('PS_DIMENSION_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_width', + 'required' => false, + 'hint' => $this->trans('Maximum width managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package height (%s)', ['%s' => Configuration::get('PS_DIMENSION_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_height', + 'required' => false, + 'hint' => $this->trans('Maximum height managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package depth (%s)', ['%s' => Configuration::get('PS_DIMENSION_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_depth', + 'required' => false, + 'hint' => $this->trans('Maximum depth managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package weight (%s)', ['%s' => Configuration::get('PS_WEIGHT_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_weight', + 'required' => false, + 'hint' => $this->trans('Maximum weight managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'group', + 'label' => $this->trans('Group access', [], 'Admin.Shipping.Feature'), + 'name' => 'groupBox', + 'values' => Group::getGroups(Context::getContext()->language->id), + 'hint' => $this->trans('Mark the groups that are allowed access to this carrier.', [], 'Admin.Shipping.Help'), + ], + ], + ], + ]; + + $fields_value = $this->getStepFourFieldsValues($carrier); + + // Added values of object Group + $carrier_groups = $carrier->getGroups(); + $carrier_groups_ids = []; + if (is_array($carrier_groups)) { + foreach ($carrier_groups as $carrier_group) { + $carrier_groups_ids[] = $carrier_group['id_group']; + } + } + + $groups = Group::getGroups($this->context->language->id); + + foreach ($groups as $group) { + $fields_value['groupBox_' . $group['id_group']] = Tools::getValue('groupBox_' . $group['id_group'], (in_array($group['id_group'], $carrier_groups_ids) || empty($carrier_groups_ids) && !$carrier->id)); + } + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value); + } + + public function renderStepFive($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_summary', + 'input' => [ + [ + 'type' => 'switch', + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + ], + [ + 'id' => 'active_off', + 'value' => 0, + ], + ], + 'hint' => $this->trans('Enable the carrier in the front office.', [], 'Admin.Shipping.Help'), + ], + ], + ], + ]; + $template = $this->createTemplate('controllers/carrier_wizard/summary.tpl'); + $fields_value = $this->getStepFiveFieldsValues($carrier); + $active_form = $this->renderGenericForm(['form' => $this->fields_form], $fields_value); + $active_form = str_replace(['
', '
'], '', $active_form); + $template->assign('active_form', $active_form); + + return $template->fetch(); + } + + /** + * @param Carrier $carrier + * @param array $tpl_vars + * @param array $fields_value + */ + protected function getTplRangesVarsAndValues($carrier, &$tpl_vars, &$fields_value) + { + $tpl_vars['zones'] = Zone::getZones(false, true); + $carrier_zones = $carrier->getZones(); + $carrier_zones_ids = []; + if (is_array($carrier_zones)) { + foreach ($carrier_zones as $carrier_zone) { + $carrier_zones_ids[] = $carrier_zone['id_zone']; + } + } + + $range_table = $carrier->getRangeTable(); + $shipping_method = $carrier->getShippingMethod(); + + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + $fields_value['zones'][$zone['id_zone']] = Tools::getValue('zone_' . $zone['id_zone'], (in_array($zone['id_zone'], $carrier_zones_ids))); + } + + if ($shipping_method == Carrier::SHIPPING_METHOD_FREE) { + $range_obj = $carrier->getRangeObject($carrier->shipping_method); + $price_by_range = []; + } else { + $range_obj = $carrier->getRangeObject(); + $price_by_range = Carrier::getDeliveryPriceByRanges($range_table, (int) $carrier->id); + } + + foreach ($price_by_range as $price) { + $tpl_vars['price_by_range'][$price['id_' . $range_table]][$price['id_zone']] = $price['price']; + } + + $tmp_range = $range_obj->getRanges((int) $carrier->id); + $tpl_vars['ranges'] = []; + if ($shipping_method != Carrier::SHIPPING_METHOD_FREE) { + foreach ($tmp_range as $id => $range) { + $tpl_vars['ranges'][$range['id_' . $range_table]] = $range; + $tpl_vars['ranges'][$range['id_' . $range_table]]['id_range'] = $range['id_' . $range_table]; + } + } + + // init blank range + if (!count($tpl_vars['ranges'])) { + $tpl_vars['ranges'][] = ['id_range' => 0, 'delimiter1' => 0, 'delimiter2' => 0]; + } + } + + public function renderGenericForm($fields_form, $fields_value, $tpl_vars = []) + { + $helper = new HelperForm(); + $helper->show_toolbar = false; + $helper->table = $this->table; + $lang = new Language((int) Configuration::get('PS_LANG_DEFAULT')); + $helper->default_form_language = $lang->id; + $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') ? Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') : 0; + $this->fields_form = []; + $helper->id = (int) Tools::getValue('id_carrier'); + $helper->identifier = $this->identifier; + $helper->tpl_vars = array_merge([ + 'fields_value' => $fields_value, + 'languages' => $this->getLanguages(), + 'id_language' => $this->context->language->id, + ], $tpl_vars); + $helper->override_folder = 'carrier_wizard/'; + + return $helper->generateForm($fields_form); + } + + public function getStepOneFieldsValues($carrier) + { + return [ + 'id_carrier' => $this->getFieldValue($carrier, 'id_carrier'), + 'name' => $this->getFieldValue($carrier, 'name'), + 'delay' => $this->getFieldValue($carrier, 'delay'), + 'grade' => $this->getFieldValue($carrier, 'grade'), + 'url' => $this->getFieldValue($carrier, 'url'), + ]; + } + + public function getStepTwoFieldsValues($carrier) + { + return ['shop' => $this->getFieldValue($carrier, 'shop')]; + } + + public function getStepThreeFieldsValues($carrier) + { + $id_tax_rules_group = (is_object($this->object) && !$this->object->id) ? Carrier::getIdTaxRulesGroupMostUsed() : Carrier::getIdTaxRulesGroupByIdCarrier($this->object->id); + + $shipping_handling = (is_object($this->object) && !$this->object->id) ? 0 : $this->getFieldValue($carrier, 'shipping_handling'); + + return [ + 'is_free' => $this->getFieldValue($carrier, 'is_free'), + 'id_tax_rules_group' => (int) $id_tax_rules_group, + 'shipping_handling' => $shipping_handling, + 'shipping_method' => $this->getFieldValue($carrier, 'shipping_method'), + 'range_behavior' => $this->getFieldValue($carrier, 'range_behavior'), + 'zones' => $this->getFieldValue($carrier, 'zones'), + ]; + } + + public function getStepFourFieldsValues($carrier) + { + return [ + 'range_behavior' => $this->getFieldValue($carrier, 'range_behavior'), + 'max_height' => $this->getFieldValue($carrier, 'max_height'), + 'max_width' => $this->getFieldValue($carrier, 'max_width'), + 'max_depth' => $this->getFieldValue($carrier, 'max_depth'), + 'max_weight' => $this->getFieldValue($carrier, 'max_weight'), + 'group' => $this->getFieldValue($carrier, 'group'), + ]; + } + + public function getStepFiveFieldsValues($carrier) + { + return ['active' => $this->getFieldValue($carrier, 'active')]; + } + + public function ajaxProcessChangeRanges() + { + if ((Validate::isLoadedObject($this->object) && !$this->access('edit')) || !$this->access('add')) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', [], 'Admin.Shipping.Notification'); + + return; + } + if ((!(int) $shipping_method = Tools::getValue('shipping_method')) || !in_array($shipping_method, [Carrier::SHIPPING_METHOD_PRICE, Carrier::SHIPPING_METHOD_WEIGHT])) { + return; + } + + $carrier = $this->loadObject(true); + $carrier->shipping_method = $shipping_method; + + $tpl_vars = []; + $fields_value = $this->getStepThreeFieldsValues($carrier); + $this->getTplRangesVarsAndValues($carrier, $tpl_vars, $fields_value); + $template = $this->createTemplate('controllers/carrier_wizard/helpers/form/form_ranges.tpl'); + $template->assign($tpl_vars); + $template->assign('change_ranges', 1); + + $template->assign('fields_value', $fields_value); + $template->assign('input', ['type' => 'zone', 'name' => 'zones']); + + $currency = $this->getActualCurrency(); + + $template->assign('currency_sign', $currency->sign); + $template->assign('PS_WEIGHT_UNIT', Configuration::get('PS_WEIGHT_UNIT')); + + die($template->fetch()); + } + + protected function validateForm($die = true) + { + $step_number = (int) Tools::getValue('step_number'); + $return = ['has_error' => false]; + + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', [], 'Admin.Shipping.Notification'); + } else { + if (Shop::isFeatureActive() && $step_number == 2) { + if (!Tools::getValue('checkBoxShopAsso_carrier')) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('You must choose at least one shop or group shop.', [], 'Admin.Shipping.Notification'); + } + } else { + $this->validateRules(); + } + } + + if (count($this->errors)) { + $return['has_error'] = true; + $return['errors'] = $this->errors; + } + if (count($this->errors) || $die) { + die(json_encode($return)); + } + } + + public function ajaxProcessValidateStep() + { + $this->validateForm(true); + } + + public function processRanges($id_carrier) + { + if (!$this->access('edit') || !$this->access('add')) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', [], 'Admin.Shipping.Notification'); + + return; + } + + $carrier = new Carrier((int) $id_carrier); + if (!Validate::isLoadedObject($carrier)) { + return false; + } + + $range_inf = Tools::getValue('range_inf'); + $range_sup = Tools::getValue('range_sup'); + $range_type = Tools::getValue('shipping_method'); + + $fees = Tools::getValue('fees'); + + $carrier->deleteDeliveryPrice($carrier->getRangeTable()); + if ($range_type != Carrier::SHIPPING_METHOD_FREE) { + foreach ($range_inf as $key => $delimiter1) { + if (!isset($range_sup[$key])) { + continue; + } + $range = $carrier->getRangeObject((int) $range_type); + $range->id_carrier = (int) $carrier->id; + $range->delimiter1 = (float) $delimiter1; + $range->delimiter2 = (float) $range_sup[$key]; + $range->save(); + + if (!Validate::isLoadedObject($range)) { + return false; + } + $price_list = []; + if (is_array($fees) && count($fees)) { + foreach ($fees as $id_zone => $fee) { + $price_list[] = [ + 'id_range_price' => ($range_type == Carrier::SHIPPING_METHOD_PRICE ? (int) $range->id : null), + 'id_range_weight' => ($range_type == Carrier::SHIPPING_METHOD_WEIGHT ? (int) $range->id : null), + 'id_carrier' => (int) $carrier->id, + 'id_zone' => (int) $id_zone, + 'price' => isset($fee[$key]) ? (float) str_replace(',', '.', $fee[$key]) : 0, + ]; + } + } + + if (count($price_list) && !$carrier->addDeliveryPrice($price_list, true)) { + return false; + } + } + } + + return true; + } + + public function ajaxProcessUploadLogo() + { + if (!$this->access('edit')) { + die(''); + } + + $allowedExtensions = ['jpeg', 'gif', 'png', 'jpg']; + + $logo = (isset($_FILES['carrier_logo_input']) ? $_FILES['carrier_logo_input'] : false); + if ($logo && !empty($logo['tmp_name']) && $logo['tmp_name'] != 'none' + && (!isset($logo['error']) || !$logo['error']) + && preg_match('/\.(jpe?g|gif|png)$/', $logo['name']) + && is_uploaded_file($logo['tmp_name']) + && ImageManager::isRealImage($logo['tmp_name'], $logo['type'])) { + $file = $logo['tmp_name']; + do { + $tmp_name = uniqid() . '.jpg'; + } while (file_exists(_PS_TMP_IMG_DIR_ . $tmp_name)); + if (!ImageManager::resize($file, _PS_TMP_IMG_DIR_ . $tmp_name)) { + die(''); + } + @unlink($file); + die(''); + } else { + die(''); + } + } + + public function ajaxProcessFinishStep() + { + $return = ['has_error' => false]; + if (!$this->access('edit')) { + $return = [ + 'has_error' => true, + $return['errors'][] = $this->trans('You do not have permission to use this wizard.', [], 'Admin.Shipping.Notification'), + ]; + } else { + $this->validateForm(false); + if ($id_carrier = Tools::getValue('id_carrier')) { + $current_carrier = new Carrier((int) $id_carrier); + + // if update we duplicate current Carrier + /** @var Carrier $new_carrier */ + $new_carrier = $current_carrier->duplicateObject(); + + if (Validate::isLoadedObject($new_carrier)) { + // Set flag deteled to true for historization + $current_carrier->deleted = true; + $current_carrier->update(); + + // Fill the new carrier object + $this->copyFromPost($new_carrier, $this->table); + $new_carrier->position = $current_carrier->position; + $new_carrier->update(); + + $this->updateAssoShop((int) $new_carrier->id); + $this->duplicateLogo((int) $new_carrier->id, (int) $current_carrier->id); + $this->changeGroups((int) $new_carrier->id); + + //Copy default carrier + if (Configuration::get('PS_CARRIER_DEFAULT') == $current_carrier->id) { + Configuration::updateValue('PS_CARRIER_DEFAULT', (int) $new_carrier->id); + } + + // Call of hooks + Hook::exec('actionCarrierUpdate', [ + 'id_carrier' => (int) $current_carrier->id, + 'carrier' => $new_carrier, + ]); + $this->postImage($new_carrier->id); + $this->changeZones($new_carrier->id); + $new_carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group')); + $carrier = $new_carrier; + } + } else { + $carrier = new Carrier(); + $this->copyFromPost($carrier, $this->table); + if (!$carrier->add()) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving this carrier.', [], 'Admin.Shipping.Notification'); + } + } + + if ($carrier->is_free) { + //if carrier is free delete shipping cost + $carrier->deleteDeliveryPrice('range_weight'); + $carrier->deleteDeliveryPrice('range_price'); + } + + if (Validate::isLoadedObject($carrier)) { + if (!$this->changeGroups((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier groups.', [], 'Admin.Shipping.Notification'); + } + + if (!$this->changeZones((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier zones.', [], 'Admin.Shipping.Notification'); + } + + if (!$carrier->is_free) { + if (!$this->processRanges((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier ranges.', [], 'Admin.Shipping.Notification'); + } + } + + if (Shop::isFeatureActive() && !$this->updateAssoShop((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving associations of shops.', [], 'Admin.Shipping.Notification'); + } + + if (!$carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group'))) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving the tax rules group.', [], 'Admin.Shipping.Notification'); + } + + if (Tools::getValue('logo')) { + if (Tools::getValue('logo') == 'null' && file_exists(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg')) { + unlink(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg'); + } else { + $logo = basename(Tools::getValue('logo')); + if (!file_exists(_PS_TMP_IMG_DIR_ . $logo) || !copy(_PS_TMP_IMG_DIR_ . $logo, _PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg')) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier logo.', [], 'Admin.Shipping.Notification'); + } + } + } + $return['id_carrier'] = $carrier->id; + } + } + die(json_encode($return)); + } + + protected function changeGroups($id_carrier, $delete = true) + { + $carrier = new Carrier((int) $id_carrier); + if (!Validate::isLoadedObject($carrier)) { + return false; + } + + return $carrier->setGroups(Tools::getValue('groupBox')); + } + + public function changeZones($id) + { + $return = true; + $carrier = new Carrier($id); + if (!Validate::isLoadedObject($carrier)) { + die($this->trans('The object cannot be loaded.', [], 'Admin.Notifications.Error')); + } + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + if (count($carrier->getZone($zone['id_zone']))) { + if (!isset($_POST['zone_' . $zone['id_zone']]) || !$_POST['zone_' . $zone['id_zone']]) { + $return &= $carrier->deleteZone((int) $zone['id_zone']); + } + } elseif (isset($_POST['zone_' . $zone['id_zone']]) && $_POST['zone_' . $zone['id_zone']]) { + $return &= $carrier->addZone((int) $zone['id_zone']); + } + } + + return $return; + } + + public function getValidationRules() + { + $step_number = (int) Tools::getValue('step_number'); + if (!$step_number) { + return; + } + + if ($step_number == 4 && !Shop::isFeatureActive() || $step_number == 5 && Shop::isFeatureActive()) { + return ['fields' => []]; + } + + $step_fields = [ + 1 => ['name', 'delay', 'grade', 'url'], + 2 => ['is_free', 'id_tax_rules_group', 'shipping_handling', 'shipping_method', 'range_behavior'], + 3 => ['range_behavior', 'max_height', 'max_width', 'max_depth', 'max_weight'], + 4 => [], + ]; + if (Shop::isFeatureActive()) { + $tmp = $step_fields; + $step_fields = array_slice($tmp, 0, 1, true) + [2 => ['shop']]; + $step_fields[3] = $tmp[2]; + $step_fields[4] = $tmp[3]; + } + + $definition = ObjectModel::getDefinition('Carrier'); + foreach ($definition['fields'] as $field => $def) { + if (is_array($step_fields[$step_number]) && !in_array($field, $step_fields[$step_number])) { + unset($definition['fields'][$field]); + } + } + + return $definition; + } + + public static function displayFieldName($field) + { + return $field; + } + + public function duplicateLogo($new_id, $old_id) + { + $old_logo = _PS_SHIP_IMG_DIR_ . '/' . (int) $old_id . '.jpg'; + if (file_exists($old_logo)) { + copy($old_logo, _PS_SHIP_IMG_DIR_ . '/' . (int) $new_id . '.jpg'); + } + + $old_tmp_logo = _PS_TMP_IMG_DIR_ . '/carrier_mini_' . (int) $old_id . '.jpg'; + if (file_exists($old_tmp_logo)) { + if (!isset($_FILES['logo'])) { + copy($old_tmp_logo, _PS_TMP_IMG_DIR_ . '/carrier_mini_' . $new_id . '.jpg'); + } + unlink($old_tmp_logo); + } + } + + public function getActualCurrency() + { + if ($this->type_context == Shop::CONTEXT_SHOP) { + Shop::setContext($this->type_context, $this->old_context->shop->id); + } elseif ($this->type_context == Shop::CONTEXT_GROUP) { + Shop::setContext($this->type_context, $this->old_context->shop->id_shop_group); + } + + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + + Shop::setContext(Shop::CONTEXT_ALL); + + return $currency; + } +} diff --git a/controllers/admin/AdminCarriersController.php b/controllers/admin/AdminCarriersController.php new file mode 100644 index 00000000..a1e943a8 --- /dev/null +++ b/controllers/admin/AdminCarriersController.php @@ -0,0 +1,737 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Carrier $object + */ +class AdminCarriersControllerCore extends AdminController +{ + protected $position_identifier = 'id_carrier'; + + public function __construct() + { + if ($id_carrier = Tools::getValue('id_carrier') && !Tools::isSubmit('deletecarrier') && !Tools::isSubmit('statuscarrier') && !Tools::isSubmit('isFreecarrier')) { + Tools::redirectAdmin(Context::getContext()->link->getAdminLink('AdminCarrierWizard', true, [], ['id_carrier' => (int) $id_carrier])); + } + + $this->bootstrap = true; + $this->table = 'carrier'; + $this->className = 'Carrier'; + $this->lang = false; + $this->deleted = true; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_defaultOrderBy = 'position'; + + parent::__construct(); + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Notifications.Info'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + ], + ]; + + $this->fieldImageSettings = [ + 'name' => 'logo', + 'dir' => 's', + ]; + + $this->fields_list = [ + 'id_carrier' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + ], + 'image' => [ + 'title' => $this->trans('Logo', [], 'Admin.Global'), + 'align' => 'center', + 'image' => 's', + 'class' => 'fixed-width-xs', + 'orderby' => false, + 'search' => false, + ], + 'delay' => [ + 'title' => $this->trans('Delay', [], 'Admin.Shipping.Feature'), + 'orderby' => false, + ], + 'active' => [ + 'title' => $this->trans('Status', [], 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'class' => 'fixed-width-sm', + 'orderby' => false, + ], + 'is_free' => [ + 'title' => $this->trans('Free Shipping', [], 'Admin.Shipping.Feature'), + 'align' => 'center', + 'active' => 'isFree', + 'type' => 'bool', + 'class' => 'fixed-width-sm', + 'orderby' => false, + ], + 'position' => [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'position' => 'position', + ], + ]; + } + + public function initToolbar() + { + parent::initToolbar(); + + if (isset($this->toolbar_btn['new']) && $this->display != 'view') { + $this->toolbar_btn['new']['href'] = $this->context->link->getAdminLink('AdminCarrierWizard'); + } + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_title = $this->trans('Carriers', [], 'Admin.Shipping.Feature'); + if ($this->display != 'view') { + $this->page_header_toolbar_btn['new_carrier'] = [ + 'href' => $this->context->link->getAdminLink('AdminCarrierWizard'), + 'desc' => $this->trans('Add new carrier', [], 'Admin.Shipping.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->_select = 'b.*'; + $this->_join = 'INNER JOIN `' . _DB_PREFIX_ . 'carrier_lang` b ON a.id_carrier = b.id_carrier' . Shop::addSqlRestrictionOnLang('b') . ' AND b.id_lang = ' . (int) $this->context->language->id . ' LEFT JOIN `' . _DB_PREFIX_ . 'carrier_tax_rules_group_shop` ctrgs ON (a.`id_carrier` = ctrgs.`id_carrier` AND ctrgs.id_shop=' . (int) $this->context->shop->id . ')'; + $this->_use_found_rows = false; + + // Removes the Recommended modules button + unset($this->page_header_toolbar_btn['modules-list']); + + // test if need to show header alert. + $sql = 'SELECT COUNT(1) FROM `' . _DB_PREFIX_ . 'carrier` WHERE deleted = 0 AND id_reference > 2'; + $showHeaderAlert = (Db::getInstance()->executeS($sql, false)->fetchColumn(0) == 0); + + // Assign them in two steps! Because renderModulesList needs it before to be called. + $this->context->smarty->assign('panel_title', $this->trans('Use one of our recommended carrier modules', [], 'Admin.Shipping.Feature')); + $this->context->smarty->assign('panel_id', 'recommended-carriers-panel'); + + $this->context->smarty->assign([ + 'showHeaderAlert' => $showHeaderAlert, + 'modules_list' => $this->renderModulesList('back-office,AdminCarriers,new'), + ]); + + return parent::renderList(); + } + + public function renderForm() + { + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Carriers', [], 'Admin.Shipping.Feature'), + 'icon' => 'icon-truck', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Company', [], 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'hint' => [ + $this->trans('Allowed characters: letters, spaces and "%special_chars%".', ['%special_chars%' => '().-'], 'Admin.Shipping.Help'), + $this->trans('Carrier name displayed during checkout', [], 'Admin.Shipping.Help'), + $this->trans('For in-store pickup, enter 0 to replace the carrier name with your shop name.', [], 'Admin.Shipping.Help'), + ], + ], + [ + 'type' => 'file', + 'label' => $this->trans('Logo', [], 'Admin.Global'), + 'name' => 'logo', + 'hint' => $this->trans('Upload a logo from your computer.', [], 'Admin.Shipping.Help') . ' (.gif, .jpg, .jpeg ' . $this->trans('or', [], 'Admin.Shipping.Help') . ' .png)', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Transit time', [], 'Admin.Shipping.Feature'), + 'name' => 'delay', + 'lang' => true, + 'required' => true, + 'maxlength' => 512, + 'hint' => $this->trans('Estimated delivery time will be displayed during checkout.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Speed grade', [], 'Admin.Shipping.Feature'), + 'name' => 'grade', + 'required' => false, + 'hint' => $this->trans('Enter "0" for a longest shipping delay, or "9" for the shortest shipping delay.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('URL', [], 'Admin.Global'), + 'name' => 'url', + 'hint' => $this->trans('Delivery tracking URL: Type \'@\' where the tracking number should appear. It will then be automatically replaced by the tracking number.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'checkbox', + 'label' => $this->trans('Zone', [], 'Admin.Global'), + 'name' => 'zone', + 'values' => [ + 'query' => Zone::getZones(false), + 'id' => 'id_zone', + 'name' => 'name', + ], + 'hint' => $this->trans('The zones in which this carrier will be used.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'group', + 'label' => $this->trans('Group access', [], 'Admin.Shipping.Help'), + 'name' => 'groupBox', + 'values' => Group::getGroups(Context::getContext()->language->id), + 'hint' => $this->trans('Mark the groups that are allowed access to this carrier.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Status', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Enable the carrier in the front office.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Apply shipping cost', [], 'Admin.Shipping.Feature'), + 'name' => 'is_free', + 'required' => false, + 'class' => 't', + 'values' => [ + [ + 'id' => 'is_free_on', + 'value' => 0, + 'label' => '' . $this->trans('Yes', [], 'Admin.Global') . '', + ], + [ + 'id' => 'is_free_off', + 'value' => 1, + 'label' => '' . $this->trans('No', [], 'Admin.Global') . '', + ], + ], + 'hint' => $this->trans('Apply both regular shipping cost and product-specific shipping costs.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Tax', [], 'Admin.Global'), + 'name' => 'id_tax_rules_group', + 'options' => [ + 'query' => TaxRulesGroup::getTaxRulesGroups(true), + 'id' => 'id_tax_rules_group', + 'name' => 'name', + 'default' => [ + 'label' => $this->trans('No Tax', [], 'Admin.Global'), + 'value' => 0, + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Shipping and handling', [], 'Admin.Shipping.Feature'), + 'name' => 'shipping_handling', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'shipping_handling_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'shipping_handling_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Include the shipping and handling costs in the carrier price.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'radio', + 'label' => $this->trans('Billing', [], 'Admin.Shipping.Feature'), + 'name' => 'shipping_method', + 'required' => false, + 'class' => 't', + 'br' => true, + 'values' => [ + [ + 'id' => 'billing_default', + 'value' => Carrier::SHIPPING_METHOD_DEFAULT, + 'label' => $this->trans('Default behavior', [], 'Admin.Shipping.Feature'), + ], + [ + 'id' => 'billing_price', + 'value' => Carrier::SHIPPING_METHOD_PRICE, + 'label' => $this->trans('According to total price', [], 'Admin.Shipping.Feature'), + ], + [ + 'id' => 'billing_weight', + 'value' => Carrier::SHIPPING_METHOD_WEIGHT, + 'label' => $this->trans('According to total weight', [], 'Admin.Shipping.Feature'), + ], + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Out-of-range behavior', [], 'Admin.Shipping.Feature'), + 'name' => 'range_behavior', + 'options' => [ + 'query' => [ + [ + 'id' => 0, + 'name' => $this->trans('Apply the cost of the highest defined range', [], 'Admin.Shipping.Help'), + ], + [ + 'id' => 1, + 'name' => $this->trans('Disable carrier', [], 'Admin.Shipping.Feature'), + ], + ], + 'id' => 'id', + 'name' => 'name', + ], + 'hint' => $this->trans('Out-of-range behavior occurs when none is defined (e.g. when a customer\'s cart weight is greater than the highest range limit).', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package height', [], 'Admin.Shipping.Feature'), + 'name' => 'max_height', + 'required' => false, + 'hint' => $this->trans('Maximum height managed by this carrier. Set the value to "0," or leave this field blank to ignore.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package width', [], 'Admin.Shipping.Feature'), + 'name' => 'max_width', + 'required' => false, + 'hint' => $this->trans('Maximum width managed by this carrier. Set the value to "0," or leave this field blank to ignore.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package depth', [], 'Admin.Shipping.Feature'), + 'name' => 'max_depth', + 'required' => false, + 'hint' => $this->trans('Maximum depth managed by this carrier. Set the value to "0," or leave this field blank to ignore.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package weight', [], 'Admin.Shipping.Feature'), + 'name' => 'max_weight', + 'required' => false, + 'hint' => $this->trans('Maximum weight managed by this carrier. Set the value to "0," or leave this field blank to ignore.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'hidden', + 'name' => 'is_module', + ], + [ + 'type' => 'hidden', + 'name' => 'external_module_name', + ], + [ + 'type' => 'hidden', + 'name' => 'shipping_external', + ], + [ + 'type' => 'hidden', + 'name' => 'need_range', + ], + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->getFieldsValues($obj); + + return parent::renderForm(); + } + + public function postProcess() + { + if (Tools::getValue('action') == 'GetModuleQuickView' && Tools::getValue('ajax') == '1') { + $this->ajaxProcessGetModuleQuickView(); + } + + if (Tools::getValue('submitAdd' . $this->table)) { + /* Checking fields validity */ + $this->validateRules(); + if (!count($this->errors)) { + $id = (int) Tools::getValue('id_' . $this->table); + + /* Object update */ + if (isset($id) && !empty($id)) { + try { + if ($this->access('edit')) { + $current_carrier = new Carrier($id); + if (!Validate::isLoadedObject($current_carrier)) { + throw new PrestaShopException('Cannot load Carrier object'); + } + + /** @var Carrier $new_carrier */ + // Duplicate current Carrier + $new_carrier = $current_carrier->duplicateObject(); + if (Validate::isLoadedObject($new_carrier)) { + // Set flag deteled to true for historization + $current_carrier->deleted = true; + $current_carrier->update(); + + // Fill the new carrier object + $this->copyFromPost($new_carrier, $this->table); + $new_carrier->position = $current_carrier->position; + $new_carrier->update(); + + $this->updateAssoShop($new_carrier->id); + $new_carrier->copyCarrierData((int) $current_carrier->id); + $this->changeGroups($new_carrier->id); + // Call of hooks + Hook::exec('actionCarrierUpdate', [ + 'id_carrier' => (int) $current_carrier->id, + 'carrier' => $new_carrier, + ]); + $this->postImage($new_carrier->id); + $this->changeZones($new_carrier->id); + $new_carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group')); + Tools::redirectAdmin(self::$currentIndex . '&id_' . $this->table . '=' . $current_carrier->id . '&conf=4&token=' . $this->token); + } else { + $this->errors[] = $this->trans('An error occurred while updating an object.', [], 'Admin.Notifications.Error') . ' ' . $this->table . ''; + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } catch (PrestaShopException $e) { + $this->errors[] = $e->getMessage(); + } + } else { + // Object creation + if ($this->access('add')) { + // Create new Carrier + $carrier = new Carrier(); + $this->copyFromPost($carrier, $this->table); + $carrier->position = Carrier::getHigherPosition() + 1; + if ($carrier->add()) { + if (($_POST['id_' . $this->table] = $carrier->id /* voluntary */) && $this->postImage($carrier->id) && $this->_redirect) { + $carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group'), true); + $this->changeZones($carrier->id); + $this->changeGroups($carrier->id); + $this->updateAssoShop($carrier->id); + Tools::redirectAdmin(self::$currentIndex . '&id_' . $this->table . '=' . $carrier->id . '&conf=3&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('An error occurred while creating an object.', [], 'Admin.Notifications.Error') . ' ' . $this->table . ''; + } + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } + } + parent::postProcess(); + } elseif (isset($_GET['isFree' . $this->table])) { + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + + return; + } + + $this->processIsFree(); + } else { + // if deletion : removes the carrier from the warehouse/carrier association + if (Tools::isSubmit('delete' . $this->table)) { + $id = (int) Tools::getValue('id_' . $this->table); + // Delete from the reference_id and not from the carrier id + $carrier = new Carrier((int) $id); + Warehouse::removeCarrier($carrier->id_reference); + } elseif (Tools::isSubmit($this->table . 'Box') && count(Tools::getValue($this->table . 'Box', [])) > 0) { + $ids = Tools::getValue($this->table . 'Box'); + array_walk($ids, 'intval'); + foreach ($ids as $id) { + // Delete from the reference_id and not from the carrier id + $carrier = new Carrier((int) $id); + Warehouse::removeCarrier($carrier->id_reference); + } + } + parent::postProcess(); + Carrier::cleanPositions(); + } + } + + public function processIsFree() + { + $carrier = new Carrier($this->id_object); + if (!Validate::isLoadedObject($carrier)) { + $this->errors[] = $this->trans('An error occurred while updating carrier information.', [], 'Admin.Shipping.Notification'); + } + $carrier->is_free = $carrier->is_free ? 0 : 1; + if (!$carrier->update()) { + $this->errors[] = $this->trans('An error occurred while updating carrier information.', [], 'Admin.Shipping.Notification'); + } + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + + /** + * Overload the property $fields_value. + * + * @param object $obj + */ + public function getFieldsValues($obj) + { + if ($this->getFieldValue($obj, 'is_module')) { + $this->fields_value['is_module'] = 1; + } + + if ($this->getFieldValue($obj, 'shipping_external')) { + $this->fields_value['shipping_external'] = 1; + } + + if ($this->getFieldValue($obj, 'need_range')) { + $this->fields_value['need_range'] = 1; + } + // Added values of object Zone + $carrier_zones = $obj->getZones(); + $carrier_zones_ids = []; + if (is_array($carrier_zones)) { + foreach ($carrier_zones as $carrier_zone) { + $carrier_zones_ids[] = $carrier_zone['id_zone']; + } + } + + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + $this->fields_value['zone_' . $zone['id_zone']] = Tools::getValue('zone_' . $zone['id_zone'], (in_array($zone['id_zone'], $carrier_zones_ids))); + } + + // Added values of object Group + $carrier_groups = $obj->getGroups(); + $carrier_groups_ids = []; + if (is_array($carrier_groups)) { + foreach ($carrier_groups as $carrier_group) { + $carrier_groups_ids[] = $carrier_group['id_group']; + } + } + + $groups = Group::getGroups($this->context->language->id); + + foreach ($groups as $group) { + $this->fields_value['groupBox_' . $group['id_group']] = Tools::getValue('groupBox_' . $group['id_group'], (in_array($group['id_group'], $carrier_groups_ids) || empty($carrier_groups_ids) && !$obj->id)); + } + + $this->fields_value['id_tax_rules_group'] = $this->object->getIdTaxRulesGroup($this->context); + } + + /** + * @param Carrier $object + * + * @return int + */ + protected function beforeDelete($object) + { + return $object->isUsed(); + } + + protected function changeGroups($id_carrier, $delete = true) + { + if ($delete) { + Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'carrier_group WHERE id_carrier = ' . (int) $id_carrier); + } + $groups = Db::getInstance()->executeS('SELECT id_group FROM `' . _DB_PREFIX_ . 'group`'); + foreach ($groups as $group) { + if (Tools::getIsset('groupBox') && in_array($group['id_group'], Tools::getValue('groupBox'))) { + Db::getInstance()->execute(' + INSERT INTO ' . _DB_PREFIX_ . 'carrier_group (id_group, id_carrier) + VALUES(' . (int) $group['id_group'] . ',' . (int) $id_carrier . ') + '); + } + } + } + + public function changeZones($id) + { + /** @var Carrier $carrier */ + $carrier = new $this->className($id); + if (!Validate::isLoadedObject($carrier)) { + die($this->trans('The object cannot be loaded.', [], 'Admin.Notifications.Error')); + } + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + if (count($carrier->getZone($zone['id_zone']))) { + if (!isset($_POST['zone_' . $zone['id_zone']]) || !$_POST['zone_' . $zone['id_zone']]) { + $carrier->deleteZone($zone['id_zone']); + } + } elseif (isset($_POST['zone_' . $zone['id_zone']]) && $_POST['zone_' . $zone['id_zone']]) { + $carrier->addZone($zone['id_zone']); + } + } + } + + /** + * Modifying initial getList method to display position feature (drag and drop). + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + foreach ($this->_list as $key => $list) { + if ($list['name'] == '0') { + $this->_list[$key]['name'] = Carrier::getCarrierNameFromShopName(); + } + } + } + + public function ajaxProcessUpdatePositions() + { + $way = (int) (Tools::getValue('way')); + $id_carrier = (int) (Tools::getValue('id')); + $positions = Tools::getValue($this->table); + + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_carrier) { + if ($carrier = new Carrier((int) $pos[2])) { + if (isset($position) && $carrier->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for carrier ' . (int) $pos[1] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update carrier ' . (int) $id_carrier . ' to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "This carrier (' . (int) $id_carrier . ') can t be loaded"}'; + } + + break; + } + } + } + + public function displayEditLink($token, $id, $name = null) + { + if ($this->access('edit')) { + $tpl = $this->createTemplate('helpers/list/list_action_edit.tpl'); + if (!array_key_exists('Edit', self::$cache_lang)) { + self::$cache_lang['Edit'] = $this->trans('Edit', [], 'Admin.Actions'); + } + + $tpl->assign([ + 'href' => $this->context->link->getAdminLink('AdminCarrierWizard', true, [], ['id_carrier' => (int) $id]), + 'action' => self::$cache_lang['Edit'], + 'id' => $id, + ]); + + return $tpl->fetch(); + } else { + return; + } + } + + public function displayDeleteLink($token, $id, $name = null) + { + if ($this->access('delete')) { + $tpl = $this->createTemplate('helpers/list/list_action_delete.tpl'); + + if (!array_key_exists('Delete', self::$cache_lang)) { + self::$cache_lang['Delete'] = $this->trans('Delete', [], 'Admin.Actions'); + } + + if (!array_key_exists('DeleteItem', self::$cache_lang)) { + self::$cache_lang['DeleteItem'] = $this->trans('Delete selected item?', [], 'Admin.Notifications.Info'); + } + + if (!array_key_exists('Name', self::$cache_lang)) { + self::$cache_lang['Name'] = $this->trans('Name:', [], 'Admin.Shipping.Feature'); + } + + if (null !== $name) { + $name = '\n\n' . self::$cache_lang['Name'] . ' ' . $name; + } + + $data = [ + $this->identifier => $id, + 'href' => $this->context->link->getAdminLink('AdminCarriers', true, [], ['id_carrier' => (int) $id, 'deletecarrier' => 1]), + 'action' => self::$cache_lang['Delete'], + ]; + + if ($this->specificConfirmDelete !== false) { + $data['confirm'] = null !== $this->specificConfirmDelete ? '\r' . $this->specificConfirmDelete : addcslashes(Tools::htmlentitiesDecodeUTF8(self::$cache_lang['DeleteItem'] . $name), '\''); + } + + $tpl->assign(array_merge($this->tpl_delete_link_vars, $data)); + + return $tpl->fetch(); + } else { + return; + } + } +} diff --git a/controllers/admin/AdminCartRulesController.php b/controllers/admin/AdminCartRulesController.php new file mode 100644 index 00000000..7a17fa31 --- /dev/null +++ b/controllers/admin/AdminCartRulesController.php @@ -0,0 +1,761 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property CartRule $object + */ +class AdminCartRulesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'cart_rule'; + $this->className = 'CartRule'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->_orderWay = 'DESC'; + + parent::__construct(); + + $this->bulk_actions = ['delete' => ['text' => $this->trans('Delete selected', [], 'Admin.Actions'), 'icon' => 'icon-trash', 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning')]]; + + $this->fields_list = [ + 'id_cart_rule' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'], + 'name' => ['title' => $this->trans('Name', [], 'Admin.Global')], + 'priority' => ['title' => $this->trans('Priority', [], 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'], + 'code' => ['title' => $this->trans('Code', [], 'Admin.Global'), 'class' => 'fixed-width-sm'], + 'quantity' => ['title' => $this->trans('Quantity', [], 'Admin.Catalog.Feature'), 'align' => 'center', 'class' => 'fixed-width-xs'], + 'date_to' => ['title' => $this->trans('Expiration date', [], 'Admin.Catalog.Feature'), 'type' => 'datetime', 'class' => 'fixed-width-lg'], + 'active' => ['title' => $this->trans('Status', [], 'Admin.Global'), 'active' => 'status', 'type' => 'bool', 'align' => 'center', 'class' => 'fixed-width-xs', 'orderby' => false], + ]; + } + + public function ajaxProcessLoadCartRules() + { + if (!$this->access('view')) { + return die(json_encode(['error' => 'You do not have the right permission'])); + } + + $type = $token = $search = ''; + $limit = $count = $id_cart_rule = 0; + if (Tools::getIsset('limit')) { + $limit = Tools::getValue('limit'); + } + + if (Tools::getIsset('type')) { + $type = Tools::getValue('type'); + } + + if (Tools::getIsset('count')) { + $count = Tools::getValue('count'); + } + + if (Tools::getIsset('id_cart_rule')) { + $id_cart_rule = Tools::getValue('id_cart_rule'); + } + + if (Tools::getIsset('search')) { + $search = Tools::getValue('search'); + } + + $page = floor($count / $limit); + + $html = ''; + $next_link = ''; + + if (($page * $limit) + 1 == $count || $count == 0) { + if ($count == 0) { + $count = 1; + } + + /** @var CartRule $current_object */ + $current_object = $this->loadObject(true); + $cart_rules = $current_object->getAssociatedRestrictions('cart_rule', false, true, ($page) * $limit, $limit, $search); + + if ($type == 'selected') { + $i = 1; + foreach ($cart_rules['selected'] as $cart_rule) { + $html .= ''; + if ($i == $limit) { + break; + } + ++$i; + } + if ($i == $limit) { + $next_link = Context::getContext()->link->getAdminLink('AdminCartRules') . '&ajaxMode=1&ajax=1&id_cart_rule=' . (int) $id_cart_rule . '&action=loadCartRules&limit=' . (int) $limit . '&type=selected&count=' . ($count - 1 + count($cart_rules['selected']) . '&search=' . urlencode($search)); + } + } else { + $i = 1; + foreach ($cart_rules['unselected'] as $cart_rule) { + $html .= ''; + if ($i == $limit) { + break; + } + ++$i; + } + if ($i == $limit) { + $next_link = Context::getContext()->link->getAdminLink('AdminCartRules') . '&ajaxMode=1&ajax=1&id_cart_rule=' . (int) $id_cart_rule . '&action=loadCartRules&limit=' . (int) $limit . '&type=unselected&count=' . ($count - 1 + count($cart_rules['unselected']) . '&search=' . urlencode($search)); + } + } + } + echo json_encode(['html' => $html, 'next_link' => $next_link]); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin(['typewatch', 'fancybox', 'autocomplete']); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_cart_rule'] = [ + 'href' => self::$currentIndex . '&addcart_rule&token=' . $this->token, + 'desc' => $this->trans('Add new cart rule', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitAddcart_rule') || Tools::isSubmit('submitAddcart_ruleAndStay')) { + // If the reduction is associated to a specific product, then it must be part of the product restrictions + if ((int) Tools::getValue('reduction_product') && Tools::getValue('apply_discount_to') == 'specific' && Tools::getValue('apply_discount') != 'off') { + $reduction_product = (int) Tools::getValue('reduction_product'); + + // First, check if it is not already part of the restrictions + $already_restricted = false; + if (is_array($rule_group_array = Tools::getValue('product_rule_group')) && count($rule_group_array) && Tools::getValue('product_restriction')) { + foreach ($rule_group_array as $rule_group_id) { + if (is_array($rule_array = Tools::getValue('product_rule_' . $rule_group_id)) && count($rule_array)) { + foreach ($rule_array as $rule_id) { + if (Tools::getValue('product_rule_' . $rule_group_id . '_' . $rule_id . '_type') == 'products' + && in_array($reduction_product, Tools::getValue('product_rule_select_' . $rule_group_id . '_' . $rule_id))) { + $already_restricted = true; + + break 2; + } + } + } + } + } + if ($already_restricted == false) { + // Check the product restriction + $_POST['product_restriction'] = 1; + + // Add a new rule group + $rule_group_id = 1; + if (is_array($rule_group_array)) { + // Empty for (with a ; at the end), that just find the first rule_group_id available in rule_group_array + for ($rule_group_id = 1; in_array($rule_group_id, $rule_group_array); ++$rule_group_id) { + 42; + } + $_POST['product_rule_group'][] = $rule_group_id; + } else { + $_POST['product_rule_group'] = [$rule_group_id]; + } + + // Set a quantity of 1 for this new rule group + $_POST['product_rule_group_' . $rule_group_id . '_quantity'] = 1; + // Add one rule to the new rule group + $_POST['product_rule_' . $rule_group_id] = [1]; + // Set a type 'product' for this 1 rule + $_POST['product_rule_' . $rule_group_id . '_1_type'] = 'products'; + // Add the product in the selected products + $_POST['product_rule_select_' . $rule_group_id . '_1'] = [$reduction_product]; + } + } + + // These are checkboxes (which aren't sent through POST when they are not check), so they are forced to 0 + foreach (['country', 'carrier', 'group', 'cart_rule', 'product', 'shop'] as $type) { + if (!Tools::getValue($type . '_restriction')) { + $_POST[$type . '_restriction'] = 0; + } + } + + // Remove the gift if the radio button is set to "no" + if (!(int) Tools::getValue('free_gift')) { + $_POST['gift_product'] = 0; + } + + // Retrieve the product attribute id of the gift (if available) + if ($id_product = (int) Tools::getValue('gift_product')) { + $_POST['gift_product_attribute'] = (int) Tools::getValue('ipa_' . $id_product); + } + + // Idiot-proof control + if (strtotime(Tools::getValue('date_from')) > strtotime(Tools::getValue('date_to'))) { + $this->errors[] = $this->trans('The voucher cannot end before it begins.', [], 'Admin.Catalog.Notification'); + } + if ((int) Tools::getValue('minimum_amount') < 0) { + $this->errors[] = $this->trans('The minimum amount cannot be lower than zero.', [], 'Admin.Catalog.Notification'); + } + if ((float) Tools::getValue('reduction_percent') < 0 || (float) Tools::getValue('reduction_percent') > 100) { + $this->errors[] = $this->trans('Reduction percentage must be between 0% and 100%', [], 'Admin.Catalog.Notification'); + } + if ((int) Tools::getValue('reduction_amount') < 0) { + $this->errors[] = $this->trans('Reduction amount cannot be lower than zero.', [], 'Admin.Catalog.Notification'); + } + if (Tools::getValue('code') && ($same_code = (int) CartRule::getIdByCode(Tools::getValue('code'))) && $same_code != Tools::getValue('id_cart_rule')) { + $this->errors[] = $this->trans('This cart rule code is already used (conflict with cart rule %rulename%)', ['%rulename%' => $same_code], 'Admin.Catalog.Notification'); + } + if (Tools::getValue('apply_discount') == 'off' && !Tools::getValue('free_shipping') && !Tools::getValue('free_gift')) { + $this->errors[] = $this->trans('An action is required for this cart rule.', [], 'Admin.Catalog.Notification'); + } + } + + return parent::postProcess(); + } + + public function processDelete() + { + $res = parent::processDelete(); + if (Tools::isSubmit('delete' . $this->table)) { + $back = urldecode(Tools::getValue('back', '')); + if (!empty($back)) { + $this->redirect_after = $back; + } + } + + return $res; + } + + protected function afterUpdate($current_object) + { + // All the associations are deleted for an update, then recreated when we call the "afterAdd" method + $id_cart_rule = Tools::getValue('id_cart_rule'); + foreach (['country', 'carrier', 'group', 'product_rule_group', 'shop'] as $type) { + Db::getInstance()->delete('cart_rule_' . $type, '`id_cart_rule` = ' . (int) $id_cart_rule); + } + + Db::getInstance()->delete('cart_rule_product_rule', 'NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'cart_rule_product_rule_group` + WHERE `' . _DB_PREFIX_ . 'cart_rule_product_rule`.`id_product_rule_group` = `' . _DB_PREFIX_ . 'cart_rule_product_rule_group`.`id_product_rule_group`)'); + Db::getInstance()->delete('cart_rule_product_rule_value', 'NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'cart_rule_product_rule` + WHERE `' . _DB_PREFIX_ . 'cart_rule_product_rule_value`.`id_product_rule` = `' . _DB_PREFIX_ . 'cart_rule_product_rule`.`id_product_rule`)'); + Db::getInstance()->delete('cart_rule_combination', '`id_cart_rule_1` = ' . (int) $id_cart_rule . ' OR `id_cart_rule_2` = ' . (int) $id_cart_rule); + + $this->afterAdd($current_object); + } + + public function processAdd() + { + if ($cart_rule = parent::processAdd()) { + $this->context->smarty->assign('new_cart_rule', $cart_rule); + } + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + if ($cart_rule) { + $this->context->smarty->assign('refresh_cart', true); + $this->display = 'edit'; + } + } + + return $cart_rule; + } + + /** + * @TODO Move this function into CartRule + * + * @param ObjectModel $currentObject + * + * @throws PrestaShopDatabaseException + */ + protected function afterAdd($currentObject) + { + // Add restrictions for generic entities like country, carrier and group + foreach (['country', 'carrier', 'group', 'shop'] as $type) { + if (Tools::getValue($type . '_restriction') && is_array($array = Tools::getValue($type . '_select')) && count($array)) { + $values = []; + foreach ($array as $id) { + $values[] = '(' . (int) $currentObject->id . ',' . (int) $id . ')'; + } + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_' . $type . '` (`id_cart_rule`, `id_' . $type . '`) VALUES ' . implode(',', $values)); + } + } + // Add cart rule restrictions + if (Tools::getValue('cart_rule_restriction') && is_array($array = Tools::getValue('cart_rule_select')) && count($array)) { + $values = []; + foreach ($array as $id) { + $values[] = '(' . (int) $currentObject->id . ',' . (int) $id . ')'; + } + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`) VALUES ' . implode(',', $values)); + } + // Add product rule restrictions + if (Tools::getValue('product_restriction') && is_array($ruleGroupArray = Tools::getValue('product_rule_group')) && count($ruleGroupArray)) { + foreach ($ruleGroupArray as $ruleGroupId) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_product_rule_group` (`id_cart_rule`, `quantity`) + VALUES (' . (int) $currentObject->id . ', ' . (int) Tools::getValue('product_rule_group_' . $ruleGroupId . '_quantity') . ')'); + $id_product_rule_group = Db::getInstance()->Insert_ID(); + + if (is_array($ruleArray = Tools::getValue('product_rule_' . $ruleGroupId)) && count($ruleArray)) { + foreach ($ruleArray as $ruleId) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_product_rule` (`id_product_rule_group`, `type`) + VALUES (' . (int) $id_product_rule_group . ', "' . pSQL(Tools::getValue('product_rule_' . $ruleGroupId . '_' . $ruleId . '_type')) . '")'); + $id_product_rule = Db::getInstance()->Insert_ID(); + + $values = []; + foreach (Tools::getValue('product_rule_select_' . $ruleGroupId . '_' . $ruleId) as $id) { + $values[] = '(' . (int) $id_product_rule . ',' . (int) $id . ')'; + } + $values = array_unique($values); + if (count($values)) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_product_rule_value` (`id_product_rule`, `id_item`) VALUES ' . implode(',', $values)); + } + } + } + } + } + + // If the new rule has no cart rule restriction, then it must be added to the white list of the other cart rules that have restrictions + if (!Tools::getValue('cart_rule_restriction')) { + Db::getInstance()->execute(' + INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`) ( + SELECT id_cart_rule, ' . (int) $currentObject->id . ' FROM `' . _DB_PREFIX_ . 'cart_rule` WHERE cart_rule_restriction = 1 + )'); + } else { + // And if the new cart rule has restrictions, previously unrestricted cart rules may now be restricted (a mug of coffee is strongly advised to understand this sentence) + $ruleCombinations = Db::getInstance()->executeS(' + SELECT cr.id_cart_rule + FROM ' . _DB_PREFIX_ . 'cart_rule cr + WHERE cr.id_cart_rule != ' . (int) $currentObject->id . ' + AND cr.cart_rule_restriction = 0 + AND NOT EXISTS ( + SELECT 1 + FROM ' . _DB_PREFIX_ . 'cart_rule_combination + WHERE cr.id_cart_rule = ' . _DB_PREFIX_ . 'cart_rule_combination.id_cart_rule_2 AND ' . (int) $currentObject->id . ' = id_cart_rule_1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM ' . _DB_PREFIX_ . 'cart_rule_combination + WHERE cr.id_cart_rule = ' . _DB_PREFIX_ . 'cart_rule_combination.id_cart_rule_1 AND ' . (int) $currentObject->id . ' = id_cart_rule_2 + ) + '); + foreach ($ruleCombinations as $incompatibleRule) { + Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'cart_rule` SET cart_rule_restriction = 1 WHERE id_cart_rule = ' . (int) $incompatibleRule['id_cart_rule'] . ' LIMIT 1'); + Db::getInstance()->execute(' + INSERT IGNORE INTO `' . _DB_PREFIX_ . 'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`) ( + SELECT id_cart_rule, ' . (int) $incompatibleRule['id_cart_rule'] . ' FROM `' . _DB_PREFIX_ . 'cart_rule` + WHERE active = 1 + AND id_cart_rule != ' . (int) $currentObject->id . ' + AND id_cart_rule != ' . (int) $incompatibleRule['id_cart_rule'] . ' + )'); + } + } + } + + /** + * Retrieve the cart rule product rule groups in the POST data + * if available, and in the database if there is none. + * + * @param CartRule $cart_rule + * + * @return array + */ + public function getProductRuleGroupsDisplay($cart_rule) + { + $productRuleGroupsArray = []; + if (Tools::getValue('product_restriction') && is_array($array = Tools::getValue('product_rule_group')) && count($array)) { + $i = 1; + foreach ($array as $ruleGroupId) { + $productRulesArray = []; + if (is_array($array = Tools::getValue('product_rule_' . $ruleGroupId)) && count($array)) { + foreach ($array as $ruleId) { + $productRulesArray[] = $this->getProductRuleDisplay( + $ruleGroupId, + $ruleId, + Tools::getValue('product_rule_' . $ruleGroupId . '_' . $ruleId . '_type'), + Tools::getValue('product_rule_select_' . $ruleGroupId . '_' . $ruleId) + ); + } + } + + $productRuleGroupsArray[] = $this->getProductRuleGroupDisplay( + $i++, + (int) Tools::getValue('product_rule_group_' . $ruleGroupId . '_quantity'), + $productRulesArray + ); + } + } else { + $i = 1; + foreach ($cart_rule->getProductRuleGroups() as $productRuleGroup) { + $j = 1; + $productRulesDisplay = []; + foreach ($productRuleGroup['product_rules'] as $productRule) { + $productRulesDisplay[] = $this->getProductRuleDisplay($i, $j++, $productRule['type'], $productRule['values']); + } + $productRuleGroupsArray[] = $this->getProductRuleGroupDisplay($i++, $productRuleGroup['quantity'], $productRulesDisplay); + } + } + + return $productRuleGroupsArray; + } + + /* Return the form for a single cart rule group either with or without product_rules set up */ + public function getProductRuleGroupDisplay($product_rule_group_id, $product_rule_group_quantity = 1, $product_rules = null) + { + Context::getContext()->smarty->assign('product_rule_group_id', $product_rule_group_id); + Context::getContext()->smarty->assign('product_rule_group_quantity', $product_rule_group_quantity); + Context::getContext()->smarty->assign('product_rules', $product_rules); + + return $this->createTemplate('product_rule_group.tpl')->fetch(); + } + + public function getProductRuleDisplay($product_rule_group_id, $product_rule_id, $product_rule_type, $selected = []) + { + Context::getContext()->smarty->assign( + [ + 'product_rule_group_id' => (int) $product_rule_group_id, + 'product_rule_id' => (int) $product_rule_id, + 'product_rule_type' => $product_rule_type, + ] + ); + + switch ($product_rule_type) { + case 'attributes': + $attributes = ['selected' => [], 'unselected' => []]; + $results = Db::getInstance()->executeS(' + SELECT CONCAT(agl.name, " - ", al.name) as name, a.id_attribute as id + FROM ' . _DB_PREFIX_ . 'attribute_group_lang agl + LEFT JOIN ' . _DB_PREFIX_ . 'attribute a ON a.id_attribute_group = agl.id_attribute_group + LEFT JOIN ' . _DB_PREFIX_ . 'attribute_lang al ON (a.id_attribute = al.id_attribute AND al.id_lang = ' . (int) Context::getContext()->language->id . ') + WHERE agl.id_lang = ' . (int) Context::getContext()->language->id . ' + ORDER BY agl.name, al.name'); + foreach ($results as $row) { + $attributes[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $attributes); + $choose_content = $this->createTemplate('controllers/cart_rules/product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + + break; + case 'products': + $products = ['selected' => [], 'unselected' => []]; + $results = Db::getInstance()->executeS(' + SELECT DISTINCT name, p.id_product as id + FROM ' . _DB_PREFIX_ . 'product p + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl + ON (p.`id_product` = pl.`id_product` + AND pl.`id_lang` = ' . (int) Context::getContext()->language->id . Shop::addSqlRestrictionOnLang('pl') . ') + ' . Shop::addSqlAssociation('product', 'p') . ' + WHERE id_lang = ' . (int) Context::getContext()->language->id . ' + ORDER BY name'); + foreach ($results as $row) { + $products[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $products); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + + break; + case 'manufacturers': + $products = ['selected' => [], 'unselected' => []]; + $results = Db::getInstance()->executeS(' + SELECT name, id_manufacturer as id + FROM ' . _DB_PREFIX_ . 'manufacturer + ORDER BY name'); + foreach ($results as $row) { + $products[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $products); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + + break; + case 'suppliers': + $products = ['selected' => [], 'unselected' => []]; + $results = Db::getInstance()->executeS(' + SELECT name, id_supplier as id + FROM ' . _DB_PREFIX_ . 'supplier + ORDER BY name'); + foreach ($results as $row) { + $products[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $products); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + + break; + case 'categories': + $categories = ['selected' => [], 'unselected' => []]; + $results = Db::getInstance()->executeS(' + SELECT DISTINCT name, c.id_category as id + FROM ' . _DB_PREFIX_ . 'category c + LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl + ON (c.`id_category` = cl.`id_category` + AND cl.`id_lang` = ' . (int) Context::getContext()->language->id . Shop::addSqlRestrictionOnLang('cl') . ') + ' . Shop::addSqlAssociation('category', 'c') . ' + WHERE id_lang = ' . (int) Context::getContext()->language->id . ' + ORDER BY name'); + foreach ($results as $row) { + $categories[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $categories); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + + break; + default: + Context::getContext()->smarty->assign('product_rule_itemlist', ['selected' => [], 'unselected' => []]); + Context::getContext()->smarty->assign('product_rule_choose_content', ''); + } + + return $this->createTemplate('product_rule.tpl')->fetch(); + } + + public function ajaxProcess() + { + if (Tools::isSubmit('newProductRule')) { + die($this->getProductRuleDisplay(Tools::getValue('product_rule_group_id'), Tools::getValue('product_rule_id'), Tools::getValue('product_rule_type'))); + } + if (Tools::isSubmit('newProductRuleGroup') && $product_rule_group_id = Tools::getValue('product_rule_group_id')) { + die($this->getProductRuleGroupDisplay($product_rule_group_id, Tools::getValue('product_rule_group_' . $product_rule_group_id . '_quantity', 1))); + } + + if (Tools::isSubmit('customerFilter')) { + $search_query = trim(Tools::getValue('q')); + $customers = Db::getInstance()->executeS(' + SELECT `id_customer`, `email`, CONCAT(`firstname`, \' \', `lastname`) as cname + FROM `' . _DB_PREFIX_ . 'customer` + WHERE `deleted` = 0 AND is_guest = 0 AND active = 1 + AND ( + `id_customer` = ' . (int) $search_query . ' + OR `email` LIKE "%' . pSQL($search_query) . '%" + OR `firstname` LIKE "%' . pSQL($search_query) . '%" + OR `lastname` LIKE "%' . pSQL($search_query) . '%" + ) + ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) . ' + ORDER BY `firstname`, `lastname` ASC + LIMIT 50'); + die(json_encode($customers)); + } + // Both product filter (free product and product discount) search for products + if (Tools::isSubmit('giftProductFilter') || Tools::isSubmit('reductionProductFilter')) { + $products = Product::searchByName(Context::getContext()->language->id, trim(Tools::getValue('q'))); + die(json_encode($products)); + } + } + + protected function searchProducts($search) + { + if ($products = Product::searchByName((int) $this->context->language->id, $search)) { + foreach ($products as &$product) { + $combinations = []; + $productObj = new Product((int) $product['id_product'], false, (int) $this->context->language->id); + $attributes = $productObj->getAttributesGroups((int) $this->context->language->id); + $product['formatted_price'] = $this->context->getCurrentLocale()->formatPrice(Tools::convertPrice($product['price_tax_incl'], $this->context->currency), $this->context->currency->iso_code); + + foreach ($attributes as $attribute) { + if (!isset($combinations[$attribute['id_product_attribute']]['attributes'])) { + $combinations[$attribute['id_product_attribute']]['attributes'] = ''; + } + $combinations[$attribute['id_product_attribute']]['attributes'] .= $attribute['attribute_name'] . ' - '; + $combinations[$attribute['id_product_attribute']]['id_product_attribute'] = $attribute['id_product_attribute']; + $combinations[$attribute['id_product_attribute']]['default_on'] = $attribute['default_on']; + if (!isset($combinations[$attribute['id_product_attribute']]['price'])) { + $price_tax_incl = Product::getPriceStatic((int) $product['id_product'], true, $attribute['id_product_attribute']); + $combinations[$attribute['id_product_attribute']]['formatted_price'] = $this->context->getCurrentLocale()->formatPrice(Tools::convertPrice($price_tax_incl, $this->context->currency), $this->context->currency->iso_code); + } + } + + foreach ($combinations as &$combination) { + $combination['attributes'] = rtrim($combination['attributes'], ' - '); + } + $product['combinations'] = $combinations; + } + + return [ + 'products' => $products, + 'found' => true, + ]; + } else { + return ['found' => false, 'notfound' => $this->trans('No product has been found.', [], 'Admin.Catalog.Notification')]; + } + } + + public function ajaxProcessSearchProducts() + { + $array = $this->searchProducts(Tools::getValue('product_search')); + $this->content = trim(json_encode($array)); + } + + public function renderForm() + { + $limit = 40; + $this->toolbar_btn['save'] = [ + 'href' => '#', + 'desc' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + $this->toolbar_btn['save-and-stay'] = [ + 'href' => '#', + 'desc' => $this->trans('Save and stay', [], 'Admin.Actions'), + ]; + + /** @var CartRule $current_object */ + $current_object = $this->loadObject(true); + + // All the filter are prefilled with the correct information + $customer_filter = ''; + if (Validate::isUnsignedId($current_object->id_customer) && + ($customer = new Customer($current_object->id_customer)) && + Validate::isLoadedObject($customer)) { + $customer_filter = $customer->firstname . ' ' . $customer->lastname . ' (' . $customer->email . ')'; + } + + $gift_product_filter = ''; + if (Validate::isUnsignedId($current_object->gift_product) && + ($product = new Product($current_object->gift_product, false, $this->context->language->id)) && + Validate::isLoadedObject($product)) { + $gift_product_filter = (!empty($product->reference) ? $product->reference : $product->name); + } + + $reduction_product_filter = ''; + if (Validate::isUnsignedId($current_object->reduction_product) && + ($product = new Product($current_object->reduction_product, false, $this->context->language->id)) && + Validate::isLoadedObject($product)) { + $reduction_product_filter = (!empty($product->reference) ? $product->reference : $product->name); + } + + $product_rule_groups = $this->getProductRuleGroupsDisplay($current_object); + + $attribute_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + $currencies = Currency::getCurrencies(false, true, true); + $languages = Language::getLanguages(); + $countries = $current_object->getAssociatedRestrictions('country', true, true); + $groups = $current_object->getAssociatedRestrictions('group', false, true); + $shops = $current_object->getAssociatedRestrictions('shop', false, false); + $cart_rules = $current_object->getAssociatedRestrictions('cart_rule', false, true, 0, $limit); + $carriers = $current_object->getAssociatedRestrictions('carrier', true, true); + + foreach ($carriers as &$carriers2) { + $prev_id_carrier = 0; + + foreach ($carriers2 as $key => &$carrier) { + if ($prev_id_carrier == $carrier['id_carrier']) { + unset($carriers2[$key]); + + continue; + } + + foreach ($carrier as $field => &$value) { + if ($field == 'name') { + if ($value == '0') { + $value = $carrier['id_carrier'] . ' - ' . Configuration::get('PS_SHOP_NAME'); + } else { + $value = $carrier['id_carrier'] . ' - ' . $carrier['name']; + if ($carrier['name']) { + $value .= ' (' . $carrier['delay'] . ')'; + } + } + } + } + + $prev_id_carrier = $carrier['id_carrier']; + } + } + + $gift_product_select = ''; + $gift_product_attribute_select = ''; + if ((int) $current_object->gift_product) { + $search_products = $this->searchProducts($gift_product_filter); + if (isset($search_products['products']) && is_array($search_products['products'])) { + foreach ($search_products['products'] as $product) { + $gift_product_select .= ' + '; + + if (count($product['combinations'])) { + $gift_product_attribute_select .= ''; + } + } + } + } + + $product = new Product($current_object->gift_product); + $this->context->smarty->assign( + [ + 'show_toolbar' => true, + 'toolbar_btn' => $this->toolbar_btn, + 'toolbar_scroll' => $this->toolbar_scroll, + 'title' => [$this->trans('Payment:', [], 'Admin.Catalog.Feature'), $this->trans('Cart Rules', [], 'Admin.Catalog.Feature')], + 'defaultDateFrom' => date('Y-m-d H:00:00'), + 'defaultDateTo' => date('Y-m-d H:00:00', strtotime('+1 month')), + 'customerFilter' => $customer_filter, + 'giftProductFilter' => $gift_product_filter, + 'gift_product_select' => $gift_product_select, + 'gift_product_attribute_select' => $gift_product_attribute_select, + 'reductionProductFilter' => $reduction_product_filter, + 'defaultCurrency' => Configuration::get('PS_CURRENCY_DEFAULT'), + 'id_lang_default' => Configuration::get('PS_LANG_DEFAULT'), + 'languages' => $languages, + 'currencies' => $currencies, + 'countries' => $countries, + 'carriers' => $carriers, + 'groups' => $groups, + 'shops' => $shops, + 'cart_rules' => $cart_rules, + 'product_rule_groups' => $product_rule_groups, + 'product_rule_groups_counter' => count($product_rule_groups), + 'attribute_groups' => $attribute_groups, + 'currentIndex' => self::$currentIndex, + 'currentToken' => $this->token, + 'currentObject' => $current_object, + 'currentTab' => $this, + 'hasAttribute' => $product->hasAttributes(), + ] + ); + Media::addJsDef(['baseHref' => $this->context->link->getAdminLink('AdminCartRules') . '&ajaxMode=1&ajax=1&id_cart_rule=' . + (int) Tools::getValue('id_cart_rule') . '&action=loadCartRules&limit=' . (int) $limit . '&count=0', ]); + $this->content .= $this->createTemplate('form.tpl')->fetch(); + + $this->addJqueryUI('ui.datepicker'); + $this->addJqueryPlugin(['jscroll', 'typewatch']); + + return parent::renderForm(); + } + + public function displayAjaxSearchCartRuleVouchers() + { + $found = false; + if ($vouchers = CartRule::getCartsRuleByCode(Tools::getValue('q'), (int) $this->context->language->id, true)) { + $found = true; + } + echo json_encode(['found' => $found, 'vouchers' => $vouchers]); + } +} diff --git a/controllers/admin/AdminCartsController.php b/controllers/admin/AdminCartsController.php new file mode 100644 index 00000000..59dc25a5 --- /dev/null +++ b/controllers/admin/AdminCartsController.php @@ -0,0 +1,962 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Cart $object + */ +class AdminCartsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'cart'; + $this->className = 'Cart'; + $this->lang = false; + $this->explicitSelect = true; + + parent::__construct(); + + $this->addRowAction('view'); + $this->addRowAction('delete'); + $this->allow_export = true; + $this->_orderWay = 'DESC'; + + $this->_select = 'CONCAT(LEFT(c.`firstname`, 1), \'. \', c.`lastname`) `customer`, a.id_cart total, ca.name carrier, o.id_order, + IF (IFNULL(o.id_order, \'' . $this->trans('Non ordered', [], 'Admin.Orderscustomers.Feature') . '\') = \'' . $this->trans('Non ordered', [], 'Admin.Orderscustomers.Feature') . '\', IF(TIME_TO_SEC(TIMEDIFF(\'' . pSQL(date('Y-m-d H:i:00', time())) . '\', a.`date_add`)) > 86400, \'' . $this->trans('Abandoned cart', [], 'Admin.Orderscustomers.Feature') . '\', \'' . $this->trans('Non ordered', [], 'Admin.Orderscustomers.Feature') . '\'), o.id_order) AS status, IF(o.id_order, 1, 0) badge_success, IF(o.id_order, 0, 1) badge_danger, IF(co.id_guest, 1, 0) id_guest'; + $this->_join = 'LEFT JOIN ' . _DB_PREFIX_ . 'customer c ON (c.id_customer = a.id_customer) + LEFT JOIN ' . _DB_PREFIX_ . 'currency cu ON (cu.id_currency = a.id_currency) + LEFT JOIN ' . _DB_PREFIX_ . 'carrier ca ON (ca.id_carrier = a.id_carrier) + LEFT JOIN ' . _DB_PREFIX_ . 'orders o ON (o.id_cart = a.id_cart) + LEFT JOIN ( + SELECT `id_guest` + FROM `' . _DB_PREFIX_ . 'connections` + WHERE + TIME_TO_SEC(TIMEDIFF(\'' . pSQL(date('Y-m-d H:i:00', time())) . '\', `date_add`)) < 1800 + LIMIT 1 + ) AS co ON co.`id_guest` = a.`id_guest`'; + + if (Tools::getValue('action') && Tools::getValue('action') == 'filterOnlyAbandonedCarts') { + $this->_having = 'status = \'' . $this->trans('Abandoned cart', [], 'Admin.Orderscustomers.Feature') . '\''; + } else { + $this->_use_found_rows = false; + } + + $this->fields_list = [ + 'id_cart' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + ], + 'status' => [ + 'title' => $this->trans('Order ID', [], 'Admin.Orderscustomers.Feature'), + 'align' => 'text-center', + 'badge_danger' => true, + 'havingFilter' => true, + ], + 'customer' => [ + 'title' => $this->trans('Customer', [], 'Admin.Global'), + 'filter_key' => 'c!lastname', + ], + 'total' => [ + 'title' => $this->trans('Total', [], 'Admin.Global'), + 'callback' => 'getOrderTotalUsingTaxCalculationMethod', + 'orderby' => false, + 'search' => false, + 'align' => 'text-right', + 'badge_success' => true, + ], + 'carrier' => [ + 'title' => $this->trans('Carrier', [], 'Admin.Shipping.Feature'), + 'align' => 'text-left', + 'callback' => 'replaceZeroByShopName', + 'filter_key' => 'ca!name', + ], + 'date_add' => [ + 'title' => $this->trans('Date', [], 'Admin.Global'), + 'align' => 'text-left', + 'type' => 'datetime', + 'class' => 'fixed-width-lg', + 'filter_key' => 'a!date_add', + ], + ]; + + if (Configuration::get('PS_GUEST_CHECKOUT_ENABLED')) { + $this->fields_list['id_guest'] = [ + 'title' => $this->trans('Online', [], 'Admin.Global'), + 'align' => 'text-center', + 'type' => 'bool', + 'havingFilter' => true, + 'class' => 'fixed-width-xs', + ]; + } + + $this->shopLinkType = 'shop'; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['export_cart'] = [ + 'href' => self::$currentIndex . '&exportcart&token=' . $this->token, + 'desc' => $this->trans('Export carts', [], 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-export', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderKpis() + { + $time = time(); + $kpis = []; + + /* The data generation is located in AdminStatsControllerCore */ + $helper = new HelperKpi(); + $helper->id = 'box-conversion-rate'; + $helper->icon = 'icon-sort-by-attributes-alt'; + //$helper->chart = true; + $helper->color = 'color1'; + $helper->title = $this->trans('Conversion Rate', [], 'Admin.Global'); + $helper->subtitle = $this->trans('30 days', [], 'Admin.Global'); + if (ConfigurationKPI::get('CONVERSION_RATE') !== false) { + $helper->value = ConfigurationKPI::get('CONVERSION_RATE'); + } + if (ConfigurationKPI::get('CONVERSION_RATE_CHART') !== false) { + $helper->data = ConfigurationKPI::get('CONVERSION_RATE_CHART'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=conversion_rate'; + $helper->refresh = (bool) (ConfigurationKPI::get('CONVERSION_RATE_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-carts'; + $helper->icon = 'icon-shopping-cart'; + $helper->color = 'color2'; + $helper->title = $this->trans('Abandoned Carts', [], 'Admin.Orderscustomers.Feature'); + $date_from = date(Context::getContext()->language->date_format_lite, strtotime('-2 day')); + $date_to = date(Context::getContext()->language->date_format_lite, strtotime('-1 day')); + $helper->subtitle = $this->trans('From %date1% to %date2%', ['%date1%' => $date_from, '%date2%' => $date_to], 'Admin.Orderscustomers.Feature'); + $helper->href = $this->context->link->getAdminLink('AdminCarts') . '&action=filterOnlyAbandonedCarts'; + if (ConfigurationKPI::get('ABANDONED_CARTS') !== false) { + $helper->value = ConfigurationKPI::get('ABANDONED_CARTS'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=abandoned_cart'; + $helper->refresh = (bool) (ConfigurationKPI::get('ABANDONED_CARTS_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-average-order'; + $helper->icon = 'icon-money'; + $helper->color = 'color3'; + $helper->title = $this->trans('Average Order Value', [], 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('30 days', [], 'Admin.Global'); + if (ConfigurationKPI::get('AVG_ORDER_VALUE') !== false) { + $helper->value = $this->trans('%amount% tax excl.', ['%amount%' => ConfigurationKPI::get('AVG_ORDER_VALUE')], 'Admin.Orderscustomers.Feature'); + } + if (ConfigurationKPI::get('AVG_ORDER_VALUE_EXPIRE') < $time) { + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=average_order_value'; + } + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-net-profit-visitor'; + $helper->icon = 'icon-user'; + $helper->color = 'color4'; + $helper->title = $this->trans('Net Profit per Visitor', [], 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('30 days', [], 'Admin.Global'); + if (ConfigurationKPI::get('NETPROFIT_VISITOR') !== false) { + $helper->value = ConfigurationKPI::get('NETPROFIT_VISITOR'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=netprofit_visitor'; + $helper->refresh = (bool) (ConfigurationKPI::get('NETPROFIT_VISITOR_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + public function renderView() + { + /** @var Cart $cart */ + if (!($cart = $this->loadObject(true))) { + return; + } + $customer = new Customer($cart->id_customer); + $currency = new Currency($cart->id_currency); + $this->context->cart = $cart; + $this->context->currency = $currency; + $this->context->customer = $customer; + $this->toolbar_title = $this->trans('Cart #%ID%', ['%ID%' => $this->context->cart->id], 'Admin.Orderscustomers.Feature'); + $products = $cart->getProducts(); + $summary = $cart->getSummaryDetails(); + + /* Display order information */ + $id_order = (int) Order::getIdByCartId($cart->id); + $order = new Order($id_order); + if (Validate::isLoadedObject($order)) { + $tax_calculation_method = $order->getTaxCalculationMethod(); + $id_shop = (int) $order->id_shop; + } else { + $id_shop = (int) $cart->id_shop; + $tax_calculation_method = Group::getPriceDisplayMethod(Group::getCurrent()->id); + } + + if ($tax_calculation_method == PS_TAX_EXC) { + $total_products = $summary['total_products']; + $total_discounts = $summary['total_discounts_tax_exc']; + $total_wrapping = $summary['total_wrapping_tax_exc']; + $total_price = $summary['total_price_without_tax']; + $total_shipping = $summary['total_shipping_tax_exc']; + } else { + $total_products = $summary['total_products_wt']; + $total_discounts = $summary['total_discounts']; + $total_wrapping = $summary['total_wrapping']; + $total_price = $summary['total_price']; + $total_shipping = $summary['total_shipping']; + } + foreach ($products as &$product) { + if ($tax_calculation_method == PS_TAX_EXC) { + $product['product_price'] = $product['price']; + $product['product_total'] = $product['total']; + } else { + $product['product_price'] = $product['price_wt']; + $product['product_total'] = $product['total_wt']; + } + $image = []; + if (isset($product['id_product_attribute']) && (int) $product['id_product_attribute']) { + $image = Db::getInstance()->getRow('SELECT id_image FROM ' . _DB_PREFIX_ . 'product_attribute_image WHERE id_product_attribute = ' . (int) $product['id_product_attribute']); + } + if (!isset($image['id_image'])) { + $image = Db::getInstance()->getRow('SELECT id_image FROM ' . _DB_PREFIX_ . 'image WHERE id_product = ' . (int) $product['id_product'] . ' AND cover = 1'); + } + + $product['qty_in_stock'] = StockAvailable::getQuantityAvailableByProduct($product['id_product'], isset($product['id_product_attribute']) ? $product['id_product_attribute'] : null, (int) $id_shop); + + $image_product = new Image($image['id_image']); + $product['image'] = (isset($image['id_image']) ? ImageManager::thumbnail(_PS_IMG_DIR_ . 'p/' . $image_product->getExistingImgPath() . '.jpg', 'product_mini_' . (int) $product['id_product'] . (isset($product['id_product_attribute']) ? '_' . (int) $product['id_product_attribute'] : '') . '.jpg', 45, 'jpg') : '--'); + + $customized_datas = Product::getAllCustomizedDatas($this->context->cart->id, null, true, null, (int) $product['id_customization']); + $this->context->cart->setProductCustomizedDatas($product, $customized_datas); + if ($customized_datas) { + Product::addProductCustomizationPrice($product, $customized_datas); + } + } + + $helper = new HelperKpi(); + $helper->id = 'box-kpi-cart'; + $helper->icon = 'icon-shopping-cart'; + $helper->color = 'color1'; + $helper->title = $this->trans('Total Cart', [], 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('Cart #%ID%', ['%ID%' => $cart->id], 'Admin.Orderscustomers.Feature'); + $helper->value = $this->context->getCurrentLocale()->formatPrice($total_price, $currency->iso_code); + $kpi = $helper->generate(); + + $this->tpl_view_vars = [ + 'kpi' => $kpi, + 'products' => $products, + 'discounts' => $cart->getCartRules(), + 'order' => $order, + 'cart' => $cart, + 'currency' => $currency, + 'customer' => $customer, + 'customer_stats' => $customer->getStats(), + 'total_products' => $total_products, + 'total_discounts' => $total_discounts, + 'total_wrapping' => $total_wrapping, + 'total_price' => $total_price, + 'total_shipping' => $total_shipping, + 'tax_calculation_method' => $tax_calculation_method, + ]; + + return parent::renderView(); + } + + public function ajaxPreProcess() + { + if ($this->access('edit')) { + $id_customer = (int) Tools::getValue('id_customer'); + $customer = new Customer((int) $id_customer); + $this->context->customer = $customer; + $id_cart = (int) Tools::getValue('id_cart'); + if (!$id_cart) { + $id_cart = $customer->getLastEmptyCart(false); + } + $this->context->cart = new Cart((int) $id_cart); + + if (!$this->context->cart->id) { + $this->context->cart->recyclable = 0; + $this->context->cart->gift = 0; + } + + if (!$this->context->cart->id_customer) { + $this->context->cart->id_customer = $id_customer; + } + if (Validate::isLoadedObject($this->context->cart) && $this->context->cart->OrderExists()) { + return; + } + if (!$this->context->cart->secure_key) { + $this->context->cart->secure_key = $this->context->customer->secure_key; + } + if (!$this->context->cart->id_shop) { + $this->context->cart->id_shop = (int) $this->context->shop->id; + } + if (!$this->context->cart->id_lang) { + $this->context->cart->id_lang = (($id_lang = (int) Tools::getValue('id_lang')) ? $id_lang : Configuration::get('PS_LANG_DEFAULT')); + } + if (!$this->context->cart->id_currency) { + $this->context->cart->id_currency = (($id_currency = (int) Tools::getValue('id_currency')) ? $id_currency : Configuration::get('PS_CURRENCY_DEFAULT')); + } + + $addresses = $customer->getAddresses((int) $this->context->cart->id_lang); + $id_address_delivery = (int) Tools::getValue('id_address_delivery'); + $id_address_invoice = (int) Tools::getValue('id_address_delivery'); + + if (!$this->context->cart->id_address_invoice && isset($addresses[0])) { + $this->context->cart->id_address_invoice = (int) $addresses[0]['id_address']; + } elseif ($id_address_invoice) { + $this->context->cart->id_address_invoice = (int) $id_address_invoice; + } + if (!$this->context->cart->id_address_delivery && isset($addresses[0])) { + $this->context->cart->id_address_delivery = $addresses[0]['id_address']; + } elseif ($id_address_delivery) { + $this->context->cart->id_address_delivery = (int) $id_address_delivery; + } + $this->context->cart->setNoMultishipping(); + $this->context->cart->save(); + $currency = new Currency((int) $this->context->cart->id_currency); + $this->context->currency = $currency; + } + } + + public function ajaxProcessDeleteProduct() + { + if ($this->access('edit')) { + $errors = []; + if ((!$id_product = (int) Tools::getValue('id_product')) || !Validate::isInt($id_product)) { + $errors[] = $this->trans('Invalid product', [], 'Admin.Catalog.Notification'); + } + if (($id_product_attribute = (int) Tools::getValue('id_product_attribute')) && !Validate::isInt($id_product_attribute)) { + $errors[] = $this->trans('Invalid combination', [], 'Admin.Catalog.Notification'); + } + if (count($errors)) { + die(json_encode($errors)); + } + if ($this->context->cart->deleteProduct($id_product, $id_product_attribute, (int) Tools::getValue('id_customization'))) { + echo json_encode($this->ajaxReturnVars()); + } + } + } + + public function ajaxProcessUpdateCustomizationFields() + { + $errors = []; + if ($this->access('edit')) { + $errors = []; + if (Tools::getValue('only_display') != 1) { + if (!$this->context->cart->id || (!$id_product = (int) Tools::getValue('id_product'))) { + return; + } + $product = new Product((int) $id_product); + if (!$customization_fields = $product->getCustomizationFieldIds()) { + return; + } + foreach ($customization_fields as $customization_field) { + $field_id = 'customization_' . $id_product . '_' . $customization_field['id_customization_field']; + if ($customization_field['type'] == Product::CUSTOMIZE_TEXTFIELD) { + if (!Tools::getValue($field_id)) { + if ($customization_field['required']) { + $errors[] = $this->trans('Please fill in all the required fields.', [], 'Admin.Notifications.Error'); + } + + continue; + } + if (!Validate::isMessage(Tools::getValue($field_id))) { + $errors[] = $this->trans('Invalid message', [], 'Admin.Notifications.Error'); + } + $this->context->cart->addTextFieldToProduct((int) $product->id, (int) $customization_field['id_customization_field'], Product::CUSTOMIZE_TEXTFIELD, Tools::getValue($field_id)); + } elseif ($customization_field['type'] == Product::CUSTOMIZE_FILE) { + if (!isset($_FILES[$field_id]) || !isset($_FILES[$field_id]['tmp_name']) || empty($_FILES[$field_id]['tmp_name'])) { + if ($customization_field['required']) { + $errors[] = $this->trans('Please fill in all the required fields.', [], 'Admin.Notifications.Error'); + } + + continue; + } + if ($error = ImageManager::validateUpload($_FILES[$field_id], (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE'))) { + $errors[] = $error; + } + if (!($tmp_name = tempnam(_PS_TMP_IMG_DIR_, 'PS')) || !move_uploaded_file($_FILES[$field_id]['tmp_name'], $tmp_name)) { + $errors[] = $this->trans('An error occurred during the image upload process.', [], 'Admin.Catalog.Notification'); + } + $file_name = md5(uniqid(mt_rand(0, mt_getrandmax()), true)); + if (!ImageManager::resize($tmp_name, _PS_UPLOAD_DIR_ . $file_name)) { + continue; + } elseif (!ImageManager::resize($tmp_name, _PS_UPLOAD_DIR_ . $file_name . '_small', (int) Configuration::get('PS_PRODUCT_PICTURE_WIDTH'), (int) Configuration::get('PS_PRODUCT_PICTURE_HEIGHT'))) { + $errors[] = $this->trans('An error occurred during the image upload process.', [], 'Admin.Catalog.Notification'); + } else { + $this->context->cart->addPictureToProduct((int) $product->id, (int) $customization_field['id_customization_field'], Product::CUSTOMIZE_FILE, $file_name); + } + unlink($tmp_name); + } + } + } + $this->setMedia(false); + $this->initFooter(); + $this->context->smarty->assign(['customization_errors' => implode('
', $errors), + 'css_files' => $this->css_files, ]); + + return $this->smartyOutputContent('controllers/orders/form_customization_feedback.tpl'); + } + } + + public function ajaxProcessUpdateQty() + { + if ($this->access('edit')) { + $errors = []; + if (!$this->context->cart->id) { + return; + } + if ($this->context->cart->OrderExists()) { + $errors[] = $this->trans('An order has already been placed with this cart.', [], 'Admin.Catalog.Notification'); + } elseif (!($id_product = (int) Tools::getValue('id_product')) || !($product = new Product((int) $id_product, true, $this->context->language->id))) { + $errors[] = $this->trans('Invalid product', [], 'Admin.Catalog.Notification'); + } elseif (!($qty = Tools::getValue('qty')) || $qty == 0) { + $errors[] = $this->trans('Invalid quantity', [], 'Admin.Catalog.Notification'); + } + + // Don't try to use a product if not instanciated before due to errors + if (isset($product) && $product->id) { + if (($id_product_attribute = Tools::getValue('id_product_attribute')) != 0) { + if (!Product::isAvailableWhenOutOfStock($product->out_of_stock) && !Attribute::checkAttributeQty((int) $id_product_attribute, (int) $qty)) { + $errors[] = $this->trans('There are not enough products in stock.', [], 'Admin.Catalog.Notification'); + } + } elseif (!$product->checkQty((int) $qty)) { + $errors[] = $this->trans('There are not enough products in stock.', [], 'Admin.Catalog.Notification'); + } + if (!($id_customization = (int) Tools::getValue('id_customization', 0)) && !$product->hasAllRequiredCustomizableFields()) { + $errors[] = $this->trans('Please fill in all the required fields.', [], 'Admin.Notifications.Error'); + } + $this->context->cart->save(); + } else { + $errors[] = $this->trans('This product cannot be added to the cart.', [], 'Admin.Catalog.Notification'); + } + + if (!count($errors)) { + if ((int) $qty < 0) { + $qty = str_replace('-', '', $qty); + $operator = 'down'; + } else { + $operator = 'up'; + } + + if (!($qty_upd = $this->context->cart->updateQty($qty, $id_product, (int) $id_product_attribute, (int) $id_customization, $operator))) { + $errors[] = $this->trans('You already have the maximum quantity available for this product.', [], 'Admin.Catalog.Notification'); + } elseif ($qty_upd < 0) { + $minimal_qty = $id_product_attribute ? Attribute::getAttributeMinimalQty((int) $id_product_attribute) : $product->minimal_quantity; + $errors[] = $this->trans('You must add a minimum quantity of %d', [$minimal_qty], 'Admin.Orderscustomers.Notification'); + } + } + + echo json_encode(array_merge($this->ajaxReturnVars(), ['errors' => $errors])); + } + } + + public function ajaxProcessUpdateDeliveryOption() + { + if ($this->access('edit')) { + $delivery_option = Tools::getValue('delivery_option'); + if ($delivery_option !== false) { + $this->context->cart->setDeliveryOption([$this->context->cart->id_address_delivery => $delivery_option]); + } + if (Validate::isBool(($recyclable = (int) Tools::getValue('recyclable')))) { + $this->context->cart->recyclable = $recyclable; + } + if (Validate::isBool(($gift = (int) Tools::getValue('gift')))) { + $this->context->cart->gift = $gift; + } + if (Validate::isMessage(($gift_message = pSQL(Tools::getValue('gift_message'))))) { + $this->context->cart->gift_message = $gift_message; + } + $this->context->cart->save(); + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessUpdateOrderMessage() + { + if ($this->access('edit')) { + $id_message = false; + if ($old_message = Message::getMessageByCartId((int) $this->context->cart->id)) { + $id_message = $old_message['id_message']; + } + $message = new Message((int) $id_message); + if ($message_content = Tools::getValue('message')) { + if (Validate::isMessage($message_content)) { + $message->message = $message_content; + $message->id_cart = (int) $this->context->cart->id; + $message->id_customer = (int) $this->context->cart->id_customer; + $message->save(); + } + } elseif (Validate::isLoadedObject($message)) { + $message->delete(); + } + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessUpdateCurrency() + { + if ($this->access('edit')) { + $currency = new Currency((int) Tools::getValue('id_currency')); + if (Validate::isLoadedObject($currency) && !$currency->deleted && $currency->active) { + $this->context->cart->id_currency = (int) $currency->id; + $this->context->currency = $currency; + $this->context->cart->save(); + } + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessUpdateLang() + { + if ($this->access('edit')) { + $lang = new Language((int) Tools::getValue('id_lang')); + if (Validate::isLoadedObject($lang) && $lang->active) { + $this->context->cart->id_lang = (int) $lang->id; + $this->context->cart->save(); + } + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessDuplicateOrder() + { + if ($this->access('edit')) { + $errors = []; + if (!$id_order = Tools::getValue('id_order')) { + $errors[] = $this->trans('Invalid order', [], 'Admin.Orderscustomers.Notification'); + } + $cart = Cart::getCartByOrderId($id_order); + $new_cart = $cart->duplicate(); + if (!$new_cart || !Validate::isLoadedObject($new_cart['cart'])) { + $errors[] = $this->trans('The order cannot be renewed.', [], 'Admin.Orderscustomers.Notification'); + } elseif (!$new_cart['success']) { + $errors[] = $this->trans('The order cannot be renewed.', [], 'Admin.Orderscustomers.Notification'); + } else { + $this->context->cart = $new_cart['cart']; + echo json_encode($this->ajaxReturnVars()); + } + } + } + + public function ajaxProcessDeleteVoucher() + { + if ($this->access('edit')) { + if ($this->context->cart->removeCartRule((int) Tools::getValue('id_cart_rule'))) { + echo json_encode($this->ajaxReturnVars()); + } + } + } + + public function ajaxProcessupdateFreeShipping() + { + if ($this->access('edit')) { + if (!$id_cart_rule = CartRule::getIdByCode(CartRule::BO_ORDER_CODE_PREFIX . (int) $this->context->cart->id)) { + $cart_rule = new CartRule(); + $cart_rule->code = CartRule::BO_ORDER_CODE_PREFIX . (int) $this->context->cart->id; + $cart_rule->name = [Configuration::get('PS_LANG_DEFAULT') => $this->trans('Free Shipping', [], 'Admin.Orderscustomers.Feature')]; + $cart_rule->id_customer = (int) $this->context->cart->id_customer; + $cart_rule->free_shipping = true; + $cart_rule->quantity = 1; + $cart_rule->quantity_per_user = 1; + $cart_rule->minimum_amount_currency = (int) $this->context->cart->id_currency; + $cart_rule->reduction_currency = (int) $this->context->cart->id_currency; + $cart_rule->date_from = date('Y-m-d H:i:s', time()); + $cart_rule->date_to = date('Y-m-d H:i:s', time() + 24 * 36000); + $cart_rule->active = 1; + $cart_rule->add(); + } else { + $cart_rule = new CartRule((int) $id_cart_rule); + } + + $this->context->cart->removeCartRule((int) $cart_rule->id); + if (Tools::getValue('free_shipping')) { + $this->context->cart->addCartRule((int) $cart_rule->id); + } + + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessAddVoucher() + { + if ($this->access('edit')) { + $errors = []; + if (!($id_cart_rule = Tools::getValue('id_cart_rule')) || !$cart_rule = new CartRule((int) $id_cart_rule)) { + $errors[] = $this->trans('Invalid voucher.', [], 'Admin.Catalog.Notification'); + } elseif ($err = $cart_rule->checkValidity($this->context)) { + $errors[] = $err; + } + if (!count($errors)) { + if (!$this->context->cart->addCartRule((int) $cart_rule->id)) { + $errors[] = $this->trans('Can\'t add the voucher.', [], 'Admin.Advparameters.Notification'); + } + } + echo json_encode(array_merge($this->ajaxReturnVars(), ['errors' => $errors])); + } + } + + public function ajaxProcessUpdateAddress() + { + if ($this->access('edit')) { + echo json_encode(['addresses' => $this->context->customer->getAddresses((int) $this->context->cart->id_lang)]); + } + } + + public function ajaxProcessUpdateAddresses() + { + if ($this->access('edit')) { + if (($id_address_delivery = (int) Tools::getValue('id_address_delivery')) && + ($address_delivery = new Address((int) $id_address_delivery)) && + $address_delivery->id_customer == $this->context->cart->id_customer) { + $this->context->cart->id_address_delivery = (int) $address_delivery->id; + } + + if (($id_address_invoice = (int) Tools::getValue('id_address_invoice')) && + ($address_invoice = new Address((int) $id_address_invoice)) && + $address_invoice->id_customer = $this->context->cart->id_customer) { + $this->context->cart->id_address_invoice = (int) $address_invoice->id; + } + $this->context->cart->save(); + + echo json_encode($this->ajaxReturnVars()); + } + } + + protected function getCartSummary() + { + $summary = $this->context->cart->getSummaryDetails(null, true); + $currency = Context::getContext()->currency; + if (count($summary['products'])) { + foreach ($summary['products'] as &$product) { + $product['numeric_price'] = $product['price']; + $product['numeric_total'] = $product['total']; + $product['price'] = str_replace($currency->symbol, '', $this->context->getCurrentLocale()->formatPrice($product['price'], $currency->iso_code)); + $product['total'] = str_replace($currency->symbol, '', $this->context->getCurrentLocale()->formatPrice($product['total'], $currency->iso_code)); + $product['image_link'] = $this->context->link->getImageLink($product['link_rewrite'], $product['id_image'], 'small_default'); + if (!isset($product['attributes_small'])) { + $product['attributes_small'] = ''; + } + $product['customized_datas'] = Product::getAllCustomizedDatas((int) $this->context->cart->id, null, true, null, (int) $product['id_customization']); + } + } + if (count($summary['discounts'])) { + foreach ($summary['discounts'] as &$voucher) { + $voucher['value_real'] = $this->context->getCurrentLocale()->formatPrice($voucher['value_real'], $currency->iso_code); + } + } + + if (isset($summary['gift_products']) && count($summary['gift_products'])) { + foreach ($summary['gift_products'] as &$product) { + $product['image_link'] = $this->context->link->getImageLink($product['link_rewrite'], $product['id_image'], 'small_default'); + if (!isset($product['attributes_small'])) { + $product['attributes_small'] = ''; + } + } + } + + return $summary; + } + + protected function getDeliveryOptionList() + { + $delivery_option_list_formated = []; + $delivery_option_list = $this->context->cart->getDeliveryOptionList(); + + if (!count($delivery_option_list)) { + return []; + } + + $id_default_carrier = (int) Configuration::get('PS_CARRIER_DEFAULT'); + foreach (current($delivery_option_list) as $key => $delivery_option) { + $name = ''; + $first = true; + $id_default_carrier_delivery = false; + foreach ($delivery_option['carrier_list'] as $carrier) { + if (!$first) { + $name .= ', '; + } else { + $first = false; + } + + $name .= $carrier['instance']->name; + + if ($delivery_option['unique_carrier']) { + $name .= ' - ' . $carrier['instance']->delay[$this->context->employee->id_lang]; + } + + if (!$id_default_carrier_delivery) { + $id_default_carrier_delivery = (int) $carrier['instance']->id; + } + if ($carrier['instance']->id == $id_default_carrier) { + $id_default_carrier_delivery = $id_default_carrier; + } + if (!$this->context->cart->id_carrier) { + $this->context->cart->setDeliveryOption([$this->context->cart->id_address_delivery => (int) $carrier['instance']->id . ',']); + $this->context->cart->save(); + } + } + $delivery_option_list_formated[] = ['name' => $name, 'key' => $key]; + } + + return $delivery_option_list_formated; + } + + public function displayAjaxSearchCarts() + { + $id_customer = (int) Tools::getValue('id_customer'); + $carts = Cart::getCustomerCarts((int) $id_customer, false); + $orders = Order::getCustomerOrders((int) $id_customer); + + if (count($carts)) { + foreach ($carts as $key => &$cart) { + $cart_obj = new Cart((int) $cart['id_cart']); + if ($cart['id_cart'] == $this->context->cart->id) { + unset($carts[$key]); + + continue; + } + $currency = new Currency((int) $cart['id_currency']); + $cart['total_price'] = $this->context->getCurrentLocale()->formatPrice($cart_obj->getOrderTotal(), $currency->iso_code); + } + } + if (count($orders)) { + foreach ($orders as &$order) { + $currency = new Currency((int) $order['id_currency']); + $order['total_paid_real'] = $this->context->getCurrentLocale()->formatPrice($order['total_paid_real'], $currency->iso_code); + } + } + if ($orders || $carts) { + $to_return = array_merge( + $this->ajaxReturnVars(), + [ + 'carts' => $carts, + 'orders' => $orders, + 'found' => true, + ] + ); + } else { + $to_return = array_merge($this->ajaxReturnVars(), ['found' => false]); + } + + echo json_encode($to_return); + } + + public function ajaxReturnVars() + { + $id_cart = (int) $this->context->cart->id; + $message_content = ''; + if ($message = Message::getMessageByCartId((int) $this->context->cart->id)) { + $message_content = $message['message']; + } + $cart_rules = $this->context->cart->getCartRules(CartRule::FILTER_ACTION_SHIPPING); + + $free_shipping = false; + if (count($cart_rules)) { + foreach ($cart_rules as $cart_rule) { + if ($cart_rule['id_cart_rule'] == CartRule::getIdByCode(CartRule::BO_ORDER_CODE_PREFIX . (int) $this->context->cart->id)) { + $free_shipping = true; + + break; + } + } + } + + $addresses = $this->context->customer->getAddresses((int) $this->context->cart->id_lang); + + foreach ($addresses as &$data) { + $address = new Address((int) $data['id_address']); + $data['formated_address'] = AddressFormat::generateAddress($address, [], '
'); + } + + return [ + 'summary' => $this->getCartSummary(), + 'delivery_option_list' => $this->getDeliveryOptionList(), + 'cart' => $this->context->cart, + 'currency' => new Currency($this->context->cart->id_currency), + 'addresses' => $addresses, + 'id_cart' => $id_cart, + 'order_message' => $message_content, + 'link_order' => $this->context->link->getPageLink( + 'order', + false, + (int) $this->context->cart->id_lang, + 'step=3&recover_cart=' . $id_cart . '&token_cart=' . md5(_COOKIE_KEY_ . 'recover_cart_' . $id_cart) + ), + 'free_shipping' => (int) $free_shipping, + ]; + } + + public function initToolbar() + { + parent::initToolbar(); + unset($this->toolbar_btn['new']); + } + + /** + * Display an image as a download. + */ + public function displayAjaxCustomizationImage() + { + if (!Tools::isSubmit('img') || !Tools::isSubmit('name')) { + return; + } + + $img = Tools::getValue('img'); + $name = Tools::getValue('name'); + $path = _PS_UPLOAD_DIR_ . $img; + + if (Validate::isMd5($img) && Validate::isGenericName($path)) { + header('Content-type: image/jpeg'); + header('Content-Disposition: attachment; filename="' . $name . '.jpg"'); + readfile($path); + } + } + + public function displayAjaxGetSummary() + { + echo json_encode($this->ajaxReturnVars()); + } + + public function ajaxProcessUpdateProductPrice() + { + if ($this->access('edit')) { + SpecificPrice::deleteByIdCart((int) $this->context->cart->id, (int) Tools::getValue('id_product'), (int) Tools::getValue('id_product_attribute')); + $specific_price = new SpecificPrice(); + $specific_price->id_cart = (int) $this->context->cart->id; + $specific_price->id_shop = 0; + $specific_price->id_shop_group = 0; + $specific_price->id_currency = 0; + $specific_price->id_country = 0; + $specific_price->id_group = 0; + $specific_price->id_customer = (int) $this->context->customer->id; + $specific_price->id_product = (int) Tools::getValue('id_product'); + $specific_price->id_product_attribute = (int) Tools::getValue('id_product_attribute'); + $specific_price->price = (float) Tools::getValue('price'); + $specific_price->from_quantity = 1; + $specific_price->reduction = 0; + $specific_price->reduction_type = 'amount'; + $specific_price->from = '0000-00-00 00:00:00'; + $specific_price->to = '0000-00-00 00:00:00'; + $specific_price->add(); + echo json_encode($this->ajaxReturnVars()); + } + } + + public static function getOrderTotalUsingTaxCalculationMethod($id_cart) + { + $context = Context::getContext(); + $context->cart = new Cart($id_cart); + $context->currency = new Currency((int) $context->cart->id_currency); + $context->customer = new Customer((int) $context->cart->id_customer); + + return Cart::getTotalCart($id_cart, true, Cart::BOTH_WITHOUT_SHIPPING); + } + + public static function replaceZeroByShopName($echo, $tr) + { + return $echo == '0' ? Carrier::getCarrierNameFromShopName() : $echo; + } + + public function displayDeleteLink($token, $id, $name = null) + { + // don't display ordered carts + foreach ($this->_list as $row) { + if ($row['id_cart'] == $id && isset($row['id_order']) && is_numeric($row['id_order'])) { + return; + } + } + + return $this->helper->displayDeleteLink($token, $id, $name); + } + + public function renderList() + { + if (!($this->fields_list && is_array($this->fields_list))) { + return false; + } + $this->getList($this->context->language->id); + + $helper = new HelperList(); + + // Empty list is ok + if (!is_array($this->_list)) { + $this->displayWarning($this->trans('Bad SQL query', [], 'Admin.Notifications.Error') . '
' . htmlspecialchars($this->_list_error)); + + return false; + } + + $this->setHelperDisplay($helper); + $helper->tpl_vars = $this->tpl_list_vars; + $helper->tpl_delete_link_vars = $this->tpl_delete_link_vars; + + // For compatibility reasons, we have to check standard actions in class attributes + foreach ($this->actions_available as $action) { + if (!in_array($action, $this->actions) && isset($this->$action) && $this->$action) { + $this->actions[] = $action; + } + } + $helper->is_cms = $this->is_cms; + $skip_list = []; + + foreach ($this->_list as $row) { + if (isset($row['id_order']) && is_numeric($row['id_order'])) { + $skip_list[] = $row['id_cart']; + } + } + + if (array_key_exists('delete', $helper->list_skip_actions)) { + $helper->list_skip_actions['delete'] = array_merge($helper->list_skip_actions['delete'], (array) $skip_list); + } else { + $helper->list_skip_actions['delete'] = (array) $skip_list; + } + + $list = $helper->generateList($this->_list, $this->fields_list); + + return $list; + } +} diff --git a/controllers/admin/AdminCmsCategoriesController.php b/controllers/admin/AdminCmsCategoriesController.php new file mode 100644 index 00000000..53597203 --- /dev/null +++ b/controllers/admin/AdminCmsCategoriesController.php @@ -0,0 +1,320 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CMSCategory $object + */ +class AdminCmsCategoriesControllerCore extends AdminController +{ + /** @var object CMSCategory() instance for navigation */ + protected $cms_category; + + protected $position_identifier = 'id_cms_category_to_move'; + + /** + * @deprecated since 1.7.6, to be removed in the next minor + */ + public function __construct() + { + @trigger_error( + 'The AdminCmsCategoriesController is deprecated and will be removed in the next minor', + E_USER_DEPRECATED + ); + + $this->bootstrap = true; + $this->is_cms = true; + $this->table = 'cms_category'; + $this->list_id = 'cms_category'; + $this->className = 'CMSCategory'; + $this->lang = true; + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->_orderBy = 'position'; + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + $this->tpl_list_vars['icon'] = 'icon-folder-close'; + $this->tpl_list_vars['title'] = $this->trans('Categories', array(), 'Admin.Catalog.Feature'); + $this->fields_list = array( + 'id_cms_category' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'width' => 'auto', 'callback' => 'hideCMSCategoryPosition', 'callback_object' => 'CMSCategory'), + 'description' => array('title' => $this->trans('Description', array(), 'Admin.Global'), 'maxlength' => 90, 'orderby' => false), + 'position' => array('title' => $this->trans('Position', array(), 'Admin.Global'), 'filter_key' => 'position', 'align' => 'center', 'class' => 'fixed-width-sm', 'position' => 'position'), + 'active' => array( + 'title' => $this->trans('Displayed', array(), 'Admin.Global'), 'class' => 'fixed-width-sm', 'active' => 'status', + 'align' => 'center', 'type' => 'bool', 'orderby' => false, + ), + ); + + // The controller can't be call directly + // In this case, AdminCmsContentController::getCurrentCMSCategory() is null + if (!AdminCmsContentController::getCurrentCMSCategory()) { + $this->redirect_after = '?controller=AdminCmsContent&token=' . Tools::getAdminTokenLite('AdminCmsContent'); + $this->redirect(); + } + + $this->cms_category = AdminCmsContentController::getCurrentCMSCategory(); + $this->_where = ' AND `id_parent` = ' . (int) $this->cms_category->id; + $this->_select = 'position '; + } + + public function getTabSlug() + { + return 'ROLE_MOD_TAB_ADMINCMSCONTENT_'; + } + + public function renderList() + { + $this->initToolbar(); + $this->_group = 'GROUP BY a.`id_cms_category`'; + if (isset($this->toolbar_btn['new'])) { + $this->toolbar_btn['new']['href'] .= '&id_parent=' . (int) Tools::getValue('id_cms_category'); + } + + return parent::renderList(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitAdd' . $this->table)) { + $this->action = 'save'; + if ($id_cms_category = (int) Tools::getValue('id_cms_category')) { + $this->id_object = $id_cms_category; + if (!CMSCategory::checkBeforeMove($id_cms_category, (int) Tools::getValue('id_parent'))) { + $this->errors[] = $this->trans('The page Category cannot be moved here.', array(), 'Admin.Design.Notification'); + + return false; + } + } + $object = parent::postProcess(); + $this->updateAssoShop((int) Tools::getValue('id_cms_category')); + if ($object !== false) { + Tools::redirectAdmin(self::$currentIndex . '&conf=3&id_cms_category=' . (int) $object->id . '&token=' . Tools::getValue('token')); + } + + return $object; + } elseif (Tools::isSubmit('statuscms_category') && Tools::getValue($this->identifier)) { + // Change object statuts (active, inactive) + if ($this->access('edit')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + if ($object->toggleStatus()) { + $identifier = ((int) $object->id_parent ? '&id_cms_category=' . (int) $object->id_parent : ''); + Tools::redirectAdmin(self::$currentIndex . '&conf=5' . $identifier . '&token=' . Tools::getValue('token')); + } else { + $this->errors[] = $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('delete' . $this->table)) { + // Delete object + if ($this->access('delete')) { + if (Validate::isLoadedObject($object = $this->loadObject()) && isset($this->fieldImageSettings)) { + // check if request at least one object with noZeroObject + if (isset($object->noZeroObject) && count($taxes = call_user_func(array($this->className, $object->noZeroObject))) <= 1) { + $this->errors[] = $this->trans('You need at least one object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . '
' . $this->trans('You cannot delete all of the items.', array(), 'Admin.Notifications.Error'); + } else { + $identifier = ((int) $object->id_parent ? '&' . $this->identifier . '=' . (int) $object->id_parent : ''); + if ($this->deleted) { + $object->deleted = 1; + if ($object->update()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getValue('token') . $identifier); + } + } elseif ($object->delete()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getValue('token') . $identifier); + } + $this->errors[] = $this->trans('An error occurred during deletion.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while deleting the object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('position')) { + $object = new CMSCategory((int) Tools::getValue($this->identifier, Tools::getValue('id_cms_category_to_move', 1))); + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object)) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + $identifier = ((int) $object->id_parent ? '&' . $this->identifier . '=' . (int) $object->id_parent : ''); + $token = Tools::getAdminTokenLite('AdminCmsContent'); + Tools::redirectAdmin( + self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . $identifier . '&token=' . $token + ); + } + } elseif (Tools::getValue('submitDel' . $this->table) || Tools::getValue('submitBulkdelete' . $this->table)) { + // Delete multiple objects + if ($this->access('delete')) { + if (Tools::isSubmit($this->table . 'Box')) { + $cms_category = new CMSCategory(); + $result = true; + $result = $cms_category->deleteSelection(Tools::getValue($this->table . 'Box')); + if ($result) { + $cms_category->cleanPositions((int) Tools::getValue('id_cms_category')); + $token = Tools::getAdminTokenLite('AdminCmsContent'); + Tools::redirectAdmin(self::$currentIndex . '&conf=2&token=' . $token . '&id_cms_category=' . (int) Tools::getValue('id_cms_category')); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', array(), 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } + parent::postProcess(); + } + + public function renderForm() + { + $this->display = 'edit'; + $this->initToolbar(); + if (!$this->loadObject(true)) { + return; + } + + $categories = CMSCategory::getCategories($this->context->language->id, false); + $html_categories = CMSCategory::recurseCMSCategory($categories, $categories[0][1], 1, $this->getFieldValue($this->object, 'id_parent'), 1); + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('CMS Category', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-folder-close', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'class' => 'copyMeta2friendlyURL', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Displayed', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + // custom template + array( + 'type' => 'select_category', + 'label' => $this->trans('Parent category', array(), 'Admin.Design.Feature'), + 'name' => 'id_parent', + 'options' => array( + 'html' => $html_categories, + ), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'rows' => 5, + 'cols' => 40, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta title', array(), 'Admin.Global'), + 'name' => 'meta_title', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta description', array(), 'Admin.Global'), + 'name' => 'meta_description', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta keywords', array(), 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Friendly URL', array(), 'Admin.Global'), + 'name' => 'link_rewrite', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Only letters and the minus (-) character are allowed.', array(), 'Admin.Catalog.Help'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->tpl_form_vars['PS_ALLOW_ACCENTED_CHARS_URL'] = (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL'); + + return parent::renderForm(); + } +} diff --git a/controllers/admin/AdminCmsContentController.php b/controllers/admin/AdminCmsContentController.php new file mode 100644 index 00000000..76ceff62 --- /dev/null +++ b/controllers/admin/AdminCmsContentController.php @@ -0,0 +1,314 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CMS $object + */ +class AdminCmsContentControllerCore extends AdminController +{ + /** @var object adminCMSCategories() instance */ + protected $admin_cms_categories; + + /** @var object adminCMS() instance */ + protected $admin_cms; + + /** @var object Category() instance for navigation */ + protected static $category = null; + + /** + * @deprecated since 1.7.6, to be removed in the next minor + */ + public function __construct() + { + @trigger_error( + 'The AdminCmsContentController is deprecated and will be removed in the next minor', + E_USER_DEPRECATED + ); + + $this->bootstrap = true; + /* Get current category */ + $id_cms_category = (int) Tools::getValue('id_cms_category', Tools::getValue('id_cms_category_parent', 1)); + self::$category = new CMSCategory($id_cms_category); + if (!Validate::isLoadedObject(self::$category)) { + die('Category cannot be loaded'); + } + + parent::__construct(); + + $this->table = 'cms'; + $this->className = 'CMS'; + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->admin_cms_categories = new AdminCmsCategoriesController(); + $this->admin_cms_categories->tabAccess = $this->tabAccess; + $this->admin_cms_categories->init(); + $this->admin_cms = new AdminCmsController(); + $this->admin_cms->tabAccess = $this->tabAccess; + $this->admin_cms->init(); + $this->context->controller = $this; + } + + /** + * Return current category. + * + * @return object + */ + public static function getCurrentCMSCategory() + { + return self::$category; + } + + public function initProcess() + { + if (((Tools::isSubmit('submitAddcms_category') || Tools::isSubmit('submitAddcms_categoryAndStay')) && count($this->admin_cms_categories->errors)) + || Tools::isSubmit('updatecms_category') + || Tools::isSubmit('addcms_category')) { + $this->display = 'edit_category'; + } elseif (((Tools::isSubmit('submitAddcms') || Tools::isSubmit('submitAddcmsAndStay')) && count($this->admin_cms->errors)) + || Tools::isSubmit('updatecms') + || Tools::isSubmit('addcms')) { + $this->display = 'edit_page'; + } else { + $this->display = 'list'; + } + } + + public function initContent() + { + $id_cms_category = (int) Tools::getValue('id_cms_category'); + + if (!$id_cms_category) { + $id_cms_category = 1; + } + + if ($this->display == 'list') { + $this->page_header_toolbar_btn['new_cms_category'] = array( + 'href' => self::$currentIndex . '&addcms_category&token=' . $this->token, + 'desc' => $this->trans('Add new page category', array(), 'Admin.Design.Help'), + 'icon' => 'process-icon-new', + ); + $this->page_header_toolbar_btn['new_cms_page'] = array( + 'href' => self::$currentIndex . '&addcms&id_cms_category=' . (int) $id_cms_category . '&token=' . $this->token, + 'desc' => $this->trans('Add new page', array(), 'Admin.Design.Help'), + 'icon' => 'process-icon-new', + ); + } + + $this->page_header_toolbar_title = implode(' ' . Configuration::get('PS_NAVIGATION_PIPE') . ' ', $this->toolbar_title); + + if (is_array($this->page_header_toolbar_btn) + && $this->page_header_toolbar_btn instanceof Traversable + || trim($this->page_header_toolbar_title) != '') { + $this->show_page_header_toolbar = true; + } + + $this->admin_cms_categories->token = $this->token; + $this->admin_cms->token = $this->token; + + if ($this->display == 'edit_category') { + $this->content .= $this->admin_cms_categories->renderForm(); + } elseif ($this->display == 'edit_page') { + $this->content .= $this->admin_cms->renderForm(); + } elseif ($this->display == 'list') { + $id_cms_category = (int) Tools::getValue('id_cms_category'); + if (!$id_cms_category) { + $id_cms_category = 1; + } + + // CMS categories breadcrumb + $cms_tabs = array('cms_category', 'cms'); + // Cleaning links + $cat_bar_index = self::$currentIndex; + foreach ($cms_tabs as $tab) { + if (Tools::getValue($tab . 'Orderby') && Tools::getValue($tab . 'Orderway')) { + $cat_bar_index = preg_replace('/&' . $tab . 'Orderby=([a-z _]*)&' . $tab . 'Orderway=([a-z]*)/i', '', self::$currentIndex); + } + } + $this->context->smarty->assign(array( + 'cms_breadcrumb' => Tools::getPath($cat_bar_index, $id_cms_category, '', '', 'cms'), + )); + + $this->content .= $this->admin_cms_categories->renderList(); + $this->admin_cms->id_cms_category = $id_cms_category; + $this->content .= $this->admin_cms->renderList(); + } + + $this->context->smarty->assign(array( + 'content' => $this->content, + 'show_page_header_toolbar' => $this->show_page_header_toolbar, + 'title' => $this->page_header_toolbar_title, + 'toolbar_btn' => $this->page_header_toolbar_btn, + 'page_header_toolbar_btn' => $this->page_header_toolbar_btn, + 'page_header_toolbar_title' => $this->toolbar_title, + )); + } + + public function initToolbarTitle() + { + $this->toolbar_title = is_array($this->breadcrumbs) ? array_unique($this->breadcrumbs) : array($this->breadcrumbs); + + $id_cms_category = (int) Tools::getValue('id_cms_category'); + if ($id_cms_category && $id_cms_category !== 1) { + $cms_category = new CMSCategory($id_cms_category); + } + $id_cms_page = Tools::getValue('id_cms'); + + if ($this->display == 'edit_category') { + if (Tools::getValue('addcms_category') !== false) { + $this->toolbar_title[] = $this->trans('Add new category', array(), 'Admin.Design.Feature'); + } else { + if (isset($cms_category)) { + $this->toolbar_title[] = $this->trans('Edit category: %name%', array('%name%' => $cms_category->name[$this->context->employee->id_lang]), 'Admin.Design.Feature'); + } + } + } elseif ($this->display == 'edit_page') { + if (Tools::getValue('addcms') !== false) { + $this->toolbar_title[] = $this->trans('Add new page', array(), 'Admin.Design.Feature'); + } elseif ($id_cms_page) { + $cms_page = new CMS($id_cms_page); + $this->toolbar_title[] = $this->trans('Edit page: %meta_title%', array('%meta_title%' => $cms_page->meta_title[$this->context->employee->id_lang]), 'Admin.Design.Feature'); + } + } elseif ($this->display == 'list' && isset($cms_category)) { + $this->toolbar_title[] = $this->trans('Category: %category%', array('%category%' => $cms_category->name[$this->context->employee->id_lang]), 'Admin.Design.Feature'); + } + } + + public function postProcess() + { + $this->admin_cms->postProcess(); + $this->admin_cms_categories->postProcess(); + + parent::postProcess(); + + if (isset($this->admin_cms->errors)) { + $this->errors = array_merge($this->errors, $this->admin_cms->errors); + } + + if (isset($this->admin_cms_categories->errors)) { + $this->errors = array_merge($this->errors, $this->admin_cms_categories->errors); + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryUi('ui.widget'); + $this->addJqueryPlugin('tagify'); + } + + public function ajaxProcessUpdateCmsPositions() + { + if ($this->access('edit')) { + $id_cms = (int) Tools::getValue('id_cms'); + $id_category = (int) Tools::getValue('id_cms_category'); + $way = (int) Tools::getValue('way'); + $positions = Tools::getValue('cms'); + if (is_array($positions)) { + foreach ($positions as $key => $value) { + $pos = explode('_', $value); + if ((isset($pos[1], $pos[2])) && ($pos[1] == $id_category && $pos[2] == $id_cms)) { + $position = $key; + + break; + } + } + } + $cms = new CMS($id_cms); + if (Validate::isLoadedObject($cms)) { + if (isset($position) && $cms->updatePosition($way, $position)) { + die(true); + } else { + die('{"hasError" : true, "errors" : "Can not update cms position"}'); + } + } else { + die('{"hasError" : true, "errors" : "This cms can not be loaded"}'); + } + } + } + + public function ajaxProcessUpdateCmsCategoriesPositions() + { + if ($this->access('edit')) { + $id_cms_category_to_move = (int) Tools::getValue('id_cms_category_to_move'); + $id_cms_category_parent = (int) Tools::getValue('id_cms_category_parent'); + $way = (int) Tools::getValue('way'); + $positions = Tools::getValue('cms_category'); + if (is_array($positions)) { + foreach ($positions as $key => $value) { + $pos = explode('_', $value); + if ((isset($pos[1], $pos[2])) && ($pos[1] == $id_cms_category_parent && $pos[2] == $id_cms_category_to_move)) { + $position = $key; + + break; + } + } + } + $cms_category = new CMSCategory($id_cms_category_to_move); + if (Validate::isLoadedObject($cms_category)) { + if (isset($position) && $cms_category->updatePosition($way, $position)) { + die(true); + } else { + die('{"hasError" : true, "errors" : "Can not update cms categories position"}'); + } + } else { + die('{"hasError" : true, "errors" : "This cms category can not be loaded"}'); + } + } + } + + public function ajaxProcessPublishCMS() + { + if ($this->access('edit')) { + if ($id_cms = (int) Tools::getValue('id_cms')) { + $bo_cms_url = _PS_BASE_URL_ . __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/index.php?tab=AdminCmsContent&id_cms=' . (int) $id_cms . '&updatecms&token=' . $this->token; + + if (Tools::getValue('redirect')) { + die($bo_cms_url); + } + + $cms = new CMS((int) (Tools::getValue('id_cms'))); + if (!Validate::isLoadedObject($cms)) { + die('error: invalid id'); + } + + $cms->active = 1; + if ($cms->save()) { + die($bo_cms_url); + } else { + die('error: saving'); + } + } else { + die('error: parameters'); + } + } + } +} diff --git a/controllers/admin/AdminCmsController.php b/controllers/admin/AdminCmsController.php new file mode 100644 index 00000000..43fa77bb --- /dev/null +++ b/controllers/admin/AdminCmsController.php @@ -0,0 +1,468 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CMS $object + */ +class AdminCmsControllerCore extends AdminController +{ + protected $category; + + public $id_cms_category; + + protected $position_identifier = 'id_cms'; + + /** + * @deprecated since 1.7.6, to be removed in the next minor + */ + public function __construct() + { + @trigger_error( + 'The AdminCmsController is deprecated and will be removed in the next minor', + E_USER_DEPRECATED + ); + + $this->bootstrap = true; + $this->table = 'cms'; + $this->list_id = 'cms'; + $this->className = 'CMS'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->_orderBy = 'position'; + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + $this->fields_list = array( + 'id_cms' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'link_rewrite' => array( + 'title' => $this->trans('URL', array(), 'Admin.Global'), + ), + 'meta_title' => array( + 'title' => $this->trans('Title', array(), 'Admin.Global'), + 'filter_key' => 'b!meta_title', + 'maxlength' => 50, + ), + 'head_seo_title' => array( + 'title' => $this->trans('Meta title', array(), 'Admin.Global'), + 'filter_key' => 'b!head_seo_title', + 'maxlength' => 50, + ), + 'position' => array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'position' => 'position', + ), + 'active' => array( + 'title' => $this->trans('Displayed', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'class' => 'fixed-width-sm', + 'type' => 'bool', + 'orderby' => false, + ), + ); + + // The controller can't be call directly + // In this case, AdminCmsContentController::getCurrentCMSCategory() is null + if (!AdminCmsContentController::getCurrentCMSCategory()) { + $this->redirect_after = '?controller=AdminCmsContent&token=' . Tools::getAdminTokenLite('AdminCmsContent'); + $this->redirect(); + } + + $this->_category = AdminCmsContentController::getCurrentCMSCategory(); + $this->tpl_list_vars['icon'] = 'icon-folder-close'; + $this->tpl_list_vars['title'] = $this->trans('Pages in category "%name%"', array('%name%' => $this->_category->name[Context::getContext()->employee->id_lang]), 'Admin.Design.Feature'); + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'cms_category` c ON (c.`id_cms_category` = a.`id_cms_category`)'; + $this->_select = 'a.position '; + $this->_where = ' AND c.id_cms_category = ' . (int) $this->_category->id; + } + + public function getTabSlug() + { + return 'ROLE_MOD_TAB_ADMINCMSCONTENT_'; + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_btn['save-and-preview'] = array( + 'href' => '#', + 'desc' => $this->trans('Save and preview', array(), 'Admin.Actions'), + ); + $this->page_header_toolbar_btn['save-and-stay'] = array( + 'short' => $this->trans('Save and stay', array(), 'Admin.Actions'), + 'href' => '#', + 'desc' => $this->trans('Save and stay', array(), 'Admin.Actions'), + ); + + return parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + if (!$this->loadObject(true)) { + return; + } + + if (Validate::isLoadedObject($this->object)) { + $this->display = 'edit'; + } else { + $this->display = 'add'; + } + + $this->initToolbar(); + $this->initPageHeaderToolbar(); + + $categories = CMSCategory::getCategories($this->context->language->id, false); + $html_categories = CMSCategory::recurseCMSCategory($categories, $categories[0][1], 1, $this->getFieldValue($this->object, 'id_cms_category'), 1); + + $this->fields_form = array( + 'tinymce' => true, + 'legend' => array( + 'title' => $this->l('Page'), + 'icon' => 'icon-folder-close', + ), + 'input' => array( + // custom template + array( + 'type' => 'select_category', + 'label' => $this->trans('Page Category', array(), 'Admin.Design.Feature'), + 'name' => 'id_cms_category', + 'options' => array( + 'html' => $html_categories, + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Title', array(), 'Admin.Global'), + 'name' => 'meta_title', + 'id' => 'name', // for copyMeta2friendlyURL compatibility + 'lang' => true, + 'required' => true, + 'class' => 'copyMeta2friendlyURL', + 'hint' => array( + $this->trans('Used in the h1 page tag, and as the default title tag value.', array(), 'Admin.Design.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta title', array(), 'Admin.Global'), + 'name' => 'head_seo_title', + 'lang' => true, + 'hint' => array( + $this->trans('Used to override the title tag value. If left blank, the default title value is used.', array(), 'Admin.Design.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta description', array(), 'Admin.Global'), + 'name' => 'meta_description', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'tags', + 'label' => $this->trans('Meta keywords', array(), 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'hint' => array( + $this->trans('To add tags, click in the field, write something, and then press the "Enter" key.', array(), 'Admin.Shopparameters.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Friendly URL', array(), 'Admin.Global'), + 'name' => 'link_rewrite', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Only letters and the hyphen (-) character are allowed.', array(), 'Admin.Design.Feature'), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Page content', array(), 'Admin.Design.Feature'), + 'name' => 'content', + 'autoload_rte' => true, + 'lang' => true, + 'rows' => 5, + 'cols' => 40, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Indexation by search engines', array(), 'Admin.Design.Feature'), + 'name' => 'indexation', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'indexation_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'indexation_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Displayed', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + 'buttons' => array( + 'save_and_preview' => array( + 'name' => 'viewcms', + 'type' => 'submit', + 'title' => $this->trans('Save and preview', array(), 'Admin.Actions'), + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-preview', + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + if (Validate::isLoadedObject($this->object)) { + $this->context->smarty->assign('url_prev', $this->getPreviewUrl($this->object)); + } + + $this->tpl_form_vars = array( + 'active' => $this->object->active, + 'PS_ALLOW_ACCENTED_CHARS_URL', (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL'), + ); + + return parent::renderForm(); + } + + public function renderList() + { + $this->_group = 'GROUP BY a.`id_cms`'; + //self::$currentIndex = self::$currentIndex.'&cms'; + $this->position_group_identifier = (int) $this->id_cms_category; + + $this->toolbar_title = $this->trans('Pages in this category', array(), 'Admin.Design.Feature'); + $this->toolbar_btn['new'] = array( + 'href' => self::$currentIndex . '&add' . $this->table . '&id_cms_category=' . (int) $this->id_cms_category . '&token=' . $this->token, + 'desc' => $this->trans('Add new', array(), 'Admin.Actions'), + ); + + return parent::renderList(); + } + + public function displayList($token = null) + { + /* Display list header (filtering, pagination and column names) */ + $this->displayListHeader($token); + if (!count($this->_list)) { + echo '' . $this->trans('No items found', array(), 'Admin.Design.Notification') . ''; + } + + /* Show the content of the table */ + $this->displayListContent($token); + + /* Close list table and submit button */ + $this->displayListFooter($token); + } + + public function postProcess() + { + if (Tools::isSubmit('viewcms') && ($id_cms = (int) Tools::getValue('id_cms'))) { + parent::postProcess(); + if (($cms = new CMS($id_cms, $this->context->language->id)) && Validate::isLoadedObject($cms)) { + Tools::redirectAdmin(self::$currentIndex . '&id_cms=' . $id_cms . '&conf=4&updatecms&token=' . Tools::getAdminTokenLite('AdminCmsContent') . '&url_preview=1'); + } + } elseif (Tools::isSubmit('deletecms')) { + if (Tools::getValue('id_cms') == Configuration::get('PS_CONDITIONS_CMS_ID')) { + Configuration::updateValue('PS_CONDITIONS', 0); + Configuration::updateValue('PS_CONDITIONS_CMS_ID', 0); + } + $cms = new CMS((int) Tools::getValue('id_cms')); + $cms->cleanPositions($cms->id_cms_category); + if (!$cms->delete()) { + $this->errors[] = $this->trans('An error occurred while deleting the object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } else { + Tools::redirectAdmin(self::$currentIndex . '&id_cms_category=' . $cms->id_cms_category . '&conf=1&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } + } elseif (Tools::getValue('submitDel' . $this->table)) { + // Delete multiple objects + if ($this->access('delete')) { + if (Tools::isSubmit($this->table . 'Box')) { + $cms = new CMS(); + $result = true; + $result = $cms->deleteSelection(Tools::getValue($this->table . 'Box')); + if ($result) { + $cms->cleanPositions((int) Tools::getValue('id_cms_category')); + $token = Tools::getAdminTokenLite('AdminCmsContent'); + Tools::redirectAdmin(self::$currentIndex . '&conf=2&token=' . $token . '&id_cms_category=' . (int) Tools::getValue('id_cms_category')); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', array(), 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitAddcms') || Tools::isSubmit('submitAddcmsAndPreview')) { + parent::validateRules(); + if (count($this->errors)) { + return false; + } + if (!$id_cms = (int) Tools::getValue('id_cms')) { + $cms = new CMS(); + $this->copyFromPost($cms, 'cms'); + if (!$cms->add()) { + $this->errors[] = $this->trans('An error occurred while creating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } else { + $this->updateAssoShop($cms->id); + } + } else { + $cms = new CMS($id_cms); + $this->copyFromPost($cms, 'cms'); + if (!$cms->update()) { + $this->errors[] = $this->trans('An error occurred while updating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } else { + $this->updateAssoShop($cms->id); + } + } + if (Tools::isSubmit('view' . $this->table)) { + Tools::redirectAdmin(self::$currentIndex . '&id_cms=' . $cms->id . '&conf=4&updatecms&token=' . Tools::getAdminTokenLite('AdminCmsContent') . '&url_preview=1'); + } elseif (Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) { + Tools::redirectAdmin(self::$currentIndex . '&' . $this->identifier . '=' . $cms->id . '&conf=4&update' . $this->table . '&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } else { + Tools::redirectAdmin(self::$currentIndex . '&id_cms_category=' . $cms->id_cms_category . '&conf=4&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } + } elseif (Tools::isSubmit('way') && Tools::isSubmit('id_cms') && (Tools::isSubmit('position'))) { + /* @var CMS $object */ + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object = $this->loadObject())) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=4&id_cms_category=' . (int) $object->id_cms_category . '&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } + } elseif (Tools::isSubmit('statuscms') && Tools::isSubmit($this->identifier)) { + // Change object status (active, inactive) + if ($this->access('edit')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var CMS $object */ + if ($object->toggleStatus()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&id_cms_category=' . (int) $object->id_cms_category . '&token=' . Tools::getValue('token')); + } else { + $this->errors[] = $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitBulkdeletecms')) { + // Delete multiple CMS content + if ($this->access('delete')) { + $this->action = 'bulkdelete'; + $this->boxes = Tools::getValue($this->table . 'Box'); + if (is_array($this->boxes) && array_key_exists(0, $this->boxes)) { + $firstCms = new CMS((int) $this->boxes[0]); + $id_cms_category = (int) $firstCms->id_cms_category; + if (!$res = parent::postProcess(true)) { + return $res; + } + Tools::redirectAdmin(self::$currentIndex . '&conf=2&token=' . Tools::getAdminTokenLite('AdminCmsContent') . '&id_cms_category=' . $id_cms_category); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } else { + parent::postProcess(true); + } + } + + public function getPreviewUrl(CMS $cms) + { + $preview_url = $this->context->link->getCMSLink($cms, null, null, $this->context->language->id); + if (!$cms->active) { + $params = http_build_query( + array( + 'adtoken' => Tools::getAdminTokenLite('AdminCmsContent'), + 'ad' => basename(_PS_ADMIN_DIR_), + 'id_employee' => (int) $this->context->employee->id, + ) + ); + $preview_url .= (strpos($preview_url, '?') === false ? '?' : '&') . $params; + } + + return $preview_url; + } +} diff --git a/controllers/admin/AdminCountriesController.php b/controllers/admin/AdminCountriesController.php new file mode 100644 index 00000000..e84d6efd --- /dev/null +++ b/controllers/admin/AdminCountriesController.php @@ -0,0 +1,527 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Country $object + */ +class AdminCountriesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'country'; + $this->className = 'Country'; + $this->lang = true; + $this->deleted = false; + $this->_defaultOrderBy = 'name'; + $this->_defaultOrderWay = 'ASC'; + + $this->explicitSelect = true; + $this->addRowAction('edit'); + + parent::__construct(); + + $this->bulk_actions = [ + 'delete' => ['text' => $this->trans('Delete selected', [], 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Actions')], + 'AffectZone' => ['text' => $this->trans('Assign to a new zone', [], 'Admin.International.Feature')], + ]; + + $this->fieldImageSettings = [ + 'name' => 'logo', + 'dir' => 'st', + ]; + + $this->fields_options = [ + 'general' => [ + 'title' => $this->trans('Country options', [], 'Admin.International.Feature'), + 'fields' => [ + 'PS_RESTRICT_DELIVERED_COUNTRIES' => [ + 'title' => $this->trans('Restrict country selections in front office to those covered by active carriers', [], 'Admin.International.Help'), + 'cast' => 'intval', + 'type' => 'bool', + 'default' => '0', + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + ]; + + $zones_array = []; + $this->zones = Zone::getZones(); + foreach ($this->zones as $zone) { + $zones_array[$zone['id_zone']] = $zone['name']; + } + + $this->fields_list = [ + 'id_country' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Country', [], 'Admin.Global'), + 'filter_key' => 'b!name', + ], + 'iso_code' => [ + 'title' => $this->trans('ISO code', [], 'Admin.International.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'call_prefix' => [ + 'title' => $this->trans('Call prefix', [], 'Admin.International.Feature'), + 'align' => 'center', + 'callback' => 'displayCallPrefix', + 'class' => 'fixed-width-sm', + ], + 'zone' => [ + 'title' => $this->trans('Zone', [], 'Admin.Global'), + 'type' => 'select', + 'list' => $zones_array, + 'filter_key' => 'z!id_zone', + 'filter_type' => 'int', + 'order_key' => 'z!name', + ], + 'active' => [ + 'title' => $this->trans('Enabled', [], 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'filter_key' => 'a!active', + 'class' => 'fixed-width-sm', + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_country'] = [ + 'href' => self::$currentIndex . '&addcountry&token=' . $this->token, + 'desc' => $this->trans('Add new country', [], 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + /** + * AdminController::setMedia() override. + * + * @see AdminController::setMedia() + */ + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJqueryPlugin('fieldselection'); + } + + public function renderList() + { + $this->_select = 'z.`name` AS zone'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'zone` z ON (z.`id_zone` = a.`id_zone`)'; + $this->_use_found_rows = false; + + $this->tpl_list_vars['zones'] = Zone::getZones(); + $this->tpl_list_vars['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + $this->tpl_list_vars['POST'] = $_POST; + + return parent::renderList(); + } + + public function renderForm() + { + if (!($obj = $this->loadObject(true))) { + return; + } + + $address_layout = AddressFormat::getAddressCountryFormat($obj->id); + if ($value = Tools::getValue('address_layout')) { + $address_layout = $value; + } + + $default_layout = ''; + + // TODO: Use format from XML + $default_layout_tab = [ + ['firstname', 'lastname'], + ['company'], + ['vat_number'], + ['address1'], + ['address2'], + ['postcode', 'city'], + ['Country:name'], + ['phone'], + ]; + + foreach ($default_layout_tab as $line) { + $default_layout .= implode(' ', $line) . AddressFormat::FORMAT_NEW_LINE; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Countries', [], 'Admin.International.Feature'), + 'icon' => 'icon-globe', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Country', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Country name', [], 'Admin.International.Feature') . ' - ' . $this->trans('Invalid characters:', [], 'Admin.Global') . ' <>;=#{} ', + ], + [ + 'type' => 'text', + 'label' => $this->trans('ISO code', [], 'Admin.International.Feature'), + 'name' => 'iso_code', + 'maxlength' => 3, + 'class' => 'uppercase', + 'required' => true, + 'hint' => $this->trans('Two -- or three -- letter ISO code (e.g. "us" for United States).', [], 'Admin.International.Help'), + /* @TODO - add two lines for the hint? */ + /*'desc' => $this->trans('Two -- or three -- letter ISO code (e.g. U.S. for United States)', [], 'Admin.International.Help').'. + '. + $this->trans('Official list here', [], 'Admin.International.Feature').' + .'*/ + ], + [ + 'type' => 'text', + 'label' => $this->trans('Call prefix', [], 'Admin.International.Feature'), + 'name' => 'call_prefix', + 'maxlength' => 3, + 'class' => 'uppercase', + 'required' => true, + 'hint' => $this->trans('International call prefix, (e.g. 1 for United States).', [], 'Admin.International.Help'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Default currency', [], 'Admin.International.Feature'), + 'name' => 'id_currency', + 'options' => [ + 'query' => Currency::getCurrencies(false, true, true), + 'id' => 'id_currency', + 'name' => 'name', + 'default' => [ + 'label' => $this->trans('Default store currency', [], 'Admin.International.Feature'), + 'value' => 0, + ], + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Zone', [], 'Admin.Global'), + 'name' => 'id_zone', + 'options' => [ + 'query' => Zone::getZones(), + 'id' => 'id_zone', + 'name' => 'name', + ], + 'hint' => $this->trans('Geographical region.', [], 'Admin.International.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Does it need Zip/postal code?', [], 'Admin.International.Feature'), + 'name' => 'need_zip_code', + 'required' => false, + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'need_zip_code_on', + 'value' => 1, + 'label' => $this->trans('Yes', [], 'Admin.Global'), + ], + [ + 'id' => 'need_zip_code_off', + 'value' => 0, + 'label' => $this->trans('No', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Zip/postal code format', [], 'Admin.International.Feature'), + 'name' => 'zip_code_format', + 'required' => true, + 'desc' => $this->trans('Indicate the format of the postal code: use L for a letter, N for a number, and C for the country\'s ISO 3166-1 alpha-2 code. For example, NNNNN for the United States, France, Poland and many other; LNNNNLLL for Argentina, etc. If you do not want PrestaShop to verify the postal code for this country, leave it blank.', [], 'Admin.International.Help'), + ], + [ + 'type' => 'address_layout', + 'label' => $this->trans('Address format', [], 'Admin.International.Feature'), + 'name' => 'address_layout', + 'address_layout' => $address_layout, + 'encoding_address_layout' => urlencode($address_layout), + 'encoding_default_layout' => urlencode($default_layout), + 'display_valid_fields' => $this->displayValidFields(), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Active', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Display this country to your customers (the selected country will always be displayed in the Back Office).', [], 'Admin.International.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Contains states', [], 'Admin.International.Feature'), + 'name' => 'contains_states', + 'required' => false, + 'values' => [ + [ + 'id' => 'contains_states_on', + 'value' => 1, + 'label' => '' . $this->trans('Yes', [], 'Admin.Global') . '' . $this->trans('Yes', [], 'Admin.Global'), + ], + [ + 'id' => 'contains_states_off', + 'value' => 0, + 'label' => '' . $this->trans('No', [], 'Admin.Global') . '' . $this->trans('No', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Do you need a tax identification number?', [], 'Admin.International.Feature'), + 'name' => 'need_identification_number', + 'required' => false, + 'values' => [ + [ + 'id' => 'need_identification_number_on', + 'value' => 1, + 'label' => '' . $this->trans('Yes', [], 'Admin.Global') . '' . $this->trans('Yes', [], 'Admin.Global'), + ], + [ + 'id' => 'need_identification_number_off', + 'value' => 0, + 'label' => '' . $this->trans('No', [], 'Admin.Global') . '' . $this->trans('No', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Display tax label (e.g. "Tax incl.")', [], 'Admin.International.Feature'), + 'name' => 'display_tax_label', + 'required' => false, + 'values' => [ + [ + 'id' => 'display_tax_label_on', + 'value' => 1, + 'label' => '' . $this->trans('Yes', [], 'Admin.Global') . '' . $this->trans('Yes', [], 'Admin.Global'), + ], + [ + 'id' => 'display_tax_label_off', + 'value' => 0, + 'label' => '' . $this->trans('No', [], 'Admin.Global') . '' . $this->trans('No', [], 'Admin.Global'), + ], + ], + ], + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + return parent::renderForm(); + } + + public function processUpdate() + { + /** @var Country $country */ + $country = $this->loadObject(); + if (Validate::isLoadedObject($country) && Tools::getValue('id_zone')) { + $old_id_zone = $country->id_zone; + $results = Db::getInstance()->executeS('SELECT `id_state` FROM `' . _DB_PREFIX_ . 'state` WHERE `id_country` = ' . (int) $country->id . ' AND `id_zone` = ' . (int) $old_id_zone); + + if ($results && count($results)) { + $ids = []; + foreach ($results as $res) { + $ids[] = (int) $res['id_state']; + } + + if (count($ids)) { + $res = Db::getInstance()->execute( + 'UPDATE `' . _DB_PREFIX_ . 'state` + SET `id_zone` = ' . (int) Tools::getValue('id_zone') . ' + WHERE `id_state` IN (' . implode(',', $ids) . ')' + ); + } + } + } + + return parent::processUpdate(); + } + + public function postProcess() + { + if (!Tools::getValue('id_' . $this->table)) { + if (Validate::isLanguageIsoCode(Tools::getValue('iso_code')) && (int) Country::getByIso(Tools::getValue('iso_code'))) { + $this->errors[] = $this->trans('This ISO code already exists.You cannot create two countries with the same ISO code.', [], 'Admin.International.Notification'); + } + } elseif (Validate::isLanguageIsoCode(Tools::getValue('iso_code'))) { + $id_country = (int) Country::getByIso(Tools::getValue('iso_code')); + if ($id_country != 0 && $id_country != Tools::getValue('id_' . $this->table)) { + $this->errors[] = $this->trans('This ISO code already exists.You cannot create two countries with the same ISO code.', [], 'Admin.International.Notification'); + } + } + + return parent::postProcess(); + } + + public function processSave() + { + if (!$this->id_object) { + $tmp_addr_format = new AddressFormat(); + } else { + $tmp_addr_format = new AddressFormat($this->id_object); + } + + $tmp_addr_format->format = Tools::getValue('address_layout'); + + if (!$tmp_addr_format->checkFormatFields()) { + $error_list = $tmp_addr_format->getErrorList(); + foreach ($error_list as $error) { + $this->errors[] = $error; + } + } + if (strlen($tmp_addr_format->format) <= 0) { + $this->errors[] = $this->trans('Address format invalid', [], 'Admin.Notifications.Error'); + } + + $country = parent::processSave(); + + if (!count($this->errors)) { + if (null === $tmp_addr_format->id_country) { + $tmp_addr_format->id_country = $country->id; + } + + if (!$tmp_addr_format->save()) { + $this->errors[] = $this->trans('Invalid address layout %s', [Db::getInstance()->getMsgError()], 'Admin.International.Notification'); + } + } + + return $country; + } + + public function processStatus() + { + parent::processStatus(); + + /** @var Country $object */ + if (Validate::isLoadedObject($object = $this->loadObject()) && $object->active == 1) { + return Country::addModuleRestrictions([], [['id_country' => $object->id]], []); + } + + return false; + } + + /** + * Allow the assignation of zone only if the form is displayed. + */ + protected function processBulkAffectZone() + { + $zone_to_affect = Tools::getValue('zone_to_affect'); + if ($zone_to_affect && $zone_to_affect !== 0) { + parent::processBulkAffectZone(); + } + + if (Tools::getIsset('submitBulkAffectZonecountry')) { + $this->tpl_list_vars['assign_zone'] = true; + } + } + + protected function displayValidFields() + { + /* The following translations are needed later - don't remove the comments! + $this->trans('Customer', array(), 'Admin.Global'); + $this->trans('Warehouse', [], 'Admin.Global'); + $this->trans('Country', array(), 'Admin.Global'); + $this->trans('State', [], 'Admin.Global'); + $this->trans('Address', [], 'Admin.Global'); + */ + + $html_tabnav = ''; + $html_tabcontent .= ''; + + return $html_tabnav . $html_tabcontent; + } + + public static function displayCallPrefix($prefix) + { + return (int) $prefix ? '+' . $prefix : '-'; + } +} diff --git a/controllers/admin/AdminCustomerThreadsController.php b/controllers/admin/AdminCustomerThreadsController.php new file mode 100644 index 00000000..0a2f16f6 --- /dev/null +++ b/controllers/admin/AdminCustomerThreadsController.php @@ -0,0 +1,1205 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property CustomerThread $object + */ +class AdminCustomerThreadsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->context = Context::getContext(); + $this->table = 'customer_thread'; + $this->className = 'CustomerThread'; + $this->lang = false; + + $contact_array = []; + $contacts = Contact::getContacts($this->context->language->id); + + foreach ($contacts as $contact) { + $contact_array[$contact['id_contact']] = $contact['name']; + } + + $language_array = []; + $languages = Language::getLanguages(); + foreach ($languages as $language) { + $language_array[$language['id_lang']] = $language['name']; + } + + parent::__construct(); + + $icon_array = [ + 'open' => ['class' => 'icon-circle text-success', 'alt' => $this->trans('Open', [], 'Admin.Catalog.Feature')], + 'closed' => ['class' => 'icon-circle text-danger', 'alt' => $this->trans('Closed', [], 'Admin.Catalog.Feature')], + 'pending1' => ['class' => 'icon-circle text-warning', 'alt' => $this->trans('Pending 1', [], 'Admin.Catalog.Feature')], + 'pending2' => ['class' => 'icon-circle text-warning', 'alt' => $this->trans('Pending 2', [], 'Admin.Catalog.Feature')], + ]; + + $status_array = []; + foreach ($icon_array as $k => $v) { + $status_array[$k] = $v['alt']; + } + + $this->fields_list = [ + 'id_customer_thread' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'customer' => [ + 'title' => $this->trans('Customer', [], 'Admin.Global'), + 'filter_key' => 'customer', + 'tmpTableFilter' => true, + ], + 'email' => [ + 'title' => $this->trans('Email', [], 'Admin.Global'), + 'filter_key' => 'a!email', + ], + 'contact' => [ + 'title' => $this->trans('Type', [], 'Admin.Catalog.Feature'), + 'type' => 'select', + 'list' => $contact_array, + 'filter_key' => 'cl!id_contact', + 'filter_type' => 'int', + ], + 'language' => [ + 'title' => $this->trans('Language', [], 'Admin.Global'), + 'type' => 'select', + 'list' => $language_array, + 'filter_key' => 'l!id_lang', + 'filter_type' => 'int', + ], + 'status' => [ + 'title' => $this->trans('Status', [], 'Admin.Global'), + 'type' => 'select', + 'list' => $status_array, + 'icon' => $icon_array, + 'align' => 'center', + 'filter_key' => 'a!status', + 'filter_type' => 'string', + ], + 'employee' => [ + 'title' => $this->trans('Employee', [], 'Admin.Global'), + 'filter_key' => 'employee', + 'tmpTableFilter' => true, + ], + 'messages' => [ + 'title' => $this->trans('Messages', [], 'Admin.Catalog.Feature'), + 'filter_key' => 'messages', + 'tmpTableFilter' => true, + 'maxlength' => 40, + ], + 'private' => [ + 'title' => $this->trans('Private', [], 'Admin.Catalog.Feature'), + 'type' => 'select', + 'filter_key' => 'private', + 'align' => 'center', + 'cast' => 'intval', + 'callback' => 'printOptinIcon', + 'list' => [ + '0' => $this->trans('No', [], 'Admin.Global'), + '1' => $this->trans('Yes', [], 'Admin.Global'), + ], + ], + 'date_upd' => [ + 'title' => $this->trans('Last message', [], 'Admin.Catalog.Feature'), + 'havingFilter' => true, + 'type' => 'datetime', + ], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->shopLinkType = 'shop'; + + $this->fields_options = [ + 'contact' => [ + 'title' => $this->trans('Contact options', [], 'Admin.Catalog.Feature'), + 'fields' => [ + 'PS_CUSTOMER_SERVICE_FILE_UPLOAD' => [ + 'title' => $this->trans('Allow file uploading', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Allow customers to upload files using the contact page.', [], 'Admin.Catalog.Help'), + 'type' => 'bool', + ], + 'PS_CUSTOMER_SERVICE_SIGNATURE' => [ + 'title' => $this->trans('Default message', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Please fill out the message fields that appear by default when you answer a thread on the customer service page.', [], 'Admin.Catalog.Help'), + 'type' => 'textareaLang', + 'lang' => true, + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + 'general' => [ + 'title' => $this->trans('Customer service options', [], 'Admin.Catalog.Feature'), + 'fields' => [ + 'PS_SAV_IMAP_URL' => [ + 'title' => $this->trans('IMAP URL', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('URL for your IMAP server (ie.: mail.server.com).', [], 'Admin.Catalog.Help'), + 'type' => 'text', + 'validation' => 'isValidImapUrl', + ], + 'PS_SAV_IMAP_PORT' => [ + 'title' => $this->trans('IMAP port', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Port to use to connect to your IMAP server.', [], 'Admin.Catalog.Help'), + 'type' => 'text', + 'defaultValue' => 143, + ], + 'PS_SAV_IMAP_USER' => [ + 'title' => $this->trans('IMAP user', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('User to use to connect to your IMAP server.', [], 'Admin.Catalog.Help'), + 'type' => 'text', + ], + 'PS_SAV_IMAP_PWD' => [ + 'title' => $this->trans('IMAP password', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Password to use to connect your IMAP server.', [], 'Admin.Catalog.Help'), + 'type' => 'password', + ], + 'PS_SAV_IMAP_DELETE_MSG' => [ + 'title' => $this->trans('Delete messages', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Delete messages after synchronization. If you do not enable this option, the synchronization will take more time.', [], 'Admin.Catalog.Help'), + 'type' => 'bool', + ], + 'PS_SAV_IMAP_CREATE_THREADS' => [ + 'title' => $this->trans('Create new threads', [], 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Create new threads for unrecognized emails.', [], 'Admin.Catalog.Help'), + 'type' => 'bool', + ], + 'PS_SAV_IMAP_OPT_POP3' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/pop3)', + 'hint' => $this->trans('Use POP3 instead of IMAP.', [], 'Admin.Catalog.Help'), + 'type' => 'bool', + ], + 'PS_SAV_IMAP_OPT_NORSH' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/norsh)', + 'type' => 'bool', + 'hint' => $this->trans('Do not use RSH or SSH to establish a preauthenticated IMAP sessions.', [], 'Admin.Catalog.Help'), + ], + 'PS_SAV_IMAP_OPT_SSL' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/ssl)', + 'type' => 'bool', + 'hint' => $this->trans('Use the Secure Socket Layer (TLS/SSL) to encrypt the session.', [], 'Admin.Catalog.Help'), + ], + 'PS_SAV_IMAP_OPT_VALIDATE-CERT' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/validate-cert)', + 'type' => 'bool', + 'hint' => $this->trans('Validate certificates from the TLS/SSL server.', [], 'Admin.Catalog.Help'), + ], + 'PS_SAV_IMAP_OPT_NOVALIDATE-CERT' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/novalidate-cert)', + 'type' => 'bool', + 'hint' => $this->trans('Do not validate certificates from the TLS/SSL server. This is only needed if a server uses self-signed certificates.', [], 'Admin.Catalog.Help'), + ], + 'PS_SAV_IMAP_OPT_TLS' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/tls)', + 'type' => 'bool', + 'hint' => $this->trans('Force use of start-TLS to encrypt the session, and reject connection to servers that do not support it.', [], 'Admin.Catalog.Help'), + ], + 'PS_SAV_IMAP_OPT_NOTLS' => [ + 'title' => $this->trans('IMAP options', [], 'Admin.Catalog.Feature') . ' (/notls)', + 'type' => 'bool', + 'hint' => $this->trans('Do not use start-TLS to encrypt the session, even with servers that support it.', [], 'Admin.Catalog.Help'), + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + ]; + } + + public function renderList() + { + // Check the new IMAP messages before rendering the list + $this->renderProcessSyncImap(); + + $this->addRowAction('view'); + $this->addRowAction('delete'); + + $this->_select = ' + CONCAT(c.`firstname`," ",c.`lastname`) as customer, cl.`name` as contact, l.`name` as language, group_concat(cm.`message`) as messages, cm.private, + ( + SELECT IFNULL(CONCAT(LEFT(e.`firstname`, 1),". ",e.`lastname`), "--") + FROM `' . _DB_PREFIX_ . 'customer_message` cm2 + INNER JOIN ' . _DB_PREFIX_ . 'employee e + ON e.`id_employee` = cm2.`id_employee` + WHERE cm2.id_employee > 0 + AND cm2.`id_customer_thread` = a.`id_customer_thread` + ORDER BY cm2.`date_add` DESC LIMIT 1 + ) as employee'; + + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'customer` c + ON c.`id_customer` = a.`id_customer` + LEFT JOIN `' . _DB_PREFIX_ . 'customer_message` cm + ON cm.`id_customer_thread` = a.`id_customer_thread` + LEFT JOIN `' . _DB_PREFIX_ . 'lang` l + ON l.`id_lang` = a.`id_lang` + LEFT JOIN `' . _DB_PREFIX_ . 'contact_lang` cl + ON (cl.`id_contact` = a.`id_contact` AND cl.`id_lang` = ' . (int) $this->context->language->id . ')'; + + if ($id_order = Tools::getValue('id_order')) { + $this->_where .= ' AND id_order = ' . (int) $id_order; + } + + $this->_group = 'GROUP BY cm.id_customer_thread'; + $this->_orderBy = 'id_customer_thread'; + $this->_orderWay = 'DESC'; + + $contacts = CustomerThread::getContacts(); + + $categories = Contact::getCategoriesContacts(); + + $params = [ + $this->trans('Total threads', [], 'Admin.Catalog.Feature') => $all = CustomerThread::getTotalCustomerThreads(), + $this->trans('Threads pending', [], 'Admin.Catalog.Feature') => $pending = CustomerThread::getTotalCustomerThreads('status LIKE "%pending%"'), + $this->trans('Total number of customer messages', [], 'Admin.Catalog.Feature') => CustomerMessage::getTotalCustomerMessages('id_employee = 0'), + $this->trans('Total number of employee messages', [], 'Admin.Catalog.Feature') => CustomerMessage::getTotalCustomerMessages('id_employee != 0'), + $this->trans('Unread threads', [], 'Admin.Catalog.Feature') => $unread = CustomerThread::getTotalCustomerThreads('status = "open"'), + $this->trans('Closed threads', [], 'Admin.Catalog.Feature') => $all - ($unread + $pending), + ]; + + $this->tpl_list_vars = [ + 'contacts' => $contacts, + 'categories' => $categories, + 'params' => $params, + ]; + + return parent::renderList(); + } + + public function initToolbar() + { + parent::initToolbar(); + unset($this->toolbar_btn['new']); + } + + public function printOptinIcon($value, $customer) + { + return $value ? '' : ''; + } + + public function postProcess() + { + if ($id_customer_thread = (int) Tools::getValue('id_customer_thread')) { + if (($id_contact = (int) Tools::getValue('id_contact'))) { + $result = Db::getInstance()->execute( + ' + UPDATE ' . _DB_PREFIX_ . 'customer_thread + SET id_contact = ' . $id_contact . ' + WHERE id_customer_thread = ' . $id_customer_thread + ); + if ($result) { + $this->object->id_contact = $id_contact; + } + } + if ($id_status = (int) Tools::getValue('setstatus')) { + $status_array = [1 => 'open', 2 => 'closed', 3 => 'pending1', 4 => 'pending2']; + $result = Db::getInstance()->execute(' + UPDATE ' . _DB_PREFIX_ . 'customer_thread + SET status = "' . $status_array[$id_status] . '" + WHERE id_customer_thread = ' . $id_customer_thread . ' LIMIT 1 + '); + if ($result) { + $this->object->status = $status_array[$id_status]; + } + } + if (isset($_POST['id_employee_forward'])) { + $messages = Db::getInstance()->getRow(' + SELECT ct.*, cm.*, cl.name subject, CONCAT(e.firstname, \' \', e.lastname) employee_name, + CONCAT(c.firstname, \' \', c.lastname) customer_name, c.firstname + FROM ' . _DB_PREFIX_ . 'customer_thread ct + LEFT JOIN ' . _DB_PREFIX_ . 'customer_message cm + ON (ct.id_customer_thread = cm.id_customer_thread) + LEFT JOIN ' . _DB_PREFIX_ . 'contact_lang cl + ON (cl.id_contact = ct.id_contact AND cl.id_lang = ' . (int) $this->context->language->id . ') + LEFT OUTER JOIN ' . _DB_PREFIX_ . 'employee e + ON e.id_employee = cm.id_employee + LEFT OUTER JOIN ' . _DB_PREFIX_ . 'customer c + ON (c.email = ct.email) + WHERE ct.id_customer_thread = ' . (int) Tools::getValue('id_customer_thread') . ' + ORDER BY cm.date_add DESC + '); + $output = $this->displayMessage($messages, true, (int) Tools::getValue('id_employee_forward')); + $cm = new CustomerMessage(); + $cm->id_employee = (int) $this->context->employee->id; + $cm->id_customer_thread = (int) Tools::getValue('id_customer_thread'); + $cm->ip_address = (int) ip2long(Tools::getRemoteAddr()); + $current_employee = $this->context->employee; + $id_employee = (int) Tools::getValue('id_employee_forward'); + $employee = new Employee($id_employee); + $email = Tools::getValue('email'); + $message = Tools::getValue('message_forward'); + if (($error = $cm->validateField('message', $message, null, [], true)) !== true) { + $this->errors[] = $error; + } elseif ($id_employee && $employee && Validate::isLoadedObject($employee)) { + $params = [ + '{messages}' => Tools::stripslashes($output), + '{employee}' => $current_employee->firstname . ' ' . $current_employee->lastname, + '{comment}' => Tools::stripslashes(Tools::nl2br($_POST['message_forward'])), + '{firstname}' => $employee->firstname, + '{lastname}' => $employee->lastname, + ]; + + if (Mail::Send( + $this->context->language->id, + 'forward_msg', + $this->trans( + 'Fwd: Customer message', + [], + 'Emails.Subject', + $this->context->language->locale + ), + $params, + $employee->email, + $employee->firstname . ' ' . $employee->lastname, + $current_employee->email, + $current_employee->firstname . ' ' . $current_employee->lastname, + null, + null, + _PS_MAIL_DIR_, + true + )) { + $cm->private = 1; + $cm->message = $this->trans('Message forwarded to', [], 'Admin.Catalog.Feature') . ' ' . $employee->firstname . ' ' . $employee->lastname . "\n" . $this->trans('Comment:') . ' ' . $message; + $cm->add(); + } + } elseif ($email && Validate::isEmail($email)) { + $params = [ + '{messages}' => Tools::nl2br(Tools::stripslashes($output)), + '{employee}' => $current_employee->firstname . ' ' . $current_employee->lastname, + '{comment}' => Tools::stripslashes($_POST['message_forward']), + '{firstname}' => '', + '{lastname}' => '', + ]; + + if (Mail::Send( + $this->context->language->id, + 'forward_msg', + $this->trans( + 'Fwd: Customer message', + [], + 'Emails.Subject', + $this->context->language->locale + ), + $params, + $email, + null, + $current_employee->email, + $current_employee->firstname . ' ' . $current_employee->lastname, + null, + null, + _PS_MAIL_DIR_, + true + )) { + $cm->message = $this->trans('Message forwarded to', [], 'Admin.Catalog.Feature') . ' ' . $email . "\n" . $this->trans('Comment:') . ' ' . $message; + $cm->add(); + } + } else { + $this->errors[] = '
' . $this->trans('The email address is invalid.', [], 'Admin.Notifications.Error') . '
'; + } + } + if (Tools::isSubmit('submitReply')) { + $ct = new CustomerThread($id_customer_thread); + + ShopUrl::cacheMainDomainForShop((int) $ct->id_shop); + + $cm = new CustomerMessage(); + $cm->id_employee = (int) $this->context->employee->id; + $cm->id_customer_thread = $ct->id; + $cm->ip_address = (int) ip2long(Tools::getRemoteAddr()); + $cm->message = Tools::getValue('reply_message'); + if (($error = $cm->validateField('message', $cm->message, null, [], true)) !== true) { + $this->errors[] = $error; + } elseif (isset($_FILES) && !empty($_FILES['joinFile']['name']) && $_FILES['joinFile']['error'] != 0) { + $this->errors[] = $this->trans('An error occurred during the file upload process.', [], 'Admin.Notifications.Error'); + } elseif ($cm->add()) { + $file_attachment = null; + if (!empty($_FILES['joinFile']['name'])) { + $file_attachment['content'] = file_get_contents($_FILES['joinFile']['tmp_name']); + $file_attachment['name'] = $_FILES['joinFile']['name']; + $file_attachment['mime'] = $_FILES['joinFile']['type']; + } + $customer = new Customer($ct->id_customer); + + $params = [ + '{reply}' => Tools::nl2br(Tools::htmlentitiesUTF8(Tools::getValue('reply_message'))), + '{link}' => Tools::url( + $this->context->link->getPageLink('contact', true, null, null, false, $ct->id_shop), + 'id_customer_thread=' . (int) $ct->id . '&token=' . $ct->token + ), + '{firstname}' => $customer->firstname, + '{lastname}' => $customer->lastname, + ]; + //#ct == id_customer_thread #tc == token of thread <== used in the synchronization imap + $contact = new Contact((int) $ct->id_contact, (int) $ct->id_lang); + + if (Validate::isLoadedObject($contact)) { + $from_name = $contact->name; + $from_email = $contact->email; + } else { + $from_name = null; + $from_email = null; + } + + $language = new Language((int) $ct->id_lang); + + if (Mail::Send( + (int) $ct->id_lang, + 'reply_msg', + $this->trans( + 'An answer to your message is available #ct%thread_id% #tc%thread_token%', + [ + '%thread_id%' => $ct->id, + '%thread_token%' => $ct->token, + ], + 'Emails.Subject', + $language->locale + ), + $params, + Tools::getValue('msg_email'), + null, + $from_email, + $from_name, + $file_attachment, + null, + _PS_MAIL_DIR_, + true, + $ct->id_shop + )) { + $ct->status = 'closed'; + $ct->update(); + } + Tools::redirectAdmin( + self::$currentIndex . '&id_customer_thread=' . (int) $id_customer_thread . '&viewcustomer_thread&token=' . Tools::getValue('token') + ); + } else { + $this->errors[] = $this->trans('An error occurred. Your message was not sent. Please contact your system administrator.', [], 'Admin.Orderscustomers.Notification'); + } + } + } + + return parent::postProcess(); + } + + public function initContent() + { + if (isset($_GET['filename']) && file_exists(_PS_UPLOAD_DIR_ . $_GET['filename']) && Validate::isFileName($_GET['filename'])) { + AdminCustomerThreadsController::openUploadedFile(); + } + + return parent::initContent(); + } + + protected function openUploadedFile() + { + $filename = $_GET['filename']; + + $extensions = [ + '.txt' => 'text/plain', + '.rtf' => 'application/rtf', + '.doc' => 'application/msword', + '.docx' => 'application/msword', + '.pdf' => 'application/pdf', + '.zip' => 'multipart/x-zip', + '.png' => 'image/png', + '.jpeg' => 'image/jpeg', + '.gif' => 'image/gif', + '.jpg' => 'image/jpeg', + ]; + + $extension = false; + foreach ($extensions as $key => $val) { + if (substr(Tools::strtolower($filename), -4) == $key || substr(Tools::strtolower($filename), -5) == $key) { + $extension = $val; + + break; + } + } + + if (!$extension || !Validate::isFileName($filename)) { + die(Tools::displayError()); + } + + if (ob_get_level() && ob_get_length() > 0) { + ob_end_clean(); + } + header('Content-Type: ' . $extension); + header('Content-Disposition:attachment;filename="' . $filename . '"'); + readfile(_PS_UPLOAD_DIR_ . $filename); + die; + } + + public function renderKpis() + { + $time = time(); + $kpis = []; + + /* The data generation is located in AdminStatsControllerCore */ + + $helper = new HelperKpi(); + $helper->id = 'box-pending-messages'; + $helper->icon = 'icon-envelope'; + $helper->color = 'color1'; + $helper->href = $this->context->link->getAdminLink('AdminCustomerThreads'); + $helper->title = $this->trans('Pending Discussion Threads', [], 'Admin.Catalog.Feature'); + if (ConfigurationKPI::get('PENDING_MESSAGES') !== false) { + $helper->value = ConfigurationKPI::get('PENDING_MESSAGES'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=pending_messages'; + $helper->refresh = (bool) (ConfigurationKPI::get('PENDING_MESSAGES_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-age'; + $helper->icon = 'icon-time'; + $helper->color = 'color2'; + $helper->title = $this->trans('Average Response Time', [], 'Admin.Catalog.Feature'); + $helper->subtitle = $this->trans('30 days', [], 'Admin.Global'); + if (ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME') !== false) { + $helper->value = ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=avg_msg_response_time'; + $helper->refresh = (bool) (ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-messages-per-thread'; + $helper->icon = 'icon-copy'; + $helper->color = 'color3'; + $helper->title = $this->trans('Messages per Thread', [], 'Admin.Catalog.Feature'); + $helper->subtitle = $this->trans('30 day', [], 'Admin.Global'); + if (ConfigurationKPI::get('MESSAGES_PER_THREAD') !== false) { + $helper->value = ConfigurationKPI::get('MESSAGES_PER_THREAD'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=messages_per_thread'; + $helper->refresh = (bool) (ConfigurationKPI::get('MESSAGES_PER_THREAD_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + public function renderView() + { + if (!$id_customer_thread = (int) Tools::getValue('id_customer_thread')) { + return; + } + + $this->context = Context::getContext(); + if (!($thread = $this->loadObject())) { + return; + } + $this->context->cookie->{'customer_threadFilter_cl!id_contact'} = $thread->id_contact; + + $employees = Employee::getEmployees(); + + $messages = CustomerThread::getMessageCustomerThreads($id_customer_thread); + + foreach ($messages as $key => $mess) { + if ($mess['id_employee']) { + $employee = new Employee($mess['id_employee']); + $messages[$key]['employee_image'] = $employee->getImage(); + } + if (isset($mess['file_name']) && $mess['file_name'] != '') { + $messages[$key]['file_name'] = _THEME_PROD_PIC_DIR_ . $mess['file_name']; + } else { + unset($messages[$key]['file_name']); + } + + if ($mess['id_product']) { + $product = new Product((int) $mess['id_product'], false, $this->context->language->id); + if (Validate::isLoadedObject($product)) { + $messages[$key]['product_name'] = $product->name; + $messages[$key]['product_link'] = $this->context->link->getAdminLink('AdminProducts') . '&updateproduct&id_product=' . (int) $product->id; + } + } + } + + $next_thread = CustomerThread::getNextThread((int) $thread->id); + + $contacts = Contact::getContacts($this->context->language->id); + + $actions = []; + + if ($next_thread) { + $next_thread = [ + 'href' => self::$currentIndex . '&id_customer_thread=' . (int) $next_thread . '&viewcustomer_thread&token=' . $this->token, + 'name' => $this->trans('Reply to the next unanswered message in this thread', [], 'Admin.Catalog.Feature'), + ]; + } + + if ($thread->status != 'closed') { + $actions['closed'] = [ + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=2&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Mark as "handled"', [], 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 2, + ]; + } else { + $actions['open'] = [ + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=1&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Re-open', [], 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 1, + ]; + } + + if ($thread->status != 'pending1') { + $actions['pending1'] = [ + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=3&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Mark as "pending 1" (will be answered later)', [], 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 3, + ]; + } else { + $actions['pending1'] = [ + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=1&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Disable pending status', [], 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 1, + ]; + } + + if ($thread->status != 'pending2') { + $actions['pending2'] = [ + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=4&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Mark as "pending 2" (will be answered later)', [], 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 4, + ]; + } else { + $actions['pending2'] = [ + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=1&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Disable pending status', [], 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 1, + ]; + } + + if ($thread->id_customer) { + $customer = new Customer($thread->id_customer); + $orders = Order::getCustomerOrders($customer->id); + if ($orders && count($orders)) { + $total_ok = 0; + $orders_ok = []; + foreach ($orders as $key => $order) { + if ($order['valid']) { + $orders_ok[] = $order; + $total_ok += $order['total_paid_real'] / $order['conversion_rate']; + } + $orders[$key]['date_add'] = Tools::displayDate($order['date_add']); + $orders[$key]['total_paid_real'] = $this->context->getCurrentLocale()->formatPrice($order['total_paid_real'], Currency::getIsoCodeById((int) $order['id_currency'])); + } + } + + $products = $customer->getBoughtProducts(); + if ($products && count($products)) { + foreach ($products as $key => $product) { + $products[$key]['date_add'] = Tools::displayDate($product['date_add'], null, true); + } + } + } + $timeline_items = $this->getTimeline($messages, $thread->id_order); + $first_message = $messages[0]; + + if (!$messages[0]['id_employee']) { + unset($messages[0]); + } + + $contact = ''; + foreach ($contacts as $c) { + if ($c['id_contact'] == $thread->id_contact) { + $contact = $c['name']; + } + } + + $this->tpl_view_vars = [ + 'id_customer_thread' => $id_customer_thread, + 'thread' => $thread, + 'actions' => $actions, + 'employees' => $employees, + 'current_employee' => $this->context->employee, + 'messages' => $messages, + 'first_message' => $first_message, + 'contact' => $contact, + 'next_thread' => $next_thread, + 'orders' => isset($orders) ? $orders : false, + 'customer' => isset($customer) ? $customer : false, + 'products' => isset($products) ? $products : false, + 'total_ok' => isset($total_ok) ? $this->context->getCurrentLocale()->formatPrice($total_ok, $this->context->currency->iso_code) : false, + 'orders_ok' => isset($orders_ok) ? $orders_ok : false, + 'count_ok' => isset($orders_ok) ? count($orders_ok) : false, + 'PS_CUSTOMER_SERVICE_SIGNATURE' => str_replace('\r\n', "\n", Configuration::get('PS_CUSTOMER_SERVICE_SIGNATURE', (int) $thread->id_lang)), + 'timeline_items' => $timeline_items, + ]; + + if ($next_thread) { + $this->tpl_view_vars['next_thread'] = $next_thread; + } + + return parent::renderView(); + } + + public function getTimeline($messages, $id_order) + { + $timeline = []; + foreach ($messages as $message) { + $product = new Product((int) $message['id_product'], false, $this->context->language->id); + + $content = ''; + if (!$message['private']) { + $content .= $this->trans('Message to:', [], 'Admin.Catalog.Feature') . ' ' . (!$message['id_employee'] ? $message['subject'] : $message['customer_name']) . '
'; + } + if (Validate::isLoadedObject($product)) { + $content .= '
' . $this->trans('Product:', [], 'Admin.Catalog.Feature') . '' . $product->name . '

'; + } + $content .= Tools::safeOutput($message['message']); + + $timeline[$message['date_add']][] = [ + 'arrow' => 'left', + 'background_color' => '', + 'icon' => 'icon-envelope', + 'content' => $content, + 'date' => $message['date_add'], + ]; + } + + $order = new Order((int) $id_order); + if (Validate::isLoadedObject($order)) { + $order_history = $order->getHistory($this->context->language->id); + foreach ($order_history as $history) { + $parameters = ['vieworder' => 1, 'id_order' => (int) $order->id]; + $link_order = $this->context->link->getAdminLink('AdminOrders', true, [], $parameters); + + $content = '' . $this->trans('Order', [], 'Admin.Global') . ' #' . (int) $order->id . '

'; + + $content .= '' . $this->trans('Status:', [], 'Admin.Catalog.Feature') . ' ' . $history['ostate_name'] . ''; + + $timeline[$history['date_add']][] = [ + 'arrow' => 'right', + 'alt' => true, + 'background_color' => $history['color'], + 'icon' => 'icon-credit-card', + 'content' => $content, + 'date' => $history['date_add'], + 'see_more_link' => $link_order, + ]; + } + } + krsort($timeline); + + return $timeline; + } + + protected function displayMessage($message, $email = false, $id_employee = null) + { + $tpl = $this->createTemplate('message.tpl'); + + $contacts = Contact::getContacts($this->context->language->id); + foreach ($contacts as $contact) { + $contact_array[$contact['id_contact']] = ['id_contact' => $contact['id_contact'], 'name' => $contact['name']]; + } + $contacts = $contact_array; + + if (!$email) { + if (!empty($message['id_product']) && empty($message['employee_name'])) { + $id_order_product = Order::getIdOrderProduct((int) $message['id_customer'], (int) $message['id_product']); + } + } + $message['date_add'] = Tools::displayDate($message['date_add'], null, true); + $message['user_agent'] = strip_tags($message['user_agent']); + $message['message'] = preg_replace( + '/(https?:\/\/[a-z0-9#%&_=\(\)\.\? \+\-@\/]{6,1000})([\s\n<])/Uui', + '\1\2', + html_entity_decode( + $message['message'], + ENT_QUOTES, + 'UTF-8' + ) + ); + + $is_valid_order_id = true; + $order = new Order((int) $message['id_order']); + + if (!Validate::isLoadedObject($order)) { + $is_valid_order_id = false; + } + + $tpl->assign([ + 'thread_url' => Tools::getAdminUrl(basename(_PS_ADMIN_DIR_) . '/' . + $this->context->link->getAdminLink('AdminCustomerThreads') . '&id_customer_thread=' + . (int) $message['id_customer_thread'] . '&viewcustomer_thread=1'), + 'link' => Context::getContext()->link, + 'current' => self::$currentIndex, + 'token' => $this->token, + 'message' => $message, + 'id_order_product' => isset($id_order_product) ? $id_order_product : null, + 'email' => $email, + 'id_employee' => $id_employee, + 'PS_SHOP_NAME' => Configuration::get('PS_SHOP_NAME'), + 'file_name' => file_exists(_PS_UPLOAD_DIR_ . $message['file_name']), + 'contacts' => $contacts, + 'is_valid_order_id' => $is_valid_order_id, + ]); + + return $tpl->fetch(); + } + + protected function displayButton($content) + { + return '

' . $content . '

'; + } + + public function renderOptions() + { + if (Configuration::get('PS_SAV_IMAP_URL') + && Configuration::get('PS_SAV_IMAP_PORT') + && Configuration::get('PS_SAV_IMAP_USER') + && Configuration::get('PS_SAV_IMAP_PWD')) { + $this->tpl_option_vars['use_sync'] = true; + } else { + $this->tpl_option_vars['use_sync'] = false; + } + + return parent::renderOptions(); + } + + public function updateOptionPsSavImapOpt($value) + { + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + if (!$this->errors && $value) { + Configuration::updateValue('PS_SAV_IMAP_OPT', implode('', $value)); + } + } + + public function ajaxProcessMarkAsRead() + { + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + $id_thread = Tools::getValue('id_thread'); + $messages = CustomerThread::getMessageCustomerThreads($id_thread); + if (count($messages)) { + Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customer_message` set `read` = 1 WHERE `id_employee` = ' . (int) $this->context->employee->id . ' AND `id_customer_thread` = ' . (int) $id_thread); + } + } + + /** + * Call the IMAP synchronization during an AJAX process. + * + * @throws PrestaShopException + */ + public function ajaxProcessSyncImap() + { + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('syncImapMail')) { + die(json_encode($this->syncImap())); + } + } + + /** + * Call the IMAP synchronization during the render process. + */ + public function renderProcessSyncImap() + { + // To avoid an error if the IMAP isn't configured, we check the configuration here, like during + // the synchronization. All parameters will exists. + if (!(Configuration::get('PS_SAV_IMAP_URL') + || Configuration::get('PS_SAV_IMAP_PORT') + || Configuration::get('PS_SAV_IMAP_USER') + || Configuration::get('PS_SAV_IMAP_PWD'))) { + return; + } + + // Executes the IMAP synchronization. + $sync_errors = $this->syncImap(); + + // Show the errors. + if (isset($sync_errors['hasError']) && $sync_errors['hasError']) { + if (isset($sync_errors['errors'])) { + foreach ($sync_errors['errors'] as &$error) { + $this->displayWarning($error); + } + } + } + } + + /** + * Imap synchronization method. + * + * @return array errors list + */ + public function syncImap() + { + if (!($url = Configuration::get('PS_SAV_IMAP_URL')) + || !($port = Configuration::get('PS_SAV_IMAP_PORT')) + || !($user = Configuration::get('PS_SAV_IMAP_USER')) + || !($password = Configuration::get('PS_SAV_IMAP_PWD'))) { + return ['hasError' => true, 'errors' => ['IMAP configuration is not correct']]; + } + + $conf = Configuration::getMultiple([ + 'PS_SAV_IMAP_OPT_POP3', 'PS_SAV_IMAP_OPT_NORSH', 'PS_SAV_IMAP_OPT_SSL', + 'PS_SAV_IMAP_OPT_VALIDATE-CERT', 'PS_SAV_IMAP_OPT_NOVALIDATE-CERT', + 'PS_SAV_IMAP_OPT_TLS', 'PS_SAV_IMAP_OPT_NOTLS', ]); + + $conf_str = ''; + if ($conf['PS_SAV_IMAP_OPT_POP3']) { + $conf_str .= '/pop3'; + } + if ($conf['PS_SAV_IMAP_OPT_NORSH']) { + $conf_str .= '/norsh'; + } + if ($conf['PS_SAV_IMAP_OPT_SSL']) { + $conf_str .= '/ssl'; + } + if ($conf['PS_SAV_IMAP_OPT_VALIDATE-CERT']) { + $conf_str .= '/validate-cert'; + } + if ($conf['PS_SAV_IMAP_OPT_NOVALIDATE-CERT']) { + $conf_str .= '/novalidate-cert'; + } + if ($conf['PS_SAV_IMAP_OPT_TLS']) { + $conf_str .= '/tls'; + } + if ($conf['PS_SAV_IMAP_OPT_NOTLS']) { + $conf_str .= '/notls'; + } + + if (!function_exists('imap_open')) { + return ['hasError' => true, 'errors' => ['imap is not installed on this server']]; + } + + $mbox = @imap_open('{' . $url . ':' . $port . $conf_str . '}', $user, $password); + + //checks if there is no error when connecting imap server + $errors = imap_errors(); + if (is_array($errors)) { + $errors = array_unique($errors); + } + $str_errors = ''; + $str_error_delete = ''; + + if (count($errors) && is_array($errors)) { + $str_errors = ''; + foreach ($errors as $error) { + $str_errors .= $error . ', '; + } + $str_errors = rtrim(trim($str_errors), ','); + } + //checks if imap connexion is active + if (!$mbox) { + return ['hasError' => true, 'errors' => ['Cannot connect to the mailbox :
' . ($str_errors)]]; + } + + //Returns information about the current mailbox. Returns FALSE on failure. + $check = imap_check($mbox); + if (!$check) { + return ['hasError' => true, 'errors' => ['Fail to get information about the current mailbox']]; + } + + if ($check->Nmsgs == 0) { + return ['hasError' => true, 'errors' => ['NO message to sync']]; + } + + $result = imap_fetch_overview($mbox, "1:{$check->Nmsgs}", 0); + $message_errors = []; + foreach ($result as $overview) { + //check if message exist in database + if (isset($overview->subject)) { + $subject = $overview->subject; + } else { + $subject = ''; + } + //Creating an md5 to check if message has been allready processed + $md5 = md5($overview->date . $overview->from . $subject . $overview->msgno); + $exist = Db::getInstance()->getValue( + 'SELECT `md5_header` + FROM `' . _DB_PREFIX_ . 'customer_message_sync_imap` + WHERE `md5_header` = \'' . pSQL($md5) . '\'' + ); + if ($exist) { + if (Configuration::get('PS_SAV_IMAP_DELETE_MSG')) { + if (!imap_delete($mbox, $overview->msgno)) { + $str_error_delete = ', Fail to delete message'; + } + } + } else { + //check if subject has id_order + preg_match('/\#ct([0-9]*)/', $subject, $matches1); + preg_match('/\#tc([0-9-a-z-A-Z]*)/', $subject, $matches2); + $match_found = false; + if (isset($matches1[1], $matches2[1])) { + $match_found = true; + } + + $new_ct = (Configuration::get('PS_SAV_IMAP_CREATE_THREADS') && !$match_found && (strpos($subject, '[no_sync]') == false)); + + $fetch_succeed = true; + if ($match_found || $new_ct) { + if ($new_ct) { + // parse from attribute and fix it if needed + $from_parsed = []; + if (!isset($overview->from) + || (!preg_match('/<(' . Tools::cleanNonUnicodeSupport('[a-z\p{L}0-9!#$%&\'*+\/=?^`{}|~_-]+[.a-z\p{L}0-9!#$%&\'*+\/=?^`{}|~_-]*@[a-z\p{L}0-9]+[._a-z\p{L}0-9-]*\.[a-z0-9]+') . ')>/', $overview->from, $from_parsed) + && !Validate::isEmail($overview->from))) { + $message_errors[] = $this->trans('Cannot create message in a new thread.', [], 'Admin.Orderscustomers.Notification'); + + continue; + } + + // fix email format: from "Mr Sanders " to "sanders@blueforest.com" + $from = $overview->from; + if (isset($from_parsed[1])) { + $from = $from_parsed[1]; + } + + // we want to assign unrecognized mails to the right contact category + $contacts = Contact::getContacts($this->context->language->id); + if (!$contacts) { + continue; + } + + foreach ($contacts as $contact) { + if (isset($overview->to) && strpos($overview->to, $contact['email']) !== false) { + $id_contact = $contact['id_contact']; + } + } + + if (!isset($id_contact)) { // if not use the default contact category + $id_contact = $contacts[0]['id_contact']; + } + + $customer = new Customer(); + $client = $customer->getByEmail($from); //check if we already have a customer with this email + $ct = new CustomerThread(); + if (isset($client->id)) { //if mail is owned by a customer assign to him + $ct->id_customer = $client->id; + } + $ct->email = $from; + $ct->id_contact = $id_contact; + $ct->id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $ct->id_shop = $this->context->shop->id; //new customer threads for unrecognized mails are not shown without shop id + $ct->status = 'open'; + $ct->token = Tools::passwdGen(12); + $ct->add(); + } else { + $ct = new CustomerThread((int) $matches1[1]); + } //check if order exist in database + + if (Validate::isLoadedObject($ct) && ((isset($matches2[1]) && $ct->token == $matches2[1]) || $new_ct)) { + $structure = imap_bodystruct($mbox, $overview->msgno, '1'); + if ($structure->type == 0) { + $message = imap_fetchbody($mbox, $overview->msgno, '1'); + } elseif ($structure->type == 1) { + $structure = imap_bodystruct($mbox, $overview->msgno, '1.1'); + $message = imap_fetchbody($mbox, $overview->msgno, '1.1'); + } else { + continue; + } + + switch ($structure->encoding) { + case 3: + $message = imap_base64($message); + + break; + case 4: + $message = imap_qprint($message); + + break; + } + $message = iconv($this->getEncoding($structure), 'utf-8', $message); + $message = nl2br($message); + if (!$message || strlen($message) == 0) { + $message_errors[] = $this->trans('The message body is empty, cannot import it.', [], 'Admin.Orderscustomers.Notification'); + $fetch_succeed = false; + + continue; + } + $cm = new CustomerMessage(); + $cm->id_customer_thread = $ct->id; + if (empty($message) || !Validate::isCleanHtml($message)) { + $str_errors .= $this->trans('Invalid message content for subject: %s', [$subject], 'Admin.Orderscustomers.Notification'); + } else { + try { + $cm->message = $message; + $cm->add(); + } catch (PrestaShopException $pse) { + $message_errors[] = $this->trans('The message content is not valid, cannot import it.', [], 'Admin.Orderscustomers.Notification'); + $fetch_succeed = false; + + continue; + } + } + } + } + if ($fetch_succeed) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'customer_message_sync_imap` (`md5_header`) VALUES (\'' . pSQL($md5) . '\')'); + } + } + } + imap_expunge($mbox); + imap_close($mbox); + if (count($message_errors) > 0) { + if (($more_error = $str_errors . $str_error_delete) && strlen($more_error) > 0) { + $message_errors = array_merge([$more_error], $message_errors); + } + + return ['hasError' => true, 'errors' => $message_errors]; + } + if ($str_errors . $str_error_delete) { + return ['hasError' => true, 'errors' => [$str_errors . $str_error_delete]]; + } else { + return ['hasError' => false, 'errors' => '']; + } + } + + protected function getEncoding($structure) + { + foreach ($structure->parameters as $parameter) { + if ($parameter->attribute == 'CHARSET') { + return $parameter->value; + } + } + + return 'utf-8'; + } +} diff --git a/controllers/admin/AdminDashboardController.php b/controllers/admin/AdminDashboardController.php new file mode 100644 index 00000000..f42a99ac --- /dev/null +++ b/controllers/admin/AdminDashboardController.php @@ -0,0 +1,514 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; + +class AdminDashboardControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->display = 'view'; + + parent::__construct(); + + if (Tools::isSubmit('profitability_conf') || Tools::isSubmit('submitOptionsconfiguration')) { + $this->fields_options = $this->getOptionFields(); + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJqueryUI('ui.datepicker'); + $this->addJS([ + _PS_JS_DIR_ . 'vendor/d3.v3.min.js', + __PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/vendor/nv.d3.min.js', + _PS_JS_DIR_ . '/admin/dashboard.js', + ]); + $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/css/vendor/nv.d3.css'); + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_title = $this->trans('Dashboard', [], 'Admin.Dashboard.Feature'); + $this->page_header_toolbar_btn['switch_demo'] = [ + 'desc' => $this->trans('Demo mode', [], 'Admin.Dashboard.Feature'), + 'icon' => 'process-icon-toggle-' . (Configuration::get('PS_DASHBOARD_SIMULATION') ? 'on' : 'off'), + 'help' => $this->trans('This mode displays sample data so you can try your dashboard without real numbers.', [], 'Admin.Dashboard.Help'), + ]; + + parent::initPageHeaderToolbar(); + + // Remove the last element on this controller to match the title with the rule of the others + array_pop($this->meta_title); + } + + protected function getOptionFields() + { + $forms = []; + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $carriers = Carrier::getCarriers((int) $this->context->language->id, true, false, false, null, Carrier::ALL_CARRIERS); + $modules = Module::getModulesOnDisk(true); + + $forms = [ + 'payment' => ['title' => $this->trans('Average bank fees per payment method', [], 'Admin.Dashboard.Feature'), 'id' => 'payment'], + 'carriers' => ['title' => $this->trans('Average shipping fees per shipping method', [], 'Admin.Dashboard.Feature'), 'id' => 'carriers'], + 'other' => ['title' => $this->trans('Other settings', [], 'Admin.Dashboard.Feature'), 'id' => 'other'], + ]; + foreach ($forms as &$form) { + $form['icon'] = 'tab-preferences'; + $form['fields'] = []; + $form['submit'] = ['title' => $this->trans('Save', [], 'Admin.Actions')]; + } + + foreach ($modules as $module) { + if (isset($module->tab) && $module->tab == 'payments_gateways' && $module->id) { + $moduleClass = Module::getInstanceByName($module->name); + if (!$moduleClass->isEnabledForShopContext()) { + continue; + } + + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_FIXED'] = [ + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a fixed fee for each order placed in %currency% with %module%.', + [ + '%currency' => $currency->iso_code, + '%module%' => $module->displayName, + ], + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPrice', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => $currency->iso_code, + ]; + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_VAR'] = [ + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a variable fee for each order placed in %currency% with %module%. It will be applied on the total paid with taxes.', + [ + '%currency' => $currency->iso_code, + '%module%' => $module->displayName, + ], + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ]; + + if (Currency::isMultiCurrencyActivated()) { + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_FIXED_FOREIGN'] = [ + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a fixed fee for each order placed with a foreign currency with %module%.', + [ + '%module%' => $module->displayName, + ], + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPrice', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => $currency->iso_code, + ]; + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_VAR_FOREIGN'] = [ + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a variable fee for each order placed with a foreign currency with %module%. It will be applied on the total paid with taxes.', + ['%module%' => $module->displayName], + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ]; + } + } + } + + foreach ($carriers as $carrier) { + $forms['carriers']['fields']['CONF_' . strtoupper($carrier['id_reference']) . '_SHIP'] = [ + 'title' => $carrier['name'], + 'desc' => $this->trans( + 'For the carrier named %s, indicate the domestic delivery costs in percentage of the price charged to customers.', + [ + '%s' => $carrier['name'], + ], + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ]; + $forms['carriers']['fields']['CONF_' . strtoupper($carrier['id_reference']) . '_SHIP_OVERSEAS'] = [ + 'title' => $carrier['name'], + 'desc' => $this->trans( + 'For the carrier named %s, indicate the overseas delivery costs in percentage of the price charged to customers.', + [ + '%s' => $carrier['name'], + ], + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ]; + } + + $forms['carriers']['description'] = $this->trans('Method: Indicate the percentage of your carrier margin. For example, if you charge $10 of shipping fees to your customer for each shipment, but you really pay $4 to this carrier, then you should indicate "40" in the percentage field.', [], 'Admin.Dashboard.Help'); + + $forms['other']['fields']['CONF_AVERAGE_PRODUCT_MARGIN'] = [ + 'title' => $this->trans('Average gross margin percentage', [], 'Admin.Dashboard.Feature'), + 'desc' => $this->trans('You should calculate this percentage as follows: ((total sales revenue) - (cost of goods sold)) / (total sales revenue) * 100. This value is only used to calculate the Dashboard approximate gross margin, if you do not specify the wholesale price for each product.', [], 'Admin.Dashboard.Help'), + 'validation' => 'isPercentage', + 'cast' => 'intval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ]; + + $forms['other']['fields']['CONF_ORDER_FIXED'] = [ + 'title' => $this->trans('Other fees per order', [], 'Admin.Dashboard.Feature'), + 'desc' => $this->trans('You should calculate this value by making the sum of all of your additional costs per order.', [], 'Admin.Dashboard.Help'), + 'validation' => 'isPrice', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => $currency->iso_code, + ]; + + Media::addJsDef([ + 'dashboard_ajax_url' => $this->context->link->getAdminLink('AdminDashboard'), + 'read_more' => '', + ]); + + return $forms; + } + + public function renderView() + { + if (Tools::isSubmit('profitability_conf')) { + return parent::renderOptions(); + } + + // $translations = array( + // 'Calendar' => $this->trans('Calendar', array(),'Admin.Global'), + // 'Day' => $this->trans('Day', array(), 'Admin.Global'), + // 'Month' => $this->trans('Month', array(), 'Admin.Global'), + // 'Year' => $this->trans('Year', array(), 'Admin.Global'), + // 'From' => $this->trans('From:', array(), 'Admin.Global'), + // 'To' => $this->trans('To:', array(), 'Admin.Global'), + // 'Save' => $this->trans('Save', array(), 'Admin.Global') + // ); + + $testStatsDateUpdate = $this->context->cookie->__get('stats_date_update'); + if (!empty($testStatsDateUpdate) && $this->context->cookie->__get('stats_date_update') < strtotime(date('Y-m-d'))) { + switch ($this->context->employee->preselect_date_range) { + case 'day': + $date_from = date('Y-m-d'); + $date_to = date('Y-m-d'); + + break; + case 'prev-day': + $date_from = date('Y-m-d', strtotime('-1 day')); + $date_to = date('Y-m-d', strtotime('-1 day')); + + break; + case 'month': + default: + $date_from = date('Y-m-01'); + $date_to = date('Y-m-d'); + + break; + case 'prev-month': + $date_from = date('Y-m-01', strtotime('-1 month')); + $date_to = date('Y-m-t', strtotime('-1 month')); + + break; + case 'year': + $date_from = date('Y-01-01'); + $date_to = date('Y-m-d'); + + break; + case 'prev-year': + $date_from = date('Y-m-01', strtotime('-1 year')); + $date_to = date('Y-12-t', strtotime('-1 year')); + + break; + } + $this->context->employee->stats_date_from = $date_from; + $this->context->employee->stats_date_to = $date_to; + $this->context->employee->update(); + $this->context->cookie->__set('stats_date_update', strtotime(date('Y-m-d'))); + $this->context->cookie->write(); + } + + $calendar_helper = new HelperCalendar(); + + $calendar_helper->setDateFrom(Tools::getValue('date_from', $this->context->employee->stats_date_from)); + $calendar_helper->setDateTo(Tools::getValue('date_to', $this->context->employee->stats_date_to)); + + $stats_compare_from = $this->context->employee->stats_compare_from; + $stats_compare_to = $this->context->employee->stats_compare_to; + + if (null === $stats_compare_from || $stats_compare_from == '0000-00-00') { + $stats_compare_from = null; + } + + if (null === $stats_compare_to || $stats_compare_to == '0000-00-00') { + $stats_compare_to = null; + } + + $calendar_helper->setCompareDateFrom($stats_compare_from); + $calendar_helper->setCompareDateTo($stats_compare_to); + $calendar_helper->setCompareOption(Tools::getValue('compare_date_option', $this->context->employee->stats_compare_option)); + + $params = [ + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + ]; + + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + $this->tpl_view_vars = [ + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + 'hookDashboardZoneOne' => Hook::exec('dashboardZoneOne', $params), + 'hookDashboardZoneTwo' => Hook::exec('dashboardZoneTwo', $params), + //'translations' => $translations, + 'action' => '#', + 'warning' => $this->getWarningDomainName(), + 'new_version_url' => Tools::getCurrentUrlProtocolPrefix() . _PS_API_DOMAIN_ . '/version/check_version.php?v=' . _PS_VERSION_ . '&lang=' . $this->context->language->iso_code . '&autoupgrade=' . (int) ($moduleManager->isInstalled('autoupgrade') && $moduleManager->isEnabled('autoupgrade')) . '&hosted_mode=' . (int) defined('_PS_HOST_MODE_'), + 'dashboard_use_push' => Configuration::get('PS_DASHBOARD_USE_PUSH'), + 'calendar' => $calendar_helper->generate(), + 'PS_DASHBOARD_SIMULATION' => Configuration::get('PS_DASHBOARD_SIMULATION'), + 'datepickerFrom' => Tools::getValue('datepickerFrom', $this->context->employee->stats_date_from), + 'datepickerTo' => Tools::getValue('datepickerTo', $this->context->employee->stats_date_to), + 'preselect_date_range' => Tools::getValue('preselectDateRange', $this->context->employee->preselect_date_range), + 'help_center_link' => $this->getHelpCenterLink($this->context->language->iso_code), + ]; + + return parent::renderView(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitDateRealTime')) { + if ($use_realtime = (int) Tools::getValue('submitDateRealTime')) { + $this->context->employee->stats_date_from = date('Y-m-d'); + $this->context->employee->stats_date_to = date('Y-m-d'); + $this->context->employee->stats_compare_option = HelperCalendar::DEFAULT_COMPARE_OPTION; + $this->context->employee->stats_compare_from = null; + $this->context->employee->stats_compare_to = null; + $this->context->employee->update(); + } + Configuration::updateValue('PS_DASHBOARD_USE_PUSH', $use_realtime); + } + + if (Tools::isSubmit('submitDateRange')) { + if (!Validate::isDate(Tools::getValue('date_from')) + || !Validate::isDate(Tools::getValue('date_to'))) { + $this->errors[] = $this->trans('The selected date range is not valid.', [], 'Admin.Notifications.Error'); + } + + if (Tools::getValue('datepicker_compare')) { + if (!Validate::isDate(Tools::getValue('compare_date_from')) + || !Validate::isDate(Tools::getValue('compare_date_to'))) { + $this->errors[] = $this->trans('The selected date range is not valid.', [], 'Admin.Notifications.Error'); + } + } + + if (!count($this->errors)) { + $this->context->employee->stats_date_from = Tools::getValue('date_from'); + $this->context->employee->stats_date_to = Tools::getValue('date_to'); + $this->context->employee->preselect_date_range = Tools::getValue('preselectDateRange'); + + if (Tools::getValue('datepicker_compare')) { + $this->context->employee->stats_compare_from = Tools::getValue('compare_date_from'); + $this->context->employee->stats_compare_to = Tools::getValue('compare_date_to'); + $this->context->employee->stats_compare_option = Tools::getValue('compare_date_option'); + } else { + $this->context->employee->stats_compare_from = null; + $this->context->employee->stats_compare_to = null; + $this->context->employee->stats_compare_option = HelperCalendar::DEFAULT_COMPARE_OPTION; + } + + $this->context->employee->update(); + } + } + + parent::postProcess(); + } + + protected function getWarningDomainName() + { + $warning = false; + if (Shop::isFeatureActive()) { + return; + } + + $shop = Context::getContext()->shop; + if ($_SERVER['HTTP_HOST'] != $shop->domain && $_SERVER['HTTP_HOST'] != $shop->domain_ssl && Tools::getValue('ajax') == false && !defined('_PS_HOST_MODE_')) { + $warning = $this->trans('You are currently connected under the following domain name:', [], 'Admin.Dashboard.Notification') . ' ' . $_SERVER['HTTP_HOST'] . '
'; + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) { + $warning .= $this->trans( + 'This is different from the shop domain name set in the Multistore settings: "%s".', + [ + '%s' => $shop->domain, + ], + 'Admin.Dashboard.Notification' + ) . $this->trans( + 'If this is your main domain, please {link}change it now{/link}.', + [ + '{link}' => '', + '{/link}' => '', + ], + 'Admin.Dashboard.Notification' + ); + } else { + $warning .= $this->trans('This is different from the domain name set in the "SEO & URLs" tab.', [], 'Admin.Dashboard.Notification') . ' + ' . $this->trans( + 'If this is your main domain, please {link}change it now{/link}.', + [ + '{link}' => '', + '{/link}' => '', + ], + 'Admin.Dashboard.Notification' + ); + } + } + + return $warning; + } + + public function ajaxProcessRefreshDashboard() + { + $id_module = null; + if ($module = Tools::getValue('module')) { + $module_obj = Module::getInstanceByName($module); + if (Validate::isLoadedObject($module_obj)) { + $id_module = $module_obj->id; + } + } + + $params = [ + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + 'compare_from' => $this->context->employee->stats_compare_from, + 'compare_to' => $this->context->employee->stats_compare_to, + 'dashboard_use_push' => (int) Tools::getValue('dashboard_use_push'), + 'extra' => (int) Tools::getValue('extra'), + ]; + + die(json_encode(Hook::exec('dashboardData', $params, $id_module, true, true, (int) Tools::getValue('dashboard_use_push')))); + } + + public function ajaxProcessSetSimulationMode() + { + Configuration::updateValue('PS_DASHBOARD_SIMULATION', (int) Tools::getValue('PS_DASHBOARD_SIMULATION')); + die('k' . Configuration::get('PS_DASHBOARD_SIMULATION') . 'k'); + } + + /** + * Returns last news from the blog + * + * @throws PrestaShopException + */ + public function displayAjaxGetBlogRss() + { + $newsFetcher = $this->get('prestashop.adapter.news.provider'); + $return = $newsFetcher->getData($this->context->language->iso_code); + + // Response + header('Content-Type: application/json'); + $this->ajaxRender(json_encode($return)); + } + + public function ajaxProcessSaveDashConfig() + { + $return = ['has_errors' => false, 'errors' => []]; + $module = Tools::getValue('module'); + $hook = Tools::getValue('hook'); + $configs = Tools::getValue('configs'); + + $params = [ + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + ]; + + if (Validate::isModuleName($module) && $module_obj = Module::getInstanceByName($module)) { + $return['errors'] = $module_obj->validateDashConfig($configs); + if (count($return['errors'])) { + $return['has_errors'] = true; + } else { + $return['has_errors'] = $module_obj->saveDashConfig($configs); + } + } + + if (Validate::isHookName($hook) && method_exists($module_obj, $hook)) { + $return['widget_html'] = $module_obj->$hook($params); + } + + die(json_encode($return)); + } + + /** + * Returns the Help center link for the provided locale + * + * @param string $languageCode 2-letter locale code + * + * @return string + */ + private function getHelpCenterLink($languageCode) + { + $links = [ + 'fr' => 'https://www.prestashop.com/fr/contact?utm_source=back-office&utm_medium=links&utm_campaign=help-center-fr&utm_content=download17', + 'en' => 'https://www.prestashop.com/en/contact?utm_source=back-office&utm_medium=links&utm_campaign=help-center-en&utm_content=download17', + 'es' => 'https://www.prestashop.com/es/contacto?utm_source=back-office&utm_medium=links&utm_campaign=help-center-es&utm_content=download17', + 'de' => 'https://www.prestashop.com/de/kontakt?utm_source=back-office&utm_medium=links&utm_campaign=help-center-de&utm_content=download17', + 'it' => 'https://www.prestashop.com/it/contatti?utm_source=back-office&utm_medium=links&utm_campaign=help-center-it&utm_content=download17', + 'nl' => 'https://www.prestashop.com/nl/contacteer-ons?utm_source=back-office&utm_medium=links&utm_campaign=help-center-nl&utm_content=download17', + 'pt' => 'https://www.prestashop.com/pt/contato?utm_source=back-office&utm_medium=links&utm_campaign=help-center-pt&utm_content=download17', + 'pl' => 'https://www.prestashop.com/pl/kontakt?utm_source=back-office&utm_medium=links&utm_campaign=help-center-pl&utm_content=download17', + ]; + + return isset($links[$languageCode]) ? $links[$languageCode] : $links['en']; + } +} diff --git a/controllers/admin/AdminFeaturesController.php b/controllers/admin/AdminFeaturesController.php new file mode 100644 index 00000000..915f7d95 --- /dev/null +++ b/controllers/admin/AdminFeaturesController.php @@ -0,0 +1,656 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Feature $object + */ +class AdminFeaturesControllerCore extends AdminController +{ + public $bootstrap = true; + protected $position_identifier = 'id_feature'; + protected $feature_name; + + public function __construct() + { + $this->table = 'feature'; + $this->className = 'Feature'; + $this->list_id = 'feature'; + $this->identifier = 'id_feature'; + $this->lang = true; + + parent::__construct(); + + $this->fields_list = [ + 'id_feature' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'width' => 'auto', + 'filter_key' => 'b!name', + ], + 'value' => [ + 'title' => $this->trans('Values', [], 'Admin.Global'), + 'orderby' => false, + 'search' => false, + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'position' => [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'position' => 'position', + ], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + ], + ]; + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + /** + * Change object type to feature value (use when processing a feature value). + */ + protected function setTypeValue() + { + $this->table = 'feature_value'; + $this->className = 'FeatureValue'; + $this->identifier = 'id_feature_value'; + } + + /** + * Change object type to feature (use when processing a feature). + */ + protected function setTypeFeature() + { + $this->table = 'feature'; + $this->className = 'Feature'; + $this->identifier = 'id_feature'; + } + + public function renderView() + { + if (($id = (int) Tools::getValue('id_feature'))) { + $this->setTypeValue(); + $this->list_id = 'feature_value'; + $this->lang = true; + + // Action for list + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + if (!Validate::isLoadedObject($obj = new Feature((int) $id))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + + return; + } + + $this->feature_name = $obj->name; + $this->toolbar_title = $this->feature_name[$this->context->employee->id_lang]; + $this->fields_list = [ + 'id_feature_value' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'value' => [ + 'title' => $this->trans('Value', [], 'Admin.Global'), + ], + ]; + + $this->_where = sprintf('AND `id_feature` = %d', (int) $id); + self::$currentIndex = self::$currentIndex . '&id_feature=' . (int) $id . '&viewfeature'; + $this->processFilter(); + + return parent::renderList(); + } + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $this->toolbar_title = $this->trans('Add a new feature', [], 'Admin.Catalog.Feature'); + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Feature', [], 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'size' => 33, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + 'required' => true, + ], + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + return parent::renderForm(); + } + + public function initPageHeaderToolbar() + { + if (Feature::isFeatureActive()) { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_feature'] = [ + 'href' => self::$currentIndex . '&addfeature&token=' . $this->token, + 'desc' => $this->trans('Add new feature', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + + $this->page_header_toolbar_btn['new_feature_value'] = [ + 'href' => self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token, + 'desc' => $this->trans('Add new feature value', [], 'Admin.Catalog.Help'), + 'icon' => 'process-icon-new', + ]; + } + } + + if ($this->display == 'view') { + $this->page_header_toolbar_btn['new_feature_value'] = [ + 'href' => self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token, + 'desc' => $this->trans('Add new feature value', [], 'Admin.Catalog.Help'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + /** + * AdminController::initToolbar() override. + * + * @see AdminController::initToolbar() + */ + public function initToolbar() + { + switch ($this->display) { + case 'editFeatureValue': + case 'add': + case 'edit': + $this->toolbar_btn['save'] = [ + 'href' => '#', + 'desc' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if ($this->display == 'editFeatureValue') { + $this->toolbar_btn['save-and-stay'] = [ + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save and add another value', [], 'Admin.Catalog.Help'), + 'force_desc' => true, + ]; + } + + // Default cancel button - like old back link + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + + $this->toolbar_btn['back'] = [ + 'href' => $back, + 'desc' => $this->trans('Back to the list', [], 'Admin.Catalog.Help'), + ]; + + break; + case 'view': + $this->toolbar_btn['newAttributes'] = [ + 'href' => self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token, + 'desc' => $this->trans('Add new feature values', [], 'Admin.Catalog.Help'), + ]; + $this->toolbar_btn['back'] = [ + 'href' => self::$currentIndex . '&token=' . $this->token, + 'desc' => $this->trans('Back to the list', [], 'Admin.Catalog.Help'), + ]; + + break; + default: + parent::initToolbar(); + } + } + + public function initToolbarTitle() + { + $bread_extended = $this->breadcrumbs; + + switch ($this->display) { + case 'edit': + $bread_extended[] = $this->trans('Edit New Feature', [], 'Admin.Catalog.Feature'); + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + + break; + + case 'add': + $bread_extended[] = $this->trans('Add New Feature', [], 'Admin.Catalog.Feature'); + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + + break; + + case 'view': + $bread_extended[] = $this->feature_name[$this->context->employee->id_lang]; + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + + break; + + case 'editFeatureValue': + if (Tools::getValue('id_feature_value')) { + if (($id = (int) Tools::getValue('id_feature'))) { + if (Validate::isLoadedObject($obj = new Feature((int) $id))) { + $bread_extended[] = '' . $obj->name[$this->context->employee->id_lang] . ''; + } + + if (Validate::isLoadedObject($obj = new FeatureValue((int) Tools::getValue('id_feature_value')))) { + $bread_extended[] = $this->trans('Edit: %value%', ['%value%' => $obj->value[$this->context->employee->id_lang]], 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Edit Value', [], 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Add New Value', [], 'Admin.Catalog.Feature'); + } + + if (count($bread_extended) > 0) { + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + } + + break; + } + + $this->toolbar_title = $bread_extended; + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function initFormFeatureValue() + { + $this->setTypeValue(); + + $this->fields_form[0]['form'] = [ + 'legend' => [ + 'title' => $this->trans('Feature value', [], 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ], + 'input' => [ + [ + 'type' => 'select', + 'label' => $this->trans('Feature', [], 'Admin.Catalog.Feature'), + 'name' => 'id_feature', + 'options' => [ + 'query' => Feature::getFeatures($this->context->language->id), + 'id' => 'id_feature', + 'name' => 'name', + ], + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Value', [], 'Admin.Global'), + 'name' => 'value', + 'lang' => true, + 'size' => 33, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + 'required' => true, + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + 'buttons' => [ + 'save-and-stay' => [ + 'title' => $this->trans('Save then add another value', [], 'Admin.Catalog.Feature'), + 'name' => 'submitAdd' . $this->table . 'AndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ], + ], + ]; + + $this->fields_value['id_feature'] = (int) Tools::getValue('id_feature'); + + // Create Object FeatureValue + $feature_value = new FeatureValue(Tools::getValue('id_feature_value')); + + $this->tpl_vars = [ + 'feature_value' => $feature_value, + ]; + + $this->getlanguages(); + $helper = new HelperForm(); + $helper->show_cancel_button = true; + + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + if (!Validate::isCleanHtml($back)) { + die(Tools::displayError()); + } + + $helper->back_url = $back; + $helper->currentIndex = self::$currentIndex; + $helper->token = $this->token; + $helper->table = $this->table; + $helper->identifier = $this->identifier; + $helper->override_folder = 'feature_value/'; + $helper->id = $feature_value->id; + $helper->toolbar_scroll = false; + $helper->tpl_vars = $this->tpl_vars; + $helper->languages = $this->_languages; + $helper->default_form_language = $this->default_form_language; + $helper->allow_employee_form_lang = $this->allow_employee_form_lang; + $helper->fields_value = $this->getFieldsValue($feature_value); + $helper->toolbar_btn = $this->toolbar_btn; + $helper->title = $this->trans('Add a new feature value', [], 'Admin.Catalog.Feature'); + $this->content .= $helper->generateForm($this->fields_form); + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (Feature::isFeatureActive()) { + if ($this->display == 'edit' || $this->display == 'add') { + if (!$this->loadObject(true)) { + return; + } + $this->content .= $this->renderForm(); + } elseif ($this->display == 'view') { + // Some controllers use the view action without an object + if ($this->className) { + $this->loadObject(true); + } + $this->content .= $this->renderView(); + } elseif ($this->display == 'editFeatureValue') { + if (!$this->object = new FeatureValue((int) Tools::getValue('id_feature_value'))) { + return; + } + $this->content .= $this->initFormFeatureValue(); + } elseif ($this->display != 'view' && !$this->ajax) { + // If a feature value was saved, we need to reset the values to display the list + $this->setTypeFeature(); + $this->content .= $this->renderList(); + /* reset all attributes filter */ + if (!Tools::getValue('submitFilterfeature_value', 0) && !Tools::getIsset('id_feature_value')) { + $this->processResetFilters('feature_value'); + } + } + } else { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + $url = '' . $this->trans('Performance', [], 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %url%.', ['%url%' => $url], 'Admin.Catalog.Notification')); + } + + $this->context->smarty->assign([ + 'content' => $this->content, + ]); + } + + public function initProcess() + { + // Are we working on feature values? + if ((Tools::getValue('id_feature_value') && !Tools::getValue('id_feature')) + || Tools::isSubmit('deletefeature_value') + || Tools::isSubmit('submitAddfeature_value') + || Tools::isSubmit('addfeature_value') + || Tools::isSubmit('updatefeature_value') + || Tools::isSubmit('submitBulkdeletefeature_value')) { + $this->setTypeValue(); + } + + if (Tools::getIsset('viewfeature')) { + $this->list_id = 'feature_value'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'feature'; + $this->_defaultOrderBy = 'position'; + $this->_defaultOrderWay = 'ASC'; + } + + parent::initProcess(); + } + + public function postProcess() + { + if (!Feature::isFeatureActive()) { + return; + } + + /* set location with current index */ + if (Tools::getIsset('id_feature') && Tools::getIsset('viewfeature')) { + self::$currentIndex = self::$currentIndex . '&id_feature=' . Tools::getValue('id_feature', 0) . '&viewfeature'; + } + + if ($this->table == 'feature_value' && ($this->action == 'save' || $this->action == 'delete' || $this->action == 'bulkDelete')) { + Hook::exec( + 'displayFeatureValuePostProcess', + ['errors' => &$this->errors] + ); + } // send errors as reference to allow displayFeatureValuePostProcess to stop saving process + else { + Hook::exec( + 'displayFeaturePostProcess', + ['errors' => &$this->errors] + ); + } // send errors as reference to allow displayFeaturePostProcess to stop saving process + + parent::postProcess(); + + if ($this->table == 'feature_value' && ($this->display == 'edit' || $this->display == 'add')) { + $this->display = 'editFeatureValue'; + } + } + + /** + * Override processAdd to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processAdd() + */ + public function processAdd() + { + $object = parent::processAdd(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->table == 'feature_value' && ($this->display == 'edit' || $this->display == 'add')) { + $this->redirect_after = self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } + } elseif (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && count($this->errors)) { + $this->display = 'editFeatureValue'; + } + + return $object; + } + + /** + * Override processUpdate to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processUpdate() + { + $object = parent::processUpdate(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } + + return $object; + } + + /** + * Call the right method for creating or updating object. + * + * @return mixed + */ + public function processSave() + { + if ($this->table == 'feature') { + $id_feature = (int) Tools::getValue('id_feature'); + // Adding last position to the feature if not exist + if ($id_feature <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'feature` + ORDER BY position DESC'; + // set the position of the new feature in $_POST for postProcess() method + $_POST['position'] = Db::getInstance()->getValue($sql); + } + // clean \n\r characters + foreach ($_POST as $key => $value) { + if (preg_match('/^name_/Ui', $key)) { + $_POST[$key] = str_replace('\n', '', str_replace('\r', '', $value)); + } + } + } + + return parent::processSave(); + } + + /** + * AdminController::getList() override. + * + * @see AdminController::getList() + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + if ($this->table == 'feature_value') { + $this->_where .= ' AND (a.custom = 0 OR a.custom IS NULL)'; + } + + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if ($this->table == 'feature') { + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + $item = &$this->_list[$i]; + + $query = new DbQuery(); + $query->select('COUNT(fv.id_feature_value) as count_values'); + $query->from('feature_value', 'fv'); + $query->where('fv.id_feature =' . (int) $item['id_feature']); + $query->where('(fv.custom=0 OR fv.custom IS NULL)'); + $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); + $item['value'] = (int) $res; + unset($query); + } + } + } + + public function ajaxProcessUpdatePositions() + { + if ($this->access('edit')) { + $way = (int) Tools::getValue('way'); + $id_feature = (int) Tools::getValue('id'); + $positions = Tools::getValue('feature'); + + $new_positions = []; + foreach ($positions as $v) { + if (!empty($v)) { + $new_positions[] = $v; + } + } + + foreach ($new_positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_feature) { + if ($feature = new Feature((int) $pos[2])) { + if (isset($position) && $feature->updatePosition($way, $position, $id_feature)) { + echo 'ok position ' . (int) $position . ' for feature ' . (int) $pos[1] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update feature ' . (int) $id_feature . ' to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "This feature (' . (int) $id_feature . ') can t be loaded"}'; + } + + break; + } + } + } + } +} diff --git a/controllers/admin/AdminGendersController.php b/controllers/admin/AdminGendersController.php new file mode 100644 index 00000000..a8ba1f10 --- /dev/null +++ b/controllers/admin/AdminGendersController.php @@ -0,0 +1,232 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Gender $object + */ +class AdminGendersControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'gender'; + $this->className = 'Gender'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->default_image_height = 16; + $this->default_image_width = 16; + + $this->fieldImageSettings = [ + 'name' => 'image', + 'dir' => 'genders', + ]; + + $this->fields_list = [ + 'id_gender' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Social title', [], 'Admin.Shopparameters.Feature'), + 'filter_key' => 'b!name', + ], + 'type' => [ + 'title' => $this->trans('Gender', [], 'Admin.Global'), + 'orderby' => false, + 'type' => 'select', + 'list' => [ + 0 => $this->trans('Male', [], 'Admin.Shopparameters.Feature'), + 1 => $this->trans('Female', [], 'Admin.Shopparameters.Feature'), + 2 => $this->trans('Neutral', [], 'Admin.Shopparameters.Feature'), + ], + 'filter_key' => 'a!type', + 'callback' => 'displayGenderType', + 'callback_object' => $this, + ], + 'image' => [ + 'title' => $this->trans('Image', [], 'Admin.Global'), + 'align' => 'center', + 'image' => 'genders', + 'orderby' => false, + 'search' => false, + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_gender'] = [ + 'href' => self::$currentIndex . '&addgender&token=' . $this->token, + 'desc' => $this->trans('Add new social title', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Social titles', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-male', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Social title', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Shopparameters.Help') . ' 0-9!<>,;?=+()@#"�{}_$%:', + 'required' => true, + ], + [ + 'type' => 'radio', + 'label' => $this->trans('Gender', [], 'Admin.Global'), + 'name' => 'type', + 'required' => false, + 'class' => 't', + 'values' => [ + [ + 'id' => 'type_male', + 'value' => 0, + 'label' => $this->trans('Male', [], 'Admin.Shopparameters.Feature'), + ], + [ + 'id' => 'type_female', + 'value' => 1, + 'label' => $this->trans('Female', [], 'Admin.Shopparameters.Feature'), + ], + [ + 'id' => 'type_neutral', + 'value' => 2, + 'label' => $this->trans('Neutral', [], 'Admin.Shopparameters.Feature'), + ], + ], + ], + [ + 'type' => 'file', + 'label' => $this->trans('Image', [], 'Admin.Global'), + 'name' => 'image', + 'col' => 6, + 'value' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Image width', [], 'Admin.Shopparameters.Feature'), + 'name' => 'img_width', + 'col' => 2, + 'hint' => $this->trans('Image width in pixels. Enter "0" to use the original size.', [], 'Admin.Shopparameters.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Image height', [], 'Admin.Shopparameters.Feature'), + 'name' => 'img_height', + 'col' => 2, + 'hint' => $this->trans('Image height in pixels. Enter "0" to use the original size.', [], 'Admin.Shopparameters.Help'), + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + /** @var Gender $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_value = [ + 'img_width' => $this->default_image_width, + 'img_height' => $this->default_image_height, + 'image' => $obj->getImage(), + ]; + + return parent::renderForm(); + } + + public function displayGenderType($value, $tr) + { + return $this->fields_list['type']['list'][$value]; + } + + protected function postImage($id) + { + if (isset($this->fieldImageSettings['name'], $this->fieldImageSettings['dir'])) { + if (!Validate::isInt(Tools::getValue('img_width')) || !Validate::isInt(Tools::getValue('img_height'))) { + $this->errors[] = $this->trans('Width and height must be numeric values.', [], 'Admin.Shopparameters.Notification'); + } else { + if ((int) Tools::getValue('img_width') > 0 && (int) Tools::getValue('img_height') > 0) { + $width = (int) Tools::getValue('img_width'); + $height = (int) Tools::getValue('img_height'); + } else { + $width = null; + $height = null; + } + + return $this->uploadImage($id, $this->fieldImageSettings['name'], $this->fieldImageSettings['dir'] . '/', false, $width, $height); + } + } + + return !count($this->errors) ? true : false; + } + + protected function afterImageUpload() + { + parent::afterImageUpload(); + + if (($id_gender = (int) Tools::getValue('id_gender')) && + isset($_FILES) && count($_FILES) && file_exists(_PS_GENDERS_DIR_ . $id_gender . '.jpg')) { + $current_file = _PS_TMP_IMG_DIR_ . 'gender_mini_' . $id_gender . '_' . $this->context->shop->id . '.jpg'; + + if (file_exists($current_file)) { + unlink($current_file); + } + } + + return true; + } +} diff --git a/controllers/admin/AdminGroupsController.php b/controllers/admin/AdminGroupsController.php new file mode 100644 index 00000000..2d62d421 --- /dev/null +++ b/controllers/admin/AdminGroupsController.php @@ -0,0 +1,656 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Group $object + */ +class AdminGroupsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'group'; + $this->className = 'Group'; + $this->list_id = 'group'; + $this->lang = true; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $groups_to_keep = [ + Configuration::get('PS_UNIDENTIFIED_GROUP'), + Configuration::get('PS_GUEST_GROUP'), + Configuration::get('PS_CUSTOMER_GROUP'), + ]; + + $this->fields_list = [ + 'id_group' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Group name', [], 'Admin.Shopparameters.Feature'), + 'filter_key' => 'b!name', + ], + 'reduction' => [ + 'title' => $this->trans('Discount (%)', [], 'Admin.Shopparameters.Feature'), + 'align' => 'right', + 'type' => 'percent', + ], + 'nb' => [ + 'title' => $this->trans('Members', [], 'Admin.Shopparameters.Feature'), + 'align' => 'center', + 'havingFilter' => true, + ], + 'show_prices' => [ + 'title' => $this->trans('Show prices', [], 'Admin.Shopparameters.Feature'), + 'align' => 'center', + 'type' => 'bool', + 'orderby' => false, + ], + 'date_add' => [ + 'title' => $this->trans('Creation date', [], 'Admin.Shopparameters.Feature'), + 'type' => 'date', + 'align' => 'right', + ], + ]; + + $this->addRowActionSkipList('delete', $groups_to_keep); + + $this->_select .= '(SELECT COUNT(jcg.`id_customer`) + FROM `' . _DB_PREFIX_ . 'customer_group` jcg + LEFT JOIN `' . _DB_PREFIX_ . 'customer` jc ON (jc.`id_customer` = jcg.`id_customer`) + WHERE jc.`deleted` != 1 + ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) . ' + AND jcg.`id_group` = a.`id_group`) AS nb'; + $this->_use_found_rows = false; + + $groups = Group::getGroups(Context::getContext()->language->id, true); + + if (Group::isFeatureActive()) { + $this->fields_options = [ + 'general' => [ + 'title' => $this->trans('Default groups options', [], 'Admin.Shopparameters.Feature'), + 'fields' => [ + 'PS_UNIDENTIFIED_GROUP' => [ + 'title' => $this->trans('Visitors group', [], 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('The group defined for your un-identified visitors.', [], 'Admin.Shopparameters.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'list' => $groups, + 'identifier' => 'id_group', + ], + 'PS_GUEST_GROUP' => [ + 'title' => $this->trans('Guests group', [], 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('The group defined for your identified guest customers (used in guest checkout).', [], 'Admin.Shopparameters.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'list' => $groups, + 'identifier' => 'id_group', + ], + 'PS_CUSTOMER_GROUP' => [ + 'title' => $this->trans('Customers group', [], 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('The group defined for your identified registered customers.', [], 'Admin.Shopparameters.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'list' => $groups, + 'identifier' => 'id_group', + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ], + ]; + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin('fancybox'); + $this->addJqueryUi('ui.sortable'); + } + + public function initToolbar() + { + if ($this->display == 'add' || $this->display == 'edit') { + $this->toolbar_btn['save-and-stay'] = [ + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save, then add a category reduction.', [], 'Admin.Shopparameters.Feature'), + 'force_desc' => true, + ]; + } + parent::initToolbar(); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_group'] = [ + 'href' => self::$currentIndex . '&addgroup&token=' . $this->token, + 'desc' => $this->trans('Add new group', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function initProcess() + { + $this->id_object = Tools::getValue('id_' . $this->table); + + if (Tools::isSubmit('changeShowPricesVal') && $this->id_object) { + $this->action = 'change_show_prices_val'; + } + + if (Tools::getIsset('viewgroup')) { + $this->list_id = 'customer_group'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + + if (Tools::isSubmit('submitFilter')) { + self::$currentIndex .= '&id_group=' . (int) Tools::getValue('id_group') . '&viewgroup'; + } + } else { + $this->list_id = 'group'; + } + + parent::initProcess(); + } + + public function renderView() + { + $this->context = Context::getContext(); + if (!($group = $this->loadObject(true))) { + return; + } + + $this->tpl_view_vars = [ + 'group' => $group, + 'language' => $this->context->language, + 'customerList' => $this->renderCustomersList($group), + 'categorieReductions' => $this->formatCategoryDiscountList($group->id), + ]; + + return parent::renderView(); + } + + protected function renderCustomersList($group) + { + $genders = [0 => '?']; + $genders_icon = ['default' => 'unknown.gif']; + foreach (Gender::getGenders() as $gender) { + /* @var Gender $gender */ + $genders_icon[$gender->id] = '../genders/' . (int) $gender->id . '.jpg'; + $genders[$gender->id] = $gender->name; + } + $this->table = 'customer_group'; + $this->lang = false; + $this->list_id = 'customer_group'; + $this->actions = []; + $this->addRowAction('edit'); + $this->identifier = 'id_customer'; + $this->bulk_actions = false; + $this->list_no_link = true; + $this->explicitSelect = true; + + $this->fields_list = ([ + 'id_customer' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'filter_key' => 'c!id_customer', + 'class' => 'fixed-width-xs', + ], + 'id_gender' => [ + 'title' => $this->trans('Social title', [], 'Admin.Global'), + 'icon' => $genders_icon, + 'list' => $genders, + ], + 'firstname' => [ + 'title' => $this->trans('First name', [], 'Admin.Global'), + 'maxlength' => 30, + ], + 'lastname' => [ + 'title' => $this->trans('Last name', [], 'Admin.Global'), + 'maxlength' => 30, + ], + 'email' => [ + 'title' => $this->trans('Email address', [], 'Admin.Global'), + 'filter_key' => 'c!email', + 'orderby' => true, + 'maxlength' => 50, + ], + 'birthday' => [ + 'title' => $this->trans('Date of birth', [], 'Admin.Global'), + 'type' => 'date', + 'class' => 'fixed-width-md', + 'align' => 'center', + ], + 'date_add' => [ + 'title' => $this->trans('Registration date', [], 'Admin.Shopparameters.Feature'), + 'type' => 'date', + 'class' => 'fixed-width-md', + 'align' => 'center', + ], + 'active' => [ + 'title' => $this->trans('Enabled', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'type' => 'bool', + 'search' => false, + 'orderby' => false, + 'filter_key' => 'c!active', + 'callback' => 'printOptinIcon', + ], + ]); + $this->_select = 'c.*, a.id_group'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (a.`id_customer` = c.`id_customer`)'; + $this->_where = 'AND a.`id_group` = ' . (int) $group->id . ' AND c.`deleted` != 1'; + $this->_where .= Shop::addSqlRestriction(Shop::SHARE_CUSTOMER, 'c'); + self::$currentIndex = self::$currentIndex . '&id_group=' . (int) $group->id . '&viewgroup'; + + $this->processFilter(); + + return parent::renderList(); + } + + public function printOptinIcon($value, $customer) + { + return $value ? '' : ''; + } + + public function renderForm() + { + if (!($group = $this->loadObject(true))) { + return; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Customer group', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-group', + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Forbidden characters:', [], 'Admin.Notifications.Info') . ' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Discount', [], 'Admin.Global'), + 'name' => 'reduction', + 'suffix' => '%', + 'col' => 1, + 'hint' => $this->trans('Automatically apply this value as a discount on all products for members of this customer group.', [], 'Admin.Shopparameters.Help'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Price display method', [], 'Admin.Shopparameters.Feature'), + 'name' => 'price_display_method', + 'col' => 2, + 'hint' => $this->trans('How prices are displayed in the order summary for this customer group.', [], 'Admin.Shopparameters.Help'), + 'options' => [ + 'query' => [ + [ + 'id_method' => PS_TAX_EXC, + 'name' => $this->trans('Tax excluded', [], 'Admin.Global'), + ], + [ + 'id_method' => PS_TAX_INC, + 'name' => $this->trans('Tax included', [], 'Admin.Global'), + ], + ], + 'id' => 'id_method', + 'name' => 'name', + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Show prices', [], 'Admin.Shopparameters.Feature'), + 'name' => 'show_prices', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'show_prices_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'show_prices_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Customers in this group can view prices.', [], 'Admin.Shopparameters.Help'), + 'desc' => $this->trans('Need to hide prices for all groups? Save time, enable catalog mode in Product Settings instead.', [], 'Admin.Shopparameters.Help'), + ], + [ + 'type' => 'group_discount_category', + 'label' => $this->trans('Category discount', [], 'Admin.Shopparameters.Feature'), + 'name' => 'reduction', + 'values' => ($group->id ? $this->formatCategoryDiscountList((int) $group->id) : []), + ], + [ + 'type' => 'modules', + 'label' => $this->trans('Modules authorization', [], 'Admin.Shopparameters.Feature'), + 'name' => 'auth_modules', + 'values' => $this->formatModuleListAuth($group->id), + ], + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + if (Tools::getIsset('addgroup')) { + $this->fields_value['price_display_method'] = Configuration::get('PRICE_DISPLAY_METHOD'); + } + + $this->fields_value['reduction'] = isset($group->reduction) ? $group->reduction : 0; + + $tree = new HelperTreeCategories('categories-tree'); + $this->tpl_form_vars['categoryTreeView'] = $tree->setRootCategory((int) Category::getRootCategory()->id)->render(); + + return parent::renderForm(); + } + + protected function formatCategoryDiscountList($id_group) + { + $group_reductions = GroupReduction::getGroupReductions((int) $id_group, $this->context->language->id); + $category_reductions = []; + $category_reduction = Tools::getValue('category_reduction'); + + foreach ($group_reductions as $category) { + if (is_array($category_reduction) && array_key_exists($category['id_category'], $category_reduction)) { + $category['reduction'] = $category_reduction[$category['id_category']]; + } + + $category_reductions[(int) $category['id_category']] = [ + 'path' => Tools::getPath(Context::getContext()->link->getAdminLink('AdminCategories'), (int) $category['id_category']), + 'reduction' => (float) $category['reduction'] * 100, + 'id_category' => (int) $category['id_category'], + ]; + } + + if (is_array($category_reduction)) { + foreach ($category_reduction as $key => $val) { + if (!array_key_exists($key, $category_reductions)) { + $category_reductions[(int) $key] = [ + 'path' => Tools::getPath(Context::getContext()->link->getAdminLink('AdminCategories'), $key), + 'reduction' => (float) $val * 100, + 'id_category' => (int) $key, + ]; + } + } + } + + return $category_reductions; + } + + public function formatModuleListAuth($id_group) + { + $modules = Module::getModulesInstalled(); + $authorized_modules = ''; + + $auth_modules = []; + $unauth_modules = []; + + $shops = Shop::getContextListShopID(); + + if ($id_group) { + $authorized_modules = Module::getAuthorizedModules($id_group, $shops); + } + + if (is_array($authorized_modules)) { + foreach ($modules as $module) { + $authorized = false; + foreach ($authorized_modules as $auth_module) { + if ($module['id_module'] == $auth_module['id_module']) { + $authorized = true; + } + } + + if ($authorized) { + $auth_modules[] = $module; + } else { + $unauth_modules[] = $module; + } + } + } else { + $auth_modules = $modules; + } + $auth_modules_tmp = []; + foreach ($auth_modules as $key => $val) { + if ($module = Module::getInstanceById($val['id_module'])) { + $auth_modules_tmp[] = $module; + } + } + + $auth_modules = $auth_modules_tmp; + + $unauth_modules_tmp = []; + foreach ($unauth_modules as $key => $val) { + if (($tmp_obj = Module::getInstanceById($val['id_module']))) { + $unauth_modules_tmp[] = $tmp_obj; + } + } + + $unauth_modules = $unauth_modules_tmp; + + return ['unauth_modules' => $unauth_modules, 'auth_modules' => $auth_modules]; + } + + public function processSave() + { + if (!$this->validateDiscount(Tools::getValue('reduction'))) { + $this->errors[] = $this->trans('The discount value is incorrect (must be a percentage).', [], 'Admin.Shopparameters.Notification'); + } else { + $this->updateCategoryReduction(); + $object = parent::processSave(); + $this->updateRestrictions(); + + return $object; + } + } + + protected function validateDiscount($reduction) + { + if (!Validate::isPrice($reduction) || $reduction > 100 || $reduction < 0) { + return false; + } else { + return true; + } + } + + public function ajaxProcessAddCategoryReduction() + { + $category_reduction = Tools::getValue('category_reduction'); + $id_category = Tools::getValue('id_category'); //no cast validation is done with Validate::isUnsignedId($id_category) + + $result = []; + if (!Validate::isUnsignedId($id_category)) { + $result['errors'][] = $this->trans('Wrong category ID.', [], 'Admin.Shopparameters.Notification'); + $result['hasError'] = true; + } elseif (!$this->validateDiscount($category_reduction)) { + $result['errors'][] = $this->trans('The discount value is incorrect (must be a percentage).', [], 'Admin.Shopparameters.Notification'); + $result['hasError'] = true; + } else { + $result['id_category'] = (int) $id_category; + $result['catPath'] = Tools::getPath(self::$currentIndex . '?tab=AdminCategories', (int) $id_category); + $result['discount'] = $category_reduction; + $result['hasError'] = false; + } + die(json_encode($result)); + } + + /** + * Update (or create) restrictions for modules by group. + */ + protected function updateRestrictions() + { + $id_group = Tools::getValue('id_group'); + $auth_modules = Tools::getValue('modulesBoxAuth'); + $return = true; + if ($id_group) { + $shops = Shop::getContextListShopID(); + if (is_array($auth_modules)) { + $return &= Group::addModulesRestrictions($id_group, $auth_modules, $shops); + } + } + + // update module list by hook cache + Cache::clean(Hook::MODULE_LIST_BY_HOOK_KEY . '*'); + + return $return; + } + + protected function updateCategoryReduction() + { + $category_reduction = Tools::getValue('category_reduction'); + Db::getInstance()->execute( + ' + DELETE FROM `' . _DB_PREFIX_ . 'group_reduction` + WHERE `id_group` = ' . (int) Tools::getValue('id_group') + ); + Db::getInstance()->execute( + ' + DELETE FROM `' . _DB_PREFIX_ . 'product_group_reduction_cache` + WHERE `id_group` = ' . (int) Tools::getValue('id_group') + ); + if (is_array($category_reduction) && count($category_reduction)) { + if (!Configuration::getGlobalValue('PS_GROUP_FEATURE_ACTIVE')) { + Configuration::updateGlobalValue('PS_GROUP_FEATURE_ACTIVE', 1); + } + foreach ($category_reduction as $cat => $reduction) { + if (!Validate::isUnsignedId($cat) || !$this->validateDiscount($reduction)) { + $this->errors[] = $this->trans('The discount value is incorrect.', [], 'Admin.Shopparameters.Notification'); + } else { + $category = new Category((int) $cat); + $category->addGroupsIfNoExist((int) Tools::getValue('id_group')); + $group_reduction = new GroupReduction(); + $group_reduction->id_group = (int) Tools::getValue('id_group'); + $group_reduction->reduction = (float) ($reduction / 100); + $group_reduction->id_category = (int) $cat; + if (!$group_reduction->save()) { + $this->errors[] = $this->trans('You cannot save group reductions.', [], 'Admin.Shopparameters.Notification'); + } + } + } + } + } + + /** + * Toggle show prices flag. + */ + public function processChangeShowPricesVal() + { + $group = new Group($this->id_object); + if (!Validate::isLoadedObject($group)) { + $this->errors[] = $this->trans('An error occurred while updating this group.', [], 'Admin.Shopparameters.Notification'); + } + $update = Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'group` SET show_prices = ' . ($group->show_prices ? 0 : 1) . ' WHERE `id_group` = ' . (int) $group->id); + if (!$update) { + $this->errors[] = $this->trans('An error occurred while updating this group.', [], 'Admin.Shopparameters.Notification'); + } + Tools::clearSmartyCache(); + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + + public function renderList() + { + $unidentified = new Group(Configuration::get('PS_UNIDENTIFIED_GROUP')); + $guest = new Group(Configuration::get('PS_GUEST_GROUP')); + $default = new Group(Configuration::get('PS_CUSTOMER_GROUP')); + + $unidentified_group_information = $this->trans('%group_name% - All persons without a customer account or customers that are not logged in.', ['%group_name%' => '' . $unidentified->name[$this->context->language->id] . ''], 'Admin.Shopparameters.Help'); + $guest_group_information = $this->trans('%group_name% - All persons who placed an order through Guest Checkout.', ['%group_name%' => '' . $guest->name[$this->context->language->id] . ''], 'Admin.Shopparameters.Help'); + $default_group_information = $this->trans('%group_name% - All persons who created an account on this site.', ['%group_name%' => '' . $default->name[$this->context->language->id] . ''], 'Admin.Shopparameters.Help'); + + $this->displayInformation($this->trans('PrestaShop has three default customer groups:', [], 'Admin.Shopparameters.Help')); + $this->displayInformation($unidentified_group_information); + $this->displayInformation($guest_group_information); + $this->displayInformation($default_group_information); + + return parent::renderList(); + } + + public function displayEditLink($token, $id) + { + $tpl = $this->createTemplate('helpers/list/list_action_edit.tpl'); + if (!array_key_exists('Edit', self::$cache_lang)) { + self::$cache_lang['Edit'] = $this->trans('Edit', [], 'Admin.Actions'); + } + + $href = self::$currentIndex . '&' . $this->identifier . '=' . $id . '&update' . $this->table . '&token=' . ($token != null ? $token : $this->token); + + if ($this->display == 'view') { + $href = Context::getContext()->link->getAdminLink('AdminCustomers', true, [], [ + 'id_customer' => $id, + 'updatecustomer' => 1, + 'back' => urlencode($href), + ]); + } + + $tpl->assign([ + 'href' => $href, + 'action' => self::$cache_lang['Edit'], + 'id' => $id, + ]); + + return $tpl->fetch(); + } +} diff --git a/controllers/admin/AdminImagesController.php b/controllers/admin/AdminImagesController.php new file mode 100644 index 00000000..bdd9b0b2 --- /dev/null +++ b/controllers/admin/AdminImagesController.php @@ -0,0 +1,755 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property ImageType $object + */ +class AdminImagesControllerCore extends AdminController +{ + protected $start_time = 0; + protected $max_execution_time = 7200; + protected $display_move; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'image_type'; + $this->className = 'ImageType'; + $this->lang = false; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->fields_list = [ + 'id_image_type' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'], + 'name' => ['title' => $this->trans('Name', [], 'Admin.Global')], + 'width' => ['title' => $this->trans('Width', [], 'Admin.Global'), 'suffix' => ' px'], + 'height' => ['title' => $this->trans('Height', [], 'Admin.Global'), 'suffix' => ' px'], + 'products' => ['title' => $this->trans('Products', [], 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false], + 'categories' => ['title' => $this->trans('Categories', [], 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false], + 'manufacturers' => ['title' => $this->trans('Brands', [], 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false], + 'suppliers' => ['title' => $this->trans('Suppliers', [], 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false], + 'stores' => ['title' => $this->trans('Stores', [], 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false], + ]; + + // No need to display the old image system migration tool except if product images are in _PS_PROD_IMG_DIR_ + $this->display_move = false; + $dir = _PS_PROD_IMG_DIR_; + if (is_dir($dir)) { + if ($dh = opendir($dir)) { + while (($file = readdir($dh)) !== false && $this->display_move == false) { + if (!is_dir($dir . DIRECTORY_SEPARATOR . $file) && $file[0] != '.' && is_numeric($file[0])) { + $this->display_move = true; + } + } + closedir($dh); + } + } + + $this->fields_options = [ + 'images' => [ + 'title' => $this->trans('Images generation options', [], 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + 'top' => '', + 'bottom' => '', + 'description' => $this->trans('JPEG images have a small file size and standard quality. PNG images have a larger file size, a higher quality and support transparency. Note that in all cases the image files will have the .jpg extension.', [], 'Admin.Design.Help') . ' +

' . $this->trans('WARNING: This feature may not be compatible with your theme, or with some of your modules. In particular, PNG mode is not compatible with the Watermark module. If you encounter any issues, turn it off by selecting "Use JPEG".', [], 'Admin.Design.Help'), + 'fields' => [ + 'PS_IMAGE_QUALITY' => [ + 'title' => $this->trans('Image format', [], 'Admin.Design.Feature'), + 'show' => true, + 'required' => true, + 'type' => 'radio', + 'choices' => ['jpg' => $this->trans('Use JPEG.', [], 'Admin.Design.Feature'), 'png' => $this->trans('Use PNG only if the base image is in PNG format.', [], 'Admin.Design.Feature'), 'png_all' => $this->trans('Use PNG for all images.', [], 'Admin.Design.Feature')], + ], + 'PS_JPEG_QUALITY' => [ + 'title' => $this->trans('JPEG compression', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('Ranges from 0 (worst quality, smallest file) to 100 (best quality, biggest file).', [], 'Admin.Design.Help') . ' ' . $this->trans('Recommended: 90.', [], 'Admin.Design.Help'), + 'validation' => 'isUnsignedId', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + ], + 'PS_PNG_QUALITY' => [ + 'title' => $this->trans('PNG compression', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('PNG compression is lossless: unlike JPG, you do not lose image quality with a high compression ratio. However, photographs will compress very badly.', [], 'Admin.Design.Help') . ' ' . $this->trans('Ranges from 0 (biggest file) to 9 (smallest file, slowest decompression).', [], 'Admin.Design.Help') . ' ' . $this->trans('Recommended: 7.', [], 'Admin.Design.Help'), + 'validation' => 'isUnsignedId', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + ], + 'PS_IMAGE_GENERATION_METHOD' => [ + 'title' => $this->trans('Generate images based on one side of the source image', [], 'Admin.Design.Feature'), + 'validation' => 'isUnsignedId', + 'required' => false, + 'cast' => 'intval', + 'type' => 'select', + 'list' => [ + [ + 'id' => '0', + 'name' => $this->trans('Automatic (longest side)', [], 'Admin.Design.Feature'), + ], + [ + 'id' => '1', + 'name' => $this->trans('Width', [], 'Admin.Global'), + ], + [ + 'id' => '2', + 'name' => $this->trans('Height', [], 'Admin.Global'), + ], + ], + 'identifier' => 'id', + 'visibility' => Shop::CONTEXT_ALL, + ], + 'PS_PRODUCT_PICTURE_MAX_SIZE' => [ + 'title' => $this->trans('Maximum file size of product customization pictures', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('The maximum file size of pictures that customers can upload to customize a product (in bytes).', [], 'Admin.Design.Help'), + 'validation' => 'isUnsignedInt', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + 'suffix' => $this->trans('bytes', [], 'Admin.Design.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ], + 'PS_PRODUCT_PICTURE_WIDTH' => [ + 'title' => $this->trans('Product picture width', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('Width of product customization pictures that customers can upload (in pixels).', [], 'Admin.Design.Help'), + 'validation' => 'isUnsignedInt', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + 'width' => 'px', + 'suffix' => $this->trans('pixels', [], 'Admin.Design.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ], + 'PS_PRODUCT_PICTURE_HEIGHT' => [ + 'title' => $this->trans('Product picture height', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('Height of product customization pictures that customers can upload (in pixels).', [], 'Admin.Design.Help'), + 'validation' => 'isUnsignedInt', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + 'height' => 'px', + 'suffix' => $this->trans('pixels', [], 'Admin.Design.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ], + 'PS_HIGHT_DPI' => [ + 'type' => 'bool', + 'title' => $this->trans('Generate high resolution images', [], 'Admin.Design.Feature'), + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This will generate an additional file for each image (thus doubling your total amount of images). Resolution of these images will be twice higher.', [], 'Admin.Design.Help'), + 'desc' => $this->trans('Enable to optimize the display of your images on high pixel density screens.', [], 'Admin.Design.Help'), + 'visibility' => Shop::CONTEXT_ALL, + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + ]; + + if ($this->display_move) { + $this->fields_options['product_images']['fields']['PS_LEGACY_IMAGES'] = [ + 'title' => $this->trans('Use the legacy image filesystem', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('This should be set to yes unless you successfully moved images in "Images" page under the "Preferences" menu.', [], 'Admin.Design.Help'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'required' => false, + 'type' => 'bool', + 'visibility' => Shop::CONTEXT_ALL, + ]; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Image type', [], 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name for the image type', [], 'Admin.Design.Feature'), + 'name' => 'name', + 'required' => true, + 'hint' => $this->trans('Letters, underscores and hyphens only (e.g. "small_custom", "cart_medium", "large", "thickbox_extra-large").', [], 'Admin.Design.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Width', [], 'Admin.Global'), + 'name' => 'width', + 'required' => true, + 'maxlength' => 5, + 'suffix' => $this->trans('pixels', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('Maximum image width in pixels.', [], 'Admin.Design.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Height', [], 'Admin.Global'), + 'name' => 'height', + 'required' => true, + 'maxlength' => 5, + 'suffix' => $this->trans('pixels', [], 'Admin.Design.Feature'), + 'hint' => $this->trans('Maximum image height in pixels.', [], 'Admin.Design.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Products', [], 'Admin.Global'), + 'name' => 'products', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Product images.', [], 'Admin.Design.Help'), + 'values' => [ + [ + 'id' => 'products_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'products_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Categories', [], 'Admin.Global'), + 'name' => 'categories', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Category images.', [], 'Admin.Design.Help'), + 'values' => [ + [ + 'id' => 'categories_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'categories_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Brands', [], 'Admin.Global'), + 'name' => 'manufacturers', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Brand images.', [], 'Admin.Design.Help'), + 'values' => [ + [ + 'id' => 'manufacturers_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'manufacturers_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Suppliers', [], 'Admin.Global'), + 'name' => 'suppliers', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Supplier images.', [], 'Admin.Design.Help'), + 'values' => [ + [ + 'id' => 'suppliers_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'suppliers_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Stores', [], 'Admin.Global'), + 'name' => 'stores', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Store images.', [], 'Admin.Design.Help'), + 'values' => [ + [ + 'id' => 'stores_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'stores_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + } + + public function postProcess() + { + // When moving images, if duplicate images were found they are moved to a folder named duplicates/ + if (file_exists(_PS_PROD_IMG_DIR_ . 'duplicates/')) { + $this->warnings[] = $this->trans('Duplicate images were found when moving the product images. This is likely caused by unused demonstration images. Please make sure that the folder %folder% only contains demonstration images, and then delete it.', ['%folder%' => _PS_PROD_IMG_DIR_ . 'duplicates/'], 'Admin.Design.Notification'); + } + + if (Tools::isSubmit('submitRegenerate' . $this->table)) { + if ($this->access('edit')) { + if ($this->_regenerateThumbnails(Tools::getValue('type'), Tools::getValue('erase'))) { + Tools::redirectAdmin(self::$currentIndex . '&conf=9' . '&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitMoveImages' . $this->table)) { + if ($this->access('edit')) { + if ($this->_moveImagesToNewFileSystem()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=25' . '&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitOptions' . $this->table)) { + if ($this->access('edit')) { + if ((int) Tools::getValue('PS_JPEG_QUALITY') < 0 + || (int) Tools::getValue('PS_JPEG_QUALITY') > 100) { + $this->errors[] = $this->trans('Incorrect value for the selected JPEG image compression.', [], 'Admin.Design.Notification'); + } elseif ((int) Tools::getValue('PS_PNG_QUALITY') < 0 + || (int) Tools::getValue('PS_PNG_QUALITY') > 9) { + $this->errors[] = $this->trans('Incorrect value for the selected PNG image compression.', [], 'Admin.Design.Notification'); + } elseif (!Configuration::updateValue('PS_IMAGE_QUALITY', Tools::getValue('PS_IMAGE_QUALITY')) + || !Configuration::updateValue('PS_JPEG_QUALITY', Tools::getValue('PS_JPEG_QUALITY')) + || !Configuration::updateValue('PS_PNG_QUALITY', Tools::getValue('PS_PNG_QUALITY'))) { + $this->errors[] = $this->trans('Unknown error.', [], 'Admin.Notifications.Error'); + } else { + $this->confirmations[] = $this->_conf[6]; + } + + return parent::postProcess(); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } else { + return parent::postProcess(); + } + } + + public static function printEntityActiveIcon($value, $object) + { + return $value ? '' : ''; + } + + protected function _childValidation() + { + if (!Tools::getValue('id_image_type') && Validate::isImageTypeName($typeName = Tools::getValue('name')) && ImageType::typeAlreadyExists($typeName)) { + $this->errors[] = $this->trans('This name already exists.', [], 'Admin.Design.Notification'); + } + } + + /** + * Init display for the thumbnails regeneration block. + */ + public function initRegenerate() + { + $types = [ + 'categories' => $this->trans('Categories', [], 'Admin.Global'), + 'manufacturers' => $this->trans('Brands', [], 'Admin.Global'), + 'suppliers' => $this->trans('Suppliers', [], 'Admin.Global'), + 'products' => $this->trans('Products', [], 'Admin.Global'), + 'stores' => $this->trans('Stores', [], 'Admin.Global'), + ]; + + $formats = []; + foreach ($types as $i => $type) { + $formats[$i] = ImageType::getImagesTypes($i); + } + + $this->context->smarty->assign([ + 'types' => $types, + 'formats' => $formats, + ]); + } + + /** + * Delete resized image then regenerate new one with updated settings. + * + * @param string $dir + * @param array $type + * @param bool $product + * + * @return bool + */ + protected function _deleteOldImages($dir, $type, $product = false) + { + if (!is_dir($dir)) { + return false; + } + $toDel = scandir($dir, SCANDIR_SORT_NONE); + + foreach ($toDel as $d) { + foreach ($type as $imageType) { + if (preg_match('/^[0-9]+\-' . ($product ? '[0-9]+\-' : '') . $imageType['name'] . '\.jpg$/', $d) + || (count($type) > 1 && preg_match('/^[0-9]+\-[_a-zA-Z0-9-]*\.jpg$/', $d)) + || preg_match('/^([[:lower:]]{2})\-default\-' . $imageType['name'] . '\.jpg$/', $d)) { + if (file_exists($dir . $d)) { + unlink($dir . $d); + } + } + } + } + + // delete product images using new filesystem. + if ($product) { + $productsImages = Image::getAllImages(); + foreach ($productsImages as $image) { + $imageObj = new Image($image['id_image']); + $imageObj->id_product = $image['id_product']; + if (file_exists($dir . $imageObj->getImgFolder())) { + $toDel = scandir($dir . $imageObj->getImgFolder(), SCANDIR_SORT_NONE); + foreach ($toDel as $d) { + foreach ($type as $imageType) { + if (preg_match('/^[0-9]+\-' . $imageType['name'] . '\.jpg$/', $d) || (count($type) > 1 && preg_match('/^[0-9]+\-[_a-zA-Z0-9-]*\.jpg$/', $d))) { + if (file_exists($dir . $imageObj->getImgFolder() . $d)) { + unlink($dir . $imageObj->getImgFolder() . $d); + } + } + } + } + } + } + } + } + + /** + * Regenerate images. + * + * @param $dir + * @param $type + * @param bool $productsImages + * + * @return bool|string + */ + protected function _regenerateNewImages($dir, $type, $productsImages = false) + { + if (!is_dir($dir)) { + return false; + } + + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + if (!$productsImages) { + $formated_medium = ImageType::getFormattedName('medium'); + foreach (scandir($dir, SCANDIR_SORT_NONE) as $image) { + if (preg_match('/^[0-9]*\.jpg$/', $image)) { + foreach ($type as $k => $imageType) { + // Customizable writing dir + $newDir = $dir; + if (!file_exists($newDir)) { + continue; + } + + if (($dir == _PS_CAT_IMG_DIR_) && ($imageType['name'] == $formated_medium) && is_file(_PS_CAT_IMG_DIR_ . str_replace('.', '_thumb.', $image))) { + $image = str_replace('.', '_thumb.', $image); + } + + if (!file_exists($newDir . substr($image, 0, -4) . '-' . stripslashes($imageType['name']) . '.jpg')) { + if (!file_exists($dir . $image) || !filesize($dir . $image)) { + $this->errors[] = $this->trans('Source file does not exist or is empty (%filepath%)', ['%filepath%' => $dir . $image], 'Admin.Design.Notification'); + } elseif (!ImageManager::resize($dir . $image, $newDir . substr(str_replace('_thumb.', '.', $image), 0, -4) . '-' . stripslashes($imageType['name']) . '.jpg', (int) $imageType['width'], (int) $imageType['height'])) { + $this->errors[] = $this->trans('Failed to resize image file (%filepath%)', ['%filepath%' => $dir . $image], 'Admin.Design.Notification'); + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($dir . $image, $newDir . substr($image, 0, -4) . '-' . stripslashes($imageType['name']) . '2x.jpg', (int) $imageType['width'] * 2, (int) $imageType['height'] * 2)) { + $this->errors[] = $this->trans('Failed to resize image file to high resolution (%filepath%)', ['%filepath%' => $dir . $image], 'Admin.Design.Notification'); + } + } + } + // stop 4 seconds before the timeout, just enough time to process the end of the page on a slow server + if (time() - $this->start_time > $this->max_execution_time - 4) { + return 'timeout'; + } + } + } + } + } else { + foreach (Image::getAllImages() as $image) { + $imageObj = new Image($image['id_image']); + $existing_img = $dir . $imageObj->getExistingImgPath() . '.jpg'; + if (file_exists($existing_img) && filesize($existing_img)) { + foreach ($type as $imageType) { + if (!file_exists($dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg')) { + if (!ImageManager::resize($existing_img, $dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg', (int) $imageType['width'], (int) $imageType['height'])) { + $this->errors[] = $this->trans( + 'Original image is corrupt (%filename%) for product ID %id% or bad permission on folder.', + [ + '%filename%' => $existing_img, + '%id%' => (int) $imageObj->id_product, + ], + 'Admin.Design.Notification' + ); + } + } + if ($generate_hight_dpi_images) { + if (!file_exists($dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '2x.jpg')) { + if (!ImageManager::resize($existing_img, $dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '2x.jpg', (int) $imageType['width'] * 2, (int) $imageType['height'] * 2)) { + $this->errors[] = $this->trans( + 'Original image is corrupt (%filename%) for product ID %id% or bad permission on folder.', + [ + '%filename%' => $existing_img, + '%id%' => (int) $imageObj->id_product, + ], + 'Admin.Design.Notification' + ); + } + } + } + } + } else { + $this->errors[] = $this->trans( + 'Original image is missing or empty (%filename%) for product ID %id%', + [ + '%filename%' => $existing_img, + '%id%' => (int) $imageObj->id_product, + ], + 'Admin.Design.Notification' + ); + } + if (time() - $this->start_time > $this->max_execution_time - 4) { // stop 4 seconds before the tiemout, just enough time to process the end of the page on a slow server + return 'timeout'; + } + } + } + + return (bool) count($this->errors); + } + + /** + * Regenerate no-pictures images. + * + * @param $dir + * @param $type + * @param $languages + * + * @return bool + */ + protected function _regenerateNoPictureImages($dir, $type, $languages) + { + $errors = false; + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + foreach ($type as $image_type) { + foreach ($languages as $language) { + $file = $dir . $language['iso_code'] . '.jpg'; + if (!file_exists($file)) { + $file = _PS_PROD_IMG_DIR_ . Language::getIsoById((int) Configuration::get('PS_LANG_DEFAULT')) . '.jpg'; + } + if (!file_exists($dir . $language['iso_code'] . '-default-' . stripslashes($image_type['name']) . '.jpg')) { + if (!ImageManager::resize($file, $dir . $language['iso_code'] . '-default-' . stripslashes($image_type['name']) . '.jpg', (int) $image_type['width'], (int) $image_type['height'])) { + $errors = true; + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($file, $dir . $language['iso_code'] . '-default-' . stripslashes($image_type['name']) . '2x.jpg', (int) $image_type['width'] * 2, (int) $image_type['height'] * 2)) { + $errors = true; + } + } + } + } + } + + return $errors; + } + + /* Hook watermark optimization */ + protected function _regenerateWatermark($dir, $type = null) + { + $result = Db::getInstance()->executeS(' + SELECT m.`name` FROM `' . _DB_PREFIX_ . 'module` m + LEFT JOIN `' . _DB_PREFIX_ . 'hook_module` hm ON hm.`id_module` = m.`id_module` + LEFT JOIN `' . _DB_PREFIX_ . 'hook` h ON hm.`id_hook` = h.`id_hook` + WHERE h.`name` = \'actionWatermark\' AND m.`active` = 1'); + + if ($result && count($result)) { + $productsImages = Image::getAllImages(); + foreach ($productsImages as $image) { + $imageObj = new Image($image['id_image']); + if (file_exists($dir . $imageObj->getExistingImgPath() . '.jpg')) { + foreach ($result as $module) { + $moduleInstance = Module::getInstanceByName($module['name']); + if ($moduleInstance && is_callable([$moduleInstance, 'hookActionWatermark'])) { + call_user_func([$moduleInstance, 'hookActionWatermark'], ['id_image' => $imageObj->id, 'id_product' => $imageObj->id_product, 'image_type' => $type]); + } + + if (time() - $this->start_time > $this->max_execution_time - 4) { // stop 4 seconds before the tiemout, just enough time to process the end of the page on a slow server + return 'timeout'; + } + } + } + } + } + } + + protected function _regenerateThumbnails($type = 'all', $deleteOldImages = false) + { + $this->start_time = time(); + ini_set('max_execution_time', $this->max_execution_time); // ini_set may be disabled, we need the real value + $this->max_execution_time = (int) ini_get('max_execution_time'); + $languages = Language::getLanguages(false); + + $process = [ + ['type' => 'categories', 'dir' => _PS_CAT_IMG_DIR_], + ['type' => 'manufacturers', 'dir' => _PS_MANU_IMG_DIR_], + ['type' => 'suppliers', 'dir' => _PS_SUPP_IMG_DIR_], + ['type' => 'products', 'dir' => _PS_PROD_IMG_DIR_], + ['type' => 'stores', 'dir' => _PS_STORE_IMG_DIR_], + ]; + + // Launching generation process + foreach ($process as $proc) { + if ($type != 'all' && $type != $proc['type']) { + continue; + } + + // Getting format generation + $formats = ImageType::getImagesTypes($proc['type']); + if ($type != 'all') { + $format = (string) (Tools::getValue('format_' . $type)); + if ($format != 'all') { + foreach ($formats as $k => $form) { + if ($form['id_image_type'] != $format) { + unset($formats[$k]); + } + } + } + } + + if ($deleteOldImages) { + $this->_deleteOldImages($proc['dir'], $formats, ($proc['type'] == 'products' ? true : false)); + } + if (($return = $this->_regenerateNewImages($proc['dir'], $formats, ($proc['type'] == 'products' ? true : false))) === true) { + if (!count($this->errors)) { + $this->errors[] = $this->trans('Cannot write images for this type: %1$s. Please check the %2$s folder\'s writing permissions.', [$proc['type'], $proc['dir']], 'Admin.Design.Notification'); + } + } elseif ($return == 'timeout') { + $this->errors[] = $this->trans('Only part of the images have been regenerated. The server timed out before finishing.', [], 'Admin.Design.Notification'); + } else { + if ($proc['type'] == 'products') { + if ($this->_regenerateWatermark($proc['dir'], $formats) == 'timeout') { + $this->errors[] = $this->trans('Server timed out. The watermark may not have been applied to all images.', [], 'Admin.Design.Notification'); + } + } + if (!count($this->errors)) { + if ($this->_regenerateNoPictureImages($proc['dir'], $formats, $languages)) { + $this->errors[] = $this->trans('Cannot write "No picture" image to %s images folder. Please check the folder\'s writing permissions.', [$proc['type']], 'Admin.Design.Notification'); + } + } + } + } + + return count($this->errors) > 0 ? false : true; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_image_type'] = [ + 'href' => self::$currentIndex . '&addimage_type&token=' . $this->token, + 'desc' => $this->trans('Add new image type', [], 'Admin.Design.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + /** + * Move product images to the new filesystem. + */ + protected function _moveImagesToNewFileSystem() + { + if (!Image::testFileSystem()) { + $this->errors[] = $this->trans('Error: Your server configuration is not compatible with the new image system. No images were moved.', [], 'Admin.Design.Notification'); + } else { + ini_set('max_execution_time', $this->max_execution_time); // ini_set may be disabled, we need the real value + $this->max_execution_time = (int) ini_get('max_execution_time'); + $result = Image::moveToNewFileSystem($this->max_execution_time); + if ($result === 'timeout') { + $this->errors[] = $this->trans( + 'Not all images have been moved. The server timed out before finishing. Click on "%move_images_label%" again to resume the moving process.', + [ + '%move_images_label%' => $this->trans('Move images', [], 'Admin.Design.Feature'), + ], + 'Admin.Design.Notification' + ); + } elseif ($result === false) { + $this->errors[] = $this->trans('Error: Some -- or all -- images cannot be moved.', [], 'Admin.Design.Notification'); + } + } + + return count($this->errors) > 0 ? false : true; + } + + public function initContent() + { + if ($this->display != 'edit' && $this->display != 'add') { + $this->initRegenerate(); + + $this->context->smarty->assign([ + 'display_regenerate' => true, + 'display_move' => $this->display_move, + ]); + } + + if ($this->display == 'edit') { + $this->warnings[] = $this->trans('After modification, do not forget to regenerate thumbnails', [], 'Admin.Design.Notification'); + } + + parent::initContent(); + } +} diff --git a/controllers/admin/AdminImportController.php b/controllers/admin/AdminImportController.php new file mode 100644 index 00000000..3336e0c3 --- /dev/null +++ b/controllers/admin/AdminImportController.php @@ -0,0 +1,4954 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +use PhpOffice\PhpSpreadsheet\IOFactory; + +@ini_set('max_execution_time', 0); +/* No max line limit since the lines can be more than 4096. Performance impact is not significant. */ +define('MAX_LINE_SIZE', 0); + +/* Used for validatefields diying without user friendly error or not */ +define('UNFRIENDLY_ERROR', false); + +/* this value set the number of columns visible on each page */ +define('MAX_COLUMNS', 6); + +/* correct Mac error on eof */ +@ini_set('auto_detect_line_endings', '1'); + +class AdminImportControllerCore extends AdminController +{ + public static $column_mask; + + public $entities = []; + + public $available_fields = []; + + public $required_fields = []; + + public static $default_values = []; + + public static $validators = [ + 'active' => ['AdminImportController', 'getBoolean'], + 'tax_rate' => ['AdminImportController', 'getPrice'], + /* Tax excluded */ + 'price_tex' => ['AdminImportController', 'getPrice'], + /* Tax included */ + 'price_tin' => ['AdminImportController', 'getPrice'], + 'reduction_price' => ['AdminImportController', 'getPrice'], + 'reduction_percent' => ['AdminImportController', 'getPrice'], + 'wholesale_price' => ['AdminImportController', 'getPrice'], + 'ecotax' => ['AdminImportController', 'getPrice'], + 'name' => ['AdminImportController', 'createMultiLangField'], + 'description' => ['AdminImportController', 'createMultiLangField'], + 'description_short' => ['AdminImportController', 'createMultiLangField'], + 'meta_title' => ['AdminImportController', 'createMultiLangField'], + 'meta_keywords' => ['AdminImportController', 'createMultiLangField'], + 'meta_description' => ['AdminImportController', 'createMultiLangField'], + 'link_rewrite' => ['AdminImportController', 'createMultiLangField'], + 'available_now' => ['AdminImportController', 'createMultiLangField'], + 'available_later' => ['AdminImportController', 'createMultiLangField'], + 'category' => ['AdminImportController', 'split'], + 'online_only' => ['AdminImportController', 'getBoolean'], + 'accessories' => ['AdminImportController', 'split'], + 'image_alt' => ['AdminImportController', 'split'], + 'delivery_in_stock' => ['AdminImportController', 'createMultiLangField'], + 'delivery_out_stock' => ['AdminImportController', 'createMultiLangField'], + ]; + + public $separator; + public $convert; + public $multiple_value_separator; + + /** + * This flag shows if import was executed in current request. + * Used for symfony migration purposes. + * + * @var bool + */ + private $importExecuted = false; + + public function __construct() + { + $this->bootstrap = true; + + parent::__construct(); + + $this->entities = [ + $this->trans('Categories', [], 'Admin.Global'), + $this->trans('Products', [], 'Admin.Global'), + $this->trans('Combinations', [], 'Admin.Global'), + $this->trans('Customers', [], 'Admin.Global'), + $this->trans('Addresses', [], 'Admin.Global'), + $this->trans('Brands', [], 'Admin.Global'), + $this->trans('Suppliers', [], 'Admin.Global'), + $this->trans('Alias', [], 'Admin.Shopparameters.Feature'), + $this->trans('Store contacts', [], 'Admin.Advparameters.Feature'), + ]; + + // @since 1.5.0 + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) { + $this->entities = array_merge( + $this->entities, + [ + $this->trans('Supply Orders', [], 'Admin.Advparameters.Feature'), + $this->trans('Supply Order Details', [], 'Admin.Advparameters.Feature'), + ] + ); + } + + $this->entities = array_flip($this->entities); + + switch ((int) Tools::getValue('entity')) { + case $this->entities[$this->trans('Combinations', [], 'Admin.Global')]: + $this->required_fields = [ + 'group', + 'attribute', + ]; + + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id_product' => ['label' => $this->trans('Product ID', [], 'Admin.Advparameters.Feature')], + 'product_reference' => ['label' => $this->trans('Product Reference', [], 'Admin.Advparameters.Feature')], + 'group' => [ + 'label' => $this->trans('Attribute (Name:Type:Position)', [], 'Admin.Advparameters.Feature') . '*', + ], + 'attribute' => [ + 'label' => $this->trans('Value (Value:Position)', [], 'Admin.Advparameters.Feature') . '*', + ], + 'supplier_reference' => ['label' => $this->trans('Supplier reference', [], 'Admin.Advparameters.Feature')], + 'reference' => ['label' => $this->trans('Reference', [], 'Admin.Global')], + 'ean13' => ['label' => $this->trans('EAN13', [], 'Admin.Advparameters.Feature')], + 'upc' => ['label' => $this->trans('UPC', [], 'Admin.Advparameters.Feature')], + 'mpn' => ['label' => $this->trans('MPN', [], 'Admin.Catalog.Feature')], + 'wholesale_price' => ['label' => $this->trans('Cost price', [], 'Admin.Catalog.Feature')], + 'price' => ['label' => $this->trans('Impact on price', [], 'Admin.Catalog.Feature')], + 'ecotax' => ['label' => $this->trans('Ecotax', [], 'Admin.Catalog.Feature')], + 'quantity' => ['label' => $this->trans('Quantity', [], 'Admin.Global')], + 'minimal_quantity' => ['label' => $this->trans('Minimal quantity', [], 'Admin.Advparameters.Feature')], + 'low_stock_threshold' => ['label' => $this->trans('Low stock level', [], 'Admin.Catalog.Feature')], + 'low_stock_alert' => ['label' => $this->trans('Send me an email when the quantity is under this level', [], 'Admin.Catalog.Feature')], + 'weight' => ['label' => $this->trans('Impact on weight', [], 'Admin.Catalog.Feature')], + 'default_on' => ['label' => $this->trans('Default (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'available_date' => ['label' => $this->trans('Combination availability date', [], 'Admin.Advparameters.Feature')], + 'image_position' => [ + 'label' => $this->trans('Choose among product images by position (1,2,3...)', [], 'Admin.Advparameters.Feature'), + ], + 'image_url' => ['label' => $this->trans('Image URLs (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'image_alt' => ['label' => $this->trans('Image alt texts (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'shop' => [ + 'label' => $this->trans('ID / Name of shop', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', [], 'Admin.Advparameters.Help'), + ], + 'advanced_stock_management' => [ + 'label' => $this->trans('Advanced Stock Management', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Enable Advanced Stock Management on product (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Help'), + ], + 'depends_on_stock' => [ + 'label' => $this->trans('Depends on stock', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('0 = Use quantity set in product, 1 = Use quantity from warehouse.', [], 'Admin.Advparameters.Help'), + ], + 'warehouse' => [ + 'label' => $this->trans('Warehouse', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('ID of the warehouse to set as storage.', [], 'Admin.Advparameters.Help'), + ], + ]; + + self::$default_values = [ + 'reference' => '', + 'supplier_reference' => '', + 'ean13' => '', + 'upc' => '', + 'mpn' => '', + 'wholesale_price' => 0, + 'price' => 0, + 'ecotax' => 0, + 'quantity' => 0, + 'minimal_quantity' => 1, + 'low_stock_threshold' => null, + 'low_stock_alert' => false, + 'weight' => 0, + 'default_on' => null, + 'advanced_stock_management' => 0, + 'depends_on_stock' => 0, + 'available_date' => date('Y-m-d'), + ]; + + break; + + case $this->entities[$this->trans('Categories', [], 'Admin.Global')]: + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'active' => ['label' => $this->trans('Active (0/1)', [], 'Admin.Advparameters.Feature')], + 'name' => ['label' => $this->trans('Name', [], 'Admin.Global')], + 'parent' => ['label' => $this->trans('Parent category', [], 'Admin.Catalog.Feature')], + 'is_root_category' => [ + 'label' => $this->trans('Root category (0/1)', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('A category root is where a category tree can begin. This is used with multistore.', [], 'Admin.Advparameters.Help'), + ], + 'description' => ['label' => $this->trans('Description', [], 'Admin.Global')], + 'meta_title' => ['label' => $this->trans('Meta title', [], 'Admin.Global')], + 'meta_keywords' => ['label' => $this->trans('Meta keywords', [], 'Admin.Global')], + 'meta_description' => ['label' => $this->trans('Meta description', [], 'Admin.Global')], + 'link_rewrite' => ['label' => $this->trans('Rewritten URL', [], 'Admin.Shopparameters.Feature')], + 'image' => ['label' => $this->trans('Image URL', [], 'Admin.Advparameters.Feature')], + 'shop' => [ + 'label' => $this->trans('ID / Name of shop', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', [], 'Admin.Advparameters.Help'), + ], + ]; + + self::$default_values = [ + 'active' => '1', + 'parent' => Configuration::get('PS_HOME_CATEGORY'), + 'link_rewrite' => '', + ]; + + break; + + case $this->entities[$this->trans('Products', [], 'Admin.Global')]: + self::$validators['image'] = [ + 'AdminImportController', + 'split', + ]; + + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'active' => ['label' => $this->trans('Active (0/1)', [], 'Admin.Advparameters.Feature')], + 'name' => ['label' => $this->trans('Name', [], 'Admin.Global')], + 'category' => ['label' => $this->trans('Categories (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'price_tex' => ['label' => $this->trans('Price tax excluded', [], 'Admin.Advparameters.Feature')], + 'price_tin' => ['label' => $this->trans('Price tax included', [], 'Admin.Advparameters.Feature')], + 'id_tax_rules_group' => ['label' => $this->trans('Tax rule ID', [], 'Admin.Advparameters.Feature')], + 'wholesale_price' => ['label' => $this->trans('Cost price', [], 'Admin.Catalog.Feature')], + 'on_sale' => ['label' => $this->trans('On sale (0/1)', [], 'Admin.Advparameters.Feature')], + 'reduction_price' => ['label' => $this->trans('Discount amount', [], 'Admin.Advparameters.Feature')], + 'reduction_percent' => ['label' => $this->trans('Discount percent', [], 'Admin.Advparameters.Feature')], + 'reduction_from' => ['label' => $this->trans('Discount from (yyyy-mm-dd)', [], 'Admin.Advparameters.Feature')], + 'reduction_to' => ['label' => $this->trans('Discount to (yyyy-mm-dd)', [], 'Admin.Advparameters.Feature')], + 'reference' => ['label' => $this->trans('Reference #', [], 'Admin.Advparameters.Feature')], + 'supplier_reference' => ['label' => $this->trans('Supplier reference #', [], 'Admin.Advparameters.Feature')], + 'supplier' => ['label' => $this->trans('Supplier', [], 'Admin.Global')], + 'manufacturer' => ['label' => $this->trans('Brand', [], 'Admin.Global')], + 'ean13' => ['label' => $this->trans('EAN13', [], 'Admin.Advparameters.Feature')], + 'upc' => ['label' => $this->trans('UPC', [], 'Admin.Advparameters.Feature')], + 'mpn' => ['label' => $this->trans('MPN', [], 'Admin.Catalog.Feature')], + 'ecotax' => ['label' => $this->trans('Ecotax', [], 'Admin.Catalog.Feature')], + 'width' => ['label' => $this->trans('Width', [], 'Admin.Global')], + 'height' => ['label' => $this->trans('Height', [], 'Admin.Global')], + 'depth' => ['label' => $this->trans('Depth', [], 'Admin.Global')], + 'weight' => ['label' => $this->trans('Weight', [], 'Admin.Global')], + 'delivery_in_stock' => [ + 'label' => $this->trans( + 'Delivery time of in-stock products:', + [], + 'Admin.Catalog.Feature' + ), + ], + 'delivery_out_stock' => [ + 'label' => $this->trans( + 'Delivery time of out-of-stock products with allowed orders:', + [], + 'Admin.Advparameters.Feature' + ), + ], + 'quantity' => ['label' => $this->trans('Quantity', [], 'Admin.Global')], + 'minimal_quantity' => ['label' => $this->trans('Minimal quantity', [], 'Admin.Advparameters.Feature')], + 'low_stock_threshold' => ['label' => $this->trans('Low stock level', [], 'Admin.Catalog.Feature')], + 'low_stock_alert' => ['label' => $this->trans('Send me an email when the quantity is under this level', [], 'Admin.Catalog.Feature')], + 'visibility' => ['label' => $this->trans('Visibility', [], 'Admin.Catalog.Feature')], + 'additional_shipping_cost' => ['label' => $this->trans('Additional shipping cost', [], 'Admin.Advparameters.Feature')], + 'unity' => ['label' => $this->trans('Unit for the price per unit', [], 'Admin.Advparameters.Feature')], + 'unit_price' => ['label' => $this->trans('Price per unit', [], 'Admin.Advparameters.Feature')], + 'description_short' => ['label' => $this->trans('Summary', [], 'Admin.Catalog.Feature')], + 'description' => ['label' => $this->trans('Description', [], 'Admin.Global')], + 'tags' => ['label' => $this->trans('Tags (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'meta_title' => ['label' => $this->trans('Meta title', [], 'Admin.Global')], + 'meta_keywords' => ['label' => $this->trans('Meta keywords', [], 'Admin.Global')], + 'meta_description' => ['label' => $this->trans('Meta description', [], 'Admin.Global')], + 'link_rewrite' => ['label' => $this->trans('Rewritten URL', [], 'Admin.Advparameters.Feature')], + 'available_now' => ['label' => $this->trans('Label when in stock', [], 'Admin.Catalog.Feature')], + 'available_later' => ['label' => $this->trans('Label when backorder allowed', [], 'Admin.Advparameters.Feature')], + 'available_for_order' => ['label' => $this->trans('Available for order (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'available_date' => ['label' => $this->trans('Product availability date', [], 'Admin.Advparameters.Feature')], + 'date_add' => ['label' => $this->trans('Product creation date', [], 'Admin.Advparameters.Feature')], + 'show_price' => ['label' => $this->trans('Show price (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'image' => ['label' => $this->trans('Image URLs (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'image_alt' => ['label' => $this->trans('Image alt texts (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'delete_existing_images' => [ + 'label' => $this->trans('Delete existing images (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature'), + ], + 'features' => ['label' => $this->trans('Feature (Name:Value:Position:Customized)', [], 'Admin.Advparameters.Feature')], + 'online_only' => ['label' => $this->trans('Available online only (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'condition' => ['label' => $this->trans('Condition', [], 'Admin.Catalog.Feature')], + 'customizable' => ['label' => $this->trans('Customizable (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'uploadable_files' => ['label' => $this->trans('Uploadable files (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'text_fields' => ['label' => $this->trans('Text fields (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'out_of_stock' => ['label' => $this->trans('Action when out of stock', [], 'Admin.Advparameters.Feature')], + 'is_virtual' => ['label' => $this->trans('Virtual product (0 = No, 1 = Yes)', [], 'Admin.Advparameters.Feature')], + 'file_url' => ['label' => $this->trans('File URL', [], 'Admin.Advparameters.Feature')], + 'nb_downloadable' => [ + 'label' => $this->trans('Number of allowed downloads', [], 'Admin.Catalog.Feature'), + 'help' => $this->trans('Number of days this file can be accessed by customers. Set to zero for unlimited access.', [], 'Admin.Catalog.Help'), + ], + 'date_expiration' => ['label' => $this->trans('Expiration date (yyyy-mm-dd)', [], 'Admin.Advparameters.Feature')], + 'nb_days_accessible' => [ + 'label' => $this->trans('Number of days', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Number of days this file can be accessed by customers. Set to zero for unlimited access.', [], 'Admin.Catalog.Help'), + ], + 'shop' => [ + 'label' => $this->trans('ID / Name of shop', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', [], 'Admin.Advparameters.Help'), + ], + 'advanced_stock_management' => [ + 'label' => $this->trans('Advanced Stock Management', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Enable Advanced Stock Management on product (0 = No, 1 = Yes).', [], 'Admin.Advparameters.Help'), + ], + 'depends_on_stock' => [ + 'label' => $this->trans('Depends on stock', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('0 = Use quantity set in product, 1 = Use quantity from warehouse.', [], 'Admin.Advparameters.Help'), + ], + 'warehouse' => [ + 'label' => $this->trans('Warehouse', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('ID of the warehouse to set as storage.', [], 'Admin.Advparameters.Help'), + ], + 'accessories' => ['label' => $this->trans('Accessories (x,y,z...)', [], 'Admin.Advparameters.Feature')], + ]; + + self::$default_values = [ + 'id_category' => [(int) Configuration::get('PS_HOME_CATEGORY')], + 'id_category_default' => null, + 'active' => '1', + 'width' => 0.000000, + 'height' => 0.000000, + 'depth' => 0.000000, + 'weight' => 0.000000, + 'visibility' => 'both', + 'additional_shipping_cost' => 0.00, + 'unit_price' => 0, + 'quantity' => 0, + 'minimal_quantity' => 1, + 'low_stock_threshold' => null, + 'low_stock_alert' => false, + 'price' => 0, + 'id_tax_rules_group' => 0, + 'description_short' => [(int) Configuration::get('PS_LANG_DEFAULT') => ''], + 'link_rewrite' => [(int) Configuration::get('PS_LANG_DEFAULT') => ''], + 'online_only' => 0, + 'condition' => 'new', + 'available_date' => date('Y-m-d'), + 'date_add' => date('Y-m-d H:i:s'), + 'date_upd' => date('Y-m-d H:i:s'), + 'customizable' => 0, + 'uploadable_files' => 0, + 'text_fields' => 0, + 'advanced_stock_management' => 0, + 'depends_on_stock' => 0, + 'is_virtual' => 0, + ]; + + break; + + case $this->entities[$this->trans('Customers', [], 'Admin.Global')]: + //Overwrite required_fields AS only email is required whereas other entities + $this->required_fields = ['email', 'passwd', 'lastname', 'firstname']; + + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'active' => ['label' => $this->trans('Active (0/1)', [], 'Admin.Advparameters.Feature')], + 'id_gender' => ['label' => $this->trans('Titles ID (Mr = 1, Ms = 2, else 0)', [], 'Admin.Advparameters.Feature')], + 'email' => ['label' => $this->trans('Email', [], 'Admin.Global') . '*'], + 'passwd' => ['label' => $this->trans('Password', [], 'Admin.Global') . '*'], + 'birthday' => ['label' => $this->trans('Birth date (yyyy-mm-dd)', [], 'Admin.Advparameters.Feature')], + 'lastname' => ['label' => $this->trans('Last name', [], 'Admin.Global') . '*'], + 'firstname' => ['label' => $this->trans('First name', [], 'Admin.Global') . '*'], + 'newsletter' => ['label' => $this->trans('Newsletter (0/1)', [], 'Admin.Advparameters.Feature')], + 'optin' => ['label' => $this->trans('Partner offers (0/1)', [], 'Admin.Advparameters.Feature')], + 'date_add' => ['label' => $this->trans('Registration date (yyyy-mm-dd)', [], 'Admin.Advparameters.Feature')], + 'group' => ['label' => $this->trans('Groups (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'id_default_group' => ['label' => $this->trans('Default group ID', [], 'Admin.Advparameters.Feature')], + 'id_shop' => [ + 'label' => $this->trans('ID / Name of shop', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', [], 'Admin.Advparameters.Help'), + ], + ]; + + self::$default_values = [ + 'active' => '1', + 'id_shop' => Configuration::get('PS_SHOP_DEFAULT'), + ]; + + break; + + case $this->entities[$this->trans('Addresses', [], 'Admin.Global')]: + //Overwrite required_fields + $this->required_fields = [ + 'alias', + 'lastname', + 'firstname', + 'address1', + 'postcode', + 'country', + 'customer_email', + 'city', + ]; + + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'alias' => ['label' => $this->trans('Alias', [], 'Admin.Shopparameters.Feature') . '*'], + 'active' => ['label' => $this->trans('Active (0/1)', [], 'Admin.Advparameters.Feature')], + 'customer_email' => ['label' => $this->trans('Customer email', [], 'Admin.Advparameters.Feature') . '*'], + 'id_customer' => ['label' => $this->trans('Customer ID', [], 'Admin.Advparameters.Feature')], + 'manufacturer' => ['label' => $this->trans('Brand', [], 'Admin.Global')], + 'supplier' => ['label' => $this->trans('Supplier', [], 'Admin.Global')], + 'company' => ['label' => $this->trans('Company', [], 'Admin.Global')], + 'lastname' => ['label' => $this->trans('Last name', [], 'Admin.Global') . '*'], + 'firstname' => ['label' => $this->trans('First name', [], 'Admin.Global') . '*'], + 'address1' => ['label' => $this->trans('Address', [], 'Admin.Global') . '*'], + 'address2' => ['label' => $this->trans('Address (2)', [], 'Admin.Global')], + 'postcode' => ['label' => $this->trans('Zip/postal code', [], 'Admin.Global') . '*'], + 'city' => ['label' => $this->trans('City', [], 'Admin.Global') . '*'], + 'country' => ['label' => $this->trans('Country', [], 'Admin.Global') . '*'], + 'state' => ['label' => $this->trans('State', [], 'Admin.Global')], + 'other' => ['label' => $this->trans('Other', [], 'Admin.Global')], + 'phone' => ['label' => $this->trans('Phone', [], 'Admin.Global')], + 'phone_mobile' => ['label' => $this->trans('Mobile Phone', [], 'Admin.Global')], + 'vat_number' => ['label' => $this->trans('VAT number', [], 'Admin.Orderscustomers.Feature')], + 'dni' => ['label' => $this->trans('Identification number', [], 'Admin.Orderscustomers.Feature')], + ]; + + self::$default_values = [ + 'alias' => 'Alias', + 'postcode' => 'X', + ]; + + break; + case $this->entities[$this->trans('Brands', [], 'Admin.Global')]: + case $this->entities[$this->trans('Suppliers', [], 'Admin.Global')]: + //Overwrite validators AS name is not MultiLangField + self::$validators = [ + 'description' => ['AdminImportController', 'createMultiLangField'], + 'short_description' => ['AdminImportController', 'createMultiLangField'], + 'meta_title' => ['AdminImportController', 'createMultiLangField'], + 'meta_keywords' => ['AdminImportController', 'createMultiLangField'], + 'meta_description' => ['AdminImportController', 'createMultiLangField'], + ]; + + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'active' => ['label' => $this->trans('Active (0/1)', [], 'Admin.Advparameters.Feature')], + 'name' => ['label' => $this->trans('Name', [], 'Admin.Global')], + 'description' => ['label' => $this->trans('Description', [], 'Admin.Global')], + 'short_description' => ['label' => $this->trans('Short description', [], 'Admin.Catalog.Feature')], + 'meta_title' => ['label' => $this->trans('Meta title', [], 'Admin.Global')], + 'meta_keywords' => ['label' => $this->trans('Meta keywords', [], 'Admin.Global')], + 'meta_description' => ['label' => $this->trans('Meta description', [], 'Admin.Global')], + 'image' => ['label' => $this->trans('Image URL', [], 'Admin.Advparameters.Feature')], + 'shop' => [ + 'label' => $this->trans('ID / Name of group shop', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', [], 'Admin.Advparameters.Help'), + ], + ]; + + self::$default_values = [ + 'shop' => Shop::getGroupFromShop(Configuration::get('PS_SHOP_DEFAULT')), + ]; + + break; + case $this->entities[$this->trans('Alias', [], 'Admin.Shopparameters.Feature')]: + //Overwrite required_fields + $this->required_fields = [ + 'alias', + 'search', + ]; + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'alias' => ['label' => $this->trans('Alias', [], 'Admin.Shopparameters.Feature') . '*'], + 'search' => ['label' => $this->trans('Search', [], 'Admin.Shopparameters.Feature') . '*'], + 'active' => ['label' => $this->trans('Active', [], 'Admin.Global')], + ]; + self::$default_values = [ + 'active' => '1', + ]; + + break; + case $this->entities[$this->trans('Store contacts', [], 'Admin.Advparameters.Feature')]: + self::$validators['hours'] = ['AdminImportController', 'split']; + self::$validators['address1'] = ['AdminImportController', 'createMultiLangField']; + self::$validators['address2'] = ['AdminImportController', 'createMultiLangField']; + + $this->required_fields = [ + 'address1', + 'city', + 'country', + 'latitude', + 'longitude', + ]; + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'active' => ['label' => $this->trans('Active (0/1)', [], 'Admin.Advparameters.Feature')], + 'name' => ['label' => $this->trans('Name', [], 'Admin.Global')], + 'address1' => ['label' => $this->trans('Address', [], 'Admin.Global') . '*'], + 'address2' => ['label' => $this->trans('Address (2)', [], 'Admin.Advparameters.Feature')], + 'postcode' => ['label' => $this->trans('Zip/postal code', [], 'Admin.Global')], + 'state' => ['label' => $this->trans('State', [], 'Admin.Global')], + 'city' => ['label' => $this->trans('City', [], 'Admin.Global') . '*'], + 'country' => ['label' => $this->trans('Country', [], 'Admin.Global') . '*'], + 'latitude' => ['label' => $this->trans('Latitude', [], 'Admin.Advparameters.Feature') . '*'], + 'longitude' => ['label' => $this->trans('Longitude', [], 'Admin.Advparameters.Feature') . '*'], + 'phone' => ['label' => $this->trans('Phone', [], 'Admin.Global')], + 'fax' => ['label' => $this->trans('Fax', [], 'Admin.Global')], + 'email' => ['label' => $this->trans('Email address', [], 'Admin.Global')], + 'note' => ['label' => $this->trans('Note', [], 'Admin.Advparameters.Feature')], + 'hours' => ['label' => $this->trans('Hours (x,y,z...)', [], 'Admin.Advparameters.Feature')], + 'image' => ['label' => $this->trans('Image URL', [], 'Admin.Advparameters.Feature')], + 'shop' => [ + 'label' => $this->trans('ID / Name of shop', [], 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', [], 'Admin.Advparameters.Help'), + ], + ]; + self::$default_values = [ + 'active' => '1', + ]; + + break; + } + + // @since 1.5.0 + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) { + switch ((int) Tools::getValue('entity')) { + case $this->entities[$this->trans('Supply Orders', [], 'Admin.Advparameters.Feature')]: + // required fields + $this->required_fields = [ + 'id_supplier', + 'id_warehouse', + 'reference', + 'date_delivery_expected', + ]; + // available fields + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'id' => ['label' => $this->trans('ID', [], 'Admin.Global')], + 'id_supplier' => ['label' => $this->trans('Supplier ID *', [], 'Admin.Advparameters.Feature')], + 'id_lang' => ['label' => $this->trans('Lang ID', [], 'Admin.Advparameters.Feature')], + 'id_warehouse' => ['label' => $this->trans('Warehouse ID *', [], 'Admin.Advparameters.Feature')], + 'id_currency' => ['label' => $this->trans('Currency ID *', [], 'Admin.Advparameters.Feature')], + 'reference' => ['label' => $this->trans('Supply Order Reference *', [], 'Admin.Advparameters.Feature')], + 'date_delivery_expected' => ['label' => $this->trans('Delivery Date (Y-M-D)*', [], 'Admin.Advparameters.Feature')], + 'discount_rate' => ['label' => $this->trans('Discount rate', [], 'Admin.Advparameters.Feature')], + 'is_template' => ['label' => $this->trans('Template', [], 'Admin.Advparameters.Feature')], + ]; + // default values + self::$default_values = [ + 'id_lang' => (int) Configuration::get('PS_LANG_DEFAULT'), + 'id_currency' => Currency::getDefaultCurrency()->id, + 'discount_rate' => '0', + 'is_template' => '0', + ]; + + break; + case $this->entities[$this->trans('Supply Order Details', [], 'Admin.Advparameters.Feature')]: + // required fields + $this->required_fields = [ + 'supply_order_reference', + 'id_product', + 'unit_price_te', + 'quantity_expected', + ]; + // available fields + $this->available_fields = [ + 'no' => ['label' => $this->trans('Ignore this column', [], 'Admin.Advparameters.Feature')], + 'supply_order_reference' => ['label' => $this->trans('Supply Order Reference *', [], 'Admin.Advparameters.Feature')], + 'id_product' => ['label' => $this->trans('Product ID *', [], 'Admin.Advparameters.Feature')], + 'id_product_attribute' => ['label' => $this->trans('Product Attribute ID', [], 'Admin.Advparameters.Feature')], + 'unit_price_te' => ['label' => $this->trans('Unit Price (tax excl.)*', [], 'Admin.Advparameters.Feature')], + 'quantity_expected' => ['label' => $this->trans('Quantity Expected *', [], 'Admin.Advparameters.Feature')], + 'discount_rate' => ['label' => $this->trans('Discount Rate', [], 'Admin.Advparameters.Feature')], + 'tax_rate' => ['label' => $this->trans('Tax Rate', [], 'Admin.Advparameters.Feature')], + ]; + // default values + self::$default_values = [ + 'discount_rate' => '0', + 'tax_rate' => '0', + ]; + + break; + } + } + + $this->separator = ($separator = Tools::substr((string) (trim(Tools::getValue('separator'))), 0, 1)) ? $separator : ';'; + $this->convert = false; + $this->multiple_value_separator = ($separator = Tools::substr((string) (trim(Tools::getValue('multiple_value_separator'))), 0, 1)) ? $separator : ','; + } + + public function setMedia($isNewTheme = false) + { + $bo_theme = ((Validate::isLoadedObject($this->context->employee) + && $this->context->employee->bo_theme) ? $this->context->employee->bo_theme : 'default'); + + if (!file_exists(_PS_BO_ALL_THEMES_DIR_ . $bo_theme . DIRECTORY_SEPARATOR + . 'template')) { + $bo_theme = 'default'; + } + + // We need to set parent media first, so that jQuery is loaded before the dependant plugins + parent::setMedia($isNewTheme); + + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.iframe-transport.js'); + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.fileupload.js'); + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.fileupload-process.js'); + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.fileupload-validate.js'); + $this->addJs(__PS_BASE_URI__ . 'js/vendor/spin.js'); + $this->addJs(__PS_BASE_URI__ . 'js/vendor/ladda.js'); + } + + public function renderForm() + { + // If import was executed - collect errors or success message + // and send them to the migrated controller. + if ($this->importExecuted) { + $session = $this->getSession(); + + if ($this->errors) { + foreach ($this->errors as $error) { + $session->getFlashBag()->add('error', $error); + } + } else { + foreach ($this->warnings as $warning) { + $session->getFlashBag()->add('warning', $warning); + } + + $session->getFlashBag()->add( + 'success', + $this->trans( + 'Your file has been successfully imported into your shop. Don\'t forget to re-build the products\' search index.', + [], + 'Admin.Advparameters.Notification' + ) + ); + } + } + + $request = $this->getSymfonyRequest(); + + if ($request && $request->isMethod(\Symfony\Component\HttpFoundation\Request::METHOD_GET)) { + // Import form is reworked in Symfony. + // If user tries to access legacy form directly, + // we redirect him to new form. + $symfonyImportForm = $this->context->link->getAdminLink('AdminImport'); + Tools::redirectAdmin($symfonyImportForm); + } + + if (!is_dir(AdminImportController::getPath())) { + return !($this->errors[] = $this->trans('The import directory doesn\'t exist. Please check your file path.', [], 'Admin.Advparameters.Notification')); + } + + if (!is_writable(AdminImportController::getPath())) { + $this->displayWarning($this->trans('The import directory must be writable (CHMOD 755 / 777).', [], 'Admin.Advparameters.Notification')); + } + + $files_to_import = scandir(AdminImportController::getPath(), SCANDIR_SORT_NONE); + uasort($files_to_import, ['AdminImportController', 'usortFiles']); + foreach ($files_to_import as $k => &$filename) { + //exclude . .. .svn and index.php and all hidden files + if (preg_match('/^\..*|index\.php/i', $filename) || is_dir(AdminImportController::getPath() . $filename)) { + unset($files_to_import[$k]); + } + } + unset($filename); + + $this->fields_form = ['']; + + $this->toolbar_scroll = false; + $this->toolbar_btn = []; + + // adds fancybox + $this->addJqueryPlugin(['fancybox']); + + $entity_selected = 0; + if (isset($this->entities[$this->trans(Tools::ucfirst(Tools::getValue('import_type')))])) { + $entity_selected = $this->entities[$this->trans(Tools::ucfirst(Tools::getValue('import_type')))]; + $this->context->cookie->entity_selected = (int) $entity_selected; + } elseif (isset($this->context->cookie->entity_selected)) { + $entity_selected = (int) $this->context->cookie->entity_selected; + } + + $csv_selected = ''; + if (isset($this->context->cookie->csv_selected) && + @filemtime(AdminImportController::getPath( + urldecode($this->context->cookie->csv_selected) + ))) { + $csv_selected = urldecode($this->context->cookie->csv_selected); + } else { + $this->context->cookie->csv_selected = $csv_selected; + } + + $id_lang_selected = ''; + if (isset($this->context->cookie->iso_lang_selected) && $this->context->cookie->iso_lang_selected) { + $id_lang_selected = (int) Language::getIdByIso(urldecode($this->context->cookie->iso_lang_selected)); + } + + $separator_selected = $this->separator; + if (isset($this->context->cookie->separator_selected) && $this->context->cookie->separator_selected) { + $separator_selected = urldecode($this->context->cookie->separator_selected); + } + + $multiple_value_separator_selected = $this->multiple_value_separator; + if (isset($this->context->cookie->multiple_value_separator_selected) && $this->context->cookie->multiple_value_separator_selected) { + $multiple_value_separator_selected = urldecode($this->context->cookie->multiple_value_separator_selected); + } + + //get post max size + $post_max_size = ini_get('post_max_size'); + $bytes = (int) trim($post_max_size); + $last = strtolower($post_max_size[strlen($post_max_size) - 1]); + + switch ($last) { + case 'g': + $bytes *= 1024; + // no break to fall-through + case 'm': + $bytes *= 1024; + // no break to fall-through + case 'k': + $bytes *= 1024; + } + + if (!isset($bytes) || $bytes == '') { + $bytes = 20971520; + } // 20Mb + + $this->tpl_form_vars = [ + 'post_max_size' => (int) $bytes, + 'module_confirmation' => Tools::isSubmit('import') && (isset($this->warnings) && !count($this->warnings)), + 'path_import' => AdminImportController::getPath(), + 'entities' => $this->entities, + 'entity_selected' => $entity_selected, + 'csv_selected' => $csv_selected, + 'separator_selected' => $separator_selected, + 'multiple_value_separator_selected' => $multiple_value_separator_selected, + 'files_to_import' => $files_to_import, + 'languages' => Language::getLanguages(false), + 'id_language' => ($id_lang_selected) ? $id_lang_selected : $this->context->language->id, + 'available_fields' => $this->getAvailableFields(), + 'truncateAuthorized' => (Shop::isFeatureActive() && $this->context->employee->isSuperAdmin()) || !Shop::isFeatureActive(), + 'PS_ADVANCED_STOCK_MANAGEMENT' => Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT'), + ]; + + return parent::renderForm(); + } + + public function ajaxProcessuploadCsv() + { + $filename_prefix = date('YmdHis') . '-'; + + if (isset($_FILES['file']) && !empty($_FILES['file']['error'])) { + switch ($_FILES['file']['error']) { + case UPLOAD_ERR_INI_SIZE: + $_FILES['file']['error'] = $this->trans('The uploaded file exceeds the upload_max_filesize directive in php.ini. If your server configuration allows it, you may add a directive in your .htaccess.', [], 'Admin.Advparameters.Notification'); + + break; + case UPLOAD_ERR_FORM_SIZE: + $_FILES['file']['error'] = $this->trans('The uploaded file exceeds the post_max_size directive in php.ini. If your server configuration allows it, you may add a directive in your .htaccess, for example:', [], 'Admin.Advparameters.Notification') + . '
+ php_value post_max_size 20M ' . + $this->trans('(click to open "Generators" page)', [], 'Admin.Advparameters.Notification') . ''; + + break; + + break; + case UPLOAD_ERR_PARTIAL: + $_FILES['file']['error'] = $this->trans('The uploaded file was only partially uploaded.', [], 'Admin.Advparameters.Notification'); + + break; + + break; + case UPLOAD_ERR_NO_FILE: + $_FILES['file']['error'] = $this->trans('No file was uploaded.', [], 'Admin.Advparameters.Notification'); + + break; + + break; + } + } elseif (!preg_match('#([^\.]*?)\.(csv|xls[xt]?|o[dt]s)$#is', $_FILES['file']['name'])) { + $_FILES['file']['error'] = $this->trans('The extension of your file should be .csv.', [], 'Admin.Advparameters.Notification'); + } elseif (!@filemtime($_FILES['file']['tmp_name']) || + !@move_uploaded_file($_FILES['file']['tmp_name'], AdminImportController::getPath() . $filename_prefix . str_replace("\0", '', $_FILES['file']['name']))) { + $_FILES['file']['error'] = $this->trans('An error occurred while uploading / copying the file.', [], 'Admin.Advparameters.Notification'); + } else { + @chmod(AdminImportController::getPath() . $filename_prefix . $_FILES['file']['name'], 0664); + $_FILES['file']['filename'] = $filename_prefix . str_replace('\0', '', $_FILES['file']['name']); + } + + die(json_encode($_FILES)); + } + + public function renderView() + { + $this->addJS(_PS_JS_DIR_ . 'admin/import.js'); + + $handle = $this->openCsvFile(); + $nb_column = $this->getNbrColumn($handle, $this->separator); + $nb_table = ceil($nb_column / MAX_COLUMNS); + + $res = []; + foreach ($this->required_fields as $elem) { + $res[] = '\'' . $elem . '\''; + } + + $data = []; + for ($i = 0; $i < $nb_table; ++$i) { + $data[$i] = $this->generateContentTable($i, $nb_column, $handle, $this->separator); + } + + $this->context->cookie->entity_selected = (int) Tools::getValue('entity'); + $this->context->cookie->iso_lang_selected = urlencode(Tools::getValue('iso_lang')); + $this->context->cookie->separator_selected = urlencode($this->separator); + $this->context->cookie->multiple_value_separator_selected = urlencode($this->multiple_value_separator); + $this->context->cookie->csv_selected = urlencode(Tools::getValue('csv')); + + $this->tpl_view_vars = [ + 'import_matchs' => Db::getInstance()->executeS('SELECT * FROM ' . _DB_PREFIX_ . 'import_match', true, false), + 'fields_value' => [ + 'csv' => Tools::getValue('csv'), + 'entity' => (int) Tools::getValue('entity'), + 'iso_lang' => Tools::getValue('iso_lang'), + 'truncate' => Tools::getValue('truncate'), + 'forceIDs' => Tools::getValue('forceIDs'), + 'regenerate' => Tools::getValue('regenerate'), + 'sendemail' => Tools::getValue('sendemail'), + 'match_ref' => Tools::getValue('match_ref'), + 'separator' => $this->separator, + 'multiple_value_separator' => $this->multiple_value_separator, + ], + 'nb_table' => $nb_table, + 'nb_column' => $nb_column, + 'res' => implode(',', $res), + 'max_columns' => MAX_COLUMNS, + 'no_pre_select' => ['price_tin', 'feature'], + 'available_fields' => $this->available_fields, + 'data' => $data, + ]; + + return parent::renderView(); + } + + public function initToolbar() + { + switch ($this->display) { + case 'import': + // Default cancel button - like old back link + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + + $this->toolbar_btn['cancel'] = [ + 'href' => $back, + 'desc' => $this->trans('Cancel', [], 'Admin.Actions'), + ]; + // Default save button - action dynamically handled in javascript + $this->toolbar_btn['save-import'] = [ + 'href' => '#', + 'desc' => $this->trans('Import .CSV data', [], 'Admin.Advparameters.Feature'), + ]; + + break; + } + } + + protected function generateContentTable($current_table, $nb_column, $handle, $glue) + { + $html = ''; + // Header + for ($i = 0; $i < $nb_column; ++$i) { + if (MAX_COLUMNS * (int) $current_table <= $i && (int) $i < MAX_COLUMNS * ((int) $current_table + 1)) { + $html .= ''; + } + } + $html .= ''; + + AdminImportController::setLocale(); + for ($current_line = 0; $current_line < 10 && $line = fgetcsv($handle, MAX_LINE_SIZE, $glue); ++$current_line) { + /* UTF-8 conversion */ + if ($this->convert) { + $line = $this->utf8EncodeArray($line); + } + $html .= ''; + foreach ($line as $nb_c => $column) { + if ((MAX_COLUMNS * (int) $current_table <= $nb_c) && ((int) $nb_c < MAX_COLUMNS * ((int) $current_table + 1))) { + $html .= ''; + } + } + $html .= ''; + } + $html .= ''; + AdminImportController::rewindBomAware($handle); + + return $html; + } + + public function init() + { + parent::init(); + if (Tools::isSubmit('submitImportFile')) { + $this->display = 'import'; + } + } + + public function initContent() + { + if ($this->display == 'import') { + if (Tools::getValue('csv')) { + $this->content .= $this->renderView(); + } else { + $this->errors[] = $this->trans('To proceed, please upload a file first.', [], 'Admin.Advparameters.Notification'); + $this->content .= $this->renderForm(); + } + } else { + $this->content .= $this->renderForm(); + } + + $this->context->smarty->assign([ + 'content' => $this->content, + ]); + } + + protected static function rewindBomAware($handle) + { + // A rewind wrapper that skips BOM signature wrongly + if (!is_resource($handle)) { + return false; + } + rewind($handle); + if (($bom = fread($handle, 3)) != "\xEF\xBB\xBF") { + rewind($handle); + } + } + + protected static function getBoolean($field) + { + return (bool) $field; + } + + protected static function getPrice($field) + { + $field = ((float) str_replace(',', '.', $field)); + $field = ((float) str_replace('%', '', $field)); + + return $field; + } + + protected static function split($field) + { + if (empty($field)) { + return []; + } + + $separator = Tools::getValue('multiple_value_separator'); + if (null === $separator || trim($separator) == '') { + $separator = ','; + } + + $tab = ''; + $uniqid_path = false; + + // try data:// protocole. If failed, old school file on filesystem. + if (($fd = @fopen('data://text/plain;base64,' . base64_encode($field), 'rb')) === false) { + do { + $uniqid_path = _PS_UPLOAD_DIR_ . uniqid(); + } while (file_exists($uniqid_path)); + file_put_contents($uniqid_path, $field); + $fd = fopen($uniqid_path, 'rb'); + } + + if ($fd === false) { + return []; + } + + $tab = fgetcsv($fd, MAX_LINE_SIZE, $separator); + fclose($fd); + if ($uniqid_path !== false && file_exists($uniqid_path)) { + @unlink($uniqid_path); + } + + if (empty($tab) || (!is_array($tab))) { + return []; + } + + return $tab; + } + + protected static function createMultiLangField($field) + { + $res = []; + foreach (Language::getIDs(false) as $id_lang) { + $res[$id_lang] = $field; + } + + return $res; + } + + protected function getTypeValuesOptions($nb_c) + { + $i = 0; + $no_pre_select = ['price_tin', 'feature']; + + $options = ''; + foreach ($this->available_fields as $k => $field) { + $options .= '