* @copyright 2007-2020 Amazzing * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) */ class AmazzingFilter extends Module { public $errors = array(); public $generated_links = array(); public function __construct() { if (!defined('_PS_VERSION_')) { exit; } $this->name = 'amazzingfilter'; $this->tab = 'front_office_features'; $this->version = '3.1.3'; $this->author = 'Amazzing'; $this->need_instance = 0; $this->module_key = '702061a17e404432e6b85a85ad14afb0'; $this->bootstrap = true; parent::__construct(); $this->displayName = $this->l('Amazzing filter'); $this->description = $this->l('Powerful layered navigation with flexible settings'); $this->definePublicVariables(); } public function definePublicVariables() { $this->csv_dir = $this->local_path.'indexes/'; $this->indexation_process_file_path = $this->csv_dir.'index_all'; $this->db = Db::getInstance(); $this->saved_txt = $this->l('Saved'); $this->error_txt = $this->l('Error'); $this->product_list_class = 'af-product-list'; $this->is_17 = Tools::substr(_PS_VERSION_, 0, 3) === '1.7'; $this->page_link_rewrite_text = $this->is_17 ? 'page' : 'p'; $this->shop_ids = Shop::getContextListShopID(); $this->all_shop_ids = Shop::getShops(false, null, true); $this->id_lang = $this->context->language->id; $this->id_shop = $this->context->shop->id; $this->custom_overrides_dir = $this->local_path.'override_files/'; $this->qs_min_values = 10; $this->i = array( 'table' => _DB_PREFIX_.'af_index', 'variable_keys' => array('p', 'n', 't'), 'default' => array('g' => 'PS_UNIDENTIFIED_GROUP', 'c' => 'PS_CURRENCY_DEFAULT'), 'max_column_suffixes' => 15, ); } public function install() { if (Shop::isFeatureActive()) { Shop::setContext(Shop::CONTEXT_ALL); } $this->installation_process = true; if (!parent::install() || !$this->registerHook('displayLeftColumn') || !$this->registerHook('displayHeader') // || !$this->registerHook('displayHome') || !$this->registerHook('displayBackOfficeHeader') || !$this->registerHook('actionProductAdd') || !$this->registerHook('actionProductUpdate') || !$this->registerHook('actionIndexProduct') || !$this->registerHook('actionObjectAddAfter') || !$this->registerHook('actionObjectDeleteAfter') || !$this->registerHook('actionObjectUpdateAfter') || !$this->registerHook('actionObjectCombinationAddAfter') || !$this->registerHook('actionAdminTagsControllerSaveAfter') || !$this->registerHook('actionAdminTagsControllerDeleteBefore') || !$this->registerHook('actionAdminTagsControllerDeleteAfter') || !$this->registerHook('actionProductDelete') || !$this->registerHook('actionProductListOverride') || !$this->registerHook('productSearchProvider') || !$this->registerHook('displayCustomerAccount') || !$this->prepareDatabaseTables() || !$this->installDemoData()) { $this->uninstall(); return false; } foreach ($this->getSettingsKeys() as $type) { if ($type == 'seopage' && Module::isInstalled('af_seopages')) { $this->sp = Module::getInstanceByName('af_seopages'); } $this->saveSettings($type); // will be saved for all shops, becasue context is set to ALL above } $this->indexationTable('install'); // should be installed and adjusted after settings are ready $this->updatePosition(Hook::getIdByName('displayLeftColumn'), 0, 1); $this->processAvailableOverrides('add'); unlink(_PS_CACHE_DIR_.'class_index.php'); // In some cases overrides are not reset automatically return true; } public function prepareDatabaseTables() { $sql = array(); $sql[] = 'CREATE TABLE IF NOT EXISTS '._DB_PREFIX_.'af_templates ( id_template int(10) unsigned NOT NULL AUTO_INCREMENT, id_shop int(10) NOT NULL, template_controller varchar(128) NOT NULL, active tinyint(1) NOT NULL DEFAULT 1, template_name text NOT NULL, template_filters text NOT NULL, additional_settings text NOT NULL, PRIMARY KEY (id_template, id_shop), KEY template_controller (template_controller), KEY active (active) ) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'; $sql[] = 'CREATE TABLE IF NOT EXISTS '._DB_PREFIX_.'af_templates_lang ( id_template int(10) unsigned NOT NULL, id_shop int(10) NOT NULL, id_lang int(10) NOT NULL, data text NOT NULL, PRIMARY KEY (id_template, id_shop, id_lang) ) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'; $sql[] = 'CREATE TABLE IF NOT EXISTS '._DB_PREFIX_.'af_settings ( id_shop int(10) unsigned NOT NULL, type varchar(16) NOT NULL, value text NOT NULL, PRIMARY KEY (id_shop, type) ) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'; $sql[] = 'CREATE TABLE IF NOT EXISTS '._DB_PREFIX_.'af_customer_filters ( id_customer int(10) unsigned NOT NULL, filters text NOT NULL, PRIMARY KEY (id_customer) ) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'; foreach ($this->getControllersWithMultipleIDs() as $controller) { $sql[] = 'CREATE TABLE IF NOT EXISTS '._DB_PREFIX_.'af_'.pSQL($controller).'_templates ( id_'.pSQL($controller).' int(10) unsigned NOT NULL, id_template int(10) NOT NULL, id_shop int(10) NOT NULL, PRIMARY KEY (id_'.pSQL($controller).', id_template, id_shop) ) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'; } $this->mergedValues()->extendSQL('install', $sql); return $this->runSql($sql); } public function installDemoData() { $installed = true; foreach ($this->getAvailableControllers(true) as $controller => $controller_name) { $template_name = sprintf($this->l('Template for %s'), $controller_name); $installed &= (bool)$this->saveTemplate(0, $controller, $template_name); } return $installed; } public function getAvailableControllers($include_category_controller = false) { $controllers = array( 'category' => $this->l('Category pages'), 'seopage' => $this->l('Custom SEO pages'), 'manufacturer' => $this->l('Manufacturer pages'), 'supplier' => $this->l('Supplier pages'), 'index' => $this->l('Home page'), 'pricesdrop' => $this->l('Prices drop page'), 'newproducts' => $this->l('New products page'), 'bestsales' => $this->l('Best sales page'), 'search' => $this->l('Search results'), ); if (!$include_category_controller) { unset($controllers['category']); } return $controllers; } public function getControllersWithMultipleIDs($only_keys = true) { $controllers = array( 'category' => $this->l('Selected categories'), 'seopage' => $this->l('Selected SEO pages'), 'manufacturer' => $this->l('Selected manufacturers'), 'supplier' => $this->l('Selected suppliers'), ); return $only_keys ? array_keys($controllers) : $controllers; } public function uninstall() { $sql = array(); $sql[] = 'DROP TABLE IF EXISTS '._DB_PREFIX_.'af_templates'; $sql[] = 'DROP TABLE IF EXISTS '._DB_PREFIX_.'af_templates_lang'; $sql[] = 'DROP TABLE IF EXISTS '._DB_PREFIX_.'af_settings'; $sql[] = 'DROP TABLE IF EXISTS '._DB_PREFIX_.'af_customer_filters'; foreach ($this->getControllersWithMultipleIDs() as $controller) { $sql[] = 'DROP TABLE IF EXISTS '._DB_PREFIX_.'af_'.pSQL($controller).'_templates'; } $this->mergedValues()->extendSQL('uninstall', $sql); if (!parent::uninstall() || !$this->runSql($sql) || !$this->indexationTable('uninstall')) { return false; } $this->cache('clear', ''); $this->processAvailableOverrides('remove'); return true; } public function runSql($sql) { foreach ($sql as $s) { if (!$this->db->Execute($s)) { return false; } } return true; } public function processAvailableOverrides($action) { $action .= 'Override'; $overrides_data = $this->getOverridesData(); foreach ($overrides_data as $data) { $this->processOverride($action, $data['path'], false); } } public function indexationTable($action) { $ret = true; switch ($action) { case 'install': $required_columns = $this->indexationColumns('getRequired'); $columns = array(); foreach ($required_columns['primary'] as $c_name) { $columns[] = $c_name.' int(10) unsigned NOT NULL'; } $specific_types = array('w' => 'decimal(20,2)', 'd' => 'datetime', 'q' => 'tinyint(1)', 'v' => 'tinyint(1)'); foreach ($required_columns['main'] as $c_name) { $type = isset($specific_types[$c_name]) ? $specific_types[$c_name] : 'TEXT'; $columns[] = $c_name.' '.$type.' NOT NULL'; } $ret &= $this->db->execute(' CREATE TABLE IF NOT EXISTS '.pSQL($this->i['table']).' ('.implode(', ', $columns).', PRIMARY KEY('.implode(', ', $required_columns['primary']).')) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8 '); $ret &= $this->indexationColumns('adjust'); break; case 'uninstall': $ret &= $this->db->execute('DROP TABLE IF EXISTS '.pSQL($this->i['table'])); break; } return $ret; } public function indexationColumns($action, $id_shop = 0, $cache_time = 0) { if ($cache_time = (int)$cache_time) { $cache_id = 'indexationColumns_'.$action.'_'.$id_shop; if (!$ret = $this->cache('get', $cache_id, '', $cache_time)) { $ret = $this->indexationColumns($action, $id_shop); $this->cache('save', $cache_id, $ret); } return $ret; } $this->defineSettings(); $ret = true; switch ($action) { case 'adjust': $required_columns = $this->indexationColumns('getRequiredFormatted'); $existing_variable_columns = array_diff( $this->indexationColumns('getExisting'), array_merge($required_columns['primary'], $required_columns['main']) ); $sql = array(); if ($to_remove = array_diff($existing_variable_columns, $required_columns['variable'])) { $sql[] = 'ALTER TABLE '.pSQL($this->i['table']).' DROP '.implode(', DROP ', $to_remove); } if (array_diff($required_columns['variable'], $existing_variable_columns)) { $to_add = array(); // IMPORTANT: keep original ordering $prev_column = end($required_columns['main']); foreach ($required_columns['variable'] as $c_name) { if (!in_array($c_name, $existing_variable_columns)) { $type = (Tools::substr($c_name, 0, 1) == 'p' ? 'decimal(20,2)' : 'TEXT'); $to_add[] = 'ADD '.$c_name.' '.$type.' NOT NULL AFTER '.$prev_column; } $prev_column = $c_name; } if ($to_add) { $sql[] = 'ALTER TABLE '.pSQL($this->i['table']).' '.implode(', ', $to_add); } } if ($sql) { $comment = $required_columns['variable'] ? 'some columns are not normalized on purpose' : ''; $sql[] = 'ALTER TABLE '.pSQL($this->i['table']).' COMMENT = \''.pSQL($comment).'\''; $sql[] = 'OPTIMIZE TABLE '.pSQL($this->i['table']); $ret &= $this->runSql($sql); } break; case 'getExisting': $ret = array_column($this->db->executeS('SHOW COLUMNS FROM '.pSQL($this->i['table'])), 'Field'); break; case 'getRequiredFormatted': $ret = $this->indexationColumns('getRequired', $id_shop); $formatted_variable_columns = array(); foreach ($ret['variable'] as $c_name => $identifiers) { foreach ($identifiers as $suffix) { $formatted_variable_columns[] = $c_name.'_'.$suffix; } } $ret['variable'] = $formatted_variable_columns; break; case 'getRequired': $ret = array( 'primary' => array('id_product', 'id_shop'), 'main' => array('c', 'a', 'f', 'm', 's', 'w', 'r', 'd', 'q', 'v', 'g'), 'variable' => $this->indexationColumns('getVariableData', $id_shop), ); break; case 'getVariableData': $ret = array(); foreach ($this->i['variable_keys'] as $c_name) { if (!empty($this->settings['indexation'][$c_name])) { switch ($c_name) { case 'p': foreach ($this->getSuffixes('group', $id_shop) as $id_group) { foreach ($this->getSuffixes('currency', $id_shop) as $id_currency) { $suffix = $id_group.'_'.$id_currency; $ret[$c_name][$suffix] = $suffix; } } break; case 'n': case 't': if ($suffixes = $this->getSuffixes('lang', $id_shop)) { $ret[$c_name] = $suffixes; } break; } } } break; case 'getAvailableSuffixesCount': $ret = array('group' => 0, 'currency' => 0, 'lang' => 0); foreach (array_keys($ret) as $t_name) { $c_name = 'id_'.$t_name; $ret[$t_name] = (int)$this->db->getValue(' SELECT COUNT('.pSQL($c_name).') FROM '._DB_PREFIX_.pSQL($t_name).' main WHERE 1 '.$this->specificIndexationQuery($c_name, 'main', 0, false).' '); } break; } return $ret; } public function indexationData($action, $params = array()) { $ret = true; switch ($action) { case 'get': $query = $this->indexationData('prepareQuery', $params); $ret = $this->db->executeS($query); break; case 'prepareQuery': $query = new DbQuery(); $query->select('i.id_product AS id, c, a, f, g')->from('af_index', 'i'); switch ($params['order']['by']) { case 'n': if ($this->settings['indexation']['n']) { $query->select('n_'.(int)$params['id_lang'].' AS n'); } else { $on = 'pl.id_product = i.id_product AND pl.id_shop = i.id_shop AND pl.id_lang = '.(int)$params['id_lang']; $query->select('pl.name AS n')->leftJoin('product_lang', 'pl', $on); } break; case 'd': case 'r': $query->select($params['order']['by']); // ordering in PHP is faster on large catalogues } foreach (array('s', 'q', 'm') as $c_name) { if (isset($params['available_options'][$c_name]) || ($c_name == 'm' && $params['order']['by'] == 'manufacturer_name')) { $query->select($c_name); } } if (isset($params['available_options']['t']) && $this->settings['indexation']['t']) { $query->select('t_'.(int)$params['id_lang'].' AS t'); } if (isset($params['available_options']['w']) || isset($params['sliders']['w'])) { $query->select('w'); } if (!empty($params['p_identifier'])) { $query->select($params['p_identifier'].' AS p'); } foreach ($this->indexationData('queryRestrictions', $params) as $restriction) { $query->where($restriction); } $ret = $query; break; case 'queryRestrictions': $ret = array( 'id_shop' => 'i.id_shop = '.(int)$params['id_shop'], 'visibility' => 'v <> '.($params['current_controller'] == 'search' ? 1 : 2), // visibility 'none' is excluded during indexation ); if ($params['current_controller'] == 'category') { $ret['controller'] = 'FIND_IN_SET('.(int)$params['id_parent_cat'].', i.c) > 0'; } elseif ($params['current_controller'] == 'manufacturer') { $ret['controller'] = 'i.m = '.(int)$params['id_manufacturer']; } elseif ($params['current_controller'] == 'supplier') { $ret['controller'] = 'FIND_IN_SET('.(int)$params['id_supplier'].', i.s) > 0'; } elseif ($params['current_controller'] != 'index' && $params['current_controller'] != 'seopage') { // newproducts, pricesdrop, bestsales, search $imploded_cpids = $this->formatIDs($params['controller_product_ids'], true) ?: 0; $ret['controller'] = 'i.id_product IN ('.pSQL($imploded_cpids).')'; } break; case 'erase': $sql = 'DELETE FROM '.pSQL($this->i['table']).' WHERE 1'; foreach (array('id_product', 'id_shop') as $c_name) { if (isset($params[$c_name]) && $imploded_ids = $this->formatIDs($params[$c_name], true)) { $sql .= ' AND '.$c_name.' IN ('.($imploded_ids).')'; } } $ret &= $this->db->execute($sql); break; case 'get_ids': $ret = array_column($this->db->executeS(' SELECT id_product AS id FROM '.pSQL($this->i['table']).' WHERE id_shop = '.(int)$params['id_shop'].' '), 'id', 'id'); break; } return $ret; } public function indexationInfo($type, $shop_ids = array(), $remove_unused = false) { $ret = array(); $shop_ids = $shop_ids ?: $this->shop_ids; switch ($type) { case 'ids': foreach ($shop_ids as $id_shop) { $indexed = $this->indexationData('get_ids', array('id_shop' => $id_shop)); $required = $this->getProductIDsForIndexation($id_shop); $ret[$id_shop]['indexed'] = array_intersect($required, $indexed); $ret[$id_shop]['missing'] = array_diff($required, $indexed); if ($remove_unused && $unused_ids = array_diff($indexed, $required)) { $this->unindexProducts($unused_ids, array($id_shop)); } } break; case 'count': $ret = $this->indexationInfo('ids', $shop_ids, $remove_unused); foreach ($ret as $id_shop => $data) { foreach ($data as $key => $ids) { $ret[$id_shop][$key] = count($ids); } } break; } return $ret; } public function hookDisplayBackOfficeHeader() { if (Tools::getValue('controller') == 'AdminProducts') { // reindexProduct after mass combinations generation if ($this->is_17) { $this->addJqueryBO(); $js_path = $this->_path.'views/js/attribute-indexer.js?v='.$this->version; $this->context->controller->js_files[] = $js_path; $ajax_path = 'index.php?controller=AdminModules&configure='.$this->name. '&token='.Tools::getAdminTokenLite('AdminModules').'&ajax=1'; return $this->bo()->assignJsVars(array('af_ajax_action_path' => $ajax_path)); } elseif (!empty($this->context->cookie->af_index_product)) { $this->indexProduct($this->context->cookie->af_index_product); $this->context->cookie->__unset('af_index_product'); } return; } elseif (Tools::getValue('configure') != $this->name) { return; } $this->addJqueryBO(); $this->context->controller->addJqueryUI('ui.sortable'); $this->context->controller->css_files[$this->_path.'views/css/back.css?v='.$this->version] = 'all'; if ($this->is_17) { $this->context->controller->css_files[$this->_path.'views/css/back-17.css?'.$this->version] = 'all'; } $this->context->controller->js_files[] = $this->_path.'views/js/back.js?v='.$this->version; if (!empty($this->sp)) { $sp_path = _MODULE_DIR_.$this->sp->name.'/'; $this->context->controller->js_files[] = $sp_path.'views/js/back.js?v='.$this->sp->version; $this->context->controller->css_files[$sp_path.'views/css/back.css?v='.$this->sp->version] = 'all'; } // mce $this->context->controller->addJS(__PS_BASE_URI__.'js/tiny_mce/tiny_mce.js'); $this->context->controller->addJS(__PS_BASE_URI__.'js/admin/tinymce.inc.js'); $js_vars = array( 'savedTxt' => $this->saved_txt, 'errorTxt' => $this->error_txt, 'deletedTxt' => $this->l('Deleted'), 'areYouSureTxt' => $this->l('Are you sure?'), ); return $this->bo()->assignJsVars($js_vars); } public function addJqueryBO() { $this->defineSettings(); if (empty($this->context->jqueryAdded)) { version_compare(_PS_VERSION_, '1.7.6.0', '>=') ? $this->context->controller->setMedia() : $this->context->controller->addJquery(); $this->context->jqueryAdded = 1; } } public function ajaxAction($action) { $ret = array(); switch ($action) { case 'CallTemplateForm': $id_template = Tools::getValue('id_template'); $ret = $this->callTemplateForm($id_template); break; case 'RunProductIndexer': $this->ajaxRunProductIndexer(Tools::getValue('all_identifier')); break; case 'SaveMultipleSettings': $ret['saved'] = true; foreach (Tools::getValue('submitted_forms') as $type => $data) { $submitted_settings = $this->parseStr($data); $ret['saved'] &= $this->saveSettings($type, $submitted_settings, null, true); } break; case 'SaveTemplate': case 'DuplicateTemplate': case 'DeleteTemplate': case 'EraseIndex': case 'UpdateHook': $method = 'ajax'.$action; $this->$method(); break; case 'ToggleActiveStatus': $id_template = Tools::getValue('id_template'); $active = Tools::getValue('active'); $ret = array('success' => $this->toggleActiveStatus($id_template, $active)); break; case 'ShowAvailableFilters': $available_filters = $this->getAvailableFiltersSorted(); $this->context->smarty->assign(array('available_filters' => $available_filters)); $html = $this->display(__FILE__, 'views/templates/admin/available-filters.tpl'); $ret['content'] = utf8_encode($html); $ret['title'] = utf8_encode($this->l('Available filtering criteria')); break; case 'RenderFilterElements': $keys = explode(',', Tools::getValue('keys')); $html = ''; $this->assignLanguageVariables(); foreach ($keys as $key) { $this->context->smarty->assign(array('filter' => $this->getFilterData($key))); $html .= $this->display(__FILE__, 'views/templates/admin/filter-form.tpl'); } $ret['html'] = utf8_encode($html); break; case 'SaveAvailableCustomerFilters': $filters = Tools::getValue('customer_filters'); $filters = $filters ? Tools::jsonEncode($filters) : ''; $ret = array('success' => Configuration::updateValue('AF_SAVED_CUSTOMER_FILTERS', $filters)); break; case 'UpdateModulePosition': case 'DisableModule': case 'UnhookModule': case 'UninstallModule': case 'EnableModule': $id_module = Tools::getValue('id_module'); $hook_name = Tools::getValue('hook_name'); $id_hook = Hook::getIdByName($hook_name); $module = Module::getInstanceById($id_module); if (Validate::isLoadedObject($module)) { if ($action == 'UpdateModulePosition') { $new_position = Tools::getValue('new_position'); $way = Tools::getValue('way'); $ret['saved'] = $module->updatePosition($id_hook, $way, $new_position); } elseif ($action == 'DisableModule') { $module->disable(); $ret['saved'] = !$module->isEnabledForShopContext(); } elseif ($action == 'UnhookModule') { $ret['saved'] = $module->unregisterHook($id_hook, $this->shop_ids); } elseif ($action == 'UninstallModule') { if ($id_module != $this->id) { $ret['saved'] = $module->uninstall(); } } elseif ($action == 'EnableModule') { $ret['saved'] = $module->enable(); } } break; case 'IndexProduct': $ret['indexed'] = $this->indexProduct(Tools::getValue('id_product')); break; case 'addOverride': case 'removeOverride': $override = Tools::getValue('override'); $ret['processed'] = $this->processOverride($action, $override); break; case 'clearCache': $this->cache('clear', ''); $ret['notice'] = utf8_encode($this->l('Cleared')); break; case 'getCachingInfo': $ret['info'] = array(); foreach (array_keys($this->getSettingsFields('caching', false)) as $name) { $ret['info'][$name] = utf8_encode($this->cache('info', $name)); } break; } exit(Tools::jsonEncode($ret)); } public function getAvailableFiltersSorted() { $filters = $this->getAvailableFilters(); $sorted = array(); foreach ($filters as $key => $f) { if ($key == 'c') { $f['name'] = $this->l('Subcategories of current page'); } $sorted[$f['prefix']][$key] = $f; } return $sorted; } public function processOverride($action, $override, $throw_error = true) { $processed = false; switch ($action) { case 'addOverride': case 'removeOverride': $file_path = $this->custom_overrides_dir.$override; $tmp_path = $this->local_path.'override/'.$override; if (file_exists($file_path)) { if (is_writable(dirname($tmp_path))) { try { // temporarily copy file to /override/ folder for processing it natively Tools::copy($file_path, $tmp_path); $class_name = basename($override, '.php'); $processed = $this->$action($class_name); unlink($tmp_path); } catch (Exception $e) { unlink($tmp_path); if ($throw_error) { $this->throwError($e->getMessage()); } } } elseif ($throw_error) { $dir_name = str_replace(_PS_ROOT_DIR_, '', dirname($tmp_path)).'/'; $txt = $this->l('Make sure the following directory is writable: %s'); $this->throwError(sprintf($txt, $dir_name)); } } break; } return $processed; } public function getImplodedContextShopIds() { return implode(', ', $this->shop_ids); } public function getContent() { if (Tools::isSubmit('ajax') && $action = Tools::getValue('action')) { $this->defineSettings(); if (!empty($this->sp) && Tools::getValue('sp')) { $this->sp->ajaxAction($action); } elseif (Tools::getValue('mergedValues')) { $this->mergedValues()->ajaxAction($action); } else { $this->ajaxAction($action); } } $this->bo()->setWarningsIfRequired(); $this->indexationColumns('adjust', 0, 86400); // just to be sure $available_customer_filters = $this->getAvailableFilters(false); $to_unset = array_merge(array('p', 'w'), array_keys($this->getSpecialFilters())); foreach ($to_unset as $k) { unset($available_customer_filters[$k]); } $settings = array(); foreach ($this->getSettingsKeys() as $type) { $settings[$type] = $this->getSettingsFields($type); } $indexation_required = false; $indexation_info = $this->indexationInfo('count', $this->shop_ids, true); foreach ($indexation_info as $id_shop => $data) { $indexation_info[$id_shop]['shop_name'] = $this->db->getValue(' SELECT name FROM '._DB_PREFIX_.'shop WHERE id_shop = '.(int)$id_shop.' '); if ($data['missing']) { $indexation_required = true; } } $smarty_variables = array( 'indexation_data' => $indexation_info, 'indexation_required' => $indexation_required, 'grouped_templates' => $this->getGroupedTemplates(), 'available_hooks' => $this->getAvailableHooks(), 'settings' => $settings, 'available_customer_filters' => $available_customer_filters, 'saved_customer_filters' => $this->getAdjustableCustomerFilters(), 'overrides_data' => $this->getOverridesData(), 'this' => $this, 'changelog_link' => $this->_path.'Readme.md?v='.$this->version, 'documentation_link' => $this->_path.'readme_en.pdf?v='.$this->version, 'contact_us_link' => 'https://addons.prestashop.com/en/write-to-developper?id_product=18575', 'other_modules_link' => 'https://addons.prestashop.com/en/2_community-developer?contributor=64815', 'files_update_warnings' => $this->bo()->getFilesUpdadeWarnings(), ); if (!empty($this->sp)) { $smarty_variables['sp'] = $this->sp->getConfigSmartyVariables(); } $this->context->smarty->assign($smarty_variables); $this->mergedValues()->assignConfigVariables(); $html = $this->display(__FILE__, 'views/templates/admin/configure.tpl'); return $html; } public function getGroupedTemplates($available_controllers = array()) { $grouped_templates = array(); $templates_multishop = $this->db->executeS(' SELECT * FROM '._DB_PREFIX_.'af_templates WHERE id_shop IN ('.pSQL($this->getImplodedContextShopIds()).') GROUP BY id_template ORDER BY id_template DESC, id_shop = '.(int)$this->id_shop.' DESC '); $available_controllers = $available_controllers?: $this->getAvailableControllers(true); $multiple_id_controllers = $this->getControllersWithMultipleIDs(false); foreach ($available_controllers as $c => $title) { if (!isset($multiple_id_controllers[$c])) { $c = 'other'; $title = $this->l('other pages'); } $grouped_templates[$c] = array( 'title' => sprintf($this->l('Templates for %s'), Tools::strtolower($title)), 'first' => !count($grouped_templates), 'additional_actions' => $c != 'other', 'templates' => array(), ); } foreach ($templates_multishop as $t) { $c = $t['template_controller']; if (isset($available_controllers[$c])) { $group = isset($multiple_id_controllers[$c]) ? $c : 'other'; if (isset($grouped_templates[$group])) { $grouped_templates[$group]['templates'][$t['id_template']] = $t; } } } foreach ($grouped_templates as $g => $t) { if ($t['templates']) { $min_id = min(array_keys($t['templates'])); $grouped_templates[$g]['templates'][$min_id]['first_in_group'] = 1; } } return $grouped_templates; } public function getGroupOptions($type, $id_lang) { $group_options = array(); switch ($type) { case 'attribute': foreach (AttributeGroup::getAttributesGroups($id_lang) as $g) { $name = $g['public_name'].($g['name'] != $g['public_name'] ? ' ('.$g['name'].')' : ''); $group_options[$g['id_attribute_group']] = $name; } break; case 'feature': foreach (Feature::getFeatures($id_lang) as $f) { $group_options[$f['id_feature']] = $f['name']; } break; } return $group_options; } public function getOverridesData() { $data_fetching_txt = $this->l('Required to avoid double data fetching on %s'); $notes = array( 'Product' => sprintf($data_fetching_txt, $this->l('prices drop and new products pages')), 'ProductSale' => sprintf($data_fetching_txt, $this->l('bestsellers page')), 'Search' => sprintf($data_fetching_txt, $this->l('search results page')), 'Manufacturer' => sprintf($data_fetching_txt, $this->l('manufacturer pages')), 'Supplier' => sprintf($data_fetching_txt, $this->l('supplier pages')), 'AdminProductsController' => $this->l('Required for proper indexation on saving the product'), 'FrontController' => $this->l('Required for applying custom sorting and number of products per page'), ); if ($this->is_17) { $notes['Product'] = $this->l('Required for improved performance on Search results page'); } $autoload = PrestaShopAutoload::getInstance(); $overrides = array(); foreach (Tools::scandir($this->custom_overrides_dir, 'php', '', true) as $file) { $class_name = basename($file, '.php'); if ($class_name != 'index' && (!$this->is_17 || $class_name == 'Product')) { $path = $autoload->getClassPath($class_name.'Core'); $overrides[$class_name] = array( 'note' => isset($notes[$class_name]) ? $notes[$class_name] : '', 'path' => $path, 'installed' => $this->isOverrideInstalled($path), ); } } return $overrides; } public function isOverrideInstalled($path) { $shop_override_path = _PS_OVERRIDE_DIR_.$path; $module_override_path = $this->custom_overrides_dir.$path; $methods_to_override = $already_overriden = array(); if (file_exists($module_override_path)) { $lines = file($module_override_path); foreach ($lines as $line) { // note: this check is available only for public functions if (Tools::substr(trim($line), 0, 6) == 'public') { $key = trim(current(explode('(', $line))); $methods_to_override[$key] = 0; } } } $name_length = Tools::strlen($this->name); if (file_exists($shop_override_path)) { $lines = file($shop_override_path); foreach ($lines as $i => $line) { if (Tools::substr(trim($line), 0, 6) == 'public') { $key = trim(current(explode('(', $line))); if (isset($methods_to_override[$key])) { unset($methods_to_override[$key]); // if there is no comment about installed override if (!isset($lines[$i - 4]) || Tools::substr(trim($lines[$i - 4]), - $name_length) !== $this->name) { $key = explode('function ', $key); if (isset($key[1])) { $already_overriden[] = $key[1].'()'; } } } } } } $installed = (bool)!$methods_to_override; if ($already_overriden) { $installed = implode(', ', $already_overriden); } return $installed; } public function getSettingsFields($type, $fill_values = true, $id_shop = false) { $fields = array(); switch ($type) { case 'general': $fields = $this->getGeneralSettingsFields(); break; case 'caching': $fields = $this->getCachingSettingsFields(); break; case 'indexation': $fields = $this->getIndexationSettingsFields(); if ($fill_values) { $this->markBlockedIndexationFields($fields); } break; case 'seopage': if (!empty($this->sp)) { $fields = $this->sp->getSettingsFields(); } break; default: $fields = $this->getSelectorSettingsFields($type); break; } if ($fill_values) { if (!$id_shop && isset($this->settings[$type])) { $saved_settings = $this->settings[$type]; } else { $saved_settings = $this->db->getValue(' SELECT value FROM '._DB_PREFIX_.'af_settings WHERE type = \''.pSQL($type).'\' AND id_shop = '.(int)$id_shop.' '); $saved_settings = $saved_settings ? Tools::jsonDecode($saved_settings, true) : array(); } foreach ($fields as $name => &$f) { if (isset($saved_settings[$name]) && empty($f['blocked'])) { $f['value'] = $saved_settings[$name]; } } } return $fields; } public function markBlockedIndexationFields(&$fields) { $suffixes_count = $this->indexationColumns('getAvailableSuffixesCount', 0, 3600); $check_values = array('p_c' => 'currency', 'p_g' => 'group', 'n' => 'lang', 't' => 'lang'); foreach ($check_values as $key => $type) { if (isset($suffixes_count[$type]) && $suffixes_count[$type] > $this->i['max_column_suffixes']) { $fields[$key]['blocked'] = 1; $fields[$key]['value'] = 0; } } } public function getGeneralSettingsFields() { $fields = array( 'layout' => array( 'display_name' => $this->l('Display type'), 'type' => 'select', 'value' => 'vertical', 'options' => $this->getOptions('layout'), ), 'count_data' => array( 'display_name' => $this->l('Show numbers of matches'), 'value' => 1, 'type' => 'switcher', ), 'hide_zero_matches' => array( 'display_name' => $this->l('Hide options with zero matches'), 'value' => 1, 'type' => 'switcher', ), 'dim_zero_matches' => array( 'display_name' => $this->l('Dim options with zero matches'), 'value' => 1, 'type' => 'switcher', ), 'sf_position' => array( 'display_name' => $this->l('Display selected filters'), 'value' => 0, 'type' => 'select', 'options' => array( 0 => $this->l('Above filter block'), 1 => $this->l('Above product list'), ), ), 'include_group' => array( 'display_name' => $this->l('Show group name in selected filters'), 'value' => 0, 'type' => 'switcher', ), 'compact' => array( 'display_name' => $this->l('Screen width for compact layout'), 'tooltip' => $this->l('Use compact layout if browser width is equal to this value or less'), 'type' => 'text', 'input_suffix' => 'px', 'value' => 767, 'validate' => 'isInt', 'related_options' => '.compact-option', 'subtitle' => $this->l('Responsive compact view'), ), 'compact_offset' => array( 'display_name' => $this->l('Compact panel offset direction'), 'type' => 'select', 'value' => 2, 'options' => array(1 => $this->l('Left'), 2 => $this->l('Right')), 'validate' => 'isInt', 'class' => 'compact-option hidden-on-0', ), 'compact_btn' => array( 'display_name' => $this->l('Compact button'), 'type' => 'select', 'value' => 1, 'options' => array( 1 => $this->l('Text + Filter icon'), 2 => $this->l('Only text'), 3 => $this->l('Only filter icon'), ), 'validate' => 'isInt', 'class' => 'compact-option hidden-on-0', ), 'npp' => array( 'display_name' => $this->l('Number of products per page'), 'value' => Configuration::get('PS_PRODUCTS_PER_PAGE'), 'type' => 'text', 'validate' => 'isInt', 'subtitle' => $this->l('Product list'), ), 'default_order_by' => array( 'display_name' => $this->l('Default order by'), 'value' => Tools::getProductsOrder('by'), 'type' => 'select', 'options' => $this->getOptions('orderby'), 'related_options' => '.order-by-option', ), 'default_order_way' => array( 'display_name' => $this->l('Default order way'), 'value' => Tools::getProductsOrder('way'), 'type' => 'select', 'options' => $this->getOptions('orderway'), 'class' => 'order-by-option hidden-on-random', ), 'random_upd' => array( 'display_name' => $this->l('Update random order'), 'value' => 1, 'type' => 'select', 'options' => array( 0 => $this->l('On every page load'), 1 => $this->l('Every hour'), 2 => $this->l('Every day'), 3 => $this->l('Every week'), ), 'class' => 'order-by-option visible-on-random', ), 'reload_action' => array( 'display_name' => $this->l('Update product list'), 'value' => 1, 'type' => 'select', 'options' => array( 1 => $this->l('Instantly'), 2 => $this->l('On button click'), ), ), 'p_type' => array( 'display_name' => $this->l('Pagination type'), 'value' => 1, 'type' => 'select', 'options' => array( 1 => $this->l('Regular'), 2 => $this->l('Load more button'), 3 => $this->l('Infinite scroll'), ), ), 'autoscroll' => array( 'display_name' => $this->l('Autoscroll to top after filtration'), 'tooltip' => $this->l('After applying filters, switching pages, changing sorting, etc...'), 'value' => 0, 'type' => 'switcher', ), 'oos_behaviour' => array( 'display_name' => $this->l('Out of stock behaviour'), 'value' => 0, 'type' => 'select', 'options' => array( 0 => $this->l('Do nothing'), 1 => $this->l('Move out of stock products to the end of the list'), 2 => $this->l('Exclude products that are out of stock except those allowed for ordering'), 3 => $this->l('Exclude all products that are out of stock'), ), 'subtitle' => $this->l('Stock and combinations'), ), 'combinations_stock' => array( 'display_name' => $this->l('Count stock for combinations'), 'tooltip' => $this->l('Count stock basing on selected attributes'), 'value' => 0, 'type' => 'switcher', ), 'combinations_existence' => array( 'display_name' => $this->l('Check combinations existence'), 'tooltip' => $this->l('Exclude products that do not have combinations with selected attributes'), 'value' => 0, 'type' => 'switcher', ), 'combination_results' => array( 'display_name' => $this->l('Display combination prices/images'), 'tooltip' => $this->l('Display prices/images basing on selected attributes'), 'value' => 0, 'type' => 'switcher', ), 'url_filters' => array( 'display_name' => $this->l('Include filter parameters in URL'), 'value' => 1, 'type' => 'switcher', 'subtitle' => $this->l('Dynamic URL params'), ), 'url_sorting' => array( 'display_name' => $this->l('Include sorting parameter in URL'), 'value' => 1, 'type' => 'switcher', ), 'url_page' => array( 'display_name' => $this->l('Include page number in URL'), 'value' => 1, 'type' => 'switcher', ), 'dec_sep' => array( 'display_name' => $this->l('Decimal separator'), 'type' => 'text', 'value' => '.', 'subtitle' => $this->l('Number format (Used in numeric sliders and sorting by numbers)'), ), 'tho_sep' => array( 'display_name' => $this->l('Thousand separator'), 'type' => 'text', 'value' => '', ), ) + $this->mergedValues()->getGeneralSettingsFields(); foreach (array('combinations_stock', 'combinations_existence', 'combination_results') as $name) { $fields[$name]['warning'] = $this->l('May increase filtering time'); } if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) { $warning_txt = $this->l('Not compatible with advanced stock management'); foreach (array('oos_behaviour', 'combinations_stock') as $name) { $fields[$name]['warning'] = $warning_txt; } } return $fields; } public function getCachingSettingsFields() { $fields = array( 'c_list' => array('display_name' => $this->l('Category options'), 'value' => 0), 'a_list' => array('display_name' => $this->l('Attribute options'), 'value' => 1), 'f_list' => array('display_name' => $this->l('Feature options'), 'value' => 1), 'comb_data' => array('display_name' => $this->l('Combinations availability'), 'value' => 1), ); foreach ($fields as $name => &$f) { $f += array('type' => 'switcher', 'class' => $name); } return $fields; } public function getSelectorSettingsFields($type) { $fields = array(); if ($selectors = $this->getSelectors($type)) { $input_prefix = '.'; $validate = 'isImageTypeName'; // a-zA-Z0-9_ - if ($type == 'themeid') { $input_prefix = '#'; $validate = 'isHookName'; // a-zA-Z0-9_- no spaces } elseif ($type == 'iconclass') { $fields['load_font'] = array( 'display_name' => $this->l('Load icon font'), 'tooltip' => $this->l('Use this option if your theme does not support icon-xx classes'), 'value' => $this->is_17 ? 1 : 0, 'type' => 'switcher', ); } foreach ($selectors as $name => $display_name) { $fields[$name] = array( 'display_name' => $display_name, 'value' => $name, 'type' => 'text', 'input_prefix' => $input_prefix, 'validate' => $validate, 'required' => 1, ); } } return $fields; } public function getIndexationSettingsFields() { $fields = array( 'auto' => array( 'display_name' => $this->l('Re-index products on saving programmatically'), 'info' => $this->l('After calling hook ActionProductUpdate or ActionProductAdd during bulk import'), 'type' => 'switcher', 'value' => 1, ), 'subcat_products' => array( 'display_name' => $this->l('Index associations for all products from subcategories'), 'info' => $this->l('Even if they are not directly associated to current category'), 'value' => 1, 'type' => 'switcher', ), 'p' => array( 'display_name' => $this->l('Include price data in indexation'), 'info' => $this->l('Required if you want to filter/sort products by price'), 'type' => 'switcher', 'value' => 1, 'related_options' => '.indexation-price-option', ), 'p_c' => array( 'display_name' => $this->l('Index prices for different currencies'), 'info' => $this->l('Required if you have specific price rules only for selected currencies'), 'type' => 'switcher', 'value' => 0, 'class' => 'indexation-price-option hidden-on-0', ), 'p_g' => array( 'display_name' => $this->l('Index prices for different customer groups'), 'info' => $this->l('Required if you have specific price rules only for selected customer groups'), 'type' => 'switcher', 'value' => 0, 'class' => 'indexation-price-option hidden-on-0', ), 't' => array( 'display_name' => $this->l('Include tags data in indexation'), 'info' => $this->l('Required if you want to filter products by tags'), 'type' => 'switcher', 'value' => 0, ), 'n' => array( 'display_name' => $this->l('Include product name in indexation'), 'info' => $this->l('Can make sorting by name faster on very large catalogues (30 000+ products)'), 'type' => 'switcher', 'value' => 0, ), ); return $fields; } public function getSelectors($type) { $selectors = array(); switch ($type) { case 'iconclass': $selectors = array( 'icon-filter' => $this->l('Filter icon'), 'u-times' => $this->l('Remove one filter icon'), 'icon-eraser' => $this->l('Remove all filters icon'), 'icon-lock' => $this->l('Locked filters icon'), 'icon-unlock-alt' => $this->l('Unlocked filters icon'), // 'icon-refresh icon-spin' => $this->l('Loading indicator icon'), // not used 'icon-minus' => $this->l('Minus icon'), 'icon-plus' => $this->l('Plus icon'), 'icon-check' => $this->l('Checked icon'), 'icon-save' => $this->l('Save icon'), ); break; case 'themeclass': $selectors = array( 'js-product-miniature' => $this->l('Product list item'), 'pagination' => $this->l('Pagination container'), ); if (!$this->is_17) { $selectors = array( 'ajax_block_product' => $selectors['js-product-miniature'], 'pagination' => $selectors['pagination'], 'product-count' => $this->l('Product count countainer'), 'heading-counter' => $this->l('Total matches container'), ); } break; case 'themeid': $selectors = array('main' => $this->l('Main column container')); if (!$this->is_17) { $selectors = array( 'center_column' => $selectors['main'], 'pagination' => $this->l('Top pagination wrapper'), 'pagination_bottom' => $this->l('Bottom pagination wrapper'), ); } break; } return $selectors; } public function saveSettings($type, $values = array(), $shop_ids = null, $throw_error = false, $fields = null) { if ($fields = $fields ?: $this->getSettingsFields($type, false)) { $settings_to_save = $settings_rows = array(); $this->addRecommendedValuesIfRequired($type, $values); $errors = $this->validateSettings($values, $fields); // values that didn't pass validation are updated if ($errors && $throw_error) { $this->throwError($errors); } foreach ($fields as $name => $field) { $settings_to_save[$name] = isset($values[$name]) ? $values[$name] : $field['value']; } $encoded_settings = Tools::jsonEncode($settings_to_save); $shop_ids = $shop_ids ?: $this->shop_ids; if ($type == 'indexation') { $shop_ids = $this->all_shop_ids; } foreach ($shop_ids as $id_shop) { $settings_rows[] = '('.(int)$id_shop.', \''.pSQL($type).'\', \''.pSQL($encoded_settings).'\')'; } if ($settings_rows && $settings_to_save && $saved = $this->db->execute(' REPLACE INTO '._DB_PREFIX_.'af_settings VALUES '.implode(', ', $settings_rows).' ')) { $this->settings[$type] = $settings_to_save; if ($type == 'indexation' && empty($this->installation_process)) { $this->cache('clear', 'indexationColumns'); $this->indexationColumns('adjust'); } return $saved; } } } public function addRecommendedValuesIfRequired($type, &$values) { switch ($type) { case 'indexation': $check_values = array( 'p_c' => array(array('id_currency', 'specific_price')), 'p_g' => array(array('id_group', 'specific_price'), array('reduction', 'group')), 't' => array(array('id_tag', 'product_tag')), ); foreach ($check_values as $key => $data) { if (!isset($values[$key])) { $value = true; foreach ($data as $d) { $value &= $this->db->getValue('SELECT '.pSQL($d[0]).' FROM '._DB_PREFIX_.pSQL($d[1])); } $values[$key] = (int)$value; } } break; } } public function defineSettings() { if (!isset($this->settings)) { $this->settings = $this->getSavedSettings(); require_once($this->local_path.'classes/ExtendedTools.php'); if (Module::isEnabled('af_seopages')) { $this->sp = Module::getInstanceByName('af_seopages'); } } } public function getSavedSettings($id_shop = false) { $settings = array(); $id_shop = $id_shop ?: $this->context->shop->id; $data = $this->db->executeS('SELECT * FROM '._DB_PREFIX_.'af_settings WHERE id_shop = '.(int)$id_shop); foreach ($data as $row) { $settings[$row['type']] = Tools::jsonDecode($row['value'], true); } return $settings; } public function getSettingsKeys() { return array('general', 'iconclass', 'themeclass', 'themeid', 'caching', 'indexation', 'seopage'); } public function getLayoutClasses() { return $this->settings['iconclass'] + $this->settings['themeclass']; } public function getProductIDsForIndexation($id_shop) { return array_column($this->db->executeS(' SELECT p.id_product AS id FROM '._DB_PREFIX_.'product p INNER JOIN '._DB_PREFIX_.'product_shop ps ON ps.id_product = p.id_product AND ps.id_shop = '.(int)$id_shop.' AND ps.id_product > 0 AND ps.active = 1 AND ps.visibility <> "none" '), 'id', 'id'); } public function getCurrentHook() { $hooks = array_flip($this->getAvailableHooks()); return isset($hooks[1]) ? $hooks[1] : ''; } public function getAvailableHooks() { $methods = get_class_methods(__CLASS__); $methods_to_exclude = array( 'hookDisplayBackOfficeHeader', 'hookDisplayHeader', 'hookDisplayCustomerAccount', 'hookDisplayHome' ); $available_hooks = array(); $hook_found = false; foreach ($methods as $m) { if (Tools::substr($m, 0, 11) === 'hookDisplay' && !in_array($m, $methods_to_exclude)) { $hook_name = str_replace('hookDisplay', 'display', $m); $selected = 0; if (!$hook_found && $this->isRegisteredInHook($hook_name)) { $hook_found = $selected = 1; } $available_hooks[$hook_name] = $selected; } } ksort($available_hooks); return $available_hooks; } /* * this method is overriden in order to take current shop context in consideration */ public function isRegisteredInHook($hook_name) { return $this->db->getValue(' SELECT COUNT(*) FROM '._DB_PREFIX_.'hook_module hm LEFT JOIN '._DB_PREFIX_.'hook h ON (h.id_hook = hm.id_hook) WHERE h.name = \''.pSQL($hook_name).'\' AND hm.id_module = '.(int)$this->id.' AND id_shop IN ('.pSQL($this->getImplodedContextShopIds()).') '); } public function verifyMethod($method_name) { if (!method_exists($this, $method_name)) { $this->throwError($this->l('Unknown method:').' '.$method_name); } } public function callTemplateForm($id_template, $full = true) { $available_controllers = $this->getAvailableControllers(true); if (!$id_template) { $controller = Tools::getValue('template_controller'); $name = isset($available_controllers[$controller]) ? $available_controllers[$controller] : $controller; $template_name = sprintf($this->l('Template for %s'), $name).' - '.date('Y-m-d H:i:s'); $id_template = $this->saveTemplate($id_template, $controller, $template_name); } $template_data = $this->db->getRow(' SELECT * FROM '._DB_PREFIX_.'af_templates WHERE id_template = '.(int)$id_template.' ORDER BY id_shop = '.(int)$this->id_shop.' DESC '); $template_data['first_in_group'] = $id_template == $this->db->getValue(' SELECT id_template FROM '._DB_PREFIX_.'af_templates WHERE template_controller = \''.pSQL($template_data['template_controller']).'\' ORDER BY id_template ASC '); $this->context->smarty->assign(array( 'controller_options' => $available_controllers, 't' => $template_data, 'is_17' => $this->is_17, )); if ($full && $template_data) { $template_filters = Tools::jsonDecode($template_data['template_filters'], true); $template_filters_lang = $this->db->executeS(' SELECT id_lang, data FROM '._DB_PREFIX_.'af_templates_lang WHERE id_template = '.(int)$template_data['id_template'].' AND id_shop = '.(int)$template_data['id_shop'].' '); foreach ($template_filters_lang as $multilang_data) { $id_lang = $multilang_data['id_lang']; $data = Tools::jsonDecode($multilang_data['data'], true); foreach ($data as $filter_key => $values) { if (isset($template_filters[$filter_key])) { foreach ($values as $name => $value) { $template_filters[$filter_key][$name][$id_lang] = $value; } } } } foreach ($template_filters as $key => $saved_values) { $template_filters[$key] = $this->getFilterData($key, $saved_values); } $controller = $template_data['template_controller']; $controller_ids = $this->getTemplateControllerIds($id_template, $controller, $template_data['id_shop']); $general_settings_fields = $this->getSettingsFields('general', true); if ($controller == 'search') { $general_settings_fields['default_order_by']['options']['position'] = $this->l('Relevance'); } $this->context->smarty->assign(array( 'template_controller_settings' => $this->getControllerSettingsFields($controller, $controller_ids), 'template_filters' => $template_filters, 'additional_settings' => $template_data['additional_settings'] ? Tools::jsonDecode($template_data['additional_settings'], true) : array(), 'general_settings_fields' => $general_settings_fields, 'additional_actions' => in_array($controller, $this->getControllersWithMultipleIDs(true)), )); } $this->assignLanguageVariables(); $ret = array( 'form_html' => utf8_encode($this->display(__FILE__, 'views/templates/admin/template-form.tpl')), 'id_template' => $id_template, ); return $ret; } public function getTemplateControllerIds($id_template, $controller, $id_shop) { $ids = array(); if (in_array($controller, $this->getControllersWithMultipleIDs())) { $ids = array_column($this->db->executeS(' SELECT DISTINCT id_'.pSQL($controller).' AS id FROM '._DB_PREFIX_.'af_'.pSQL($controller).'_templates WHERE id_template = '.(int)$id_template.' AND id_shop = '.(int)$id_shop.' '), 'id', 'id'); } return $ids; } public function getDefaultAdditionalSettings($controller) { $additional_settings = array(); if ($specific_sorting = $this->getSpecificSorting($controller)) { foreach ($specific_sorting as $name => $value) { $additional_settings['default_order_'.$name] = $value; } } return $additional_settings; } public function assignLanguageVariables() { $this->context->smarty->assign(array( 'available_languages' => $this->getAvailableLanguages(), 'id_lang_current' => $this->context->language->id, )); } public function getAvailableLanguages($only_ids = false, $only_active = false) { $available_languages = array(); foreach (Language::getLanguages($only_active) as $lang) { $available_languages[$lang['id_lang']] = $lang['iso_code']; } return $only_ids ? array_keys($available_languages) : $available_languages; } public function getControllerSettingsFields($controller, $controller_ids) { $fields = array(); $multiple_id_controllers = $this->getControllersWithMultipleIDs(false); if (isset($multiple_id_controllers[$controller])) { $field = array( 'display_name' => $multiple_id_controllers[$controller], 'value' => $controller_ids, 'type' => 'multiple_options', 'options' => $this->getOptions($controller), ); if ($controller == 'category') { $field['id_parent'] = Configuration::get('PS_ROOT_CATEGORY'); } $fields['controller_ids'] = $field; } return $fields; } public function getOptions($type) { $options = array(); switch ($type) { case 'manufacturer': case 'supplier': $items = $this->db->executeS('SELECT * FROM '._DB_PREFIX_.pSQL($type)); foreach ($items as $row) { $options[$row['id_'.$type]] = $row['name']; } break; case 'category': $categories = $this->db->executeS(' SELECT * FROM '._DB_PREFIX_.'category c '.Shop::addSqlAssociation('category', 'c').' LEFT JOIN '._DB_PREFIX_.'category_lang cl ON c.id_category = cl.id_category WHERE id_lang = '.(int)$this->id_lang.' '); foreach ($categories as $cat) { $options[$cat['id_parent']][$cat['id_category']] = $cat['name']; } break; case 'seopage': if (!empty($this->sp)) { $options = $this->sp->getPageOptions(); } break; case 'layout': $options = array( 'vertical' => $this->l('Vertical'), 'horizontal' => $this->l('Horizontal'), ); break; case 'orderby': $options = array( 'position' => $this->l('Position'), 'date_add' => $this->l('Date added'), 'name' => $this->l('Name'), 'reference' => $this->l('Reference'), 'manufacturer_name' => $this->is_17 ? $this->l('Brand name') : $this->l('Manufacturer name'), 'price' => $this->l('Price'), 'quantity' => $this->l('Quantity'), 'sales' => $this->l('Sales'), 'random' => $this->l('Random'), ); break; case 'orderway': $options = array( 'asc' => $this->l('Ascending'), 'desc' => $this->l('Descending') ); break; } return $options; } public function getFilterData($key, $saved_values = array()) { if (!isset($this->available_filters)) { $this->available_filters = $this->getAvailableFilters(); } if (isset($this->available_filters[$key])) { $filter_data = $this->available_filters[$key]; $filter_data['key'] = $key; if ($key == 'c') { $filter_data['prefix'] = $this->l('Subcategories of current page'); } $filter_data['name_original'] = $filter_data['name']; $filter_data['settings'] = $this->getFilterFields($filter_data, $saved_values); $custom_name = $filter_data['settings']['custom_name']['value']; if (is_array($custom_name) && !empty($custom_name[$this->context->language->id])) { $filter_data['name'] = $custom_name[$this->context->language->id]; } } else { $filter_data = array(); } return $filter_data; } public function getFilterFields($filter_data, $saved_values = array()) { $fields = array( 'custom_name' => array( 'display_name' => $this->l('Custom name'), 'value' => '', 'type' => 'text', 'multilang' => 1, 'class' => 'custom-name', ), 'quick_search' => array( 'display_name' => $this->l('Quick search for options'), 'tooltip' => $this->l('If there are more than 10 options'), 'value' => 0, 'type' => 'switcher', 'class' => 'type-exc not-for-3 not-for-4', ), 'slider_prefix' => array( 'display_name' => $this->l('Slider prefix'), 'value' => '', 'type' => 'text', 'multilang' => 1, 'class' => 'type-exc not-for-1 not-for-2 not-for-3 not-for-5', ), 'slider_suffix' => array( 'display_name' => $this->l('Slider suffix'), 'value' => '', 'type' => 'text', 'multilang' => 1, 'class' => 'type-exc not-for-1 not-for-2 not-for-3 not-for-5', ), 'slider_step' => array( 'display_name' => $this->l('Slider step'), 'value' => 1, 'type' => 'text', 'class' => 'type-exc not-for-1 not-for-2 not-for-3 not-for-5', // 'quick' => 1, ), 'range_step' => array( 'display_name' => $this->l('Range step'), 'value' => 100, 'type' => 'text', 'class' => 'type-exc not-for-4', 'quick' => 1, ), 'foldered' => array( 'display_name' => $this->l('Foldered structure'), 'value' => 1, 'type' => 'switcher', 'class' => 'type-exc not-for-3', ), 'nesting_lvl' => array( 'display_name' => $this->l('Nesting level'), 'value' => 0, 'type' => 'select', 'options' => array(0 => $this->l('All'), 1 => 1, 2 => 2), 'input_class' => 'nesting-lvl', ), 'color_display' => array( 'display_name' => $this->l('Color display'), 'value' => 1, 'type' => 'select', 'options' => array( 0 => $this->l('None'), 1 => $this->l('Inline color boxes'), 2 => $this->l('Color boxes with names'), ), 'class' => 'type-exc not-for-4 not-for-3 not-for-5', ), 'visible_items' => array( 'display_name' => $this->l('Max. visible items'), 'value' => 15, 'type' => 'text', 'class' => 'type-exc not-for-4 not-for-3', ), 'sort_by' => array( 'display_name' => $this->l('Sort by'), 'value' => 0, 'type' => 'select', 'options' => array( '0' => $this->l('Name'), 'first_num' => $this->l('First number in name'), 'numbers_in_name' => $this->l('All numbers in name'), 'id' => $this->l('ID'), 'position' => $this->l('Position'), ), 'class' => 'type-exc not-for-4', 'input_class' => 'sort-by', 'quick' => 1, ), 'type' => array( 'display_name' => $this->l('Type'), 'value' => 1, 'type' => 'select', 'options' => array( 1 => $this->l('Checkbox'), 2 => $this->l('Radio button'), 3 => $this->l('Select'), 4 => $this->l('Slider'), 5 => $this->l('Text box'), ), 'quick' => 1, 'input_class' => 'f-type', ), 'minimized' => array( 'display_name' => $this->l('Minimized'), 'value' => 0, 'type' => 'checkbox', 'quick' => 1, ), ); $filter_data['first_char'] = Tools::substr($filter_data['key'], 0, 1); if (!isset($saved_values['slider_prefix']) && !isset($saved_values['slider_suffix'])) { if ($slider_extensions = $this->detectSliderExtensions($filter_data['key'])) { $fields['slider_prefix']['value'] = $slider_extensions['prefix']; $fields['slider_suffix']['value'] = $slider_extensions['suffix']; } } if (!isset($saved_values['visible_items']) && !in_array($filter_data['first_char'], array('a', 'f', 'm', 's', 't'))) { $fields['visible_items']['value'] = ''; } if ($this->settings['general']['layout'] == 'horizontal') { $fields['visible_items']['class'] = 'hidden'; } $this->removeSpecificOptions($filter_data, $fields); foreach ($fields as $name => &$f) { $f['input_name'] = 'filters['.$filter_data['key'].']['.$name.']'; $f['value'] = isset($saved_values[$name]) ? $saved_values[$name] : $f['value']; if (!empty($f['multilang'])) { $f['input_name'] = str_replace('filters', 'filters[multilang]', $f['input_name']); } } return $fields; } public function detectSliderExtensions($key) { $extensions = array(); $first_char = Tools::substr($key, 0, 1); switch ($first_char) { case 'a': // possible numeric sliders case 'f': $id_group = Tools::substr($key, 1); $method = $first_char == 'a' ? 'getAttributes' : 'getFeatures'; foreach ($this->getAvailableLanguages(true) as $id_lang) { $values = $this->$method($id_lang, $id_group); foreach ($values as $i => $val) { if ($i > 3 || isset($extensions['prefix'][$id_lang])) { break; // don't spend many resourses on detecting extensions } $name = $val['name']; if ($number = $this->extractNumberFromString($name)) { $name = explode($number, $name); $possible_prefix = trim(strip_tags($name[0])); $possible_suffix = isset($name[1]) ? trim(strip_tags($name[1])) : ''; if (Tools::strlen($possible_prefix) < 4 && Tools::strlen($possible_suffix) < 4) { $extensions['prefix'][$id_lang] = $possible_prefix; $extensions['suffix'][$id_lang] = $possible_suffix; } } } } break; case 'w': // weight foreach ($this->getAvailableLanguages(true) as $id_lang) { $extensions['prefix'][$id_lang] = ''; $extensions['suffix'][$id_lang] = Configuration::get('PS_WEIGHT_UNIT'); } break; } return $extensions; } public function removeSpecificOptions($filter_data, &$fields) { $special_filters = array_keys($this->getSpecialFilters()); $slider_filters = array('p', 'w'); $numeric_slider_filters = array('a', 'f'); if ($filter_data['first_char'] != 'c') { unset($fields['foldered']); unset($fields['nesting_lvl']); } if ($filter_data['first_char'] != 'c' && $filter_data['first_char'] != 'a') { unset($fields['sort_by']['options']['position']); } if (!in_array($filter_data['key'], $slider_filters)) { unset($fields['range_step']); if (!in_array($filter_data['first_char'], $numeric_slider_filters)) { unset($fields['slider_step']); unset($fields['slider_prefix']); unset($fields['slider_suffix']); unset($fields['type']['options'][4]); } } else { if ($filter_data['key'] == 'p') { // prefix-suffux for price is based on selected currency unset($fields['slider_prefix']); unset($fields['slider_suffix']); } $fields['type']['value'] = 4; } if (in_array($filter_data['key'], $special_filters) || in_array($filter_data['key'], $slider_filters)) { unset($fields['sort_by']); } if (in_array($filter_data['key'], $special_filters)) { unset($fields['type']['options'][2]); unset($fields['type']['options'][3]); unset($fields['type']['options'][4]); unset($fields['visible_items']); $fields['quick_search']['class'] .= ' force-hidden'; } if (empty($filter_data['is_color_group'])) { unset($fields['color_display']); } } public function getParentCategories($id_lang, $id_shop) { $parents_data = $this->db->executeS(' SELECT DISTINCT(cl.id_category) AS id, cl.name AS name, c.position FROM '._DB_PREFIX_.'category c INNER JOIN '._DB_PREFIX_.'category_lang cl ON cl.id_category = c.id_parent AND cl.id_lang = '.(int)$id_lang.' AND cl.id_shop = '.(int)$id_shop.' WHERE c.level_depth > 2 ORDER BY cl.name ASC '); $parent_categories = array(); foreach ($parents_data as $data) { $parent_categories['c'.$data['id']] = $data; } return $parent_categories; } public function getSpecialFilters() { return array( 'newproducts' => $this->l('New products'), 'bestsales' => $this->l('Best sales'), 'pricesdrop' => $this->l('Prices drop'), 'in_stock' => $this->l('In stock'), ); } public function getStandardFilters() { return array( 'p' => $this->l('Price'), 'w' => $this->l('Weight'), 'm' => $this->l('Manufacturers'), 's' => $this->l('Suppliers'), 't' => $this->l('Tags'), 'q' => $this->l('Condition'), ); } public function getAvailableFilters($include_parents = true) { $id_lang = $this->context->language->id; $id_shop = $this->context->shop->id; $available_filters = array(); // cats $categories = array( 'c' => array( 'id' => 0, 'name' => $this->l('Categories'), 'position' => -1, ), ); if ($include_parents) { $categories += $this->getParentCategories($id_lang, $id_shop); } foreach ($categories as $key => $c) { $c['prefix'] = $this->l('Subcategories'); $available_filters[$key] = $c; } // atts $attribute_groups = AttributeGroup::getAttributesGroups($id_lang); $attribute_groups = $this->sortByKey($attribute_groups, 'position'); foreach ($attribute_groups as $group) { $name = $group['public_name'].($group['name'] != $group['public_name'] ? ' ('.$group['name'].')' : ''); $available_filters['a'.$group['id_attribute_group']] = array( 'id' => $group['id_attribute_group'], 'name' => $name, 'position' => $group['position'], 'prefix' => $this->l('Attribute'), 'is_color_group' => !empty($group['is_color_group']), ); } // feats $features = Feature::getFeatures($id_lang); // sorted by position initially foreach ($features as $f) { $available_filters['f'.$f['id_feature']] = array( 'id' => $f['id_feature'], 'name' => $f['name'], 'position' => $f['position'], 'prefix' => $this->l('Feature'), ); } foreach ($this->getStandardFilters() as $key => $name) { $available_filters[$key] = array( 'id' => 0, 'position' => 0, 'name' => $name, 'prefix' => $this->l('Standard parameter'), ); } foreach ($this->getSpecialFilters() as $key => $name) { $available_filters[$key] = array( 'id' => 0, 'position' => 0, 'name' => $name, 'prefix' => $this->l('Special filter'), ); } return $available_filters; } public function toggleActiveStatus($id_template, $active) { $imploded_shop_ids = $this->getImplodedContextShopIds(); if ($active) { $current_hook = $this->getCurrentHook(); $controller_name = $this->getTemplateControllerById($id_template); if (!$this->isHookAvailableOnControllerPage($current_hook, $controller_name)) { // only left/right column hooks are checked $col_txt = ($current_hook == 'displayLeftColumn') ? $this->l('Left') : $this->l('Right'); $error_txt = sprintf($this->l('%s column is not activated on selected page'), $col_txt); $error_txt .= '. '.$this->howToActivateColumnTxt(); $this->throwError($error_txt); } } $update_query = ' UPDATE '._DB_PREFIX_.'af_templates SET active = '.(int)$active.' WHERE id_template = '.(int)$id_template.' AND id_shop IN ('.pSQL($imploded_shop_ids).') '; return $this->db->execute($update_query) && $this->cache('clear', 'tpl-avl-'); } public function getTemplateControllerById($id_template) { $controller = $this->db->getValue(' SELECT template_controller FROM '._DB_PREFIX_.'af_templates WHERE id_template = '.(int)$id_template.' '); return $controller; } /* * Check if column hook is available on selected page */ public function isHookAvailableOnControllerPage($hook_name, $controller_name) { if ($controller_name == 'seopage') { return true; } $available = true; $columns = array('left', 'right'); foreach ($columns as $col) { if (Tools::strtolower($hook_name) == 'display'.$col.'column') { $page_name = $this->getPageName($controller_name); if ($this->is_17) { $layout = $this->context->shop->theme->getLayoutNameForPage($page_name); $available = $layout == 'layout-both-columns' || $layout == 'layout-'.$col.'-column' || $layout == 'layout-'.$col.'-side-column'; } else { $method_name = 'has'.Tools::ucfirst($col).'Column'; $available = $this->context->theme->$method_name($page_name); } } } return $available; } public function ajaxDuplicateTemplate() { $original_id = Tools::getValue('id_template'); if ($new_id = $this->duplciateTemplate($original_id)) { $ret = $this->callTemplateForm($new_id, false); exit(Tools::jsonEncode($ret)); } else { $this->throwError('Error'); } } public function duplciateTemplate($id_template_original) { $id_template_new = $this->getNewTemplateId(); $sql = array(); foreach ($this->getTemplateAssociatedTables() as $table_name) { $data = $this->db->executeS(' SELECT * FROM '._DB_PREFIX_.pSQL($table_name).' WHERE id_template = '.(int)$id_template_original.' '); $new_rows = array(); foreach ($data as $row) { $row['id_template'] = $id_template_new; if (isset($row['template_name'])) { $row['template_name'] .= ' '.$this->l('copy'); } $row = array_map('pSQL', $row); // note: all possible HTML is stripped here!!! $new_rows[] = '(\''.implode('\', \'', $row).'\')'; } if ($new_rows) { $sql[$table_name] = 'REPLACE INTO '._DB_PREFIX_.pSQL($table_name).' VALUES '.implode(', ', $new_rows); } } return $this->runSql($sql) ? $id_template_new : false; } public function makeSureTemplateCanBeDeleted($id_template) { $controller = $this->db->getValue(' SELECT template_controller FROM '._DB_PREFIX_.'af_templates WHERE id_template = '.(int)$id_template.' '); $other_existing_template = $this->db->getValue(' SELECT id_template FROM '._DB_PREFIX_.'af_templates WHERE template_controller = \''.pSQL($controller).'\' AND id_template <> '.(int)$id_template.' '); if (!$other_existing_template) { $this->throwError($this->l('You can not delete this template, but you can turn it off')); } } public function ajaxDeleteTemplate() { $id_template = Tools::getValue('id_template'); $this->makeSureTemplateCanBeDeleted($id_template); $result = array ( 'success' => $this->deleteTemplate($id_template), ); exit(Tools::jsonEncode($result)); } public function deleteTemplate($id_template) { $sql = array(); foreach ($this->getTemplateAssociatedTables() as $table_name) { $sql[] = 'DELETE FROM '._DB_PREFIX_.pSQL($table_name).' WHERE id_template = '.(int)$id_template.' AND id_shop IN ('.$this->getImplodedContextShopIds().')'; } return $this->runSql($sql); } public function getTemplateAssociatedTables() { $tables = array('af_templates', 'af_templates_lang'); foreach ($this->getControllersWithMultipleIDs() as $controller) { $tables[] = 'af_'.$controller.'_templates'; } return $tables; } public function ajaxSaveTemplate() { $id_template = Tools::getValue('id_template'); $template_controller = Tools::getValue('template_controller'); $template_name = Tools::getValue('template_name'); $filters_data = Tools::getValue('filters'); $controller_ids = Tools::getValue('controller_ids'); // additional settings $available_additional_settings = Tools::getValue('additional_settings'); $unlocked_additional_settings = Tools::getValue('unlocked_additional_settings', array()); $additional_settings = array(); foreach (array_keys($unlocked_additional_settings) as $name) { if (isset($available_additional_settings[$name])) { $additional_settings[$name] = $available_additional_settings[$name]; } } // validation $errors = $this->validateSettings($additional_settings, $this->getSettingsFields('general')); if (!$filters_data) { $errors['no_filters'] = $this->l('Please select at least one filter.'); } if ($template_name == '') { $errors['no_name'] = $this->l('Please add a template name'); } if ($errors) { $this->throwError($errors); } if (!$this->saveTemplate( $id_template, $template_controller, $template_name, $filters_data, $controller_ids, $additional_settings )) { $this->throwError($this->l('Template not saved')); } $ret = array ( 'hasError' => false, 'responseText' => $this->saved_txt, ); die(Tools::jsonEncode($ret)); } public function ajaxUpdateHook() { $new_hook = Tools::getValue('hook_name'); $previous_hook = $this->getCurrentHook(); $available_hooks = array_keys($this->getAvailableHooks()); $default_hook_layouts = $pages_without_this_hook = $warning = array(); foreach ($available_hooks as $hook) { $this->unregisterHook($hook, $this->shop_ids); $default_hook_layouts[$hook] = $hook != 'displayTopColumn' ? 'vertical' : 'horizontal'; } $this->registerHook($new_hook, $this->shop_ids); $this->updatePosition(Hook::getIdByName($new_hook), 0, 1); $ret = array ( 'hasError' => false, 'positions_form_html' => utf8_encode($this->renderHookPositionsForm($new_hook)), 'responseText' => $this->saved_txt, ); if ($default_hook_layouts[$new_hook] != $default_hook_layouts[$previous_hook]) { $layout = $default_hook_layouts[$new_hook]; foreach ($this->shop_ids as $id_shop) { $settings = $this->db->getValue(' SELECT value FROM '._DB_PREFIX_.'af_settings WHERE type = \'general\' AND id_shop = '.(int)$id_shop.' '); $settings = $settings ? Tools::jsonDecode($settings, true) : array(); $settings['layout'] = $layout; $this->saveSettings('general', $settings, array($id_shop)); } $warning[] = utf8_encode(sprintf($this->l('Layout type was updated to "%s"'), $layout)); } $active_templates = $this->db->executeS(' SELECT * FROM '._DB_PREFIX_.'af_templates WHERE active = 1 '); foreach ($active_templates as $t) { if (!$this->isHookAvailableOnControllerPage($new_hook, $t['template_controller'])) { $pages_without_this_hook[$t['template_controller']] = $t['template_controller']; } } if ($pages_without_this_hook) { // warning if some pages do not have selected hook $txt = sprintf($this->l('Module was succesfully hooked to %s'), $new_hook).', '; $txt .= $this->l('but this column is not activated for the following pages').':
'; ksort($pages_without_this_hook); foreach ($pages_without_this_hook as $controller_name) { $txt .= '- '.$controller_name.'
'; } $txt .= $this->howToActivateColumnTxt(); $warning[] = utf8_encode($txt); } if ($warning) { $ret['warning'] = implode('
-----
', $warning); } exit(Tools::jsonEncode($ret)); } public function howToActivateColumnTxt() { $txt = $this->l('You can activate it in %s'); if ($this->is_17) { $sprintf = $this->l('Design > Theme & Logo > Choose layouts'); } else { $sprintf = $this->l('Preferences > Themes > Advanced settings'); } return sprintf($txt, $sprintf); } public function renderHookPositionsForm($hook_name) { $this->context->smarty->assign(array( 'hook_modules' => $this->getHookModulesInfos($hook_name), 'hook_name' => $hook_name, )); return $this->display($this->local_path, 'views/templates/admin/hook-positions-form.tpl'); } public function getHookModulesInfos($hook_name) { $hook_modules = Hook::getModulesFromHook(Hook::getIdByName($hook_name)); $sorted = array(); foreach ($hook_modules as $m) { if ($instance = Module::getInstanceByName($m['name'])) { $logo_src = false; if (file_exists(_PS_MODULE_DIR_.$instance->name.'/logo.png')) { $logo_src = _MODULE_DIR_.$instance->name.'/logo.png'; } $sorted[$m['id_module']] = array( 'name' => $instance->name, 'position' => $m['m.position'], 'enabled' => $instance->isEnabledForShopContext(), 'display_name' => $instance->displayName, 'description' => $instance->description, 'logo_src' => $logo_src, ); if ($m['id_module'] == $this->id) { $sorted[$m['id_module']]['current'] = 1; } } } return $sorted; } public function getDefaultFiltersData() { $filters_data = array ( 'c' => array('type' => 1, 'nesting_lvl' => 0, 'foldered' => 1), 'p' => array('type' => 4, 'slider_step' => 1), 'm' => array('type' => 3), 'multilang' => array(), ); return $filters_data; } public function prepareMultilangData($data) { $sorted_data = array(); foreach ($data as $filter_key => $multilang_values) { foreach ($multilang_values as $name => $values) { foreach ($values as $id_lang => $value) { $sorted_data[$id_lang][$filter_key][$name] = strip_tags($value); } } } return $sorted_data; } public function validateSettings(&$values, $fields, $update_values = true) { $errors = array(); foreach ($values as $name => $value) { if (isset($fields[$name])) { if ($error = $this->validateField($value, $fields[$name], $update_values, true)) { $errors[$name] = $error; } } elseif ($update_values) { unset($values[$name]); } } return $errors; } public function validateField(&$value, $field, $update_value = true, $error_label = true) { $error = false; $validate = isset($field['validate']) ? $field['validate'] : false; if ($value === '' && !empty($field['required'])) { $error = sprintf($this->l('%s: please fill this value'), $field['display_name']); } elseif ($validate && !Validate::$validate($value)) { $error = ($error_label ? $field['display_name'].': ' : '').$this->l('incorrect value'); } if ($error && $update_value) { $value = $field['value']; } return $error; } public function saveTemplate( $id_template, $template_controller, $template_name, $filters_data = array(), $controller_ids = array(), $additional_settings = array() ) { if (!$id_template) { $id_template = $this->getNewTemplateId(); $additional_settings += $this->getDefaultAdditionalSettings($template_controller); } if (!$filters_data) { $filters_data = $this->getDefaultFiltersData(); } $multilang_data = $this->prepareMultilangData($filters_data['multilang']); unset($filters_data['multilang']); $this->validateTempalateFilters($filters_data, $template_controller); $current_hook = $this->getCurrentHook(); // active status is inserted only first time. After that it is updated using toggleActiveStatus $active = $this->isHookAvailableOnControllerPage($current_hook, $template_controller); $encoded_filters_data = Tools::jsonEncode($filters_data); $encoded_additional_settings = Tools::jsonEncode($additional_settings); $template_rows = $template_lang_rows = $controller_ids_rows = array(); foreach ($this->shop_ids as $id_shop) { $template_rows[] = '( '.(int)$id_template.', '.(int)$id_shop.', \''.pSQL($template_controller).'\', '.(int)$active.', \''.pSQL($template_name).'\', \''.pSQL($encoded_filters_data).'\', \''.pSQL($encoded_additional_settings).'\' )'; if (in_array($template_controller, $this->getControllersWithMultipleIDs())) { $controller_ids = $controller_ids ? $controller_ids : array(0); foreach ($controller_ids as $id) { $controller_ids_rows[$id.'_'.$id_shop] = '('.(int)$id.', '.(int)$id_template.', '.(int)$id_shop.')'; } } foreach ($multilang_data as $id_lang => $data) { $encoded_lang_data = Tools::jsonEncode($data); $row = (int)$id_template.', '.(int)$id_shop.', '.(int)$id_lang.', \''.pSQL($encoded_lang_data).'\''; $template_lang_rows[] = '('.$row.')'; } } $sql = array(); if ($template_rows) { $sql['template_data'] = ' INSERT INTO '._DB_PREFIX_.'af_templates VALUES '.implode(', ', $template_rows).' ON DUPLICATE KEY UPDATE template_name=VALUES(template_name), template_controller=VALUES(template_controller), template_filters=VALUES(template_filters), additional_settings=VALUES(additional_settings) '; } if ($template_lang_rows) { $sql['template_lang_data'] = ' INSERT INTO '._DB_PREFIX_.'af_templates_lang VALUES '.implode(', ', $template_lang_rows).' ON DUPLICATE KEY UPDATE data=VALUES(data) '; } if ($controller_ids_rows) { $sql['controller_ids_delete'] = ' DELETE FROM '._DB_PREFIX_.'af_'.pSQL($template_controller).'_templates WHERE id_template = '.(int)$id_template.' AND id_shop IN ('.pSQL($this->getImplodedContextShopIds()).') '; $sql['controller_ids_insert'] = ' INSERT INTO '._DB_PREFIX_.'af_'.pSQL($template_controller).'_templates VALUES '.implode(', ', $controller_ids_rows).' ON DUPLICATE KEY UPDATE id_'.pSQL($template_controller).' = VALUES(id_'.pSQL($template_controller).') '; } foreach ($sql as $s) { if (!$this->db->execute($s)) { $this->errors[] = $this->l('Template not saved'); } } if ($this->errors) { return false; } return $id_template; } public function validateTempalateFilters(&$filters_data, $template_controller) { if ($template_controller == 'manufacturer' && isset($filters_data['m'])) { unset($filters_data['m']); } if ($template_controller == 'supplier' && isset($filters_data['s'])) { unset($filters_data['s']); } foreach ($filters_data as $key => &$f) { if (isset($f['range_step'])) { $f['range_step'] = trim(preg_replace('/[^0-9,minmax-]/', '', $f['range_step']), ',') ?: 100; } if (isset($f['slider_step'])) { $step = (float)str_replace(',', '.', $f['slider_step']); $f['slider_step'] = $this->removeScientificNotation($step) ?: 1; } if (in_array($f['type'], array(3, 4))) { $f['quick_search'] = 0; // no quick_search for sliders and selects } if (isset($f['visible_items'])) { $f['visible_items'] = (int)$f['visible_items'] ?: ''; } } } public function parseStr($str) { $params = array(); parse_str(str_replace('&', '&', $str), $params); return $params; } /** * af_templates table has a composite KEY that cannot be autoincremented **/ public function getNewTemplateId() { $max_id = $this->db->getValue('SELECT MAX(id_template) FROM '._DB_PREFIX_.'af_templates'); return (int)$max_id + 1; } public function addJS($file_name, $custom_path = '') { $path = ($custom_path ? $custom_path : 'modules/'.$this->name.'/views/js/').$file_name; if ($this->is_17) { // priority should be more than 90 in order to be loaded after jqueryUI $params = array('server' => $custom_path ? 'remote' : 'local', 'priority' => 100); $this->context->controller->registerJavascript(sha1($path), $path, $params); } else { $path = $custom_path ? $path : __PS_BASE_URI__.$path; $this->context->controller->addJS($path); } } public function addCSS($file_name, $custom_path = '', $media = 'all') { $path = ($custom_path ? $custom_path : 'modules/'.$this->name.'/views/css/').$file_name; if ($this->is_17) { $params = array('media' => $media, 'server' => $custom_path ? 'remote' : 'local'); $this->context->controller->registerStylesheet(sha1($path), $path, $params); } else { $path = $custom_path ? $path : __PS_BASE_URI__.$path; $this->context->controller->addCSS($path, $media); } } public function isMobilePhone() { return $this->context->getDevice() == Context::DEVICE_MOBILE; } public function isTablet() { return $this->context->getDevice() == Context::DEVICE_TABLET; } public function addCustomMedia() { foreach (array('css', 'js') as $type) { $path = 'specific/'.$this->getSpecificThemeIdentifier().'.'.$type; if (file_exists(_PS_MODULE_DIR_.$this->name.'/views/'.$type.'/'.$path)) { $method_name = 'add'.Tools::strtoupper($type); $this->$method_name($path); } } $this->addJS('custom.js'); $this->addCSS('custom.css'); } public function loadIconFontIfRequired() { if (!empty($this->settings['iconclass']['load_font'])) { $this->addCSS('icons.css'); } } public function hookDisplayHeader() { $css = ''; if ($this->defineFilterParams()) { $this->addJS('front.js'); $this->addCSS('front.css'); $this->loadIconFontIfRequired(); if ($this->is_17) { $this->addCSS('front-17.css'); } else { Media::addJsDef(array( 'af_upd_search_form' => $this->isTemplateAvailable('search'), )); $this->addJS('front-16.js'); } if (!empty($this->slider_required)) { $this->addJS('slider.js'); $this->addCSS('slider.css'); } $this->addCustomMedia(); $load_more = $this->settings['general']['p_type'] > 1; Media::addJsDef(array( 'af_ajax_path' => $this->context->link->getModuleLink($this->name, 'ajax', array('ajax' => 1)), 'af_id_cat' => (int)$this->id_cat_current, 'current_controller' => Tools::getValue('controller'), 'load_more' => $load_more, 'af_product_count_text' => $this->products_data['product_count_text'], 'show_load_more_btn' => !$this->products_data['hide_load_more_btn'], 'af_product_list_class' => $this->product_list_class, 'page_link_rewrite_text' => $this->page_link_rewrite_text, 'af_classes' => $this->getLayoutClasses(), 'af_ids' => $this->settings['themeid'], 'is_17' => (int)$this->is_17, )); if (!empty($this->context->controller->seopage_data)) { $fixed_criteria = $this->context->controller->seopage_data['all_required_filters_hidden']; Media::addJsDef(array( 'af_sp_fixed_criteria' => $fixed_criteria, 'af_sp_base_url' => !$fixed_criteria ? $this->sp->url('build') : current(explode('?', $this->context->controller->seopage_data['canonical'])), )); } if (!$this->is_17) { $tpl_vars = $this->context->smarty->tpl_vars; $max_items = $tpl_vars['comparator_max_item']->value; $min_items_txt = $this->l('Please select at least one product'); $max_items_txt = $this->l('You cannot add more than %d product(s) to the product comparison'); Media::addJsDef(array( // comparator variables 'comparator_max_item' => $max_items, 'comparedProductsIds' => $tpl_vars['compared_products']->value, 'min_item' => addslashes($min_items_txt), 'max_item' => sprintf(addslashes($max_items_txt), $max_items), )); } if ($load_more) { // hide pagination if load more is used $css .= '.'.$this->settings['themeclass']['pagination'].'{display:none;}'; } if ($compact_width = (int)$this->settings['general']['compact']) { // position:fixed will be used to detect compact view in front.js $css .= '@media(max-width:'.(int)$compact_width.'px){#amazzing_filter{position:fixed;opacity:0;}}'; } } elseif (Tools::getValue('controller') == 'myaccount') { // additional styles on my account page $this->loadIconFontIfRequired(); if ($this->is_17) { $this->addCSS('front-17.css'); } } return $css ? '' : ''; } public function isTemplateAvailable($controller) { $cache_id = 'tpl-avl-'.$controller.'-'.$this->id_shop; $is_available = $this->cache('get', $cache_id); if ($is_available === false) { $is_available = (int)$this->db->getValue(' SELECT id_template FROM '._DB_PREFIX_.'af_templates WHERE template_controller = \''.pSQL($controller).'\' AND id_shop = '.(int)$this->id_shop.' AND active = 1 '); $this->cache('save', $cache_id, $is_available); } return $is_available; } public function getSubmittedParams() { if (is_callable(array('Tools', 'getAllValues'))) { $params = Tools::getAllValues(); } else { // retro compatibility $params = $_POST + $_GET; } return $params; } public function getInitialFiltersByGroup($filter_group) { $values = Tools::getValue($filter_group); return $values ? explode(',', $values) : array(); } public function getSubcategories($id_lang, $id_parent = false, $imploded_customer_groups = '', $nesting_lvl = 0) { $id_parent = $id_parent ? $id_parent : $this->context->shop->getCategory(); $current_category_data = $this->db->getRow(' SELECT * FROM '._DB_PREFIX_.'category WHERE id_category = '.(int)$id_parent.' '); $max_depth = $nesting_lvl ? $current_category_data['level_depth'] + $nesting_lvl : 0; $nleft = $current_category_data['nleft']; $nright = $current_category_data['nright']; $categories = $this->db->executeS(' SELECT c.id_category AS id, c.id_parent, cl.name, cl.link_rewrite AS link, category_shop.position FROM '._DB_PREFIX_.'category c '.Shop::addSqlAssociation('category', 'c').' LEFT JOIN '._DB_PREFIX_.'category_lang cl ON c.id_category = cl.id_category '.($imploded_customer_groups ? 'INNER JOIN '._DB_PREFIX_.'category_group cg ON cg.id_category = c.id_category AND cg.id_group IN ('.pSQL($imploded_customer_groups).')' : '').' WHERE id_lang = '.(int)$id_lang.' AND c.active = 1 AND c.nright < '.(int)$nright.' AND c.nleft > '.(int)$nleft.' '.($max_depth ? 'AND c.level_depth <= '.(int)$max_depth : '').' AND cl.id_shop = '.(int)$this->context->shop->id.' GROUP BY c.id_category ORDER BY cl.name ASC, c.id_category ASC '); return $categories; } public function getName($resource_type, $id, $id_lang = false, $id_shop = false) { if (!$id_shop) { $id_shop = $this->context->shop->id; } if (!$id_lang) { $id_lang = $this->context->language->id; } $name = $this->db->getValue(' SELECT name FROM `'._DB_PREFIX_.bqSQL($resource_type).'_lang` WHERE `id_'.bqSQL($resource_type).'` = '.(int)$id.' AND `id_shop` = '.(int)$id_shop.' AND `id_lang` = '.(int)$id_lang.' '); return $name; } public function prepareTplVariables($current_filters) { $this->c_groups = $this->formatIDs($this->context->customer->getGroups(), true); $filters = $this->prepareFiltersData($current_filters); $initial_params = $this->prepareInitialParams($filters); if (!empty($this->sp)) { $this->sp->processCanonical($initial_params, $this->current_controller); } $hidden_inputs = $this->prepareHiddenInputs(); $f_params = $hidden_inputs + $initial_params + array('count_all_matches' => 1); $this->products_data = $this->getFilteredProducts($f_params); if (!$this->products_data['filtered_ids_count'] && !array_column($filters, 'has_selection')) { $filters = array(); } $this->preparePaginationVars($f_params); $this->context->smarty->assign(array( 'filters' => $this->prepareFiltersForDisplay($filters, $initial_params), 'available_options' => $initial_params['available_options'], 'numeric_slider_values' => $initial_params['numeric_slider_values'], 'hidden_inputs' => $hidden_inputs, 'count_data' => $this->products_data['count_data'], 'class' => $this->product_list_class, // used in product-list.tpl 'af_classes' => $this->getLayoutClasses(), 'af_ids' => $this->settings['themeid'], 'current_controller' => $this->current_controller, 'total_products' => $this->products_data['filtered_ids_count'], 'is_17' => $this->is_17, 'af_layout_type' => $this->settings['general']['layout'], 'af' => 1, 'is_iphone' => $this->isIphone(), )); $this->context->filtered_result = array( 'products' => $this->products_data['products'], 'total' => $this->products_data['filtered_ids_count'], 'controller' => $this->current_controller, 'sorting' => $f_params['orderBy'].'.'.$f_params['orderWay'], ); } public function isIphone() { if (!isset($this->context->cookie->is_iphone)) { $this->context->cookie->__set('is_iphone', (int)$this->context->getMobileDetect()->isIphone()); } return $this->context->cookie->is_iphone; } public function prepareFiltersData($filters) { $standard_filters = $this->getStandardFilters(); $special_filters = $this->getSpecialFilters(); $range_filters = array('p' => $this->l('Price'), 'w' => $this->l('Weight')); $predefined_group_names = $standard_filters + $range_filters; $horizontal_layout = $this->settings['general']['layout'] == 'horizontal'; foreach ($filters as $key => &$f) { $f['name'] = !empty($f['custom_name']) ? $f['custom_name'] : ''; $f['classes'] = array(); if ($f['type'] == 5) { $f['type'] = $f['textbox'] = 1; $f['color_display'] = 0; } if ($f['special'] = isset($special_filters[$key])) { $f['first_char'] = $f['id_group']= $f['input_group_id'] = $f['is_slider'] = ''; $f['name'] = $f['name'] ?: $special_filters[$key]; $f['values'] = array(1 => array('name' => $f['name'], 'id' => 1, 'link' => 1, 'identifier' => $key)); $f['submit_name'] = $key; } else { $f['first_char'] = Tools::substr($key, 0, 1); $f['id_group'] = $f['input_group_id'] = Tools::substr($key, 1); $f['is_slider'] = $f['type'] == 4; if ($f['first_char'] == 'c') { $f['id_parent'] = $f['input_group_id'] = $f['id_group'] ?: $this->id_cat_current; } $f['values'] = $this->getFilterValues($f, $key); $first_value = current($f['values']) ?: array(); if ($f['is_slider'] || isset($range_filters[$key])) { $this->slider()->setExtensions($f, $this->is_17); } if (!$f['name']) { if (isset($predefined_group_names[$key])) { $f['name'] = $predefined_group_names[$key]; } elseif ($f['first_char'] == 'c') { $f['name'] = $f['id_group'] ? $this->getName('category', $f['id_group']) : $this->l('Categories'); } elseif (isset($first_value['group_name'])) { // attributes, features. Can be optimized $f['name'] = $first_value['group_name']; } } if ($f['input_group_id']) { $f['submit_name'] = $f['first_char'].'['.$f['input_group_id'].'][]'; } else { $f['submit_name'] = $key.'[]'; } if ($horizontal_layout) { $f['minimized'] = 1; $f['visible_items'] = ''; } } $f['link'] = $this->generateLink($f['name'], $key); } $this->processCustomerFiltersIfRequired($filters); return $filters; } public function prepareInitialParams(&$filters) { $initial_params = array_fill_keys(array('available_options', 'numeric_slider_values', 'sliders'), array()); $submitted_data = array(); foreach ($filters as $key => &$f) { $initial_params['available_options'][$key] = array(); $submitted_data[$f['link']] = $this->getInitialFiltersByGroup($f['link']); foreach ($f['values'] as &$v) { $id = $v['id']; if (isset($f['forced_values'][$id])) { $f['forced_values'][$id] = $v['link']; $submitted_data[$f['link']][] = $v['link']; } if ($v['selected'] = in_array($v['link'], $submitted_data[$f['link']])) { $f['has_selection'] = 1; if ($f['special']) { $initial_params[$key] = $id; } elseif ($f['input_group_id']) { $initial_params[$f['first_char']][$f['input_group_id']][] = $id; } else { $initial_params[$key][] = $id; } } $initial_params['available_options'][$key][$id] = $id; if ($f['is_slider']) { $possible_range = explode('-', $v['name']); $number = $this->extractNumberFromString($possible_range[0]); if (!empty($possible_range[1])) { $number .= '-'.$this->extractNumberFromString($possible_range[1]); } // NOTE: keep 'numeric_slider_values' synchronized with 'available_options' $initial_params['numeric_slider_values'][$key][$id] = $number; } } if ($f['is_slider']) { $f['values'] = array(); if (!empty($submitted_data[$f['link']])) { $f['has_selection'] = 1; $range = ExtendedTools::explodeRangeValue($submitted_data[$f['link']][0]); $f['values'] = array('from' => $range[0], 'to' => $range[1]); } $initial_params['sliders'][$key] = $f['values']; } elseif (isset($f['range_step'])) { $initial_params[$key.'_range_step'] = $f['range_step']; if (!empty($submitted_data[$f['link']])) { $f['has_selection'] = 1; $initial_params[$key] = $submitted_data[$f['link']]; // values will be defined later in prepareRangeFilters() } } if (empty($initial_params['primary_filter']) && !empty($f['has_selection']) && !$f['is_slider'] && !$f['special']) { $initial_params['primary_filter'] = $key; } } return $initial_params; } public function prepareHiddenInputs() { $product_sorting = $this->getProductSorting(); $hidden_inputs = array( 'id_manufacturer' => (int)Tools::getValue('id_manufacturer'), 'id_supplier' => (int)Tools::getValue('id_supplier'), 'page' => (int)Tools::getValue($this->page_link_rewrite_text, 1), 'nb_items' => $this->getNbItems(), 'controller_product_ids' => implode(',', $this->controller_product_ids), 'current_controller' => $this->current_controller, 'page_name' => $this->getPageName($this->current_controller), 'id_parent_cat' => $this->id_cat_current, 'orderBy' => $product_sorting['by'], 'orderWay' => $product_sorting['way'], 'defaultSorting' => $this->settings['general']['default_order_by']. ':'.$this->settings['general']['default_order_way'], 'customer_groups' => $this->c_groups, 'random_seed' => $this->getRandomSeed($this->settings['general']['random_upd']), ); if (!$this->is_17) { $pb_id = $this->settings['themeid']['pagination_bottom']; $pb_suffix = str_replace($this->settings['themeid']['pagination'].'_', '', $pb_id); $hidden_inputs['pagination_bottom_suffix'] = $pb_suffix; $hidden_inputs['hide_right_column'] = !$this->context->controller->display_column_right; $hidden_inputs['hide_left_column'] = !$this->context->controller->display_column_left; } return $hidden_inputs + $this->settings['general']; } public function getNbItems() { $nb_items = $this->settings['general']['npp']; $this->nb_items_options = array($nb_items, $nb_items * 2, $nb_items * 5); if ($custom_nb_items = (int)Tools::getValue('n')) { $nb_items = $custom_nb_items; if (!$this->is_17 && !in_array($custom_nb_items, $this->nb_items_options)) { $this->nb_items_options[] = $custom_nb_items; sort($this->nb_items_options); } } return $nb_items; } public function preparePaginationVars($params) { $params['total_products'] = $this->products_data['filtered_ids_count']; $this->validatePageNumber($params); if ($this->is_17) { $this->context->forced_nb_items = $params['nb_items']; } else { $this->assignCustomPaginationAndSorting($params); } } public function setClasses(&$f, $key) { $f['classes'] += array_filter(array( $key => 1, 'has-slider' => $f['is_slider'], 'type-'.$f['type'] => !$f['is_slider'], 'tb' => !empty($f['textbox']), 'special' => $f['special'], 'folderable' => isset($f['foldered']), 'foldered' => !empty($f['foldered']), 'closed' => !empty($f['minimized']), 'has-selection' => !empty($f['has_selection']), )); } public function prepareFiltersForDisplay($filters, &$initial_params) { $this->prepareRangeFilters($filters, $initial_params); $this->prepareSliderFilters($filters, $initial_params); foreach ($filters as $key => &$f) { $this->setClasses($f, $key); if ($f['is_slider']) { continue; // processed in prepareSliderFilters } if ($this->products_data['count_data'] && empty($f['has_selection'])) { $f['classes']['no-available-items'] = 1; } if ($f['first_char'] == 'c' && $f['nesting_lvl'] != 1 && !$this->settings['indexation']['subcat_products']) { $parent_ids = array_keys($this->prepareTreeValues($f['values'], $f['id_parent'])); foreach ($parent_ids as $id_parent) { // keep upper level categories without matches in tree $this->products_data['all_matches']['c-'.$id_parent] = 1; } } $remove_unused_options = in_array($f['first_char'], array('a', 'f', 'c')); foreach ($f['values'] as $i => &$v) { if ($remove_unused_options && !isset($this->products_data['all_matches'][$v['identifier']])) { unset($f['values'][$i]); unset($initial_params['available_options'][$key][$v['id']]); continue; } if (!empty($f['classes']['no-available-items']) && !empty($this->products_data['count_data'][$v['identifier']])) { unset($f['classes']['no-available-items']); if (!$remove_unused_options) { break; // will not break for colors } } if (!empty($f['color_display'])) { $this->setColorStyle($v); } } if (empty($f['values'])) { unset($filters[$key]); unset($initial_params['available_options'][$key]); } else { if (!empty($f['visible_items']) && $f['type'] < 3 && $f['visible_items'] < count($f['values'])) { $f['cut_off'] = $f['classes']['cut-off'] = $f['visible_items']; } if (!empty($f['sort_by'])) { $f['values'] = $this->sortByKey($f['values'], $f['sort_by']); } if (!empty($f['quick_search'])) { $f['quick_search'] = count($f['values']) >= $this->qs_min_values; // before prepareTreeValues() } if ($f['first_char'] == 'c' && !$f['values'] = $this->prepareTreeValues($f['values'], $f['id_parent'])) { unset($filters[$key]); unset($initial_params['available_options'][$key]); } } } return $filters; } public function prepareRangeFilters(&$filters, &$initial_params) { foreach ($this->products_data['ranges'] as $key => $r) { if (empty($r['max'])) { unset($filters[$key]); unset($initial_params['available_options'][$key]); } if (!empty($filters[$key]) && isset($r['available_range_options'])) { $initial_params['available_options'][$key] = $r['available_range_options']; $submitted_ranges = isset($initial_params[$key]) ? $initial_params[$key] : array(); foreach ($r['available_range_options'] as $range) { $filters[$key]['values'][] = array( 'name' => $filters[$key]['prefix'].$range.$filters[$key]['suffix'], 'id' => $range, 'link' => $range, 'identifier' => $filters[$key]['first_char'].'-'.$range, 'selected' => in_array($range, $submitted_ranges), ); } } } } public function prepareSliderFilters(&$filters, &$initial_params) { foreach (array_keys($initial_params['sliders']) as $key) { if (isset($filters[$key])) { if (isset($initial_params['numeric_slider_values'][$key])) { // prepare data for numeric sliders; remove sliders without matches $numbers = $initial_params['numeric_slider_values'][$key]; foreach (array_keys($numbers) as $id) { $identifier = $filters[$key]['first_char'].'-'.$id; if (!isset($this->products_data['all_matches'][$identifier])) { unset($numbers[$id]); unset($initial_params['available_options'][$key][$id]); unset($initial_params['numeric_slider_values'][$key][$id]); } } if (!$numbers) { unset($filters[$key]); unset($initial_params['available_options'][$key]); unset($initial_params['numeric_slider_values'][$key]); continue; } $number_values = explode('-', implode('-', $numbers)); $filters[$key]['values']['min'] = min($number_values); $filters[$key]['values']['max'] = max($number_values); } elseif (isset($this->products_data['ranges'][$key])) { $filters[$key]['values'] = $this->products_data['ranges'][$key]; } if (!empty($filters[$key]['values'])) { $filters[$key]['values'] = $this->slider()->fillValues($filters[$key]['values']); if ($filters[$key]['values']['max'] == 0) { $filters[$key]['classes']['hidden'] = 1; } $this->slider_required = 1; // will be used to load slider script } } } } public function getCacheIDForFilterValues($f) { if ($f['first_char'] == 'c') { $caching_params = array($f['id_parent'], $f['nesting_lvl'], $this->id_shop, $this->id_lang); $caching_params = array_merge($caching_params, $this->formatIDs($this->c_groups)); } else { $caching_params = array($f['id_group'], $this->id_shop, $this->id_lang); } return $this->cacheID($f['first_char'].'_list', $caching_params); } public function getFilterValues($f, $key) { $cache_id = $this->getCacheIDForFilterValues($f); if ($cache_id && $values = $this->cache('get', $cache_id)) { return $values; } $values = $this->getRawFilterValues($f); foreach ($values as &$v) { $v['identifier'] = $f['first_char'].'-'.$v['id']; if (!isset($v['link'])) { $v['link'] = $this->generateLink($v['name'], $v['id'], $key); } else { $v['link'] = $this->getUniqueLink($v['link'], $v['id'], $key); } } if ($cache_id) { $this->cache('save', $cache_id, $values); } return $values; } public function getRawFilterValues($f) { $values = array(); switch ($f['first_char']) { case 'c': $values = $this->getSubcategories($this->id_lang, $f['id_parent'], $this->c_groups, $f['nesting_lvl']); break; case 'a': case 'f': $method_name = $f['first_char'] == 'a' ? 'getAttributes' : 'getFeatures'; $values = $this->$method_name($this->id_lang, $f['id_group']); break; case 'm': case 's': $resource = $f['first_char'] == 'm' ? 'manufacturer' : 'supplier'; $values = $this->db->executeS(' SELECT '.pSQL($f['first_char']).'.id_'.pSQL($resource).' as id, name FROM '._DB_PREFIX_.pSQL($resource).' '.pSQL($f['first_char']).' '.Shop::addSqlAssociation($resource, $f['first_char']).' WHERE active = 1 ORDER BY name ASC '); break; case 't': $values = $this->db->executeS(' SELECT id_tag as id, name FROM '._DB_PREFIX_.'tag WHERE id_lang = '.(int)$this->id_lang.' ORDER BY name ASC '); break; case 'q': $values = array( array('id' => 1, 'name' => $this->l('New')), array('id' => 2, 'name' => $this->l('Used')), array('id' => 3, 'name' => $this->l('Refurbished')), ); break; } return $values; } public function setColorStyle(&$v) { $img_name = (isset($v['id_original']) ? $v['id_original'] : $v['id']).'.jpg'; if (file_exists(_PS_COL_IMG_DIR_.$img_name)) { $v['color_style'] = 'background:url('._THEME_COL_DIR_.$img_name.') 50% 50% no-repeat;'; } elseif (isset($v['color'])) { $v['color_style'] = 'background-color:'.($v['color'] ?: '#FFFFFF'); if (ExtendedTools::isBrightColor($v['color'])) { $v['bright'] = 1; } } } public function getFilterIdentifiers() { return array('c', 'a', 'f', 'm', 's', 't', 'q', 'p', 'w'); } public function getRandomSeed($upd_random) { $patterns = array(1 => 'ymdH', 2 => 'ymd', 3 => 'ymW'); return isset($patterns[$upd_random]) ? date($patterns[$upd_random]) : mt_rand(0, 100000); } public function validatePageNumber(&$params) { $pages_nb = $this->getNumberOfPages($params['total_products'], $params['nb_items']); $page_exceeded = $pages_nb && $pages_nb < $params['page']; if ($params['page'] < 1 || ($params['page'] == 1 && Tools::isSubmit($this->page_link_rewrite_text)) || $page_exceeded) { $updated_page = $page_exceeded ? $pages_nb : 1; $url = $this->context->link->getPaginationLink(false, false); $url = $this->updateQueryString($url, array($this->page_link_rewrite_text => $updated_page)); $this->redirect301($url); } } public function redirect301($url) { return Tools::redirect($url, __PS_BASE_URI__, null, array('HTTP/1.0 301 Moved Permanently')); } public function assignCustomPaginationAndSorting($params) { // pagination $this->assignSmartyVariablesForPagination( $params['page'], $params['total_products'], $params['nb_items'], $this->sanitizeURL($_SERVER['REQUEST_URI']) ); if (!empty($this->nb_items_options)) { $this->context->smarty->assign(array('nArray' => $this->nb_items_options)); } $this->context->controller->p = $params['page']; $this->context->controller->n = $params['nb_items']; $this->context->custom_pagination = 1; // sorting $this->context->controller->orderBy = $params['orderBy']; $this->context->controller->orderWay = $params['orderWay']; $this->context->smarty->assign(array( 'orderby' => $this->context->controller->orderBy, 'orderway' => $this->context->controller->orderWay, 'orderbydefault' => $this->settings['general']['default_order_by'], 'orderwaydefault' => $this->settings['general']['default_order_way'], 'stock_management' => (int)Configuration::get('PS_STOCK_MANAGEMENT'), )); $this->context->custom_sorting = 1; } public function getPageName($controller_name) { $custom_names = array( 'bestsales' => 'best-sales', 'pricesdrop' => 'prices-drop', 'newproducts' => 'new-products', 'seopage' => 'category', ); return isset($custom_names[$controller_name]) ? $custom_names[$controller_name] : $controller_name; } public function sanitizeURL($url, $remove_page_param = true) { $url = Tools::safeOutput($url); if ($remove_page_param) { $url = preg_replace('/(?:(\?)|&)'.$this->page_link_rewrite_text.'=\d+/', '$1', $url); } return $url; } public function prepareTreeValues($values, $id_root) { $tree_values = array(); foreach ($values as $v) { $tree_values[$v['id_parent']][$v['id']] = $v; } return isset($tree_values[$id_root]) ? $tree_values : array(); } public function getSpecificSorting($controller) { $specific_sorting = array( 'newproducts' => array('by' => 'date_add', 'way' => 'desc'), 'pricesdrop' => array('by' => 'price', 'way' => 'asc'), 'search' => array('by' => 'position', 'way' => 'desc'), ); return isset($specific_sorting[$controller]) ? $specific_sorting[$controller] : false; } public function getProductSorting() { if (!isset($this->context->forced_sorting)) { $sorting = array( 'by' => Tools::getValue('orderby', $this->settings['general']['default_order_by']), 'way' => Tools::getValue('orderway', $this->settings['general']['default_order_way']) ); if ($this->is_17) { $order = explode('.', Tools::getValue('order')); if (count($order) == 3) { $sorting = array('by' => $order[1], 'way' => $order[2]); } } $this->validateSorting($sorting); $this->context->forced_sorting = $sorting; } return $this->context->forced_sorting; } public function validateSorting(&$sorting) { foreach ($sorting as $key => $value) { $available_options = $this->getOptions('order'.$key); if (!isset($available_options[$value])) { $sorting[$key] = current(array_keys($available_options)); } } } public function displayHook($hook_name) { if (empty($this->params_defined)) { return; } $this->context->smarty->assign(array( 'hook_name' => $hook_name, )); $html = $this->display(__FILE__, 'amazzingfilter.tpl'); if ($this->settings['general']['p_type'] > 1 && $hook_name != 'displayHome') { $html .= $this->display(__FILE__, 'dynamic-loading.tpl'); } return $html; } public function sortByKey($array, $key) { $method_name = 'sortBy'.Tools::ucfirst($key); if (method_exists($this, $method_name)) { usort($array, array($this, $method_name)); } elseif (($all = $key == 'numbers_in_name') || $key == 'first_num') { foreach ($array as &$el) { $el['number'] = $this->extractNumberFromString($el['name'], $all); } $array = $this->sortByKey($array, 'number'); } return $array; } public function extractNumberFromString($string, $all = true, $no_scientific_notation = true) { if ($replacements = $this->getNumReplacements()) { $string = str_replace(array_keys($replacements), $replacements, $string); } $number = $all ? (float)preg_replace('/[^0-9.]/', '', $string) : (float)$string; if ($no_scientific_notation) { $number = $this->removeScientificNotation($number); } return $number; } public function removeScientificNotation($number) { return rtrim(rtrim(number_format($number, 12, '.', ''), 0), '.'); } public function getNumReplacements() { if (!isset($this->num_replacements)) { $this->num_replacements = array(); $standard_values = array('tho_sep' => '', 'dec_sep' => '.'); // tho before dec! foreach ($standard_values as $key => $standard_value) { if (!empty($this->settings['general'][$key]) && $this->settings['general'][$key] != $standard_value) { $this->num_replacements[$this->settings['general'][$key]] = $standard_value; } } } return $this->num_replacements; } public function sortByPosition($a, $b) { return $a['position'] - $b['position']; } public function sortById($a, $b) { return $a['id'] - $b['id']; } public function sortByNumber($a, $b) { return $a['number'] > $b['number'] ? 1 : -1; } public function sortByName($a, $b) { return strcmp($a['name'], $b['name']); } public function assignSearchResultIDs($id_lang) { // s: 1.7; search_query: 1.6 or some 3rd party modules; if ($query = Tools::getValue('s', Tools::getValue('search_query', Tools::getValue('ref')))) { $query = Tools::replaceAccentedChars(urldecode($query)); $ajax = $this->context->controller->ajax; if ($this->custom_search == 'ambjolisearch' && class_exists('AmbSearch')) { $abjolisearchmodule = Module::getInstanceByName('ambjolisearch'); $searcher = new AmbSearch(true, $this->context, $abjolisearchmodule); $id_cat = (int)Tools::getValue('ajs_cat'); $id_man = (int)Tools::getValue('ajs_man'); $searcher->search($id_lang, $query, 1, null, 'position', 'desc', $id_cat, $id_man); $this->controller_product_ids = $searcher->getResultIds(); } elseif ($this->custom_search == 'elasticjetsearch') { $ejs_module = Module::getInstanceByName('elasticjetsearch'); $params = array( 'order_by' => 'position', 'order_way' => 'desc', 'page' => 1, 'items_per_page' => 100000, 'af' => 1, // may be used for improved compatibility ); $this->controller_product_ids = $ejs_module->getElasticManager()->search($query, $params)->getList(); } else { $this->context->properties_not_required = 1; if (class_exists('IqitSearch') && !$this->is_17) { $search_query_cat = (int)Tools::getValue('search_query_cat'); $search = IqitSearch::find($id_lang, $query, $search_query_cat, 1, 100000); } elseif ($this->custom_search == 'tmsearch' && class_exists('TmSearchSearch')) { $searcher = new TmSearchSearch(); $search_query_cat = Tools::getValue('search_categories'); $search = $searcher->tmfind($id_lang, $query, $search_query_cat, 1, 100000); } elseif ($this->custom_search == 'leoproductsearch' && class_exists('ProductSearch')) { $search = ProductSearch::find( $id_lang, $query, 1, 100000, 'position', 'desc', $ajax, true, $this->context, Tools::getValue('cate') ); } else { $search = Search::find($id_lang, $query, 1, 100000, 'position', 'desc', $ajax); } $this->context->properties_not_required = 0; $search_result = isset($search['result']) ? $search['result'] : $search; foreach ($search_result as $product) { $this->controller_product_ids[] = $product['id_product']; } } // sorting by position is reverse in search results $this->controller_product_ids = array_reverse($this->controller_product_ids); } elseif ($tag = Tools::getValue('tag')) { $tag = Tools::replaceAccentedChars(urldecode($tag)); $products = $this->db->executeS(' SELECT pt.id_product FROM '._DB_PREFIX_.'tag t INNER JOIN '._DB_PREFIX_.'product_tag pt ON pt.id_tag = t.id_tag '.Shop::addSqlAssociation('product', 'pt').' WHERE t.name LIKE \'%'.pSQL($tag).'%\' AND t.id_lang = '.(int)$id_lang.' AND product_shop.active = 1 '); foreach ($products as $product) { $this->controller_product_ids[] = $product['id_product']; } } } public function detectPossibleCustomSearchController($controller) { $compatible_modules = array( 'module-ambjolisearch-jolisearch' => 'ambjolisearch', 'module-leoproductsearch-productsearch' => 'leoproductsearch', 'module-iqitsearch-searchiqit' => 'iqitsearch', 'module-tmsearch-tmsearch' => 'tmsearch', ); $this->custom_search = false; if (isset($compatible_modules[$controller])) { $this->custom_search = $compatible_modules[$controller]; } elseif ($controller == 'search' && Module::getModuleIdByName('elasticjetsearch') && Module::isEnabled('elasticjetsearch')) { $this->custom_search = 'elasticjetsearch'; } return $this->custom_search; } public function detectController() { if (!empty($this->context->controller->seopage_data)) { $controller = 'seopage'; } else { $c = Tools::getValue('controller'); if (Tools::getValue('fc') == 'module' && Tools::isSubmit('module')) { $c = 'module-'.Tools::getValue('module').'-'.$c; } $available_controllers = $this->getAvailableControllers(true); $controller = isset($available_controllers[$c]) ? $c : false; if ((!$controller || $controller == 'search') && $this->detectPossibleCustomSearchController($c)) { $controller = 'search'; } } return $controller; } public function defineFilterParams() { if (isset($this->params_defined)) { return $this->params_defined; } $this->params_defined = false; if (!$controller = $this->detectController()) { return false; } $id_lang = $this->context->language->id; $this->id_cat_current = Tools::getValue('id_category', (int)$this->context->shop->getCategory()); $this->controller_product_ids = array(); $current_id = in_array($controller, $this->getControllersWithMultipleIDs()) ? Tools::getValue('id_'.$controller) : 0; if (!empty($this->context->controller->seopage_data)) { $current_id = $this->context->controller->seopage_data['id_seopage']; } $template = $this->db->getRow(' SELECT t.id_template, t.template_filters AS filters, t.additional_settings, tl.data AS lang FROM '._DB_PREFIX_.'af_templates t LEFT JOIN '._DB_PREFIX_.'af_templates_lang tl ON tl.id_template = t.id_template AND tl.id_shop = t.id_shop AND tl.id_lang = '.(int)$id_lang.' '.($current_id ? ' INNER JOIN '._DB_PREFIX_.'af_'.pSQL($controller).'_templates ct ON ct.id_template = t.id_template AND ct.id_shop = t.id_shop AND (ct.id_'.pSQL($controller).' = '.(int)$current_id.' OR ct.id_'.pSQL($controller).' = 0)' : '').' WHERE t.active = 1 AND t.template_controller = \''.pSQL($controller).'\' AND t.id_shop = '.(int)$this->context->shop->id.' ORDER BY '.($current_id ? 'ct.id_'.pSQL($controller).' DESC, ' : '').'t.id_template DESC '); if (empty($template['filters'])) { return false; } elseif ($controller != 'category') { switch ($controller) { case 'pricesdrop': case 'bestsales': case 'newproducts': $this->controller_product_ids = $this->getSpecialControllerIds($controller); break; case 'search': $this->assignSearchResultIDs($id_lang); break; case 'manufacturer': case 'supplier': if (!Tools::getValue('id_'.$controller)) { return false; } break; case 'index': if (!$this->is_17) { $this->addCSS('product_list.css', _THEME_CSS_DIR_); } break; } } $this->defineSettings(); $this->current_controller = $controller; $additional_settings = Tools::jsonDecode($template['additional_settings'], true); $this->settings['general'] = $additional_settings + $this->settings['general']; if ($this->settings['general']['default_order_by'] == 'random') { $this->settings['general']['default_order_way'] = 'desc'; // keep same way for random ordering } $filters = Tools::jsonDecode($template['filters'], true); $filters_lang = $template['lang'] ? Tools::jsonDecode($template['lang'], true) : array(); $current_filters = array_merge_recursive($filters, $filters_lang); if (!empty($this->context->controller->seopage_data)) { $this->context->controller->seopage_data['all_required_filters_hidden'] = true; foreach ($this->context->controller->seopage_data['required_filters'] as $key => $forced_values) { if (!isset($current_filters[$key])) { $current_filters[$key] = array('type' => 'hidden'); if ($key == 'c') { $current_filters[$key]['nesting_lvl'] = 0; } } else { $this->context->controller->seopage_data['all_required_filters_hidden'] = false; } $current_filters[$key]['forced_values'] = $forced_values; } } $this->prepareTplVariables($current_filters); $this->params_defined = true; return true; } public function isNewQuery($alias = 'product_shop') { $nb_days_new = Configuration::get('PS_NB_DAYS_NEW_PRODUCT'); return pSQL($alias).'.date_add > "'.pSQL(date('Y-m-d H:i:s', strtotime('-'.(int)$nb_days_new.' DAY'))).'"'; } public function getSpecialControllerIds($controller) { $ids = $items = array(); switch ($controller) { case 'newproducts': $items = $this->db->executeS(' SELECT id_product FROM '._DB_PREFIX_.'product_shop product_shop WHERE id_shop = '.(int)$this->context->shop->id.' AND active = 1 AND '.$this->isNewQuery().' '); break; case 'bestsales': $items = $this->db->executeS(' SELECT ps.id_product FROM '._DB_PREFIX_.'product_sale ps '.Shop::addSqlAssociation('product', 'ps').' WHERE product_shop.active = 1 ORDER BY ps.quantity DESC '); break; case 'pricesdrop': $id_address = $this->context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; $a = Address::getCountryAndState($id_address); $id_country = (int)$a['id_country'] ?: (int)Configuration::get('PS_COUNTRY_DEFAULT'); $current_date = date('Y-m-d H:i:00'); $ids = SpecificPrice::getProductIdByDate( $this->context->shop->id, $this->context->currency->id, $id_country, $this->context->customer->id_default_group, $current_date, $current_date, $this->context->customer->id ); $ids = array_combine($ids, $ids); break; } foreach ($items as $i) { $ids[$i['id_product']] = $i['id_product']; } return $ids; } public function hookDisplayLeftColumn() { return $this->displayHook('displayLeftColumn'); } public function hookDisplayRightColumn() { return $this->displayHook('displayRightColumn'); } public function hookDisplayTopColumn() { return $this->displayHook('displayTopColumn'); } public function hookDisplayAmazzingFilter() { return $this->displayHook('displayAmazzingFilter'); } public function hookDisplayHome() { return $this->displayHook('displayHome'); } /** * index Product when customer clicks save in 1.6 * actionIndexProduct is defined in override/controllers/admin/AdminProductController.php */ public function hookActionIndexProduct($params) { if (!empty($params['product'])) { $id_product = is_object($params['product']) ? $params['product']->id : $params['product']; // index products only in context shops if not defined otherwise $shop_ids = isset($params['shop_ids']) ? $params['shop_ids'] : $this->shop_ids; return empty($params['unindex_all']) ? $this->indexProduct($id_product, $shop_ids) : $this->unindexProducts($id_product, $shop_ids); } } public function hookActionProductAdd($params) { return $this->hookActionProductUpdate($params); } public function hookActionProductUpdate($params) { $this->defineSettings(); if (!empty($params['id_product']) && $this->readyToIndexOnProductUpdate()) { // this hook can be called anywhere, so make sure product is indexed for all shops if not defined otherwise $shop_ids = isset($params['shop_ids']) ? $params['shop_ids'] : $this->all_shop_ids; $this->indexProduct((int)$params['id_product'], $shop_ids); } } public function readyToIndexOnProductUpdate() { if (!empty($this->context->controller) && get_class($this->context->controller) == 'AdminProductsController') { // product sheet or product list $ready = true; if ($this->is_17 && Tools::isSubmit('combinations')) { // 1.7: when standard sheet is submitted, actionProductUpdate is called multile times: // 1 time after $adminProductController->postCoreProcess() // then 1 time for each combination: $adminProductWrapper->processProductAttribute() // if no combinations, it is called 1 extra time in $adminProductWrapper->processQuantityUpdate() // check: /src/PrestaShopBundle/Controller/Admin/ProductController.php if (!isset($this->expected_extra_hook_calls)) { $this->expected_extra_hook_calls = count(Tools::getValue('combinations')) ?: 1; } if (!empty($this->expected_extra_hook_calls)) { $this->expected_extra_hook_calls--; $ready = false; // wait for last hook call } } elseif (!$this->is_17 && Tools::isSubmit('submitted_tabs')) { // 1.6: when standard sheet is submitted, not all data is available on actionProductUpdate // because $this->processFeatures() is called after $object->update() in AdminProductsController.php // so product is not indexed here. It will be indexed when actionIndexProduct is called in override $ready = false; } } else { $ready = $this->settings['indexation']['auto']; } return $ready; } public function hookActionObjectCombinationAddAfter($params) { // save this value for reindexing product after mass combinations generation in 1.6 if (!$this->is_17 && empty($this->context->cookie->af_index_product)) { $this->context->cookie->__set('af_index_product', $params['object']->id_product); } } public function hookActionObjectAddAfter($params) { $this->hookActionObjectUpdateAfter($params); } public function hookActionObjectDeleteAfter($params) { $this->hookActionObjectUpdateAfter($params); } public function hookActionObjectUpdateAfter($params) { if (isset($params['object']) && $cls = get_class($params['object'])) { $cache_dependencies = array( 'Category' => 'c_list', 'Attribute' => 'a_list', 'FeatureValue' => 'f_list', 'Combination' => 'comb_data', 'Order' => 'comb_data', 'StockAvailable' => 'comb_data', ); if (isset($cache_dependencies[$cls])) { $this->cache('clear', $cache_dependencies[$cls]); } elseif (in_array($cls, array('Language', 'Currency', 'Group'))) { $this->cache('clear', 'indexationColumns'); $this->i['suffixes'] = array(); $this->indexationColumns('adjust'); } } } public function hookActionAdminTagsControllerSaveAfter() { $id_lang = Tools::getValue('id_lang'); $id_tag = Tools::getValue('id_tag'); $product_ids = Tools::getValue('products'); $this->updateTagInIndex($id_lang, $id_tag, $product_ids); } public function hookActionAdminTagsControllerDeleteBefore($params) { $id_tag = Tools::getValue('id_tag'); $id_lang = (int)$this->db->getValue(' SELECT id_lang FROM '._DB_PREFIX_.'tag WHERE id_tag = '.(int)$id_tag.' '); $this->context->tag_to_delete = array( 'id_tag' => $id_tag, 'id_lang' => $id_lang, ); } public function hookActionAdminTagsControllerDeleteAfter($params) { if (!empty($this->context->tag_to_delete)) { $id_lang = $this->context->tag_to_delete['id_lang']; $id_tag = $this->context->tag_to_delete['id_tag']; $this->updateTagInIndex($id_lang, $id_tag); } } public function updateTagInIndex($id_lang, $id_tag, $product_ids = array()) { $var_data = $this->indexationColumns('getVariableData'); if (isset($var_data['t']) && in_array($id_lang, $var_data['t'])) { $product_ids = $this->formatIDs($product_ids); $upd_rows = array(); $t_col = 't_'.$id_lang; $upd_columns = 'id_product, id_shop, '.$t_col; // tag may be removed from some products and added to others, so check all rows $data = $this->db->executeS('SELECT '.pSQL($upd_columns).' FROM '.pSQL($this->i['table'])); foreach ($data as $row) { $tags = $this->formatIDs($row[$t_col]); if (isset($product_ids[$row['id_product']])) { $tags[$id_tag] = $id_tag; } else { unset($tags[$id_tag]); } $tags = implode(',', $tags); if ($tags != $row[$t_col]) { $row[$t_col] = $tags; $upd_rows[] = '(\''.implode('\', \'', $row).'\')'; } } if ($upd_rows) { $this->db->execute(' INSERT INTO '.pSQL($this->i['table']).' ('.pSQL($upd_columns).') VALUES '.implode(', ', $upd_rows).' ON DUPLICATE KEY UPDATE '.pSQL($t_col).'=VALUES('.pSQL($t_col).') '); } } } public function hookActionProductDelete($params) { if (!empty($params['product']->id)) { $id_product = $params['product']->id; $this->unindexProducts(array($id_product)); } } public function hookActionProductListOverride($params) { if (!isset($this->products_data)) { return; } $params['hookExecuted'] = true; $params['catProducts'] = $this->products_data['products']; $params['nbProducts'] = $this->products_data['filtered_ids_count']; } public function getFeatures($id_lang, $id_group = false, $merge_if_required = true) { $f = $this->db->executeS(' SELECT v.id_feature_value AS id, v.id_feature AS id_group, v.custom, vl.value AS name, fl.name AS group_name FROM '._DB_PREFIX_.'feature_value v INNER JOIN '._DB_PREFIX_.'feature_value_lang vl ON (v.id_feature_value = vl.id_feature_value AND vl.id_lang = '.(int)$id_lang.') INNER JOIN '._DB_PREFIX_.'feature f ON f.id_feature = v.id_feature INNER JOIN '._DB_PREFIX_.'feature_lang fl ON (fl.id_feature = v.id_feature AND fl.id_lang = '.(int)$id_lang.') '.($id_group ? ' AND v.id_feature = '.(int)$id_group : '').' ORDER BY vl.value, v.id_feature_value '); if ($merge_if_required && !empty($this->settings['general']['merged_features'])) { $f = $this->mergedValues()->mapRows($f, $id_lang, $id_group, 'feature'); } return $f; } public function getAttributes($id_lang, $id_group = false, $merge_if_required = true) { $a = $this->db->executeS(' SELECT DISTINCT a.id_attribute AS id, a.position, a.color, al.name, agl.public_name AS group_name, ag.id_attribute_group AS id_group, ag.is_color_group FROM '._DB_PREFIX_.'attribute_group ag INNER JOIN '._DB_PREFIX_.'attribute_group_lang agl ON (ag.id_attribute_group = agl.id_attribute_group AND agl.id_lang = '.(int)$id_lang.') INNER JOIN '._DB_PREFIX_.'attribute a ON a.id_attribute_group = ag.id_attribute_group INNER JOIN '._DB_PREFIX_.'attribute_lang al ON (a.id_attribute = al.id_attribute AND al.id_lang = '.(int)$id_lang.') '.Shop::addSqlAssociation('attribute_group', 'ag').' '.Shop::addSqlAssociation('attribute', 'a').' WHERE a.id_attribute IS NOT NULL AND al.name IS NOT NULL AND agl.id_attribute_group IS NOT NULL '.($id_group ? ' AND ag.id_attribute_group = '.(int)$id_group : '').' ORDER BY al.name, a.id_attribute '); if ($merge_if_required && !empty($this->settings['general']['merged_attributes'])) { $a = $this->mergedValues()->mapRows($a, $id_lang, $id_group, 'attribute'); } return $a; } public function generateLink($string, $identifier = '', $group = 'default') { $string = str_replace(array(',', '.', '*'), '-', $string); $link = Tools::str2url($string) ?: $identifier; return $this->getUniqueLink($link, $identifier, $group); } public function getUniqueLink($link, $identifier, $group) { if (!isset($this->generated_links[$group][$link])) { $this->generated_links[$group][$link] = 1; } elseif ($identifier) { $link = $identifier.($link ? '-'.$link : ''); } return $link; } public function ajaxEraseIndex() { $id_shop = Tools::getValue('id_shop'); $deleted = $this->indexationData('erase', array('id_shop' => $id_shop)); $indexation_data = $this->indexationInfo('count', array($id_shop)); $missing = isset($indexation_data[$id_shop]['missing']) ? $indexation_data[$id_shop]['missing']: 0; $ret = array( 'deleted' => $deleted, 'missing' => $missing, ); exit(Tools::jsonEncode($ret)); } public function ajaxRunProductIndexer($all_identifier, $products_per_request = 1000) { $ret = array(); if ($all_identifier) { $this->reIndexProducts($all_identifier, $products_per_request); $ret['indexation_process_data'] = $this->getIndexationProcessData($all_identifier, true); } else { $this->indexMissingProducts($products_per_request); } $ret['indexation_data'] = $this->indexationInfo('count'); exit(Tools::jsonEncode($ret)); } public function reIndexProducts($all_identifier, $products_per_request, $shop_ids = array()) { if (!$saved_data = $this->getIndexationProcessData($all_identifier, false)) { $saved_data = array('identifier' => $all_identifier, 'data' => array()); foreach ($this->indexationInfo('ids', $shop_ids, true) as $id_shop => $data) { $saved_data['data'][$id_shop]['missing'] = array_merge($data['indexed'], $data['missing']); $saved_data['data'][$id_shop]['indexed'] = array(); } } $indexed_num = 0; foreach ($saved_data['data'] as $id_shop => &$data) { if (empty($data['missing'])) { unset($saved_data['data'][$id_shop]); } elseif ($ids_to_index = array_slice($data['missing'], 0, $products_per_request)) { $indexed_ids = $this->indexProduct($ids_to_index, array($id_shop), false); $data['missing'] = array_diff($data['missing'], $indexed_ids); $data['indexed'] = array_merge($data['indexed'], $indexed_ids); if (empty($data['missing'])) { unset($saved_data['data'][$id_shop]); } $indexed_num += count($indexed_ids); break; } } $saved_data = !empty($saved_data['data']) ? $saved_data : array(); $this->saveData($this->indexation_process_file_path, $saved_data); return $indexed_num; } public function indexMissingProducts($products_per_request, $shop_ids = array()) { $indexed_num = 0; foreach ($this->indexationInfo('ids', $shop_ids, true) as $id_shop => $data) { if (!empty($data['missing'])) { $product_ids = array_slice($data['missing'], 0, $products_per_request); $this->indexProduct($product_ids, array($id_shop)); $indexed_num += count($product_ids); break; } } return $indexed_num; } public function getIndexationProcessData($all_identifier, $return_count = true) { $ret = $indexation_data = $this->getData($this->indexation_process_file_path, $all_identifier); if ($return_count && !empty($indexation_data['data'])) { $ret = array(); foreach ($indexation_data['data'] as $id_shop => $data) { foreach ($data as $name => $ids) { $ret[$id_shop][$name] = count($ids); } } } return $ret; } public function getData($path, $identifier = false) { $data = file_exists($path) ? Tools::jsonDecode(Tools::file_get_contents($path), true) : array(); if ($data && $identifier && (!isset($data['identifier']) || $data['identifier'].'' != $identifier.'')) { $time_before_reset = 60; $time_diff = $time_before_reset - (time() - filemtime($path)); if ($time_diff > 1) { $err = $this->l('Please wait, someone else is performing same action'). '. '.sprintf($this->l('%s seconds left before automatic reset.'), $time_diff); $this->throwError($err); exit($err); // may be used in cron indexation and other non-ajax requests } else { $data = array(); } } return $data; } public function saveData($path, $data, $append = false) { $data = is_string($data) ? $data : Tools::jsonEncode($data); if ($data) { return $append ? file_put_contents($path, $data, FILE_APPEND) : file_put_contents($path, $data); } else { return unlink($path); } } public function formatIDs($ids, $return_string = false) { $ids = is_array($ids) ? $ids : explode(',', $ids); $ids = array_map('intval', $ids); $ids = array_combine($ids, $ids); unset($ids[0]); return $return_string ? implode(',', $ids) : $ids; } public function getShopsForIndexation($predefined_ids = array()) { if (!$shop_ids = $predefined_ids ?: $this->shop_ids) { $shop_ids = array($this->context->shop->id); } return $shop_ids; } public function getAllParents($id_cat) { if (!isset($this->i['all_parents'][$id_cat])) { $cat_obj = new Category($id_cat); $this->i['all_parents'][$id_cat] = array_column($this->db->executeS(' SELECT id_category AS id FROM '._DB_PREFIX_.'category WHERE nleft < '.(int)$cat_obj->nleft.' AND nright > '.(int)$cat_obj->nright.' AND id_parent > 0 ORDER BY level_depth ASC '), 'id', 'id'); } return $this->i['all_parents'][$id_cat]; } public function getDataForPriceCalculation($id_shop) { if (!isset($this->i['p_data'][$id_shop])) { foreach (array('group' => 'g', 'currency' => 'c') as $name => $key) { $identifier = 'id_'.$name; $default = Configuration::get($this->i['default'][$key], null, null, $id_shop); $join_on = $identifier.' = main.'.$identifier; $this->i['p_data'][$id_shop][$name] = array(); $query = new DbQuery(); $query->select('DISTINCT(main.'.pSQL($identifier).'), sp.id_specific_price AS has_specific_price'); $query->select('main.'.pSQL($identifier).' = '.pSQL($default).' AS is_default'); if ($name == 'currency') { $query->select('s.conversion_rate'); } else { $query->select('main.reduction, main.price_display_method AS no_tax'); } $query->from($name, 'main'); $query->innerJoin($name.'_shop', 's', 's.'.pSQL($join_on).' AND s.id_shop = '.(int)$id_shop); $query->leftJoin('specific_price', 'sp', 'sp.'.pSQL($join_on).' AND sp.id_shop = s.id_shop'); // default currency/group go first $query->orderBy('main.'.pSQL($identifier).' <> '.pSQL($default).' ASC, main.'.pSQL($identifier).' ASC'); $query->where('1'.$this->specificIndexationQuery($identifier, 'main', $id_shop)); foreach ($this->db->executeS($query) as $row) { $this->i['p_data'][$id_shop][$name][$row[$identifier]] = $row; } } } return $this->i['p_data'][$id_shop]; } public function getSuffixes($resource, $id_shop = 0, $validate = true) { if (!isset($this->i['suffixes'][$id_shop][$resource])) { $c_name = 'id_'.$resource; $suffixes = array_column($this->db->executeS(' SELECT main.'.pSQL($c_name).' FROM '._DB_PREFIX_.pSQL($resource).' main '.($id_shop ? ' INNER JOIN '._DB_PREFIX_.pSQL($resource).'_shop s ON s.'.pSQL($c_name).' = main.'.pSQL($c_name).' AND s.id_shop = '.(int)$id_shop : '').' WHERE 1'.$this->specificIndexationQuery($c_name, 'main', $id_shop).' ORDER BY main.'.pSQL($c_name).' ASC '), $c_name, $c_name); // NOTE: result is ordered by ID, not like in getDataForPriceCalculation if ($validate) { $this->validateSuffixes($suffixes, $resource, $id_shop); } $this->i['suffixes'][$id_shop][$resource] = $suffixes; } return $this->i['suffixes'][$id_shop][$resource]; } public function validateSuffixes(&$suffixes, $resource, $id_shop) { if (count($suffixes) > $this->i['max_column_suffixes']) { $dependent_options = array('group' => 'p_g', 'currency' => 'p_c'); if (isset($dependent_options[$resource])) { $this->settings['indexation'][$dependent_options[$resource]] = 0; $suffixes = $this->getSuffixes($resource, $id_shop, false); } else { $suffixes = array(); } } } public function specificIndexationQuery($c_name, $t_name, $id_shop = 0, $check_settings = true) { $where = ''; $specific_resources = array('id_group' => 'g', 'id_currency' => 'c'); if (isset($specific_resources[$c_name])) { $key = $specific_resources[$c_name]; if ($check_settings && !$this->settings['indexation']['p_'.$key] && isset($this->i['default'][$key])) { $config_key = $this->i['default'][$key]; $where = ' AND '.pSQL($t_name.'.'.$c_name); if ($id_shop) { $where .= ' = '.(int)Configuration::get($config_key, null, null, $id_shop); } else { $default_ids = array(); foreach ($this->all_shop_ids as $id_shop) { $default_ids[] = Configuration::get($config_key, null, null, $id_shop); } $where .= ' IN ('.pSQL($this->formatIDs($default_ids, true)).')'; } } if ($key == 'c') { $where .= ' AND '.pSQL($t_name).'.deleted = 0 AND '.pSQL($t_name).'.active = 1'; } } elseif ($c_name == 'id_lang') { $where = ' AND '.pSQL($t_name).'.active = 1'; } return $where; } public function prepareContextForIndexation($id_shop) { if (empty($this->context->currency)) { $this->context->currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); } if (empty($this->context->employee) && empty($this->context->cart)) { $this->context->cart = new Cart(); } $this->backup_context = array( 'shop_context' => Shop::getContext(), 'shop_context_id' => null, 'shop_id' => $this->context->shop->id, 'currency_id' => $this->context->currency->id, 'customer_id' => !empty($this->context->customer) ? (int)$this->context->customer->id : 0, ); if ($this->backup_context['shop_context'] == Shop::CONTEXT_GROUP) { $this->backup_context['shop_context_id'] = $this->context->shop->id_shop_group; } elseif ($this->backup_context['shop_context'] == Shop::CONTEXT_SHOP) { $this->backup_context['shop_context_id'] = $this->context->shop->id; } // the following context values will be used for calculating prices and later restored in restoreContext() $this->context->customer = new Customer(); $this->context->shop = new Shop($id_shop); Shop::setContext(Shop::CONTEXT_SHOP, $id_shop); } public function setCustomContextValues($id_group, $id_currency) { // specifc group/currency are used for calculating prices and later restored in restoreContext() $this->context->customer->id_default_group = $id_group; if ($this->context->currency->id != $id_currency) { $this->context->currency = new Currency($id_currency); } } public function restoreContext() { if (!empty($this->backup_context)) { Shop::setContext($this->backup_context['shop_context'], $this->backup_context['shop_context_id']); $this->context->shop = new Shop($this->backup_context['shop_id']); $this->context->currency = new Currency($this->backup_context['currency_id']); $this->context->customer = new Customer($this->backup_context['customer_id']); } } public function indexProduct($product_ids, $shop_ids = array(), $return_string = true) { if (Tools::getValue('no_indexation')) { $product_ids = array(); } foreach ($this->getShopsForIndexation($shop_ids) as $id_shop) { $this->updateIndexationData($product_ids, $id_shop); } return $this->formatIDs($product_ids, $return_string); } public function updateIndexationData($product_ids, $id_shop, $params = array()) { if (!$product_ids = $this->formatIDs($product_ids)) { return; } $indexation_columns = $this->prepareColumnsForIndexation($id_shop, $params); $column_names = $rows = $upd = array(); $this->prepareContextForIndexation($id_shop); $ids_to_unindex = array(); foreach ($product_ids as $id) { $p_obj = new Product($id, false, null, $id_shop); if (!$p_obj->active || $p_obj->visibility == 'none') { $ids_to_unindex[] = $p_obj->id; continue; } $forced_values = isset($params['main_values'][$id]) ?: array(); $row = array('id_product' => $id, 'id_shop' => $id_shop); foreach ($indexation_columns['main'] as $c_name) { $value = isset($forced_values[$c_name]) ? $forced_values[$c_name] : $this->prepareIndexationValue($p_obj, $id_shop, $c_name); $row[$c_name] = pSQL(is_array($value) ? $this->formatIDs($value, true) : $value); } foreach ($indexation_columns['variable'] as $c_name => $c_suffixes) { $value = isset($forced_values[$c_name]) ? $forced_values[$c_name] : $this->prepareIndexationValue($p_obj, $id_shop, $c_name); foreach ($c_suffixes as $suffix) { $v = isset($value[$suffix]) ? $value[$suffix] : ''; $row[$c_name.'_'.$suffix] = pSQL(is_array($v) ? $this->formatIDs($v, true) : $v); } } $rows[] = '(\''.implode('\', \'', $row).'\')'; if (!$column_names) { $column_names = array_keys($row); } } if ($ids_to_unindex) { $this->unindexProducts($ids_to_unindex, array($id_shop)); } $this->restoreContext(); if ($column_names && $rows && $upd = array_diff($column_names, $indexation_columns['primary'])) { foreach ($upd as $i => $c_name) { $upd[$i] = $c_name.' = VALUES('.$c_name.')'; } $query = array(' INSERT INTO '.pSQL($this->i['table']).' ('.implode(', ', $column_names).') VALUES '.implode(', ', $rows).' ON DUPLICATE KEY UPDATE '.pSQL(implode(', ', $upd)).' '); return $this->runSql($query); } } public function prepareColumnsForIndexation($id_shop, $params = array()) { // $params = ['main_columns' => ['f'], 'variable_columns' => []]; // re-index only features // $params = ['main_columns' => ['a'], 'variable_columns' => ['t']]; // re-index only attributes and tags $indexation_columns = $this->indexationColumns('getRequired', $id_shop, 3600); if (isset($params['main_columns'])) { $indexation_columns['main'] = array_intersect($indexation_columns['main'], $params['main_columns']); } if (isset($params['variable_columns'])) { foreach (array_keys($indexation_columns['variable']) as $key) { if (!in_array($key, $params['variable_columns'])) { unset($indexation_columns['variable'][$key]); } } } return $indexation_columns; } public function prepareIndexationValue($p_obj, $id_shop, $type) { $value = array(); $id_product = $p_obj->id; switch ($type) { case 'c': $value = array_column($this->db->executeS(' SELECT id_category AS id FROM '._DB_PREFIX_.'category_product cp WHERE id_product = '.(int)$id_product.' '), 'id', 'id'); if ($this->settings['indexation']['subcat_products']) { // indexation settings are same for all shops foreach ($value as $id_cat) { $value += $this->getAllParents($id_cat); } } break; case 'a': $atts = $this->db->executeS(' SELECT pac.id_attribute, map.id_merged FROM '._DB_PREFIX_.'product_attribute_combination pac LEFT JOIN '._DB_PREFIX_.'af_merged_attribute_map map ON map.id_original = pac.id_attribute INNER JOIN '._DB_PREFIX_.'product_attribute pa ON pa.id_product_attribute = pac.id_product_attribute AND pa.id_product = '.(int)$id_product.' INNER JOIN '._DB_PREFIX_.'product_attribute_shop pas ON pas.id_product_attribute = pa.id_product_attribute AND pas.id_shop = '.(int)$id_shop.' '); foreach ($atts as $att) { $value[$att['id_attribute']] = $att['id_attribute']; if (!empty($att['id_merged'])) { $value['map'.$att['id_merged']] = 'map'.$att['id_merged']; } } $value = implode(',', $value); // skip formatIDs in updateIndexationData because of possible map_xx break; case 'f': $feats = $this->db->executeS(' SELECT fp.id_feature_value, map.id_merged FROM '._DB_PREFIX_.'feature_product fp LEFT JOIN '._DB_PREFIX_.'af_merged_feature_map map ON map.id_original = fp.id_feature_value WHERE fp.id_product = '.(int)$id_product.' '); foreach ($feats as $feat) { $value[$feat['id_feature_value']] = $feat['id_feature_value']; if (!empty($feat['id_merged'])) { $value['map'.$feat['id_merged']] = 'map'.$feat['id_merged']; } } $value = implode(',', $value); // same as atts break; case 's': $value = array_column($this->db->executeS(' SELECT id_supplier AS id FROM '._DB_PREFIX_.'product_supplier WHERE id_product = '.(int)$id_product.' '), 'id', 'id'); break; case 'w': if ($ipa = Product::getDefaultAttribute($id_product)) { $value = (float)$this->db->getValue(' SELECT SUM(p.weight + pas.weight) FROM '._DB_PREFIX_.'product p LEFT JOIN '._DB_PREFIX_.'product_attribute pa ON (pa.id_product = p.id_product AND pa.id_product_attribute = '.(int)$ipa.') LEFT JOIN '._DB_PREFIX_.'product_attribute_shop pas ON (pas.id_product_attribute = pa.id_product_attribute AND pas.id_shop = '.(int)$id_shop.') WHERE p.id_product = '.(int)$id_product.' '); } else { $value = (float)$this->db->getValue(' SELECT weight FROM '._DB_PREFIX_.'product WHERE id_product = '.(int)$id_product.' '); } break; case 'p': $default_prices = array(); $calc_data = $this->getDataForPriceCalculation($id_shop); foreach ($calc_data['currency'] as $id_currency => $c) { $group_tax_prices = array(); foreach ($calc_data['group'] as $id_group => $g) { $group_has_specific_price = $g['has_specific_price'] || $g['reduction'] > 0; if (!$c['has_specific_price'] && isset($default_prices[$id_group])) { $price = Tools::ps_round($default_prices[$id_group] * $c['conversion_rate'], 2); } elseif (!$group_has_specific_price && isset($group_tax_prices[$g['no_tax']])) { $price = $group_tax_prices[$g['no_tax']]; } else { $this->setCustomContextValues($id_group, $id_currency); $price = Product::getPriceStatic($id_product, !$g['no_tax'], null, 2); } if (!$group_has_specific_price) { $group_tax_prices[$g['no_tax']] = $price; } if ($c['is_default'] && !$c['has_specific_price']) { $default_prices[$id_group] = $price; // default currency is first in loop } $value[$id_group.'_'.$id_currency] = $price; } } break; case 't': $tags = $this->db->executeS(' SELECT t.id_tag, t.id_lang FROM '._DB_PREFIX_.'tag t INNER JOIN '._DB_PREFIX_.'product_tag pt ON (pt.id_tag = t.id_tag AND pt.id_product = '.(int)$id_product.') '); foreach ($tags as $t) { $value[$t['id_lang']][$t['id_tag']] = $t['id_tag']; } break; case 'n': case 'r': case 'd': case 'm': $fields = array('n' => 'name', 'r' => 'reference', 'd' => 'date_add', 'm' => 'id_manufacturer'); $value = $p_obj->{$fields[$type]}; break; case 'q': $q = array('new' => 1, 'used' => 2, 'refurbished' => 3); $value = isset($q[$p_obj->condition]) ? $q[$p_obj->condition] : 1; break; case 'v': // restricted visibility $v = array('both' => 0, 'catalog' => 1, 'search' => 2, 'none' => 3); $value = isset($v[$p_obj->visibility]) ? $v[$p_obj->visibility] : 0; break; case 'g': // restricted groups $groups_having_access = array_column($this->db->executeS(' SELECT DISTINCT(cg.id_group) FROM '._DB_PREFIX_.'category_group cg INNER JOIN '._DB_PREFIX_.'category_product cp ON cp.id_category = cg.id_category AND cp.id_product = '.(int)$id_product.' '), 'id_group'); if (!isset($this->i['available_groups'][$id_shop])) { $this->i['available_groups'][$id_shop] = array_column($this->db->executeS(' SELECT DISTINCT(id_group) FROM '._DB_PREFIX_.'group_shop WHERE id_shop = '.(int)$id_shop.' '), 'id_group'); } $value = array_diff($this->i['available_groups'][$id_shop], $groups_having_access); break; } return $value; } public function unindexProducts($product_ids, $shop_ids = array()) { if ($product_ids = $this->formatIDs($product_ids)) { $shop_ids = $shop_ids ? $shop_ids : $this->all_shop_ids; return $this->indexationData('erase', array('id_product' => $product_ids, 'id_shop' => $shop_ids)); } } public function assignSmartyVariablesForPagination($page, $products_num, $npp, $current_url = '') { $pages_nb = $this->getNumberOfPages($products_num, $npp); $siblings = 2; // 2 pages before and after active page in pagination $this->context->smarty->assign(array( 'current_url' => $current_url, 'p' => $page, 'start' => ($page - $siblings > 1) ? $page-$siblings : 1, 'stop' => ($page + $siblings < $pages_nb) ? $page+$siblings : $pages_nb, 'pages_nb' => $pages_nb, 'nb_products' => $products_num, 'n' => $npp, 'products_per_page' => $npp, // 'no_follow' => 1, )); } public function getNumberOfPages($products_num, $products_per_page) { return $products_per_page ? (int)ceil($products_num/$products_per_page) : 0; } public function ajaxGetFilteredProducts($params) { $this->current_controller = $params['current_controller']; $products_data = $this->getFilteredProducts($params); $this->context->forced_sorting = array('by' => $params['orderBy'], 'way' => $params['orderWay']); $this->context->controller->addColorsToProductList($products_data['products']); $this->context->smarty->assign(array( 'products' => $products_data['products'], 'class' => $this->product_list_class, 'page_name' => $params['page_name'], 'link' => $this->context->link, 'static_token' => Tools::getToken(false), 'af' => 1, )); // assign smarty variables for pagination $page = $products_data['page']; $products_num = $products_data['filtered_ids_count']; $npp = $products_data['products_per_page']; $current_url = $this->sanitizeURL(Tools::getValue('current_url')); $ret = array( 'product_count_text' => utf8_encode($products_data['product_count_text']), 'count_data' => $products_data['count_data'], 'ranges' => $products_data['ranges'], 'products_num' => $products_num, 'time' => $products_data['time'], 'hide_load_more_btn' => $products_data['hide_load_more_btn'], 'trigger' => $products_data['trigger'], ); if (!empty($params['layout_required'])) { $ret['layout'] = utf8_encode($this->renderLayout()); } $this->specificThemeAjaxActions($params); if ($this->is_17) { // $controller->page_name is often used by third party modules or theme configurators in PS 1.7 $this->context->controller->page_name = $params['page_name']; Hook::exec('actionProductSearchAfter', array('products' => $products_data['products'])); $current_sorting_option = 'product.'.$params['orderBy'].'.'.$params['orderWay']; $default_sorting_option = 'product.'.$params['default_order_by'].'.'.$params['default_order_way']; $options = $this->getSortingOptions($current_sorting_option, $default_sorting_option, $current_url); $current_label = isset($options[$current_sorting_option]['label']) ? $options[$current_sorting_option]['label'] : ''; $this->context->smarty->assign(array( 'listing' => array( 'products' => $products_data['products'], 'pagination' => $this->getPaginationVariables($page, $products_num, $npp, $current_url), 'sort_orders' => $options, 'sort_selected' => $current_label, 'current_url' => $current_url, ), 'urls' => $this->context->controller->getTemplateVarUrls(), 'configuration' => $this->context->controller->getTemplateVarConfiguration(), 'page' => array('page_name' => $params['page_name']), )); $tpl_path = 'templates/catalog/_partials/'; $product_list_html = $this->fetchThemeTpl($tpl_path.'products.tpl'); $product_list_top_html = $this->fetchThemeTpl($tpl_path.'products-top.tpl'); $product_list_bottom_html = $this->fetchThemeTpl($tpl_path.'products-bottom.tpl'); $ret['product_list_top_html'] = utf8_encode($product_list_top_html); $ret['product_list_bottom_html'] = utf8_encode($product_list_bottom_html); } else { if ($ret['trigger'] != 'af_page') { $product_total_text = $products_num == 1 ? $this->l('There is 1 product.') : sprintf($this->l('There are %d products.'), $products_num); $ret['product_total_text'] = utf8_encode($product_total_text); } $this->context->smarty->assign(array( 'hide_left_column' => $params['hide_left_column'], 'hide_right_column' => $params['hide_right_column'], )); $product_list_html = $this->context->smarty->fetch(_PS_THEME_DIR_.'product-list.tpl'); $this->assignSmartyVariablesForPagination($page, $products_num, $npp, $current_url); $pagination_html = $this->context->smarty->fetch(_PS_THEME_DIR_.'pagination.tpl'); $this->context->smarty->assign('paginationId', $params['pagination_bottom_suffix']); $pagination_bottom_html = $this->context->smarty->fetch(_PS_THEME_DIR_.'pagination.tpl'); $ret['pagination_html'] = utf8_encode($pagination_html); $ret['pagination_bottom_html'] = utf8_encode($pagination_bottom_html); } if (!$products_num) { $product_list_html = $this->display(__FILE__, 'views/templates/front/no-products.tpl'); } $ret['product_list_html'] = utf8_encode($product_list_html); if ($params['current_controller'] == 'seopage') { $this->sp->extendAjaxResponse($ret, $params); } exit(Tools::jsonEncode($ret)); } public function specificThemeAjaxActions(&$params) { $identifier = $this->getSpecificThemeIdentifier(); switch ($identifier) { case 'warehouse-17': $this->context->controller->php_self = $params['page_name']; // used in iqitthemeeditor $available_views = array('grid' => 1, 'list' => 1); $list_view = !empty($params['listView']) && !empty($available_views[$params['listView']]) ? $params['listView'] : 'grid'; $this->context->cookie->__set('product_list_view', $list_view); break; case 'warehouse-16': $this->context->controller->php_self = $params['page_name']; // used in themeeditor break; case 'ayon-16': $this->context->smarty->assign(array('nc_p_hover' => Configuration::get('NC_P_HOVERS'),)); break; case 'AngarTheme-17': $this->context->smarty->assign(array( 'display_quickview' => (int)Configuration::get('PS_QUICK_VIEW'), 'psversion' => Configuration::get('ANGARTHEMECONFIGURATOR_PSVERSION'), )); break; case 'venedor-17': if (!empty($this->context->controller->ajax) && $pkts_module = Module::getInstanceByName('pk_themesettings')) { // $pkts_module->getOptions() returns too many opions. // so we select only options, related to product_miniature // some pm_ options are encoded, but they are not used in dynamic listing $pkts_options = array_column($this->db->executeS(' SELECT name, value FROM '.pSQL($pkts_module->mdb).' WHERE id_shop = '.(int)$this->context->shop->id.' AND name LIKE \'pm_%\' '), 'value', 'name'); $this->context->smarty->assign(array('pkts' => $pkts_options)); } break; case 'at_decor-17': case 'at_classico-17': case 'at_oreo-17': $apb = Module::getInstanceByName('appagebuilder'); if ($apb->active) { $product_settings = ApPageBuilderProductsModel::getActive($apb->getConfig('USE_MOBILE_THEME')); $this->context->smarty->assign(array( 'productProfileDefault' => $product_settings['plist_key'], 'productClassWidget' => $product_settings['class'] )); if (class_exists('apPageHelper')) { apPageHelper::setGlobalVariable($this->context); } } break; } } public function fetchThemeTpl($path) { $html = ''; if (file_exists(_PS_THEME_DIR_.$path)) { $html = $this->context->smarty->fetch(_PS_THEME_DIR_.$path); } elseif (file_exists(_PS_PARENT_THEME_DIR_.$path)) { $html = $this->context->smarty->fetch(_PS_PARENT_THEME_DIR_.$path); } return $html; } public function getSpecificThemeIdentifier() { return $this->getCurrentThemeName().'-'.($this->is_17 ? '17' : '16'); } public function getCurrentThemeName() { $theme_name = _THEME_NAME_; if ($this->is_17 && _PARENT_THEME_NAME_) { $theme_name = _PARENT_THEME_NAME_; // _THEME_NAME_ can be different if child theme is used } return $theme_name; } public function renderLayout() { $this->context->smarty->assign(array( 'product_list_class' => $this->product_list_class, 'af_ids' => $this->settings['themeid'], )); return $this->display(__FILE__, 'views/templates/front/basic-layout'.($this->is_17 ? '-17' : '').'.tpl'); } public function formatOrder($by, $way) { $compact_order_names = array('name' => 'n', 'date_add' => 'd', 'reference' => 'r', 'price' => 'p'); if (isset($compact_order_names[$by])) { $by = $compact_order_names[$by]; } if (!in_array($way, array('asc', 'desc'))) { $way = 'asc'; } return array('by' => $by, 'way' => $way); } public function getFilteredProducts($params) { $start_time = microtime(true); $this->prepareParamsForFiltering($params); $filtered_data = $this->getFilteredData($params); $ret = $this->prepareDataForDisplay($filtered_data, $params); $ret['time'] = microtime(true) - $start_time; // d(microtime(true) - $start_time); return $ret; } public function prepareParamsForFiltering(&$params) { $params += array( 'id_shop' => $this->context->shop->id, 'id_shop_group' => $this->context->shop->id_shop_group, 'id_lang' => $this->context->language->id, 'id_currency' => $this->context->currency->id, 'id_customer_group' => $this->context->customer->id_default_group, 'trigger' => Tools::getValue('trigger', 'af_page'), 'order' => $this->formatOrder($params['orderBy'], $params['orderWay']), 'filter_identifiers' => $this->getFilterIdentifiers(), 'to_count_additionally' => array(), 'available_options' => array(), // empty array is used if available_options are not set in $params ); $this->use_merged_attributes = !empty($params['merged_attributes']); if (!empty($params['numeric_slider_values'])) { $this->slider()->assignParamsForNumericSliders($params); } // define "or" params basing on primary_filter if (!empty($params['primary_filter'])) { $primary_filter = $params['primary_filter']; $first_char = Tools::substr($primary_filter, 0, 1); $or_id = Tools::substr($primary_filter, 1); if (in_array($first_char, $params['filter_identifiers'])) { if ($or_id && !empty($params[$first_char][$or_id])) { $params['or-'.$first_char][$or_id] = $params[$first_char][$or_id]; unset($params[$first_char][$or_id]); $key_for_additional_params = $primary_filter; } elseif (!empty($params[$primary_filter])) { $params['or-'.$primary_filter] = $params[$primary_filter]; unset($params[$primary_filter]); $key_for_additional_params = $primary_filter; } if (isset($key_for_additional_params) && !empty($params['available_options'][$key_for_additional_params])) { $to_count_additionally = $params['available_options'][$key_for_additional_params]; if (!is_array($to_count_additionally)) { $to_count_additionally = explode(',', $to_count_additionally); } $params['to_count_additionally'] = array_flip($to_count_additionally); } } } // remove 0 options obtained from selects foreach (array('', 'or-') as $prefix) { foreach ($params['filter_identifiers'] as $i) { $i = $prefix.$i; if (!empty($params[$i])) { if (isset($params[$i][0])) { if (!$params[$i][0]) { unset($params[$i]); } } else { foreach (array_keys($params[$i]) as $id_group) { if (!$params[$i][$id_group][0]) { unset($params[$i][$id_group]); } } } if (empty($params[$i])) { unset($params[$i]); } } } } $params['selected_atts'] = isset($params['a']) ? $params['a'] : array(); if (isset($params['or-a'])) { $params['selected_atts'] += $params['or-a']; } foreach ($params['selected_atts'] as $id_group => $atts) { $params['selected_atts'][$id_group] = array_combine($atts, $atts); } ksort($params['selected_atts']); $params['check_stock'] = !empty($params['combinations_stock']); $params['check_existence'] = !empty($params['combinations_existence']) && $params['selected_atts']; if ($params['check_stock'] || ($params['selected_atts'] && $params['combination_results'])) { $this->selected_combinations = array(); } if (isset($params['controller_product_ids'])) { $params['controller_product_ids'] = $this->formatIDs($params['controller_product_ids']); } // ranges $params['ranges'] = array('p' => array(), 'w' => array()); $params['non_slider_ranges'] = array(); foreach ($params['ranges'] as $identifier => $range) { $range['selected_values'] = array(); if ($range['is_slider'] = isset($params['sliders'][$identifier])) { // sliders $slider = $params['sliders'][$identifier]; if ($this->slider()->isTriggered($slider)) { $range['selected_values'] = array(array($slider['from'], $slider['to'])); } } elseif (isset($params['available_options'][$identifier])) { // range values if (isset($params[$identifier])) { $range['selected_values'] = $params[$identifier]; } elseif (isset($params['or-'.$identifier])) { $range['selected_values'] = $params['or-'.$identifier]; } foreach ($range['selected_values'] as &$val) { $val = ExtendedTools::explodeRangeValue($val); } $range['step'] = isset($params[$identifier.'_range_step']) ? $params[$identifier.'_range_step'] : ''; $params['non_slider_ranges'][$identifier] = $identifier; } else { unset($params['ranges'][$identifier]); continue; } $range['calculate_min_max'] = Tools::getValue('controller') != 'ajax'; $params['ranges'][$identifier] = $range; } if (!empty($params['in_stock'])) { $params['oos_behaviour'] = 3; } $params['customer_groups'] = explode(',', $params['customer_groups']); $params['special_ids'] = array(); foreach (array_keys($this->getSpecialFilters()) as $s) { if ($s != 'in_stock' && !empty($params['available_options'][$s])) { $params['special_ids'][$s] = $this->getSpecialControllerIds($s); } } // adjust price-related params basing on indexations settings if (!$this->settings['indexation']['p']) { foreach (array('available_options', 'ranges', 'non_slider_ranges', 'sliders') as $param_key) { unset($params[$param_key]['p']); } } elseif (isset($params['available_options']['p']) || isset($params['sliders']['p']) || $params['order']['by'] == 'p') { $suffixes = array('g' => $params['id_customer_group'], 'c' => $params['id_currency']); foreach (array_keys($suffixes) as $key) { if (!$this->settings['indexation']['p_'.$key]) { $suffixes[$key] = Configuration::get($this->i['default'][$key]); if ($key == 'c' && $this->context->currency->conversion_rate != 1) { $suffixes[$key] .= ' * '.$this->context->currency->conversion_rate; } } } $params['p_identifier'] = 'p_'.$suffixes['g'].'_'.$suffixes['c']; } } public function getFilteredData(&$params) { // &$params is passed by reference because 'ranges' may be updated: min/max and available_range_options $filtered_ids = $move_to_the_end = $all_matches = $sorted_combinations = $sorted_qties = array(); $count_data = $this->prepareCountData($params); $stock_params = array( 'id_shop' => $params['id_shop'], 'id_shop_group' => $params['id_shop_group'], 'check_stock' => 1, 'oos_behaviour' => $params['oos_behaviour'], ); if ($params['check_stock'] || $params['check_existence']) { $stock_params['check_stock'] = $params['check_stock']; $this->prepareSortedCombinationsData($stock_params, $sorted_combinations, $sorted_qties); } $grouped_parameters = array('c', 'a', 'f'); $ungrouped_parameters = array('m', 's', 't', 'q'); $merged_parameters = array_merge($grouped_parameters, $ungrouped_parameters); foreach ($this->indexationData('get', $params) as $p) { $id = $p['id']; foreach (array('c', 'a', 'f', 'm', 's', 't', 'q', 'g') as $key) { $p[$key] = !empty($p[$key]) ? explode(',', $p[$key]) : array(); } if (!empty($p['g']) && !array_diff($params['customer_groups'], $p['g'])) { continue; } $continue = false; foreach ($grouped_parameters as $key) { // todo:: unset all_matches[c-...] if product was excluded because of OOS and no filters are selected if (isset($params['count_all_matches'])) { foreach ($p[$key] as $param_id) { $all_matches[$key.'-'.$param_id] = 1; } } if (!$continue && isset($params[$key])) { foreach ($params[$key] as $options_in_group) { if (!array_intersect($p[$key], $options_in_group)) { $continue = true; // don't exit the loop here in order to collect data for $all_matches } } } } // define min/max values for ranges foreach ($params['ranges'] as $identifier => $range) { if ($range['calculate_min_max']) { if (!isset($range['max']) || $p[$identifier] > $range['max']) { $params['ranges'][$identifier]['max'] = $p[$identifier]; } if (!isset($range['min']) || $p[$identifier] < $range['min']) { $params['ranges'][$identifier]['min'] = $p[$identifier]; } } } if ($continue) { continue; } // special filters: new product/bestsales etc foreach ($params['special_ids'] as $k => $ids) { if (!empty($params[$k]) && !isset($ids[$id])) { continue 2; } } foreach ($ungrouped_parameters as $key) { if (!empty($params[$key]) && !array_intersect($p[$key], $params[$key])) { continue 2; } } $included_in_count = array(); // ----- exclude non-existant/out-of-stock combinations if ($params['check_stock'] || $params['check_existence']) { $product_combinations = isset($sorted_combinations[$id]) ? $sorted_combinations[$id] : array(); $p['qty'] = isset($sorted_qties[$id][0]) ? $sorted_qties[$id][0] : 0; $product_included = !$product_combinations && !$params['selected_atts'] && isset($sorted_qties[$id][0]); if ($params['check_stock'] && $product_combinations) { $p['qty'] = 0; } $p['a'] = array(); foreach ($product_combinations as $id_comb => $atts) { foreach ($atts as $id_group => $id_att) { foreach ($params['selected_atts'] as $id_group_selected => $att_ids_selected) { if ($id_group_selected != $id_group && !array_intersect($att_ids_selected, $atts)) { continue 2; } } $p['a'][$id_att] = $id_att; $count_key = 'a-'.$id_att; if (isset($params['or-a'][$id_group]) && isset($count_data[$count_key]) && !isset($included_in_count[$count_key])) { $count_data[$count_key]++; $included_in_count[$count_key] = 1; } if (!$params['selected_atts'] || isset($params['selected_atts'][$id_group][$id_att])) { // other atts are already matching $product_included = true; if ($params['check_stock'] && isset($sorted_qties[$id][$id_comb]) && $sorted_qties[$id][$id_comb] > 0) { $p['qty'] += $sorted_qties[$id][$id_comb]; } foreach ($atts as $id_att) { $p['a'][$id_att] = $id_att; } if (isset($this->selected_combinations) && !isset($this->selected_combinations[$id])) { $this->selected_combinations[$id] = $id_comb; } // $p['id_product_attribute'] = $id_comb; // $p['qty_current_combination'] = $sorted_qties[$id][$id_comb]; continue 2; } } } if ($p['qty'] < 1 && $params['oos_behaviour'] == 1) { $move_to_the_end[$id] = $id; } if (!$product_included) { continue; } } if (!$params['check_stock'] && ($params['oos_behaviour'] || !empty($params['available_options']['in_stock']))) { $p['qty'] = $this->getProductQty($id, $stock_params); if ($p['qty'] < 1) { if ($params['oos_behaviour'] == 1) { $move_to_the_end[$id] = $id; } elseif ($params['oos_behaviour'] > 1 && empty($this->allow_oos[$id])) { continue; } } } foreach ($params['ranges'] as $identifier => $range) { if ($range['selected_values']) { $within_range = false; foreach ($range['selected_values'] as $from_to) { if ($p[$identifier] >= $from_to[0] && $p[$identifier] <= $from_to[1]) { $within_range = true; break; } } if (isset($params['or-'.$identifier]) && isset($count_data[$identifier])) { $k = $p[$identifier].''; if (!isset($count_data[$identifier][$k])) { $count_data[$identifier][$k] = 0; } $count_data[$identifier][$k]++; $included_in_count[$identifier][$k] = 1; } if (!$within_range) { continue 2; } } } // additional matches for triggered criteria foreach ($grouped_parameters as $key) { if (isset($params['or-'.$key])) { foreach ($p[$key] as $param_id) { if (isset($params['to_count_additionally'][$param_id])) { $k = $key.'-'.$param_id; if (isset($count_data[$k]) && !isset($included_in_count[$k])) { $count_data[$k]++; $included_in_count[$k] = 1; } } } foreach ($params['or-'.$key] as $options_in_group) { if (!array_intersect($p[$key], $options_in_group)) { continue 3; } } break; // only one 'or-...' is possible in current version } } foreach ($ungrouped_parameters as $key) { if (isset($params['or-'.$key])) { foreach ($p[$key] as $param_id) { if (isset($params['to_count_additionally'][$param_id])) { $k = $key.'-'.$param_id; if (isset($count_data[$k]) && !isset($included_in_count[$k])) { $count_data[$k]++; $included_in_count[$k] = 1; } } } if (!array_intersect($p[$key], $params['or-'.$key])) { continue 2; } break; } } foreach ($merged_parameters as $key) { foreach ($p[$key] as $param_id) { $k = $key.'-'.$param_id; if (isset($count_data[$k]) && !isset($included_in_count[$k])) { $count_data[$k]++; } } } foreach ($params['special_ids'] as $k => $ids) { if (isset($count_data[$k]) && isset($ids[$id])) { $count_data[$k]++; } } if (isset($count_data['in_stock']) && $p['qty'] > 0) { $count_data['in_stock']++; } foreach ($params['non_slider_ranges'] as $identifier) { $k = $p[$identifier].''; if (isset($count_data[$identifier]) && !isset($included_in_count[$identifier][$k])) { if (!isset($count_data[$identifier][$k])) { $count_data[$identifier][$k] = 0; } $count_data[$identifier][$k]++; } } $filtered_ids[$id] = $id; switch ($params['order']['by']) { case 'n': case 'd': case 'r': case 'p': $filtered_ids[$id] = $p[$params['order']['by']]; break; case 'quantity': $filtered_ids[$id] = isset($p['qty']) ? $p['qty'] : $this->getProductQty($id, $stock_params); break; case 'sales': $filtered_ids[$id] = isset($p['sales']) ? $p['sales'] : $this->getProductSales($id); break; case 'position': $filtered_ids[$id] = $this->getProductPosition($id, $params); break; case 'manufacturer_name': $filtered_ids[$id] = $this->getManufacturerName((int)current($p['m'])); break; } } // prepare data for ranged filters (price/weight) foreach ($params['ranges'] as $identifier => &$r) { if (isset($r['step'])) { if ($range_options = $params['available_options'][$identifier]) { $range_options = explode(',', $range_options); } else { // available_options may be empty on first page load, because min/max were not known yet // so we prepare range options here, basing on current min/max values $range_options = $r['available_range_options'] = $this->getRangeOptions($r); } $exploded_range_options = array(); foreach ($range_options as $range_option) { $exploded_range_options[$identifier.'-'.$range_option] = explode('-', $range_option); } if (!empty($count_data[$identifier])) { foreach ($count_data[$identifier] as $value => $num) { foreach ($exploded_range_options as $key => $opt) { if ($value <= $opt[1] && $value >= $opt[0]) { if (!isset($count_data[$key])) { $count_data[$key] = 0; } $count_data[$key] += $num; break; } } } unset($count_data[$identifier]); } } } unset($r); $this->sortFilteredIDs($filtered_ids, $move_to_the_end, $params); return array( 'ids' => $filtered_ids, 'count' => $count_data, 'all_matches' => $all_matches, ); } public function prepareCountData($params) { $count_data = array(); if ($params['count_data'] || $params['hide_zero_matches'] || $params['dim_zero_matches']) { foreach ($params['non_slider_ranges'] as $k) { $count_data[$k] = array(); // processed in a special way } foreach (array_keys($this->getSpecialFilters()) as $k) { if (isset($params['available_options'][$k])) { $count_data[$k] = 0; } } foreach ($params['available_options'] as $k => $options) { if ($options && !isset($count_data[$k])) { $k = Tools::substr($k, 0, 1); if (!is_array($options)) { $options = explode(',', $options); } foreach ($options as $id_opt) { $count_data[$k.'-'.$id_opt] = 0; } } } } return $count_data; } public function prepareDataForDisplay($filtered_data, $params) { $page_keepers = array('af_page', 'p_type'); if (!empty($params['page']) && in_array($params['trigger'], $page_keepers)) { $page = (int)$params['page']; } else { $page = 1; } $products_per_page = $params['nb_items']; $offset = ($page - 1) * $products_per_page; $ids = array_slice($filtered_data['ids'], $offset, $products_per_page); if (isset($this->selected_combinations)) { if (!$params['check_stock'] && !$params['check_existence'] && empty($this->selected_combinations)) { $this->selected_combinations = $this->getSelectedCombinations($ids, $params['selected_atts']); } else { $sc = array(); foreach ($ids as $id) { if (!empty($this->selected_combinations[$id])) { $sc[$id] = $this->selected_combinations[$id]; } } $this->selected_combinations = $sc; } } $total = count($filtered_data['ids']); $ret = array( 'filtered_ids_count' => $total, 'page' => $page, 'products_per_page' => $products_per_page, 'products' => $this->getProductsInfos($ids, $params['id_lang'], $params['id_shop']), 'count_data' => $filtered_data['count'], 'all_matches' => $filtered_data['all_matches'], 'ranges' => $params['ranges'], // TODO: optimize ??? 'trigger' => $params['trigger'], 'product_count_text' => '', 'hide_load_more_btn' => false, ); if ($params['p_type'] > 1) { // load more/infinite scroll $page_from = isset($params['page_from']) ? $params['page_from'] : $page; $page_to = isset($params['page_to']) ? $params['page_to'] : $page; $from = $page_from * $products_per_page - $products_per_page + 1; $to = $page_to * $products_per_page; if ($total <= $to) { $to = $total; $ret['hide_load_more_btn'] = true; } if ($total) { $txt = $this->l('Showing %1$d - %2$d of %3$d items'); $ret['product_count_text'] = sprintf($txt, $from, $to, $total); } } return $ret; } public function sortFilteredIDs(&$filtered_ids, &$move_to_the_end, $params) { if ($params['order']['by'] == 'random') { srand($params['random_seed']); shuffle($filtered_ids); // 0 => $id_0, 1 => $id_1, 2 => $id_2 etc... } else { if ($params['order']['by'] == 'p' && !empty($params['combination_results'])) { $this->adjustCombinationPrices($params['id_shop'], $filtered_ids, $params['selected_atts']); } $params['order']['way'] == 'asc' ? asort($filtered_ids) : arsort($filtered_ids); $filtered_ids = array_keys($filtered_ids); } // instockfirst if ($move_to_the_end && $params['order']['by'] != 'quantity') { $oos_ids = array(); foreach ($filtered_ids as $pos => $id) { if (!empty($move_to_the_end[$id])) { $oos_ids[] = $id; unset($filtered_ids[$pos]); } } if (is_array($filtered_ids)) { $filtered_ids = array_merge($filtered_ids, $oos_ids); } else { $filtered_ids = $oos_ids; } unset($move_to_the_end); } } public function prepareSortedCombinationsData($params, &$sorted_combinations, &$sorted_qties) { $cache_id = $this->cacheID('comb_data', $params); if (!$cache_id || !$cached_data = $this->cache('get', $cache_id)) { $raw_data = $this->db->executeS(' SELECT sa.id_product_attribute as id_comb, sa.quantity as qty, pac.id_attribute as id_att, sa.id_product, a.id_attribute_group as id_group FROM '._DB_PREFIX_.'stock_available sa INNER JOIN '._DB_PREFIX_.'product_shop ps ON ps.id_product = sa.id_product AND ps.active = 1 AND ps.id_shop = '.(int)$params['id_shop'].' LEFT JOIN '._DB_PREFIX_.'product_attribute_shop pas ON pas.id_product_attribute = sa.id_product_attribute AND pas.id_product = sa.id_product AND pas.id_shop = ps.id_shop LEFT JOIN '._DB_PREFIX_.'product_attribute_combination pac ON pac.id_product_attribute = pas.id_product_attribute LEFT JOIN '._DB_PREFIX_.'attribute a ON a.id_attribute = pac.id_attribute WHERE '.pSQL($this->stockQuery($params)).' ORDER BY sa.quantity > 0 DESC, pas.default_on DESC, pas.price ASC, pac.id_attribute ASC '); foreach ($raw_data as $d) { $sorted_qties[$d['id_product']][$d['id_comb']] = $d['qty']; if ($d['id_comb'] && $d['id_group']) { $sorted_combinations[$d['id_product']][$d['id_comb']][$d['id_group']] = $d['id_att']; } } if (!empty($this->use_merged_attributes)) { $this->mergedValues()->mapAttributesInSortedCombinations($raw_data, $sorted_combinations); } if ($cache_id) { $data = array('combinations' => $sorted_combinations, 'qties' => $sorted_qties); $this->cache('save', $cache_id, $data); } } else { $sorted_combinations = $cached_data['combinations']; $sorted_qties = $cached_data['qties']; } } public function stockQuery($params) { $query = $this->addStockShopAssociaton($params['id_shop'], $params['id_shop_group']); if ($params['check_stock'] && $params['oos_behaviour'] > 1) { $allow_ordering_oos = '-1'; if ($params['oos_behaviour'] == 2) { $allow_ordering_oos = '1'.(Configuration::get('PS_ORDER_OUT_OF_STOCK') ? ',2' : ''); } $query .= ' AND (sa.quantity > 0 OR sa.out_of_stock IN ('.$allow_ordering_oos.'))'; } return $query; } public function addStockShopAssociaton($id_shop, $id_shop_group) { $shared_stock = $this->db->getValue(' SELECT share_stock FROM '._DB_PREFIX_.'shop_group WHERE id_shop_group = '.(int)$id_shop_group.' '); return $shared_stock ? 'sa.id_shop_group = '.(int)$id_shop_group : 'sa.id_shop = '.(int)$id_shop; } public function getRangeOptions($range_data) { $range_options = array(); $min = isset($range_data['min']) ? floor($range_data['min']) : 0; $max = isset($range_data['max']) ? ceil($range_data['max']) : 0; $step = $range_data['step']; if (Tools::strpos($step, ',') !== false) { $step = str_replace(array('min', 'max'), array($min, $max), $step); $custom_options = explode(',', $step); foreach ($custom_options as $option) { $range_options[] = implode('-', ExtendedTools::explodeRangeValue($option)); } } else { $step = (int)$step ?: 100; for ($i = 0; $i < $max; $i += $step) { $to = $i + $step; if ($to < $min) { continue; } if ($to > $max) { $to = $max; } $from = count($range_options) ? $i : $min; $range_options[$i] = $from.'-'.$to; } } return $range_options; } public function getProductQty($id, $stock_params) { if (!isset($this->qty_data)) { $this->qty_data = $this->allow_oos = array(); $raw_data = $this->db->executeS(' SELECT sa.id_product, sa.quantity as qty FROM '._DB_PREFIX_.'stock_available sa INNER JOIN '._DB_PREFIX_.'product_shop ps ON ps.id_product = sa.id_product AND ps.active = 1 AND ps.id_shop = '.(int)$stock_params['id_shop'].' WHERE sa.id_product_attribute = 0 AND '.$this->stockQuery($stock_params).' '); foreach ($raw_data as $d) { $this->qty_data[$d['id_product']] = $d['qty']; if ($d['qty'] < 1 && $stock_params['oos_behaviour'] == 2) { $this->allow_oos[$d['id_product']] = 1; } } } $qty = isset($this->qty_data[$id]) ? $this->qty_data[$id] : 0; return $qty; } public function getProductSales($id) { if (!isset($this->sales_data)) { $this->sales_data = array(); $raw_data = $this->db->executeS(' SELECT ps.id_product, ps.quantity FROM '._DB_PREFIX_.'product_sale ps '.Shop::addSqlAssociation('product', 'ps').' WHERE product_shop.active = 1 AND product_shop.visibility <> "none" '); foreach ($raw_data as $d) { $this->sales_data[$d['id_product']] = $d['quantity']; } } return isset($this->sales_data[$id]) ? $this->sales_data[$id] : 0; } public function getProductPosition($id_product, $params) { if (!isset($this->all_positions)) { $this->all_positions = array(); if (!empty($params['controller_product_ids'])) { $position = 1; foreach ($params['controller_product_ids'] as $id) { $this->all_positions[$id] = $position++; } } else { $position_id_cat = $params['id_parent_cat']; // if only 1 category is checked, sort by positions in that category // TODO: add compatibility with top level category blocks (e.g. c31) foreach (array('or-c', 'c') as $k) { if (!empty($params[$k])) { foreach ($params[$k] as $categories) { if (count($categories) == 1) { $position_id_cat = current($categories); break 2; } } } } $raw_data = $this->db->executeS(' SELECT id_product AS id, position FROM '._DB_PREFIX_.'category_product WHERE id_category = '.(int)$position_id_cat.' '); foreach ($raw_data as $d) { $this->all_positions[$d['id']] = $d['position']; } } } return isset($this->all_positions[$id_product]) ? $this->all_positions[$id_product] : 'n'; } public function getManufacturerName($id_manufacturer) { if (!isset($this->m_names)) { $this->m_names = array(); $raw_data = $this->db->executeS(' SELECT id_manufacturer AS id, name FROM '._DB_PREFIX_.'manufacturer WHERE active = 1 '); foreach ($raw_data as $d) { $this->m_names[$d['id']] = $d['name']; } } return isset($this->m_names[$id_manufacturer]) ? $this->m_names[$id_manufacturer] : ''; } /* temporary workaround for calculating/predicting combination prices for proper sorting */ public function adjustCombinationPrices($id_shop, &$filtered_ids, $selected_atts) { if ($selected_atts) { if (empty($this->selected_combinations)) { $ids = array_keys($filtered_ids); $this->selected_combinations = $this->getSelectedCombinations($ids, $selected_atts); } if ($imploded_combination_ids = $this->formatIDs($this->selected_combinations, true)) { $raw_data = $this->db->executeS(' SELECT pa.id_product AS id, pa.id_product_attribute AS ipa, pa.price, pa.default_on AS df, ps.price AS base_price FROM '._DB_PREFIX_.'product_attribute pa INNER JOIN '._DB_PREFIX_.'product_shop ps ON ps.id_product = pa.id_product AND ps.id_shop = '.(int)$id_shop.' WHERE pa.id_product_attribute IN ('.pSQL($imploded_combination_ids).') OR pa.default_on = 1 '); $non_default_impacts = $rates = array(); foreach ($raw_data as $d) { $id = $d['id']; $raw_price = $d['base_price'] + $d['price']; if ($d['df']) { $indexed_price = isset($filtered_ids[$id]) ? $filtered_ids[$id] : 0; $rates[$id] = $raw_price ? $indexed_price/$raw_price : 1; } else { $non_default_impacts[$id] = $raw_price; } } foreach ($non_default_impacts as $id_product => $raw_price) { if (isset($rates[$id_product]) && isset($filtered_ids[$id_product])) { $filtered_ids[$id_product] = $raw_price * $rates[$id_product]; } } } } } public function getSelectedCombinations($product_ids, $selected_atts) { $selected_combinations = $att_ids = $sorted_combinations = array(); if (!$product_ids || !$selected_atts) { return $selected_combinations; } if (!empty($this->use_merged_attributes)) { $this->mergedValues()->replaceMergedAttsWithOriginalValues($selected_atts); } $selected_groups_count = count($selected_atts); foreach ($selected_atts as $atts) { $att_ids += $atts; } $imploded_att_ids = implode(', ', $att_ids); $imploded_product_ids = implode(', ', $product_ids); $raw_data = $this->db->executeS(' SELECT pac.id_attribute, pac.id_product_attribute as id_comb, pa.id_product FROM '._DB_PREFIX_.'product_attribute_combination pac LEFT JOIN '._DB_PREFIX_.'product_attribute pa ON pa.id_product_attribute = pac.id_product_attribute WHERE pa.id_product IN ('.pSQL($imploded_product_ids).') AND pac.id_attribute IN ('.pSQL($imploded_att_ids).') ORDER BY pa.default_on DESC, pa.id_product_attribute ASC '); foreach ($raw_data as $d) { $sorted_combinations[$d['id_product']][$d['id_comb']][$d['id_attribute']] = $d['id_attribute']; } foreach ($sorted_combinations as $id_product => $combinations) { foreach ($combinations as $id_comb => $atts) { if (!isset($selected_combinations[$id_product]) && count($atts) == $selected_groups_count) { $selected_combinations[$id_product] = $id_comb; } } } return $selected_combinations; } public function getProductsInfos($ids, $id_lang, $id_shop, $get_all_properties = true) { if (!$ids) { return array(); } $products_infos = array(); $products_data = $this->db->executeS(' SELECT p.*, product_shop.*, pl.*, image.id_image, il.legend, m.name AS manufacturer_name, '.$this->isNewQuery().' AS new FROM '._DB_PREFIX_.'product p '.Shop::addSqlAssociation('product', 'p').' INNER JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = p.id_product'.Shop::addSqlRestrictionOnLang('pl').' AND pl.id_lang = '.(int)$id_lang.') LEFT JOIN '._DB_PREFIX_.'image image ON (image.id_product = p.id_product AND image.cover = 1) LEFT JOIN '._DB_PREFIX_.'image_lang il ON (il.id_image = image.id_image AND il.id_lang = '.(int)$id_lang.') LEFT JOIN '._DB_PREFIX_.'manufacturer m ON m.id_manufacturer = p.id_manufacturer WHERE p.id_product IN ('.pSQL($this->formatIDs($ids, true)).') '); $positions = array_flip($ids); if ($this->is_17 && $get_all_properties) { $factory = new ProductPresenterFactory($this->context, new TaxConfiguration()); $factory_presenter = $factory->getPresenter(); $factory_settings = $factory->getPresentationSettings(); $lang_obj = new Language($id_lang); } if (!empty($this->selected_combinations)) { $combination_images = $this->getCombinationImages($this->selected_combinations, $id_lang); } foreach ($products_data as $pd) { $id_product = (int)$pd['id_product']; // oos data is kept updated in stock_available table // joining this table in query significantly increases time if there are many $ids $pd['out_of_stock'] = StockAvailable::outOfStock($id_product, $id_shop); // cache_default_attribute is kept up to date in indexProduct() $pd['id_product_attribute'] = $pd['cache_default_attribute']; if (!empty($this->selected_combinations[$id_product])) { $pd['id_product_attribute'] = (int)$this->selected_combinations[$id_product]; if (!empty($combination_images[$id_product])) { $pd['id_image'] = $combination_images[$id_product]['id_image']; $pd['legend'] = $combination_images[$id_product]['legend']; } } if ($get_all_properties) { $pd = Product::getProductProperties($id_lang, $pd); if ($this->is_17 && Tools::getValue('controller') == 'ajax') { $pd = $factory_presenter->present($factory_settings, $pd, $lang_obj); } if (!$this->is_17 && $pd['id_product_attribute'] != $pd['cache_default_attribute']) { $pd['link'] .= $this->addAnchor($id_product, (int)$pd['id_product_attribute'], true); } } $products_infos[$positions[$id_product]] = $pd; } ksort($products_infos); return $products_infos; } public function getCombinationImages($combination_ids, $id_lang) { $combination_images_data = $this->db->executeS(' SELECT i.id_product, i.id_image, il.legend FROM '._DB_PREFIX_.'image i INNER JOIN '._DB_PREFIX_.'product_attribute_image pai ON pai.id_image = i.id_image LEFT JOIN '._DB_PREFIX_.'image_lang il ON (il.id_image = i.id_image AND il.id_lang = '.(int)$id_lang.') WHERE pai.id_product_attribute IN ('.pSQL($this->formatIDs($combination_ids, true)).') ORDER BY i.cover DESC, i.position ASC '); $combination_images = array(); foreach ($combination_images_data as $row) { if (!isset($combination_images[$row['id_product']])) { $combination_images[$row['id_product']] = $row; } } return $combination_images; } /* * Based on $product->getAnchor() */ public function addAnchor($id_product, $id_product_attribute, $with_id = false) { $attributes = Product::getAttributesParams($id_product, $id_product_attribute); $anchor = '#'; $sep = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'); foreach ($attributes as &$a) { foreach ($a as &$b) { $b = str_replace($sep, '_', Tools::link_rewrite($b)); } $id = ($with_id && !empty($a['id_attribute']) ? (int)$a['id_attribute'].$sep : ''); $anchor .= '/'.$id.$a['group'].$sep.$a['name']; } return $anchor; } public function processCustomerFiltersIfRequired(&$filters) { $smarty_vars = array('applied_customer_filters' => array()); if ($this->context->customer->id && $this->getAdjustableCustomerFilters(false)) { $smarty_vars['my_filters_link'] = $this->context->link->getModuleLink($this->name, 'myfilters'); } if ($customer_filters = $this->getCustomerFilters($this->context->customer->id)) { foreach ($customer_filters as $key => $cf) { if (isset($filters[$key])) { if ($filters[$key]['is_slider']) { continue; // no customer filters in sliders } if (count($cf) > 1) { $filters[$key]['type'] = 1; // force checkbox if more than one filters in group are selected } foreach ($cf as $id) { $filters[$key]['forced_values'][$id] = $id; $smarty_vars['applied_customer_filters'][$key][$id] = $id; // make sure customer filter options are available even if there are no matching products $this->products_data['all_matches'][$filters[$key]['first_char'].'-'.$id] = 1; } if ($filters[$key]['type'] == '3') { // names should be defined for customer selected options $sorted_names = array_column($filters[$key]['values'], 'name', 'id'); foreach ($smarty_vars['applied_customer_filters'][$key] as $id) { $smarty_vars['applied_customer_filters'][$key][$id] = isset($sorted_names[$id]) ? $sorted_names[$id] : $id; } } } } } $this->context->smarty->assign($smarty_vars); } public function getCustomerFilters($id_customer) { if (!$this->getAdjustableCustomerFilters(false)) { return false; } $customer_filters = $this->db->getValue(' SELECT filters FROM '._DB_PREFIX_.'af_customer_filters WHERE id_customer = '.(int)$id_customer.' '); if ($customer_filters) { $customer_filters = Tools::jsonDecode($customer_filters, true); } return $customer_filters; } public function ajaxSaveCustomerFilters() { if (!$this->context->customer->id) { exit(); } $submitted_filters = Tools::getValue('filters'); $available_filters = $this->getAvailableFilters(); $data_to_save = array(); foreach (array_keys($available_filters) as $f) { if (!empty($submitted_filters[$f])) { foreach ($submitted_filters[$f] as $id) { $data_to_save[$f][$id] = $id; } } } $data_to_save = Tools::jsonEncode($data_to_save); $query = ' REPLACE INTO '._DB_PREFIX_.'af_customer_filters VALUES ('.(int)$this->context->customer->id.', \''.pSQL($data_to_save).'\') '; $ret = array('success' => $this->db->execute($query)); exit(Tools::jsonEncode($ret)); } public function getAdjustableCustomerFilters($decode = true) { $adjustable_fitlers = Configuration::get('AF_SAVED_CUSTOMER_FILTERS'); if ($decode) { $adjustable_fitlers = $adjustable_fitlers ? Tools::jsonDecode($adjustable_fitlers, true) : array(); } return $adjustable_fitlers; } public function hookDisplayCustomerAccount() { if ($this->getAdjustableCustomerFilters(false)) { $this->defineSettings(); $this->context->smarty->assign(array( 'href' => $this->context->link->getModuleLink($this->name, 'myfilters'), 'layout_classes' => $this->getLayoutClasses(), 'is_17' => $this->is_17, )); return $this->display(__FILE__, 'views/templates/hook/my-filters-tab.tpl'); } } public function cacheID($type, $params) { if (!empty($this->settings['caching'][$type])) { return $type.'_'.implode('_', array_map('intval', $params)); } } public function cache($action, $cache_id, $data = '', $cache_time = 3600) { $ret = true; $full_path = $this->local_path.'cache/'.$cache_id; switch ($action) { case 'get': if ($ret = file_exists($full_path) && (time() - filemtime($full_path) < $cache_time)) { $ret = Tools::jsonDecode(Tools::file_get_contents($full_path), true); } break; case 'save': $ret = file_put_contents($full_path, Tools::jsonEncode($data)) !== false; break; case 'clear': // cached file names can include different parameters, so we unlink all files matching main path foreach (glob($full_path.'*') as $path) { $ret &= unlink($path); } break; case 'info': if ($files = $info = glob($full_path.'*')) { $info = sprintf( $this->l('Cache size: %1$s | last updated: %2$s'), Tools::formatBytes(array_sum(array_map('filesize', $files))).'b', date('Y-m-d H:i:s', (max(array_map('filemtime', $files)))) ); } $ret = $info ?: $this->l('No data'); break; } return $ret; } public function getCronToken() { return Tools::encrypt($this->name); } public function getCronURL($id_shop, $params = array()) { $required_params = array( 'token' => $this->getCronToken(), 'id_shop' => $id_shop, ); foreach ($params as $name => $value) { $required_params[$name] = $value; } return $this->context->link->getModuleLink($this->name, 'cron', $required_params, null, null, $id_shop); } public function throwError($errors, $render_html = true) { if (!is_array($errors)) { $errors = array($errors); } if ($render_html) { $this->context->smarty->assign(array('errors' => $errors)); $html = $this->display(__FILE__, 'views/templates/admin/errors.tpl'); if (!Tools::isSubmit('ajax')) { return $html; } else { $errors = utf8_encode($html); } } die(Tools::jsonEncode(array('errors' => $errors))); } /* * new methods, since 1.7 */ public function getPaginationVariables($page, $products_num, $products_per_page, $current_url) { require_once('src/AmazzingFilterProductSearchProvider.php'); $provider = new AmazzingFilterProductSearchProvider($this); return $provider->getPaginationVariables($page, $products_num, $products_per_page, $current_url); } public function updateQueryString($url, $new_params = array()) { $url = explode('?', $url); $updated_params = !empty($url[1]) ? $this->parseStr($url[1]) : array(); foreach ($new_params as $name => $value) { $updated_params[$name] = $value; if (($name == $this->page_link_rewrite_text && $value == 1) || $value === null) { unset($updated_params[$name]); } } $replacements = array('%2F' => '/', '%2C' => ','); $updated_params = str_replace(array_keys($replacements), $replacements, http_build_query($updated_params)); $updated_url = $url[0].(!empty($updated_params) ? '?'.$updated_params : ''); return $updated_url; } public function getSortingOptions($current_option, $default_option = '', $current_url = '') { $options = $this->getAvailableSortingOptions($default_option); $url_without_order = $this->updateQueryString($current_url, array('order' => null)); $url_without_order .= !strstr($current_url, '?') ? '?' : '&'; $processed_options = array(); foreach ($options as $k => $opt_name) { $k_exploded = explode('.', $k); $processed_options[$k] = array( 'entity' => $k_exploded[0], 'field' => $k_exploded[1], 'direction' => $k_exploded[2], 'label' => $opt_name, 'urlParameter' => $k, 'url' => $url_without_order.'order='.$k, 'current' => $k == $current_option, ); } return $processed_options; // this is simplified version of ProductListingFrontController::getTemplateVarSortOrders() // standard options can be obtained like that: // use PrestaShop\PrestaShop\Core\Product\Search\SortOrderFactory; at the top // $options = (new SortOrderFactory($this->getTranslator()))->getDefaultSortOrders(); } public function getAvailableSortingOptions($default_option = '') { $options = array( // standard options 'product.position.asc' => $this->l('Relevance'), 'product.date_add.desc' => $this->l('New products first'), 'product.name.asc' => $this->l('Name, A to Z'), 'product.name.desc' => $this->l('Name, Z to A'), 'product.price.asc' => $this->l('Price, low to high'), 'product.price.desc' => $this->l('Price, high to low'), 'product.quantity.desc' => $this->l('In stock'), 'product.random.desc' => $this->l('Random'), ); $extra_options = array( // options, that can be defined additionally in module settings 'product.position.desc' => $this->l('Relevance, reverse'), 'product.date_add.asc' => $this->l('Old products first'), 'product.quantity.asc' => $this->l('Stock, reverse'), 'product.sales.desc' => $this->l('Best sales'), 'product.sales.asc' => $this->l('Lowest sales'), 'product.reference.asc' => $this->l('Reference, A to Z'), 'product.reference.desc' => $this->l('Reference, reverse'), 'product.manufacturer_name.asc' => $this->l('Brand, A to Z'), 'product.manufacturer_name.desc' => $this->l('Brand, reverse'), ); $default_option = $default_option ?: 'product.'.$this->settings['general']['default_order_by']. '.'.$this->settings['general']['default_order_way']; if (isset($extra_options[$default_option])) { $options = array($default_option => $extra_options[$default_option]) + $options; } if ($this->current_controller == 'search') { // sorting by position is reverse in search results $options = array('product.position.desc' => $options['product.position.asc']) + $options; unset($options['product.position.asc']); } return $options; } public function hookProductSearchProvider($params) { if ($this->defineFilterParams()) { require_once('src/AmazzingFilterProductSearchProvider.php'); return new AmazzingFilterProductSearchProvider($this); } else { return false; } } public function bo() { if (!isset($this->bo_obj)) { require_once($this->local_path.'classes/bo.php'); $this->bo_obj = new Bo(); } return $this->bo_obj; } public function slider() { if (!isset($this->slider_obj)) { require_once($this->local_path.'classes/Slider.php'); $this->slider_obj = new Slider($this->context); } return $this->slider_obj; } public function mergedValues() { if (!isset($this->merged_values)) { require_once($this->local_path.'classes/MergedValues.php'); $this->merged_values = new MergedValues($this); } return $this->merged_values; } }