Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@@ -0,0 +1,13 @@
# In PrestaShop 9.0 php files are now protected so we allow only specific endpoints to be accessible
<FilesMatch "ps_facetedsearch-.+\.php$">
# Apache 2.2
<IfModule !mod_authz_core.c>
Order Allow,Deny
Allow from all
</IfModule>
# Apache 2.4
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
</FilesMatch>

View File

@@ -0,0 +1,64 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.bootstrap {
.filter_list .filter_list_item {
display: table;
width: 100%;
padding: 5px 0;
margin-bottom: 4px;
background-color: white;
-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
}
.filter_panel {
min-height: 20px;
padding: 7px 7px 0px 7px;
margin-bottom: 20px;
background-color: #ebebeb;
border: 1px solid #d9d9d9;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
header {
margin-bottom: 7px;
}
}
.prestashop-switch {
span {
display: none;
}
}
}
.sortable-ghost {
color: orange;
}
#content.bootstrap {
.form-group-categories .panel {
margin-bottom: 0;
}
}

View File

@@ -0,0 +1,277 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import './blocklayered.scss';
/* eslint-disable no-unused-vars, no-alert */
window.checkForm = function checkForm() {
let isCategorySelected = false;
let isCategoryControllerSelected = false;
let isControllerSelected = false;
let isFilterSelected = false;
$('#categories-treeview input[type=checkbox]').each(function checkCategoriesCheckboxes() {
if ($(this).prop('checked')) {
isCategorySelected = true;
return false;
}
return true;
});
$('input[name="controllers[]"]').each(function checkPagesCheckboxes() {
if ($(this).prop('checked')) {
isControllerSelected = true;
if ($(this).val() === 'category') {
isCategoryControllerSelected = true;
}
}
});
$('.filter_list_item input[type=checkbox]').each(function checkFilterListCheckboxes() {
if ($(this).prop('checked')) {
isFilterSelected = true;
return false;
}
return true;
});
// If no controller is selected at all
if (!isControllerSelected) {
alert(translations.no_selected_controllers);
return false;
}
// If category controller was checked, but no category is selected
if (isCategoryControllerSelected && !isCategorySelected) {
alert(translations.no_selected_categories);
$('#categories-treeview input[type=checkbox]').first().focus();
return false;
}
// If no filter is selected at all
if (!isFilterSelected) {
alert(translations.no_selected_filters);
$('#filter_list_item input[type=checkbox]').first().focus();
return false;
}
return true;
};
$(document).ready(() => {
$('.ajaxcall').click(function onAjaxCall() {
if (this.legend === undefined) {
this.legend = $(this).html();
}
if (this.running === undefined) {
this.running = false;
}
if (this.running === true) {
return false;
}
$('.ajax-message').hide();
this.running = true;
if (typeof (this.restartAllowed) === 'undefined' || this.restartAllowed) {
$(this).html(this.legend + translations.in_progress);
$('#indexing-warning').show();
}
this.restartAllowed = false;
const type = $(this).attr('rel');
$.ajax({
url: `${this.href}&ajax=1`,
context: this,
dataType: 'json',
cache: 'false',
success() {
this.running = false;
this.restartAllowed = true;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(
type === 'price' ? translations.url_indexation_finished : translations.attribute_indexation_finished,
);
$('#ajax-message-ok').show();
},
error() {
this.restartAllowed = true;
$('#indexing-warning').hide();
$('#ajax-message-ko span').html(
type === 'price' ? translations.url_indexation_failed : translations.attribute_indexation_failed,
);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.running = false;
},
});
return false;
});
let totalCount = 0;
$('.ajaxcall-recurcive').each((it, elm) => {
$(elm).click(function onAjaxRecursiveCall(e) {
e.preventDefault();
if (this.cursor === undefined) {
this.cursor = 0;
}
if (this.legend === undefined) {
this.legend = $(this).html();
}
if (this.running === undefined) {
this.running = false;
}
if (this.running === true) {
return false;
}
$('.ajax-message').hide();
this.running = true;
if (typeof (this.restartAllowed) === 'undefined' || this.restartAllowed) {
$(this).html(this.legend + translations.in_progress);
$('#indexing-warning').show();
}
this.restartAllowed = false;
$.ajax({
url: `${this.href}&ajax=1&cursor=${this.cursor}`,
context: this,
dataType: 'json',
cache: 'false',
success(res) {
this.running = false;
if (res.result) {
this.cursor = 0;
totalCount = 0;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(translations.price_indexation_finished);
$('#ajax-message-ok').show();
return;
}
totalCount += parseInt(res.count, 10);
this.cursor = parseInt(res.cursor, 10);
$(this).html(
this.legend + translations.price_indexation_in_progress.replace(
'%s',
`${totalCount}/${res.total}`,
),
);
$(this).click();
},
error(res) {
this.restartAllowed = true;
$('#indexing-warning').hide();
$('#ajax-message-ko span').html(translations.price_indexation_failed);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.cursor = 0;
this.running = false;
},
});
return false;
});
});
if (typeof PS_LAYERED_INDEXED !== 'undefined' && PS_LAYERED_INDEXED) {
$('#url-indexe').click();
$('#full-index').click();
}
if (typeof Sortable !== 'undefined') {
const listFilters = document.getElementById('list-filters');
if (listFilters !== null) {
new Sortable(listFilters, {
animation: 150,
ghostClass: 'sortable-ghost',
});
}
} else {
$('.sortable').sortable({
forcePlaceholderSize: true,
});
}
$('.filter_list_item input[type=checkbox]').click(function onFilterLickItemCheckboxesClicked() {
const currentSelectedFiltersCount = parseInt($('#selected_filters').html(), 10);
$('#selected_filters').html(
$(this).prop('checked') ? currentSelectedFiltersCount + 1 : currentSelectedFiltersCount - 1,
);
});
if (typeof window.filters !== 'undefined') {
const filters = JSON.parse(window.filters);
let container = null;
let $el;
Object.keys(filters).forEach((filter) => {
$el = $(`#${filter}`);
$el.prop('checked', true);
$('#selected_filters').html(parseInt($('#selected_filters').html(), 10) + 1);
$(`select[name="${filter}_filter_type"]`).val(filters[filter].filter_type);
$(`select[name="${filter}_filter_show_limit"]`).val(filters[filter].filter_show_limit);
if (container === null) {
container = $(`#${filter}`).closest('ul');
$el.closest('li').detach().prependTo(container);
} else {
$el.closest('li').detach().insertAfter(container);
}
container = $el.closest('li');
});
}
});
$(document).on('ready', () => {
const layeredDefaultCategory = $('input[name="ps_layered_filter_by_default_category"]');
layeredDefaultCategory.on('change', function initializeOptions(event) {
const elm = $(this);
if (!elm.prop('checked')) {
return;
}
if (elm.val() === '1') {
$('input[name="ps_layered_full_tree"][value="0"]').prop('checked', true);
$('input[name="ps_layered_full_tree"]').prop('disabled', true);
} else {
$('input[name="ps_layered_full_tree"]').prop('disabled', false);
}
});
layeredDefaultCategory.filter('[value="1"]').trigger('change');
});

View File

@@ -0,0 +1,26 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
class LocalizationException {
constructor(message) {
this.message = message;
this.name = 'LocalizationException';
}
}
export default LocalizationException;

View File

@@ -0,0 +1,29 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import NumberFormatter from './number-formatter';
import NumberSymbol from './number-symbol';
import PriceSpecification from './specifications/price';
import NumberSpecification from './specifications/number';
export {
PriceSpecification,
NumberSpecification,
NumberFormatter,
NumberSymbol,
};

View File

@@ -0,0 +1,317 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
/**
* These placeholders are used in CLDR number formatting templates.
* They are meant to be replaced by the correct localized symbols in the number formatting process.
*/
import NumberSymbol from './number-symbol';
import PriceSpecification from './specifications/price';
import NumberSpecification from './specifications/number';
const escapeRE = require('lodash.escaperegexp');
const CURRENCY_SYMBOL_PLACEHOLDER = '¤';
const DECIMAL_SEPARATOR_PLACEHOLDER = '.';
const GROUP_SEPARATOR_PLACEHOLDER = ',';
const MINUS_SIGN_PLACEHOLDER = '-';
const PERCENT_SYMBOL_PLACEHOLDER = '%';
const PLUS_SIGN_PLACEHOLDER = '+';
class NumberFormatter {
/**
* @param NumberSpecification specification Number specification to be used
* (can be a number spec, a price spec, a percentage spec)
*/
constructor(specification) {
this.numberSpecification = specification;
}
/**
* Formats the passed number according to specifications.
*
* @param int|float|string number The number to format
* @param NumberSpecification specification Number specification to be used
* (can be a number spec, a price spec, a percentage spec)
*
* @return string The formatted number
* You should use this this value for display, without modifying it
*/
format(number, specification) {
if (specification !== undefined) {
this.numberSpecification = specification;
}
/*
* We need to work on the absolute value first.
* Then the CLDR pattern will add the sign if relevant (at the end).
*/
const num = Math.abs(number).toFixed(this.numberSpecification.getMaxFractionDigits());
let [majorDigits, minorDigits] = this.extractMajorMinorDigits(num);
majorDigits = this.splitMajorGroups(majorDigits);
minorDigits = this.adjustMinorDigitsZeroes(minorDigits);
// Assemble the final number
let formattedNumber = majorDigits;
if (minorDigits) {
formattedNumber += DECIMAL_SEPARATOR_PLACEHOLDER + minorDigits;
}
// Get the good CLDR formatting pattern. Sign is important here !
const pattern = this.getCldrPattern(number < 0);
formattedNumber = this.addPlaceholders(formattedNumber, pattern);
formattedNumber = this.replaceSymbols(formattedNumber);
formattedNumber = this.performSpecificReplacements(formattedNumber);
return formattedNumber;
}
/**
* Get number's major and minor digits.
*
* Major digits are the "integer" part (before decimal separator),
* minor digits are the fractional part
* Result will be an array of exactly 2 items: [majorDigits, minorDigits]
*
* Usage example:
* list(majorDigits, minorDigits) = this.getMajorMinorDigits(decimalNumber);
*
* @param DecimalNumber number
*
* @return string[]
*/
extractMajorMinorDigits(number) {
// Get the number's major and minor digits.
const result = number.toString().split('.');
const majorDigits = result[0];
const minorDigits = (result[1] === undefined) ? '' : result[1];
return [majorDigits, minorDigits];
}
/**
* Splits major digits into groups.
*
* e.g.: Given the major digits "1234567", and major group size
* configured to 3 digits, the result would be "1 234 567"
*
* @param string majorDigits The major digits to be grouped
*
* @return string The grouped major digits
*/
splitMajorGroups(digit) {
if (!this.numberSpecification.isGroupingUsed()) {
return digit;
}
// Reverse the major digits, since they are grouped from the right.
const majorDigits = digit.split('').reverse();
// Group the major digits.
let groups = [];
groups.push(majorDigits.splice(0, this.numberSpecification.getPrimaryGroupSize()));
while (majorDigits.length) {
groups.push(majorDigits.splice(0, this.numberSpecification.getSecondaryGroupSize()));
}
// Reverse back the digits and the groups
groups = groups.reverse();
const newGroups = [];
groups.forEach((group) => {
newGroups.push(group.reverse().join(''));
});
// Reconstruct the major digits.
return newGroups.join(GROUP_SEPARATOR_PLACEHOLDER);
}
/**
* Adds or remove trailing zeroes, depending on specified min and max fraction digits numbers.
*
* @param string minorDigits Digits to be adjusted with (trimmed or padded) zeroes
*
* @return string The adjusted minor digits
*/
adjustMinorDigitsZeroes(minorDigits) {
let digit = minorDigits;
if (digit.length > this.numberSpecification.getMaxFractionDigits()) {
// Strip any trailing zeroes.
digit = digit.replace(/0+$/, '');
}
if (digit.length < this.numberSpecification.getMinFractionDigits()) {
// Re-add needed zeroes
digit = digit.padEnd(
this.numberSpecification.getMinFractionDigits(),
'0',
);
}
return digit;
}
/**
* Get the CLDR formatting pattern.
*
* @see http://cldr.unicode.org/translation/number-patterns
*
* @param bool isNegative If true, the negative pattern
* will be returned instead of the positive one
*
* @return string The CLDR formatting pattern
*/
getCldrPattern(isNegative) {
if (isNegative) {
return this.numberSpecification.getNegativePattern();
}
return this.numberSpecification.getPositivePattern();
}
/**
* Replace placeholder number symbols with relevant numbering system's symbols.
*
* @param string number
* The number to process
*
* @return string
* The number with replaced symbols
*/
replaceSymbols(number) {
const symbols = this.numberSpecification.getSymbol();
const map = {};
map[DECIMAL_SEPARATOR_PLACEHOLDER] = symbols.getDecimal();
map[GROUP_SEPARATOR_PLACEHOLDER] = symbols.getGroup();
map[MINUS_SIGN_PLACEHOLDER] = symbols.getMinusSign();
map[PERCENT_SYMBOL_PLACEHOLDER] = symbols.getPercentSign();
map[PLUS_SIGN_PLACEHOLDER] = symbols.getPlusSign();
return this.strtr(number, map);
}
/**
* strtr() for JavaScript
* Translate characters or replace substrings
*/
strtr(str, pairs) {
const substrs = Object.keys(pairs).map(escapeRE);
return str.split(RegExp(`(${substrs.join('|')})`))
.map((part) => pairs[part] || part)
.join('');
}
/**
* Add missing placeholders to the number using the passed CLDR pattern.
*
* Missing placeholders can be the percent sign, currency symbol, etc.
*
* e.g. with a currency CLDR pattern:
* - Passed number (partially formatted): 1,234.567
* - Returned number: 1,234.567 ¤
* ("¤" symbol is the currency symbol placeholder)
*
* @see http://cldr.unicode.org/translation/number-patterns
*
* @param formattedNumber
* Number to process
* @param pattern
* CLDR formatting pattern to use
*
* @return string
*/
addPlaceholders(formattedNumber, pattern) {
/*
* Regex groups explanation:
* # : literal "#" character. Once.
* (,#+)* : any other "#" characters group, separated by ",". Zero to infinity times.
* 0 : literal "0" character. Once.
* (\.[0#]+)* : any combination of "0" and "#" characters groups, separated by '.'.
* Zero to infinity times.
*/
return pattern.replace(/#?(,#+)*0(\.[0#]+)*/, formattedNumber);
}
/**
* Perform some more specific replacements.
*
* Specific replacements are needed when number specification is extended.
* For instance, prices have an extended number specification in order to
* add currency symbol to the formatted number.
*
* @param string formattedNumber
*
* @return mixed
*/
performSpecificReplacements(formattedNumber) {
if (this.numberSpecification instanceof PriceSpecification) {
return formattedNumber
.split(CURRENCY_SYMBOL_PLACEHOLDER)
.join(this.numberSpecification.getCurrencySymbol());
}
return formattedNumber;
}
static build(specifications) {
let symbol;
if (undefined !== specifications.numberSymbols) {
symbol = new NumberSymbol(...specifications.numberSymbols);
} else {
symbol = new NumberSymbol(...specifications.symbol);
}
let specification;
if (specifications.currencySymbol) {
specification = new PriceSpecification(
specifications.positivePattern,
specifications.negativePattern,
symbol,
parseInt(specifications.maxFractionDigits, 10),
parseInt(specifications.minFractionDigits, 10),
specifications.groupingUsed,
specifications.primaryGroupSize,
specifications.secondaryGroupSize,
specifications.currencySymbol,
specifications.currencyCode,
);
} else {
specification = new NumberSpecification(
specifications.positivePattern,
specifications.negativePattern,
symbol,
parseInt(specifications.maxFractionDigits, 10),
parseInt(specifications.minFractionDigits, 10),
specifications.groupingUsed,
specifications.primaryGroupSize,
specifications.secondaryGroupSize,
);
}
return new NumberFormatter(specification);
}
}
export default NumberFormatter;

View File

@@ -0,0 +1,222 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from './exception/localization';
class NumberSymbol {
/**
* NumberSymbolList constructor.
*
* @param string decimal Decimal separator character
* @param string group Digits group separator character
* @param string list List elements separator character
* @param string percentSign Percent sign character
* @param string minusSign Minus sign character
* @param string plusSign Plus sign character
* @param string exponential Exponential character
* @param string superscriptingExponent Superscripting exponent character
* @param string perMille Permille sign character
* @param string infinity The infinity sign. Corresponds to the IEEE infinity bit pattern.
* @param string nan The NaN (Not A Number) sign. Corresponds to the IEEE NaN bit pattern.
*
* @throws LocalizationException
*/
constructor(
decimal,
group,
list,
percentSign,
minusSign,
plusSign,
exponential,
superscriptingExponent,
perMille,
infinity,
nan,
) {
this.decimal = decimal;
this.group = group;
this.list = list;
this.percentSign = percentSign;
this.minusSign = minusSign;
this.plusSign = plusSign;
this.exponential = exponential;
this.superscriptingExponent = superscriptingExponent;
this.perMille = perMille;
this.infinity = infinity;
this.nan = nan;
this.validateData();
}
/**
* Get the decimal separator.
*
* @return string
*/
getDecimal() {
return this.decimal;
}
/**
* Get the digit groups separator.
*
* @return string
*/
getGroup() {
return this.group;
}
/**
* Get the list elements separator.
*
* @return string
*/
getList() {
return this.list;
}
/**
* Get the percent sign.
*
* @return string
*/
getPercentSign() {
return this.percentSign;
}
/**
* Get the minus sign.
*
* @return string
*/
getMinusSign() {
return this.minusSign;
}
/**
* Get the plus sign.
*
* @return string
*/
getPlusSign() {
return this.plusSign;
}
/**
* Get the exponential character.
*
* @return string
*/
getExponential() {
return this.exponential;
}
/**
* Get the exponent character.
*
* @return string
*/
getSuperscriptingExponent() {
return this.superscriptingExponent;
}
/**
* Gert the per mille symbol (often "‰").
*
* @see https://en.wikipedia.org/wiki/Per_mille
*
* @return string
*/
getPerMille() {
return this.perMille;
}
/**
* Get the infinity symbol (often "∞").
*
* @see https://en.wikipedia.org/wiki/Infinity_symbol
*
* @return string
*/
getInfinity() {
return this.infinity;
}
/**
* Get the NaN (not a number) sign.
*
* @return string
*/
getNan() {
return this.nan;
}
/**
* Symbols list validation.
*
* @throws LocalizationException
*/
validateData() {
if (!this.decimal || typeof this.decimal !== 'string') {
throw new LocalizationException('Invalid decimal');
}
if (!this.group || typeof this.group !== 'string') {
throw new LocalizationException('Invalid group');
}
if (!this.list || typeof this.list !== 'string') {
throw new LocalizationException('Invalid symbol list');
}
if (!this.percentSign || typeof this.percentSign !== 'string') {
throw new LocalizationException('Invalid percentSign');
}
if (!this.minusSign || typeof this.minusSign !== 'string') {
throw new LocalizationException('Invalid minusSign');
}
if (!this.plusSign || typeof this.plusSign !== 'string') {
throw new LocalizationException('Invalid plusSign');
}
if (!this.exponential || typeof this.exponential !== 'string') {
throw new LocalizationException('Invalid exponential');
}
if (!this.superscriptingExponent || typeof this.superscriptingExponent !== 'string') {
throw new LocalizationException('Invalid superscriptingExponent');
}
if (!this.perMille || typeof this.perMille !== 'string') {
throw new LocalizationException('Invalid perMille');
}
if (!this.infinity || typeof this.infinity !== 'string') {
throw new LocalizationException('Invalid infinity');
}
if (!this.nan || typeof this.nan !== 'string') {
throw new LocalizationException('Invalid nan');
}
}
}
export default NumberSymbol;

View File

@@ -0,0 +1,170 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from '../exception/localization';
import NumberSymbol from '../number-symbol';
class NumberSpecification {
/**
* Number specification constructor.
*
* @param string positivePattern CLDR formatting pattern for positive amounts
* @param string negativePattern CLDR formatting pattern for negative amounts
* @param NumberSymbol symbol Number symbol
* @param int maxFractionDigits Maximum number of digits after decimal separator
* @param int minFractionDigits Minimum number of digits after decimal separator
* @param bool groupingUsed Is digits grouping used ?
* @param int primaryGroupSize Size of primary digits group in the number
* @param int secondaryGroupSize Size of secondary digits group in the number
*
* @throws LocalizationException
*/
constructor(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
) {
this.positivePattern = positivePattern;
this.negativePattern = negativePattern;
this.symbol = symbol;
this.maxFractionDigits = maxFractionDigits;
// eslint-disable-next-line
this.minFractionDigits = maxFractionDigits < minFractionDigits ? maxFractionDigits : minFractionDigits;
this.groupingUsed = groupingUsed;
this.primaryGroupSize = primaryGroupSize;
this.secondaryGroupSize = secondaryGroupSize;
if (!this.positivePattern || typeof this.positivePattern !== 'string') {
throw new LocalizationException('Invalid positivePattern');
}
if (!this.negativePattern || typeof this.negativePattern !== 'string') {
throw new LocalizationException('Invalid negativePattern');
}
if (!this.symbol || !(this.symbol instanceof NumberSymbol)) {
throw new LocalizationException('Invalid symbol');
}
if (typeof this.maxFractionDigits !== 'number') {
throw new LocalizationException('Invalid maxFractionDigits');
}
if (typeof this.minFractionDigits !== 'number') {
throw new LocalizationException('Invalid minFractionDigits');
}
if (typeof this.groupingUsed !== 'boolean') {
throw new LocalizationException('Invalid groupingUsed');
}
if (typeof this.primaryGroupSize !== 'number') {
throw new LocalizationException('Invalid primaryGroupSize');
}
if (typeof this.secondaryGroupSize !== 'number') {
throw new LocalizationException('Invalid secondaryGroupSize');
}
}
/**
* Get symbol.
*
* @return NumberSymbol
*/
getSymbol() {
return this.symbol;
}
/**
* Get the formatting rules for this number (when positive).
*
* This pattern uses the Unicode CLDR number pattern syntax
*
* @return string
*/
getPositivePattern() {
return this.positivePattern;
}
/**
* Get the formatting rules for this number (when negative).
*
* This pattern uses the Unicode CLDR number pattern syntax
*
* @return string
*/
getNegativePattern() {
return this.negativePattern;
}
/**
* Get the maximum number of digits after decimal separator (rounding if needed).
*
* @return int
*/
getMaxFractionDigits() {
return this.maxFractionDigits;
}
/**
* Get the minimum number of digits after decimal separator (fill with "0" if needed).
*
* @return int
*/
getMinFractionDigits() {
return this.minFractionDigits;
}
/**
* Get the "grouping" flag. This flag defines if digits
* grouping should be used when formatting this number.
*
* @return bool
*/
isGroupingUsed() {
return this.groupingUsed;
}
/**
* Get the size of primary digits group in the number.
*
* @return int
*/
getPrimaryGroupSize() {
return this.primaryGroupSize;
}
/**
* Get the size of secondary digits groups in the number.
*
* @return int
*/
getSecondaryGroupSize() {
return this.secondaryGroupSize;
}
}
export default NumberSpecification;

View File

@@ -0,0 +1,108 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from '../exception/localization';
import NumberSpecification from './number';
/**
* Currency display option: symbol notation.
*/
const CURRENCY_DISPLAY_SYMBOL = 'symbol';
class PriceSpecification extends NumberSpecification {
/**
* Price specification constructor.
*
* @param string positivePattern CLDR formatting pattern for positive amounts
* @param string negativePattern CLDR formatting pattern for negative amounts
* @param NumberSymbol symbol Number symbol
* @param int maxFractionDigits Maximum number of digits after decimal separator
* @param int minFractionDigits Minimum number of digits after decimal separator
* @param bool groupingUsed Is digits grouping used ?
* @param int primaryGroupSize Size of primary digits group in the number
* @param int secondaryGroupSize Size of secondary digits group in the number
* @param string currencySymbol Currency symbol of this price (eg. : €)
* @param currencyCode Currency code of this price (e.g.: EUR)
*
* @throws LocalizationException
*/
constructor(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
currencySymbol,
currencyCode,
) {
super(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
);
this.currencySymbol = currencySymbol;
this.currencyCode = currencyCode;
if (!this.currencySymbol || typeof this.currencySymbol !== 'string') {
throw new LocalizationException('Invalid currencySymbol');
}
if (!this.currencyCode || typeof this.currencyCode !== 'string') {
throw new LocalizationException('Invalid currencyCode');
}
}
/**
* Get type of display for currency symbol.
*
* @return string
*/
static getCurrencyDisplay() {
return CURRENCY_DISPLAY_SYMBOL;
}
/**
* Get the currency symbol
* e.g.: €.
*
* @return string
*/
getCurrencySymbol() {
return this.currencySymbol;
}
/**
* Get the currency ISO code
* e.g.: EUR.
*
* @return string
*/
getCurrencyCode() {
return this.currencyCode;
}
}
export default PriceSpecification;

View File

@@ -0,0 +1,33 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import refreshSliders from './slider';
import {showOverlay, hideOverlay} from './overlay';
$(document).ready(() => {
prestashop.on('updateProductList', () => {
hideOverlay();
refreshSliders();
});
refreshSliders();
prestashop.on('updateFacets', () => {
showOverlay();
});
});

View File

@@ -0,0 +1,56 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
@mixin text-ellipsis() {
width: calc(100% - 30px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#search_filters {
.facet {
.title {
display: flex;
.collapse-icons {
margin-left: auto;
}
}
.facet-title {
@include text-ellipsis();
}
.facet-label {
width: 100%;
text-align: left;
.custom-checkbox,
.custom-radio {
top: -7px;
margin-right: 0;
}
.color {
margin-left: 0;
}
a {
@include text-ellipsis();
}
}
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import 'jquery-ui-touch-punch';
import './events';
import './slider.scss';
import './facet.scss';

View File

@@ -0,0 +1,43 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import './overlay.scss';
const template = `<div class="faceted-overlay">
<div class="overlay__inner">
<div class="overlay__content"><span class="spinner"></span></div>
</div>
</div>`;
function show() {
if ($('.faceted-overlay').length === 1) {
return;
}
$('body').append(template);
}
function hide() {
$('.faceted-overlay').remove();
}
export {
show as showOverlay,
hide as hideOverlay,
};

View File

@@ -0,0 +1,60 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.faceted-overlay {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: fixed;
background-color: rgba(25, 25, 25, 0.5);
z-index: 100;
.overlay__inner {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: absolute;
}
.overlay__content {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.spinner {
width: 75px;
height: 75px;
display: inline-block;
border-width: 2px;
border-color: rgba(255, 255, 255, 0.05);
border-top-color: #fff;
animation: spin 1s infinite linear;
border-radius: 100%;
border-style: solid;
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import getQueryParameters from './urlparser';
import NumberFormatter from '../cldr/number-formatter';
const formatters = {};
const displayLabelBlock = (formatterId, displayBlock, min, max) => {
if (formatters[formatterId] === undefined) {
displayBlock.text(
displayBlock.text().replace(
/([^\d]*)(?:[\d\s.,]+)([^\d]+)(?:[\d\s.,]+)(.*)/,
`$1${min}$2${max}$3`,
),
);
} else {
displayBlock.text(
`${formatters[formatterId].format(min)} - ${formatters[formatterId].format(max)}`,
);
}
};
/**
* Refresh facets sliders
*/
const refreshSliders = () => {
$('.faceted-slider').each(function initializeSliders() {
const $el = $(this);
const values = $el.data('slider-values');
const specifications = $el.data('slider-specifications');
if (specifications !== null && specifications !== undefined) {
formatters[$el.data('slider-id')] = NumberFormatter.build(specifications);
}
displayLabelBlock(
$el.data('slider-id'),
$(`#facet_label_${$el.data('slider-id')}`),
values === null ? $el.data('slider-min') : values[0],
values === null ? $el.data('slider-max') : values[1],
);
$(`#slider-range_${$el.data('slider-id')}`).slider({
range: true,
min: $el.data('slider-min'),
max: $el.data('slider-max'),
values: [
values === null ? $el.data('slider-min') : values[0],
values === null ? $el.data('slider-max') : values[1],
],
stop(event, ui) {
const nextEncodedFacetsURL = $el.data('slider-encoded-url');
const urlsSplitted = nextEncodedFacetsURL.split('?');
let queryParams = [];
// Retrieve parameters if exists
if (urlsSplitted.length > 1) {
queryParams = getQueryParameters(urlsSplitted[1]);
}
let found = false;
queryParams.forEach((query) => {
if (query.name === 'q') {
found = true;
}
});
if (!found) {
queryParams.push({name: 'q', value: ''});
}
// Update query parameter
queryParams.forEach((query) => {
if (query.name === 'q') {
// eslint-disable-next-line
query.value += [
query.value.length > 0 ? '/' : '',
$el.data('slider-label'),
'-',
$el.data('slider-unit'),
'-',
ui.values[0],
'-',
ui.values[1],
].join('');
}
});
const requestUrl = [
urlsSplitted[0],
'?',
$.param(queryParams),
].join('');
prestashop.emit(
'updateFacets',
requestUrl,
);
},
slide(event, ui) {
displayLabelBlock(
$el.data('slider-id'),
$(`#facet_label_${$el.data('slider-id')}`),
ui.values[0],
ui.values[1],
);
},
});
});
};
export default refreshSliders;

View File

@@ -0,0 +1,40 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
#search_filters {
.ui-slider-horizontal {
.ui-slider-handle {
margin-left: -1px;
cursor: pointer;
}
}
.ui-widget-header {
background: #555;
}
.ui-slider {
.ui-slider-handle {
top: -.45em;
width: 0.4em;
background: #fff;
border: 1px solid #555;
}
}
.ui-slider-horizontal {
height: .4em;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
const getQueryParameters = (params) => params.split('&').map((str) => {
const [key, val] = str.split('=');
return {
name: key,
value: decodeURIComponent(val).replace(/\+/g, ' '),
};
});
export default getQueryParameters;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../');
exit;

3106
modules/ps_facetedsearch/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Faceted search]]></displayName>
<version><![CDATA[4.0.3]]></version>
<description><![CDATA[Displays a block allowing multiple filters.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[front_office_features]]></tab>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
<limited_countries></limited_countries>
</module>

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,10 @@
services:
_defaults:
public: true
prestashop.module.ps_facetedsearch.constraint.url_segment_validator:
class: PrestaShop\Module\FacetedSearch\Constraint\UrlSegmentValidator
arguments:
- '@prestashop.adapter.tools'
tags:
- { name: validator.constraint_validator }

View File

@@ -0,0 +1,80 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
class Ps_FacetedSearchCronModuleFrontController extends ModuleFrontController
{
public function __construct()
{
parent::__construct();
$this->ajax = true;
}
public function postProcess()
{
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token')) {
header('HTTP/1.1 403 Forbidden');
header('Status: 403 Forbidden');
$this->ajaxRender('Bad token');
return;
}
$action = Tools::getValue('action');
switch ($action) {
case 'indexAttributes':
Shop::setContext(Shop::CONTEXT_ALL);
$psFacetedsearch = new Ps_Facetedsearch();
$psFacetedsearch->indexAttributes();
$psFacetedsearch->indexFeatures();
$psFacetedsearch->indexAttributeGroup();
$this->ajaxRender('1');
break;
case 'clearCache':
$psFacetedsearch = new Ps_Facetedsearch();
$this->ajaxRender($psFacetedsearch->invalidateLayeredFilterBlockCache());
break;
case 'indexPrices':
Shop::setContext(Shop::CONTEXT_ALL);
$module = new Ps_Facetedsearch();
if (Tools::getValue('full')) {
$this->ajaxRender($module->fullPricesIndexProcess((int) Tools::getValue('cursor'), (bool) Tools::getValue('ajax'), true));
} else {
$this->ajaxRender($module->pricesIndexProcess((int) Tools::getValue('cursor'), (bool) Tools::getValue('ajax')));
}
break;
default:
header('HTTP/1.1 403 Forbidden');
header('Status: 403 Forbidden');
$this->ajaxRender('Unknown action');
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

16087
modules/ps_facetedsearch/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
/*
* This standalone endpoint is deprecated, it should not be used anymore and should be removed along with the
* htaccess file that still allows it to work despite the security policy from the core forbidding this kind
* of file to be executed.
*/
@trigger_error('This endpoint has been deprecated and will be removed in the next major version for this module, you should rely on Ps_FacetedSearchCronModuleFrontController instead.', E_USER_DEPRECATED);
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}
Shop::setContext(Shop::CONTEXT_ALL);
$psFacetedsearch = new Ps_Facetedsearch();
$psFacetedsearch->indexAttributes();
$psFacetedsearch->indexFeatures();
$psFacetedsearch->indexAttributeGroup();
echo 1;

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
/*
* This standalone endpoint is deprecated, it should not be used anymore and should be removed along with the
* htaccess file that still allows it to work despite the security policy from the core forbidding this kind
* of file to be executed.
*/
@trigger_error('This endpoint has been deprecated and will be removed in the next major version for this module, you should rely on Ps_FacetedSearchCronModuleFrontController instead.', E_USER_DEPRECATED);
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}
$psFacetedsearch = new Ps_Facetedsearch();
echo $psFacetedsearch->invalidateLayeredFilterBlockCache();

View File

@@ -0,0 +1,42 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
/*
* This standalone endpoint is deprecated, it should not be used anymore and should be removed along with the
* htaccess file that still allows it to work despite the security policy from the core forbidding this kind
* of file to be executed.
*/
@trigger_error('This endpoint has been deprecated and will be removed in the next major version for this module, you should rely on Ps_FacetedSearchCronModuleFrontController instead.', E_USER_DEPRECATED);
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}
Shop::setContext(Shop::CONTEXT_ALL);
$module = new Ps_Facetedsearch();
if (Tools::getValue('full')) {
echo $module->fullPricesIndexProcess((int) Tools::getValue('cursor'), (bool) Tools::getValue('ajax'), true);
} else {
echo $module->pricesIndexProcess((int) Tools::getValue('cursor'), (bool) Tools::getValue('ajax'));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{if isset($listing.rendered_active_filters)}
{$listing.rendered_active_filters nofilter}
{/if}
{if isset($listing.rendered_facets)}
{$listing.rendered_facets nofilter}
{/if}

View File

@@ -0,0 +1,313 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Doctrine\Common\Collections\ArrayCollection;
abstract class AbstractAdapter implements InterfaceAdapter
{
/**
* @var ArrayCollection
*/
protected $filters;
/**
* @var ArrayCollection
*/
protected $operationsFilters;
/**
* @var ArrayCollection
*/
protected $selectFields;
/**
* @var ArrayCollection
*/
protected $groupFields;
protected $orderField = 'id_product';
protected $orderDirection = 'DESC';
/** @var InterfaceAdapter */
protected $initialPopulation = null;
public function __construct()
{
$this->groupFields = new ArrayCollection();
$this->selectFields = new ArrayCollection();
$this->filters = new ArrayCollection();
$this->operationsFilters = new ArrayCollection();
}
public function __clone()
{
$this->filters = clone $this->filters;
$this->operationsFilters = clone $this->operationsFilters;
$this->groupFields = clone $this->groupFields;
$this->selectFields = clone $this->selectFields;
}
/**
* {@inheritdoc}
*/
public function getInitialPopulation()
{
return $this->initialPopulation;
}
/**
* {@inheritdoc}
*/
public function resetFilter($filterName)
{
if ($this->filters->offsetExists($filterName)) {
$this->filters->offsetUnset($filterName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function resetOperationsFilter($filterName)
{
if ($this->operationsFilters->offsetExists($filterName)) {
$this->operationsFilters->offsetUnset($filterName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function resetOperationsFilters()
{
$this->operationsFilters = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function resetAll()
{
$this->selectFields = new ArrayCollection();
$this->groupFields = new ArrayCollection();
$this->filters = new ArrayCollection();
$this->operationsFilters = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function getFilter($filterName)
{
if (isset($this->filters[$filterName])) {
return $this->filters[$filterName];
}
return null;
}
/**
* {@inheritdoc}
*/
public function getOrderDirection()
{
return $this->orderDirection;
}
/**
* {@inheritdoc}
*/
public function getOrderField()
{
return $this->orderField;
}
/**
* {@inheritdoc}
*/
public function getGroupFields()
{
return $this->groupFields;
}
/**
* {@inheritdoc}
*/
public function getSelectFields()
{
return $this->selectFields;
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return $this->filters;
}
/**
* {@inheritdoc}
*/
public function getOperationsFilters()
{
return $this->operationsFilters;
}
/**
* {@inheritdoc}
*/
public function copyFilters(InterfaceAdapter $adapter)
{
$this->filters = clone $adapter->getFilters();
$this->operationsFilters = clone $adapter->getOperationsFilters();
}
/**
* {@inheritdoc}
*/
public function addFilter($filterName, $values, $operator = '=')
{
$filters = $this->filters->get($filterName);
if (!isset($filters[$operator])) {
$filters[$operator] = [];
}
$filters[$operator][] = $values;
$this->filters->set($filterName, $filters);
return $this;
}
/**
* {@inheritdoc}
*/
public function addOperationsFilter($filterName, array $operations = [])
{
$this->operationsFilters->set($filterName, $operations);
return $this;
}
/**
* {@inheritdoc}
*/
public function addSelectField($fieldName)
{
if (!$this->selectFields->contains($fieldName)) {
$this->selectFields->add($fieldName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setSelectFields($selectFields)
{
$this->selectFields = new ArrayCollection($selectFields);
return $this;
}
/**
* {@inheritdoc}
*/
public function resetSelectField()
{
$this->selectFields = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function addGroupBy($groupField)
{
$this->groupFields->add($groupField);
return $this;
}
/**
* {@inheritdoc}
*/
public function setGroupFields($groupFields)
{
$this->groupFields = new ArrayCollection($groupFields);
return $this;
}
/**
* {@inheritdoc}
*/
public function resetGroupBy()
{
$this->groupFields = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function setFilter($filterName, $value)
{
if ($value !== null) {
$this->filters->set($filterName, $value);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setOrderField($fieldName)
{
$this->orderField = $fieldName;
return $this;
}
/**
* {@inheritdoc}
*/
public function setOrderDirection($direction)
{
$this->orderDirection = $direction === 'desc' ? 'desc' : 'asc';
return $this;
}
}

View File

@@ -0,0 +1,284 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
interface InterfaceAdapter
{
/**
* Set order by field
*
* @param string $fieldName
*
* @return self
*/
public function setOrderField($fieldName);
/**
* Set the order by direction for the given field
*
* @param string $direction
*
* @return self
*/
public function setOrderDirection($direction);
/**
* Execute the search
*
* @return mixed
*/
public function execute();
/**
* Get the current query
*
* @return string
*/
public function getQuery();
/**
* Get the min & max value of the field filedName associated with the current search
*
* @param string $fieldName
*
* @return mixed
*/
public function getMinMaxValue($fieldName);
/**
* Get the min & max value of the price associated with the current search
*
* @return array
*/
public function getMinMaxPriceValue();
/**
* Return order direction associated with the current search
*
* @return mixed
*/
public function getOrderDirection();
/**
* Return order field associated with the current search
*
* @return mixed
*/
public function getOrderField();
/**
* Return all group fields associated with the current search
*
* @return mixed
*/
public function getGroupFields();
/**
* Return all selected fields associated with the current search
*
* @return mixed
*/
public function getSelectFields();
/**
* Return all the filters associated with the current search
*
* @return mixed
*/
public function getFilters();
/**
* Return all the operations filters associated with the current search
*
* @return mixed
*/
public function getOperationsFilters();
/**
* Return the number of results associated for the current search
*
* @return int
*/
public function count();
/**
* Move the current search into the "initialPopulation"
* This initialPopulation will be used to generate the first derived table 'FROM (SELECT ...)' in the final query
* e.g. : SELECT ... FROM (initialPopulation) p JOIN ....
*/
public function useFiltersAsInitialPopulation();
/**
* Create a new SearchAdapter, keeping the initialPopulation of the current Search
*
* @param string $resetFilter reset this filter inside the initialPopulation
* @param bool $skipInitialPopulation if enable, do not copy the initialPopulation filter
*
* @return InterfaceAdapter
*/
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false);
/**
* Add a new filter with filterName, operator & values to the current search
* If several values are provided with the = operator, it's converted automatically to a IN () in the final query
*
* @param string $filterName
* @param array $values
* @param string $operator
*
* @return self
*/
public function addFilter($filterName, $values, $operator = '=');
/**
* Add a stack of operations with filterName. Operations must contains filterName, values and to the current search
*
* @param string $filterName
* @param array $operations
*
* @return self
*/
public function addOperationsFilter($filterName, array $operations);
/**
* Add fieldName in the current search result. If the field already exists, it's skipped.
*
* @param string $fieldName
*
* @return self
*/
public function addSelectField($fieldName);
/**
* Returns the number of distinct products, group by fieldName values
*
* @param string $fieldName
*
* @return mixed
*/
public function valueCount($fieldName = null);
/**
* Reset the operations filters
*
* @return self
*/
public function resetOperationsFilters();
/**
* Reset the operations filter for the given filterName
*
* @param string $filterName
*
* @return self
*/
public function resetOperationsFilter($filterName);
/**
* Reset the filter for the given filterName
*
* @param string $filterName
*
* @return self
*/
public function resetFilter($filterName);
/**
* Return the filter associated with filterName
*
* @param string $filterName
*
* @return mixed
*/
public function getFilter($filterName);
/**
* Set the filterName to the given array value
*
* @param string $filterName
* @param mixed $value
*
* @return mixed
*/
public function setFilter($filterName, $value);
/**
* Return the current initialPopulation
*
* @return self|null
*/
public function getInitialPopulation();
/**
* Return all the filters / groupFields / selectFields
*
* @return self
*/
public function resetAll();
/**
* Copy all the filters & operationsFilters from adapter to the current search
*
* @param InterfaceAdapter $adapter
*/
public function copyFilters(InterfaceAdapter $adapter);
/**
* Set all the select fields
*
* @param array $selectFields
*
* @return self
*/
public function setSelectFields($selectFields);
/**
* Reset all the select fields
*
* @return self
*/
public function resetSelectField();
/**
* Add a group by field
*
* @param string $groupField
*
* @return self
*/
public function addGroupBy($groupField);
/**
* Set the group by fields
*
* @param array $groupFields
*
* @return self
*/
public function setGroupFields($groupFields);
/**
* Reset the group by conditions
*
* @return self
*/
public function resetGroupBy();
}

View File

@@ -0,0 +1,877 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Configuration;
use Context;
use Db;
use Doctrine\Common\Collections\ArrayCollection;
use Product;
use StockAvailable;
class MySQL extends AbstractAdapter
{
/**
* @var string
*/
const TYPE = 'MySQL';
/**
* @var string
*/
const LEFT_JOIN = 'LEFT JOIN';
/**
* @var string
*/
const INNER_JOIN = 'INNER JOIN';
/**
* {@inheritdoc}
*/
public function getMinMaxPriceValue()
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$mysqlAdapter->setSelectFields(['price_min', 'MIN(price_min) as min, MAX(price_max) as max']);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return [floor((float) $result[0]['min']), ceil((float) $result[0]['max'])];
}
/**
* {@inheritdoc}
*/
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false)
{
$mysqlAdapter = new self();
if ($this->getInitialPopulation() !== null && !$skipInitialPopulation) {
$mysqlAdapter->initialPopulation = clone $this->getInitialPopulation();
if ($resetFilter) {
// Try to reset filter & operations filter
$mysqlAdapter->initialPopulation->resetFilter($resetFilter);
$mysqlAdapter->initialPopulation->resetOperationsFilter($resetFilter);
}
}
return $mysqlAdapter;
}
/**
* {@inheritdoc}
*/
public function execute()
{
return $this->getDatabase()->executeS($this->getQuery());
}
/**
* Construct the final sql query
*
* @return string
*/
public function getQuery()
{
// Prepare mapping for joined tables
$filterToTableMapping = $this->getFieldMapping();
// Process and generate all fields for the SQL query below
$orderField = $this->computeOrderByField($filterToTableMapping);
$selectFields = $this->computeSelectFields($filterToTableMapping);
$whereConditions = $this->computeWhereConditions($filterToTableMapping);
$joinConditions = $this->computeJoinConditions($filterToTableMapping);
$groupFields = $this->computeGroupByFields($filterToTableMapping);
// Now, let's build the query...
// If this query IS the initial population (the base table), we are selecting from product table
if ($this->getInitialPopulation() === null) {
$referenceTable = _DB_PREFIX_ . 'product';
// If not, we will call this function again but for the initial population
} else {
$referenceTable = '(' . $this->getInitialPopulation()->getQuery() . ')';
}
// Construct the base query
$query = 'SELECT ' . implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p';
// Add join conditions if any
foreach ($joinConditions as $joinAliasInfos) {
foreach ($joinAliasInfos as $tableAlias => $joinInfos) {
$query .= ' ' . $joinInfos['joinType'] . ' ' . _DB_PREFIX_ . $joinInfos['tableName'] . ' ' .
$tableAlias . ' ON ' . $joinInfos['joinCondition'];
}
}
// Add where conditions if any
if (!empty($whereConditions)) {
$query .= ' WHERE ' . implode(' AND ', $whereConditions);
}
// Add groupping
if (!empty($groupFields)) {
$query .= ' GROUP BY ' . implode(', ', $groupFields);
}
// Add ordering
if (!empty($orderField)) {
$query .= ' ORDER BY ' . $orderField;
/*
* If the result is not ordered by id_product, we add it as a fallback order,
* to avoid SQL returning it in random order.
*/
if (strpos($orderField, 'p.id_product') === false) {
$query .= ', p.id_product DESC';
}
}
return $query;
}
/**
* Define the mapping between fields and tables
*
* @return array
*/
protected function getFieldMapping()
{
$stockCondition = StockAvailable::addSqlShopRestriction(
null,
null,
'sa'
);
$filterToTableMapping = [
'id_product_attribute' => [
'tableName' => 'product_attribute',
'tableAlias' => 'pa',
'joinCondition' => '(p.id_product = pa.id_product)',
'joinType' => self::LEFT_JOIN,
],
'id_attribute' => [
'tableName' => 'product_attribute_combination',
'tableAlias' => 'pac',
'joinCondition' => '(pa.id_product_attribute = pac.id_product_attribute)',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_product_attribute',
],
'id_attribute_group' => [
'tableName' => 'attribute',
'tableAlias' => 'a',
'joinCondition' => '(a.id_attribute = pac.id_attribute)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_attribute',
],
'id_feature' => [
'tableName' => 'feature_product',
'tableAlias' => 'fp',
'joinCondition' => '(p.id_product = fp.id_product)',
'joinType' => self::INNER_JOIN,
],
'id_shop' => [
'tableName' => 'product_shop',
'tableAlias' => 'ps',
'joinCondition' => '(p.id_product = ps.id_product AND ps.id_shop = ' .
$this->getContext()->shop->id . ' AND ps.active = TRUE)',
'joinType' => self::INNER_JOIN,
],
'visibility' => [
'tableName' => 'product_shop',
'tableAlias' => 'ps',
'joinCondition' => '(p.id_product = ps.id_product AND ps.id_shop = ' .
$this->getContext()->shop->id . ' AND ps.active = TRUE)',
'joinType' => self::INNER_JOIN,
],
'id_feature_value' => [
'tableName' => 'feature_product',
'tableAlias' => 'fp',
'joinCondition' => '(p.id_product = fp.id_product)',
'joinType' => self::LEFT_JOIN,
],
'id_category' => [
'tableName' => 'category_product',
'tableAlias' => 'cp',
'joinCondition' => '(p.id_product = cp.id_product)',
'joinType' => self::INNER_JOIN,
],
'position' => [
'tableName' => 'category_product',
'tableAlias' => 'cp',
'joinCondition' => '(p.id_product = cp.id_product)',
'joinType' => self::INNER_JOIN,
],
'manufacturer_name' => [
'tableName' => 'manufacturer',
'tableAlias' => 'm',
'fieldName' => 'name',
'joinCondition' => '(p.id_manufacturer = m.id_manufacturer)',
'joinType' => self::LEFT_JOIN,
],
'name' => [
'tableName' => 'product_lang',
'tableAlias' => 'pl',
'joinCondition' => '(p.id_product = pl.id_product AND pl.id_shop = ' .
$this->getContext()->shop->id . ' AND pl.id_lang = ' . $this->getContext()->language->id . ')',
'joinType' => self::INNER_JOIN,
],
'nleft' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'nright' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'level_depth' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'out_of_stock' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_attribute',
],
'quantity' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_attribute',
'aggregateFunction' => 'SUM',
'aggregateFieldName' => 'quantity',
],
'price_min' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'price_max' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'range_start' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'range_end' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'id_group' => [
'tableName' => 'category_group',
'tableAlias' => 'cg',
'joinCondition' => '(cg.id_category = c.id_category)',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'nleft',
],
'sales' => [
'tableName' => 'product_sale',
'tableAlias' => 'psales',
'fieldName' => 'quantity',
'fieldAlias' => 'sales',
'joinCondition' => '(psales.id_product = p.id_product)',
'joinType' => self::LEFT_JOIN,
],
'reduction' => [
'tableName' => 'specific_price',
'tableAlias' => 'sp',
'joinCondition' => '(
sp.id_product = p.id_product AND
sp.id_shop IN (0, ' . $this->getContext()->shop->id . ') AND
sp.id_currency IN (0, ' . $this->getContext()->currency->id . ') AND
sp.id_country IN (0, ' . $this->getContext()->country->id . ') AND
sp.id_group IN (0, ' . $this->getContext()->customer->id_default_group . ') AND
sp.from_quantity = 1 AND
sp.reduction > 0 AND
sp.id_customer = 0 AND
sp.id_cart = 0 AND
(sp.from = \'0000-00-00 00:00:00\' OR \'' . date('Y-m-d H:i:s') . '\' >= sp.from) AND
(sp.to = \'0000-00-00 00:00:00\' OR \'' . date('Y-m-d H:i:s') . '\' <= sp.to)
)',
'joinType' => self::LEFT_JOIN,
],
];
return $filterToTableMapping;
}
/**
* Get the joined and escaped value from an multi-dimensional array
*
* @param string $separator
* @param array $values
*
* @return string Escaped string value
*/
protected function getJoinedEscapedValue($separator, array $values)
{
foreach ($values as $key => $value) {
if (is_array($value)) {
$values[$key] = $this->getJoinedEscapedValue($separator, $value);
} elseif (is_numeric($value)) {
$values[$key] = pSQL($value);
} else {
$values[$key] = "'" . pSQL($value) . "'";
}
}
return implode($separator, $values);
}
/**
* Compute the orderby fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return string
*/
protected function computeOrderByField(array $filterToTableMapping)
{
// First, we get the order field from the current instance. That can be strings like 'price', 'name', 'position', etc.
$orderField = $this->getOrderField();
// If it's empty, we just return it as is, nothing to do. This is usually a case when getting products
// for available filters, they reset the order field so we save performance
if (empty($orderField)) {
return $orderField;
}
// If we have an initial population, add the field into initial population selects, so we can use it in the outer query for sorting
if ($this->getInitialPopulation() !== null && !empty($orderField)) {
$this->getInitialPopulation()->addSelectField($orderField);
}
// Alter order by field if it's a price column
if ($orderField === 'price') {
$orderField = $this->getOrderDirection() === 'asc' ? 'price_min' : 'price_max';
}
// Do not try to process the orderField if it already has an alias, or if it's a group function
// We just append the order direction and return it
if (strpos($orderField, '.') !== false || strpos($orderField, '(') !== false) {
return $orderField . ' ' . strtoupper($this->getOrderDirection());
}
// In all other cases, add table mapping or p. prefix depending on field type
$orderField = $this->computeFieldName($orderField, $filterToTableMapping, true);
/*
* Do not try to process the orderField if it's a search page. We will use manually constructed list
* to order products by their position in the search results we got from the core, with inverted order
*/
if ($orderField == 'p.position' && !empty($this->getInitialPopulation()->getFilters()['id_product']['='][0])) {
return 'FIELD(p.id_product,' . implode(',', $this->getInitialPopulation()->getFilters()['id_product']['='][0]) . ') ' .
($this->getOrderDirection() === 'asc' ? 'DESC' : 'ASC');
}
// Alter order by field and add some products to the end of the list, if required
$orderField = $this->computeShowLast($orderField, $filterToTableMapping);
// Add sort order
$orderField .= ' ' . strtoupper($this->getOrderDirection());
// And return it
return $orderField;
}
/**
* Sort product list: InStock, OOPS with qty 0, OutOfStock
*
* @param string $orderField
* @param array $filterToTableMapping
*
* @return string
*/
protected function computeShowLast($orderField, $filterToTableMapping)
{
// allow only if feature is enabled & it is main product list query
if ($this->getInitialPopulation() === null
|| empty($orderField)
|| !Configuration::get('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
) {
return $orderField;
}
$this->addSelectField('out_of_stock');
// order by out-of-stock last
$computedQuantityField = $this->computeFieldName('quantity', $filterToTableMapping);
$byOutOfStockLast = 'IFNULL(' . $computedQuantityField . ', 0) <= 0';
/**
* Default behaviour when out of stock
* 0 - when deny orders
* 1 - when allow orders
*
* @var int
*/
$isAvailableWhenOutOfStock = (int) Product::isAvailableWhenOutOfStock(2);
// computing values for order by 'allow to order last'
$computedField = $this->computeFieldName('out_of_stock', $filterToTableMapping);
$computedValue = $isAvailableWhenOutOfStock ? 0 : 1;
$computedDirection = $isAvailableWhenOutOfStock ? 'ASC' : 'DESC';
// query: products with zero or less quantity and not available to order go to the end
$byOOPS = str_replace(
[':byOutOfStockLast', ':field', ':value', ':direction'],
[$byOutOfStockLast, $computedField, $computedValue, $computedDirection],
':byOutOfStockLast AND FIELD(:field, :value) :direction'
);
$orderField = $byOutOfStockLast . ', '
. $byOOPS . ', '
. $orderField;
return $orderField;
}
/**
* Add alias to table field name
*
* @param string $fieldName
* @param array $filterToTableMapping
*
* @return string Table Field name with an alias
*/
protected function computeFieldName($fieldName, $filterToTableMapping, $sortByField = false)
{
if (array_key_exists($fieldName, $filterToTableMapping)
&& (
// If the requested order field is in the result, no need to change tableAlias
// unless a fieldName key exists
isset($filterToTableMapping[$fieldName]['fieldName'])
|| $this->getInitialPopulation() === null
|| !$this->getInitialPopulation()->getSelectFields()->contains($fieldName)
)
) {
$joinMapping = $filterToTableMapping[$fieldName];
$fieldName = $joinMapping['tableAlias'] . '.' . (isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $fieldName);
if ($sortByField === false) {
$fieldName .= isset($joinMapping['fieldAlias']) ? ' as ' . $joinMapping['fieldAlias'] : '';
}
if (isset($joinMapping['aggregateFunction'], $joinMapping['aggregateFieldName'])) {
$fieldName = $joinMapping['aggregateFunction'] . '(' . $fieldName . ') as ' . $joinMapping['aggregateFieldName'];
}
} else {
if (strpos($fieldName, '(') === false) {
$fieldName = 'p.' . $fieldName;
}
}
return $fieldName;
}
/**
* Compute the select fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
protected function computeSelectFields(array $filterToTableMapping)
{
// Add already added select fields to current query
$selectFields = [];
foreach ($this->getSelectFields() as $key => $selectField) {
$selectFields[] = $this->computeFieldName($selectField, $filterToTableMapping);
}
return $selectFields;
}
/**
* Computer the where conditions that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
protected function computeWhereConditions(array $filterToTableMapping)
{
$whereConditions = [];
$operationIdx = 0;
foreach ($this->getOperationsFilters() as $filterName => $filterOperations) {
$operationsConditions = [];
foreach ($filterOperations as $operations) {
$conditions = [];
foreach ($operations as $idx => $operation) {
$selectAlias = 'p';
$values = $operation[1];
if (array_key_exists($operation[0], $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$operation[0]];
// If index is not the first, append to the table alias for
// multi join
$selectAlias = $joinMapping['tableAlias'] .
($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx);
$operation[0] = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $operation[0];
}
if (count($values) === 1) {
$operator = !empty($operation[2]) ? $operation[2] : '=';
$conditions[] = $selectAlias . '.' . $operation[0] . $operator . current($values);
} else {
$conditions[] = $selectAlias . '.' . $operation[0] . ' IN (' . $this->getJoinedEscapedValue(', ', $values) . ')';
}
}
$operationsConditions[] = '(' . implode(' AND ', $conditions) . ')';
}
++$operationIdx;
if (!empty($operationsConditions)) {
$whereConditions[] = '(' . implode(' OR ', $operationsConditions) . ')';
}
}
foreach ($this->getFilters() as $filterName => $filterContent) {
$selectAlias = 'p';
if (array_key_exists($filterName, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$filterName];
$selectAlias = $joinMapping['tableAlias'];
$filterName = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $filterName;
}
foreach ($filterContent as $operator => $values) {
if (count($values) == 1) {
$values = current($values);
if ($operator === '=') {
if (count($values) == 1) {
$whereConditions[] =
$selectAlias . '.' . $filterName . $operator . "'" . current($values) . "'";
} else {
$whereConditions[] =
$selectAlias . '.' . $filterName . ' IN (' . $this->getJoinedEscapedValue(', ', $values) . ')';
}
} else {
$orConditions = [];
foreach ($values as $value) {
$orConditions[] = $selectAlias . '.' . $filterName . $operator . $value;
}
$whereConditions[] = implode(' OR ', $orConditions);
}
}
}
}
// if we have several "groups" of the same filter, we need to use the intersect of the matching products
// e.g. : mix of id_feature like Composition & Styles
$idFilteredProducts = null;
foreach ($this->getFilters() as $filterName => $filterContent) {
foreach ($filterContent as $operator => $filterValues) {
if (count($filterValues) <= 1) {
continue;
}
$idTmpFilteredProducts = [];
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->addSelectField('id_product');
$mysqlAdapter->setOrderField('');
$mysqlAdapter->addFilter($filterName, $filterValues, $operator);
$idProducts = $mysqlAdapter->execute();
foreach ($idProducts as $idProduct) {
$idTmpFilteredProducts[] = $idProduct['id_product'];
}
if ($idFilteredProducts === null) {
$idFilteredProducts = $idTmpFilteredProducts;
} else {
$idFilteredProducts += array_intersect($idFilteredProducts, $idTmpFilteredProducts);
}
if (empty($idFilteredProducts)) {
// set it to 0 to make sure no result will be returned
$idFilteredProducts[] = 0;
break;
}
$whereConditions[] = 'p.id_product IN (' . implode(', ', $idFilteredProducts) . ')';
}
}
return $whereConditions;
}
/**
* Compute the joinConditions needed depending on the fields required in select, where, groupby & orderby fields
*
* @param array $filterToTableMapping
*
* @return ArrayCollection
*/
protected function computeJoinConditions(array $filterToTableMapping)
{
$joinList = new ArrayCollection();
$this->addJoinList($joinList, $this->getSelectFields(), $filterToTableMapping);
$this->addJoinList($joinList, $this->getFilters()->getKeys(), $filterToTableMapping);
$operationIdx = 0;
foreach ($this->getOperationsFilters() as $filterOperations) {
foreach ($filterOperations as $operations) {
foreach ($operations as $idx => $operation) {
if (array_key_exists($operation[0], $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$operation[0]];
if ($idx !== 0 || $operationIdx !== 0) {
// Index is not the first, append index to tableAlias on joinCondition
$joinMapping['joinCondition'] = preg_replace(
'~([\(\s=]' . $joinMapping['tableAlias'] . ')\.~',
'${1}' .
($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx) .
'.',
$joinMapping['joinCondition']
);
$joinMapping['tableAlias'] .= ($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx);
}
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
++$operationIdx;
}
$this->addJoinList($joinList, $this->getGroupFields()->getKeys(), $filterToTableMapping);
if (array_key_exists($this->getOrderField(), $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$this->getOrderField()];
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
return $joinList;
}
/**
* Helper to add tables infos to the join list.
*
* @param ArrayCollection $joinList
* @param array|ArrayCollection $list
* @param array $filterToTableMapping
*/
private function addJoinList(ArrayCollection $joinList, $list, array $filterToTableMapping)
{
foreach ($list as $field) {
if (array_key_exists($field, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$field];
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
/**
* Add the required table infos to the join list, taking care of the dependent tables
*
* @param ArrayCollection $joinList
* @param array $joinMapping
* @param array $filterToTableMapping
*/
private function addJoinConditions(ArrayCollection $joinList, array $joinMapping, array $filterToTableMapping)
{
if (array_key_exists('dependencyField', $joinMapping)) {
$dependencyJoinMapping = $filterToTableMapping[$joinMapping['dependencyField']];
$this->addJoinConditions($joinList, $dependencyJoinMapping, $filterToTableMapping);
}
$joinInfos[$joinMapping['tableAlias']] = [
'tableName' => $joinMapping['tableName'],
'joinCondition' => $joinMapping['joinCondition'],
'joinType' => $joinMapping['joinType'],
];
$joinList->set($joinMapping['tableAlias'] . '_' . $joinMapping['tableName'], $joinInfos);
}
/**
* Compute the groupby condition, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeGroupByFields(array $filterToTableMapping)
{
$groupFields = [];
if ($this->getGroupFields()->isEmpty()) {
return $groupFields;
}
foreach ($this->getGroupFields() as $key => $values) {
if (strpos($values, '.') !== false
|| strpos($values, '(') !== false) {
$groupFields[$key] = $values;
continue;
}
if (array_key_exists($values, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$values];
$groupFields[$key] = $joinMapping['tableAlias'] . '.' . $values;
} else {
$groupFields[$key] = 'p.' . $values;
}
}
return $groupFields;
}
/**
* {@inheritdoc}
*/
public function getMinMaxValue($fieldName)
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$mysqlAdapter->setSelectFields(['MIN(' . $fieldName . ') as min, MAX(' . $fieldName . ') as max']);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return [(float) $result[0]['min'], (float) $result[0]['max']];
}
/**
* {@inheritdoc}
*/
public function count()
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$result = $mysqlAdapter->valueCount();
return isset($result[0]['c']) ? (int) $result[0]['c'] : 0;
}
/**
* {@inheritdoc}
*/
public function valueCount($fieldName = null)
{
$this->resetGroupBy();
if ($fieldName !== null) {
$this->addGroupBy($fieldName);
$this->addSelectField($fieldName);
}
$this->addSelectField('COUNT(DISTINCT p.id_product) c');
$this->setOrderField('');
$this->copyOperationsFilters();
return $this->execute();
}
/**
* {@inheritdoc}
*/
public function useFiltersAsInitialPopulation()
{
// Initial population has no ORDER BY
$this->setOrderField('');
// We add basic select fields we will need to matter what
$this->setSelectFields(
[
'id_product',
'id_manufacturer',
'quantity',
'condition',
'weight',
'price',
'sales',
'on_sale',
'date_add',
]
);
// Clone it, add it to initial population
$this->initialPopulation = clone $this;
// Reset all filters so we start clean and add only the base select, we don't need anything else
$this->resetAll();
$this->addSelectField('id_product');
}
/**
* @return Context
*/
protected function getContext()
{
return Context::getContext();
}
/**
* @return Db
*/
protected function getDatabase()
{
return Db::getInstance();
}
/**
* Copy stock management operation filters
* to make sure quantity is also used
*/
protected function copyOperationsFilters()
{
$initialPopulation = $this->getInitialPopulation();
if (null === $initialPopulation) {
return;
}
$operationsFilters = clone $initialPopulation->getOperationsFilters();
foreach ($operationsFilters as $operationName => $operations) {
$this->addOperationsFilter(
$operationName,
$operations
);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Constraint;
use Symfony\Component\Validator\Constraint;
class UrlSegment extends Constraint
{
public $message = '%s is invalid.';
/**
* {@inheritdoc}
*/
public function validatedBy()
{
return UrlSegmentValidator::class;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Constraint;
use PrestaShop\PrestaShop\Adapter\Tools;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Class UrlSegmentValidator responsible for validating an URL segment.
*/
class UrlSegmentValidator extends ConstraintValidator
{
/**
* @var Tools
*/
private $tools;
/**
* @param Tools $tools
*/
public function __construct(Tools $tools)
{
$this->tools = $tools;
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof UrlSegment) {
throw new UnexpectedTypeException($constraint, UrlSegment::class);
}
if (null === $value || '' === $value) {
return;
}
if (strtolower($value) !== $this->tools->linkRewrite($value)) {
$this->context->buildViolation($constraint->message)
->setTranslationDomain('Admin.Notifications.Error')
->setParameter('%s', $this->formatValue($value))
->addViolation()
;
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Definition;
class Availability
{
const IN_STOCK = 2;
const AVAILABLE = 1;
const NOT_AVAILABLE = 0;
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,579 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Category;
use Configuration;
use Context;
use Db;
use Manufacturer;
use PrestaShop\Module\FacetedSearch\Definition\Availability;
use PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
class Converter
{
const WIDGET_TYPE_CHECKBOX = 0;
const WIDGET_TYPE_RADIO = 1;
const WIDGET_TYPE_DROPDOWN = 2;
const WIDGET_TYPE_SLIDER = 3;
const TYPE_ATTRIBUTE_GROUP = 'id_attribute_group';
const TYPE_AVAILABILITY = 'availability';
const TYPE_CATEGORY = 'category';
const TYPE_CONDITION = 'condition';
const TYPE_FEATURE = 'id_feature';
const TYPE_MANUFACTURER = 'manufacturer';
const TYPE_PRICE = 'price';
const TYPE_WEIGHT = 'weight';
const TYPE_EXTRAS = 'extras';
const PROPERTY_URL_NAME = 'url_name';
const PROPERTY_COLOR = 'color';
const PROPERTY_TEXTURE = 'texture';
/**
* @var array
*/
const RANGE_FILTERS = [self::TYPE_PRICE, self::TYPE_WEIGHT];
/**
* @var Context
*/
protected $context;
/**
* @var Db
*/
protected $database;
/**
* @var URLSerializer
*/
protected $urlSerializer;
/**
* @var Filters\DataAccessor
*/
private $dataAccessor;
/**
* @var Filters\Provider
*/
private $provider;
public function __construct(
Context $context,
Db $database,
URLSerializer $urlSerializer,
Filters\DataAccessor $dataAccessor,
Filters\Provider $provider
) {
$this->context = $context;
$this->database = $database;
$this->urlSerializer = $urlSerializer;
$this->dataAccessor = $dataAccessor;
$this->provider = $provider;
}
public function getFacetsFromFilterBlocks(array $filterBlocks)
{
$facets = [];
foreach ($filterBlocks as $filterBlock) {
if (empty($filterBlock)) {
// Empty filter, let's continue
continue;
}
$facet = new Facet();
$facet
->setLabel($filterBlock['name'])
->setProperty('filter_show_limit', $filterBlock['filter_show_limit'])
->setMultipleSelectionAllowed(true);
switch ($filterBlock['type']) {
case self::TYPE_CATEGORY:
case self::TYPE_CONDITION:
case self::TYPE_EXTRAS:
case self::TYPE_MANUFACTURER:
case self::TYPE_AVAILABILITY:
case self::TYPE_ATTRIBUTE_GROUP:
case self::TYPE_FEATURE:
$type = $filterBlock['type'];
if ($filterBlock['type'] == self::TYPE_ATTRIBUTE_GROUP) {
$type = 'attribute_group';
$facet->setProperty(self::TYPE_ATTRIBUTE_GROUP, $filterBlock['id_key']);
if (isset($filterBlock['url_name'])) {
$facet->setProperty(self::PROPERTY_URL_NAME, $filterBlock['url_name']);
}
} elseif ($filterBlock['type'] == self::TYPE_FEATURE) {
$type = 'feature';
$facet->setProperty(self::TYPE_FEATURE, $filterBlock['id_key']);
if (isset($filterBlock['url_name'])) {
$facet->setProperty(self::PROPERTY_URL_NAME, $filterBlock['url_name']);
}
}
$facet->setType($type);
$filters = [];
foreach ($filterBlock['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id);
if (isset($filterArray['url_name'])) {
$filter->setProperty(self::PROPERTY_URL_NAME, $filterArray['url_name']);
}
if (array_key_exists('checked', $filterArray)) {
$filter->setActive($filterArray['checked']);
}
if (isset($filterArray['color'])) {
if (file_exists(_PS_COL_IMG_DIR_ . $id . '.jpg')) {
$filter->setProperty(self::PROPERTY_TEXTURE, _THEME_COL_DIR_ . $id . '.jpg');
} elseif ($filterArray['color'] != '') {
$filter->setProperty(self::PROPERTY_COLOR, $filterArray['color']);
}
}
$filters[] = $filter;
}
if ((int) $filterBlock['filter_show_limit'] !== 0) {
usort($filters, [$this, 'sortFiltersByMagnitude']);
}
$this->hideZeroValuesAndShowLimit($filters, (int) $filterBlock['filter_show_limit']);
if ((int) $filterBlock['filter_show_limit'] !== 0 ||
($filterBlock['type'] !== self::TYPE_ATTRIBUTE_GROUP && $filterBlock['type'] !== self::TYPE_AVAILABILITY)
) {
usort($filters, [$this, 'sortFiltersByLabel']);
}
// No method available to add all filters
foreach ($filters as $filter) {
$facet->addFilter($filter);
}
break;
case self::TYPE_WEIGHT:
case self::TYPE_PRICE:
$facet
->setType($filterBlock['type'])
->setProperty('min', $filterBlock['min'])
->setProperty('max', $filterBlock['max'])
->setProperty('unit', $filterBlock['unit'])
->setProperty('specifications', $filterBlock['specifications'])
->setMultipleSelectionAllowed(false)
->setProperty('range', true);
$filter = new Filter();
$filter
->setActive($filterBlock['value'] !== null)
->setType($filterBlock['type'])
->setMagnitude($filterBlock['nbr'])
->setProperty('symbol', $filterBlock['unit'])
->setValue($filterBlock['value']);
$facet->addFilter($filter);
break;
}
switch ((int) $filterBlock['filter_type']) {
case self::WIDGET_TYPE_CHECKBOX:
$facet->setMultipleSelectionAllowed(true);
$facet->setWidgetType('checkbox');
break;
case self::WIDGET_TYPE_RADIO:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('radio');
break;
case self::WIDGET_TYPE_DROPDOWN:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('dropdown');
break;
case self::WIDGET_TYPE_SLIDER:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('slider');
break;
}
$facets[] = $facet;
}
return $facets;
}
/**
* This method is responsible of parsing the search filters sent in the query.
* These filters come from the URL in 99 % of cases.
*
* It will unserialize it and convert it to actual unique and valid values that
* we will later use to construct the database query. All invalid filters in the
* query (unknown value, deleted in shop etc.) are ignored.
*
* Filters that are found (if any) will be later used in initSearch method, along
* with some predefined ones related the the controller we are on.
*
* @param ProductSearchQuery $query
*
* @return array
*/
public function createFacetedSearchFiltersFromQuery(ProductSearchQuery $query)
{
$idShop = (int) $this->context->shop->id;
$idLang = (int) $this->context->language->id;
// Get category ID from the query or home category as a fallback
$idCategory = (int) $query->getIdCategory();
if (empty($idCategory)) {
$idCategory = (int) Configuration::get('PS_HOME_CATEGORY');
}
$searchFilters = [];
// Get filters configured in module settings for the current query
$configuredFilters = $this->provider->getFiltersForQuery($query, $idShop);
/*
* Parses submitted encoded facets from (URL) string into a nice array.
*
* Facets are set to the URL with a textual representation. This unfortunately does not
* work very well, because there could be duplicate values for both facet and filter.
* For example, if there are two features, feature values or categories with the same name.
*/
$receivedFilters = $this->urlSerializer->unserialize($query->getEncodedFacets());
// Go through filters that are configured and find out which should be activated,
// depending on what was provided in the encodedFacets.
foreach ($configuredFilters as $filter) {
$filterLabel = $this->convertFilterTypeToLabel($filter['type']);
switch ($filter['type']) {
case self::TYPE_MANUFACTURER:
if (!isset($receivedFilters[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$manufacturers = Manufacturer::getManufacturers(false, $idLang);
$searchFilters[$filter['type']] = [];
foreach ($manufacturers as $manufacturer) {
if (in_array($manufacturer['name'], $receivedFilters[$filterLabel])) {
$searchFilters[$filter['type']][$manufacturer['name']] = $manufacturer['id_manufacturer'];
}
}
break;
case self::TYPE_AVAILABILITY:
if (!isset($receivedFilters[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$quantityArray = [
$this->context->getTranslator()->trans(
'Not available',
[],
'Modules.Facetedsearch.Shop'
) => Availability::NOT_AVAILABLE,
$this->context->getTranslator()->trans(
'Available',
[],
'Modules.Facetedsearch.Shop'
) => Availability::AVAILABLE,
$this->context->getTranslator()->trans(
'In stock',
[],
'Modules.Facetedsearch.Shop'
) => Availability::IN_STOCK,
];
$searchFilters[$filter['type']] = [];
foreach ($quantityArray as $quantityName => $quantityId) {
if (isset($receivedFilters[$filterLabel]) && in_array($quantityName, $receivedFilters[$filterLabel])) {
$searchFilters[$filter['type']][] = $quantityId;
}
}
break;
case self::TYPE_CONDITION:
if (!isset($receivedFilters[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$conditionArray = [
$this->context->getTranslator()->trans(
'New',
[],
'Modules.Facetedsearch.Shop'
) => 'new',
$this->context->getTranslator()->trans(
'Used',
[],
'Modules.Facetedsearch.Shop'
) => 'used',
$this->context->getTranslator()->trans(
'Refurbished',
[],
'Modules.Facetedsearch.Shop'
) => 'refurbished',
];
$searchFilters[$filter['type']] = [];
foreach ($conditionArray as $conditionName => $conditionId) {
if (isset($receivedFilters[$filterLabel]) && in_array($conditionName, $receivedFilters[$filterLabel])) {
$searchFilters[$filter['type']][] = $conditionId;
}
}
break;
case self::TYPE_EXTRAS:
if (!isset($receivedFilters[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$extrasOptions = [
$this->context->getTranslator()->trans(
'New product',
[],
'Modules.Facetedsearch.Shop'
) => 'new',
$this->context->getTranslator()->trans(
'On sale',
[],
'Modules.Facetedsearch.Shop'
) => 'sale',
$this->context->getTranslator()->trans(
'Discounted',
[],
'Modules.Facetedsearch.Shop'
) => 'discount',
];
$searchFilters[$filter['type']] = [];
foreach ($extrasOptions as $extrasOption => $optionId) {
if (isset($receivedFilters[$filterLabel]) && in_array($extrasOption, $receivedFilters[$filterLabel])) {
$searchFilters[$filter['type']][] = $optionId;
}
}
break;
case self::TYPE_FEATURE:
$features = $this->dataAccessor->getFeatures($idLang);
foreach ($features as $feature) {
if ($filter['id_value'] != $feature['id_feature']) {
continue;
}
if (isset($receivedFilters[$feature['url_name']])) {
$featureValueLabels = $receivedFilters[$feature['url_name']];
} elseif (isset($receivedFilters[$feature['name']])) {
$featureValueLabels = $receivedFilters[$feature['name']];
} else {
continue;
}
$featureValues = $this->dataAccessor->getFeatureValues($feature['id_feature'], $idLang);
foreach ($featureValues as $featureValue) {
if (in_array($featureValue['url_name'], $featureValueLabels)
|| in_array($featureValue['value'], $featureValueLabels)
) {
$searchFilters['id_feature'][$feature['id_feature']][] = $featureValue['id_feature_value'];
}
}
}
break;
case self::TYPE_ATTRIBUTE_GROUP:
$attributesGroup = $this->dataAccessor->getAttributesGroups($idLang);
foreach ($attributesGroup as $attributeGroup) {
if ($filter['id_value'] != $attributeGroup['id_attribute_group']) {
continue;
}
if (isset($receivedFilters[$attributeGroup['url_name']])) {
$attributeLabels = $receivedFilters[$attributeGroup['url_name']];
} elseif (isset($receivedFilters[$attributeGroup['attribute_group_name']])) {
$attributeLabels = $receivedFilters[$attributeGroup['attribute_group_name']];
} else {
continue;
}
$attributes = $this->dataAccessor->getAttributes($idLang, $attributeGroup['id_attribute_group']);
foreach ($attributes as $attribute) {
if (in_array($attribute['url_name'], $attributeLabels)
|| in_array($attribute['name'], $attributeLabels)
) {
$searchFilters['id_attribute_group'][$attributeGroup['id_attribute_group']][] = $attribute['id_attribute'];
}
}
}
break;
case self::TYPE_PRICE:
case self::TYPE_WEIGHT:
if (isset($receivedFilters[$filterLabel])) {
$filters = $receivedFilters[$filterLabel];
if (isset($filters[1]) && isset($filters[2])) {
$from = $filters[1];
$to = $filters[2];
$searchFilters[$filter['type']][0] = $from;
$searchFilters[$filter['type']][1] = $to;
}
}
break;
case self::TYPE_CATEGORY:
if (isset($receivedFilters[$filterLabel])) {
foreach ($receivedFilters[$filterLabel] as $queryFilter) {
/*
* This works only for categories that are child of the category we are browsing (or home category).
* Categories deeper in the tree will never be found. This could be fixed by providing a unique ID
* to the URL.
*/
$categories = Category::searchByNameAndParentCategoryId($idLang, $queryFilter, (int) $idCategory);
if ($categories) {
$searchFilters[$filter['type']][] = $categories['id_category'];
}
}
}
break;
default:
if (isset($receivedFilters[$filterLabel])) {
foreach ($receivedFilters[$filterLabel] as $queryFilter) {
$searchFilters[$filter['type']][] = $queryFilter;
}
}
}
}
// Remove all empty selected filters
foreach ($searchFilters as $key => $value) {
switch ($key) {
case self::TYPE_PRICE:
case self::TYPE_WEIGHT:
if ($value[0] === '' && $value[1] === '') {
unset($searchFilters[$key]);
}
break;
default:
if ($value == '' || $value == []) {
unset($searchFilters[$key]);
}
break;
}
}
return $searchFilters;
}
/**
* Convert filter type to label
*
* @param string $filterType
*/
private function convertFilterTypeToLabel($filterType)
{
switch ($filterType) {
case self::TYPE_PRICE:
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_WEIGHT:
return $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_CONDITION:
return $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_EXTRAS:
return $this->context->getTranslator()->trans('Selections', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_AVAILABILITY:
return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_MANUFACTURER:
return $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_CATEGORY:
return $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_FEATURE:
case self::TYPE_ATTRIBUTE_GROUP:
default:
return null;
}
}
/**
* Hide entries with 0 results
* Hide depending of show limit parameter
*
* @param array $filters
*
* @return array
*/
private function hideZeroValuesAndShowLimit(array $filters, $showLimit)
{
$count = 0;
foreach ($filters as $filter) {
if ($filter->getMagnitude() === 0
|| ($showLimit > 0 && $count >= $showLimit)
) {
$filter->setDisplayed(false);
continue;
}
++$count;
}
return $filters;
}
/**
* Sort filters by magnitude
*
* @param Filter $a
* @param Filter $b
*
* @return int
*/
private function sortFiltersByMagnitude(Filter $a, Filter $b)
{
$aMagnitude = $a->getMagnitude();
$bMagnitude = $b->getMagnitude();
if ($aMagnitude == $bMagnitude) {
// Same magnitude, sort by label
return $this->sortFiltersByLabel($a, $b);
}
return $aMagnitude > $bMagnitude ? -1 : +1;
}
/**
* Sort filters by label
*
* @param Filter $a
* @param Filter $b
*
* @return int
*/
private function sortFiltersByLabel(Filter $a, Filter $b)
{
return strnatcasecmp($a->getLabel(), $b->getLabel());
}
}

View File

@@ -0,0 +1,214 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Combination;
use Db;
use Shop;
/**
* Data accessor for features and attributes
*/
class DataAccessor
{
/**
* @var array
*/
private $attributesGroup = [];
/**
* @var array
*/
private $attributes = [];
/**
* @var array
*/
private $features = [];
/**
* @var array
*/
private $featureValues = [];
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Get all attributes for a given language and attribute group.
*
* @param int $idLang
*
* @return array Attributes
*/
public function getAttributes($idLang, $idAttributeGroup)
{
if (!Combination::isFeatureActive()) {
return [];
}
if (!isset($this->attributes[$idLang][$idAttributeGroup])) {
$this->attributes[$idLang] = [$idAttributeGroup => []];
$tempAttributes = $this->database->executeS(
'SELECT DISTINCT a.`id_attribute`, ' .
'a.`color`, ' .
'al.`name`, ' .
'agl.`id_attribute_group`, ' .
'IF(lialv.`url_name` IS NULL OR lialv.`url_name` = "", NULL, lialv.`url_name`) AS url_name, ' .
'IF(lialv.`meta_title` IS NULL OR lialv.`meta_title` = "", NULL, lialv.`meta_title`) AS meta_title ' .
'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) $idLang . ') ' .
'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) $idLang . ')' .
Shop::addSqlAssociation('attribute_group', 'ag') . ' ' .
Shop::addSqlAssociation('attribute', 'a') . ' ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value` lialv ' .
'ON (a.`id_attribute` = lialv.`id_attribute` AND lialv.`id_lang` = ' . (int) $idLang . ') ' .
'WHERE ag.id_attribute_group = ' . (int) $idAttributeGroup . ' ' .
'ORDER BY agl.`name` ASC, a.`position` ASC'
);
foreach ($tempAttributes as $attribute) {
$this->attributes[$idLang][$idAttributeGroup][$attribute['id_attribute']] = $attribute;
}
}
return $this->attributes[$idLang][$idAttributeGroup];
}
/**
* Get all attributes groups for a given language.
*
* @param int $idLang Language id
*
* @return array Attributes groups
*/
public function getAttributesGroups($idLang)
{
if (!Combination::isFeatureActive()) {
return [];
}
if (!isset($this->attributesGroup[$idLang])) {
$this->attributesGroup[$idLang] = [];
$tempAttributesGroup = $this->database->executeS(
'SELECT ag.id_attribute_group, ' .
'agl.public_name as attribute_group_name, ' .
'is_color_group, ' .
'IF(liaglv.`url_name` IS NULL OR liaglv.`url_name` = "", NULL, liaglv.`url_name`) AS url_name, ' .
'IF(liaglv.`meta_title` IS NULL OR liaglv.`meta_title` = "", NULL, liaglv.`meta_title`) AS meta_title, ' .
'IFNULL(liag.indexable, TRUE) AS indexable ' .
'FROM `' . _DB_PREFIX_ . 'attribute_group` ag ' .
Shop::addSqlAssociation('attribute_group', 'ag') . ' ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ' .
'ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_group` liag ' .
'ON (ag.`id_attribute_group` = liag.`id_attribute_group`) ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value` AS liaglv ' .
'ON (ag.`id_attribute_group` = liaglv.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
'GROUP BY ag.id_attribute_group ORDER BY ag.`position` ASC'
);
foreach ($tempAttributesGroup as $attributeGroup) {
$this->attributesGroup[$idLang][$attributeGroup['id_attribute_group']] = $attributeGroup;
}
}
return $this->attributesGroup[$idLang];
}
/**
* Get features with their associated layered information.
*
* @param int $idLang
*
* @return array Features
*/
public function getFeatures($idLang)
{
if (!isset($this->features[$idLang])) {
$this->features[$idLang] = [];
$tempFeatures = $this->database->executeS(
'SELECT DISTINCT f.id_feature, f.*, fl.*, ' .
'IF(liflv.`url_name` IS NULL OR liflv.`url_name` = "", NULL, liflv.`url_name`) AS url_name, ' .
'IF(liflv.`meta_title` IS NULL OR liflv.`meta_title` = "", NULL, liflv.`meta_title`) AS meta_title, ' .
'lif.indexable ' .
'FROM `' . _DB_PREFIX_ . 'feature` f ' .
'' . Shop::addSqlAssociation('feature', 'f') . ' ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` fl ON (f.`id_feature` = fl.`id_feature` AND fl.`id_lang` = ' . (int) $idLang . ') ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature` lif ' .
'ON (f.`id_feature` = lif.`id_feature`) ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value` liflv ' .
'ON (f.`id_feature` = liflv.`id_feature` AND liflv.`id_lang` = ' . (int) $idLang . ') ' .
'ORDER BY f.`position` ASC'
);
foreach ($tempFeatures as $feature) {
$this->features[$idLang][$feature['id_feature']] = $feature;
}
}
return $this->features[$idLang];
}
/**
* Get feature values for given feature, with their associated layered information.
*
* @param int $idFeature
* @param int $idLang
*
* @return array Feature values
*/
public function getFeatureValues($idFeature, $idLang)
{
if (!isset($this->featureValues[$idLang][$idFeature])) {
$this->featureValues[$idLang] = [$idFeature => []];
$tempFeatureValues = $this->database->executeS(
'SELECT v.*, vl.*, ' .
'IF(lifvlv.`url_name` IS NULL OR lifvlv.`url_name` = "", NULL, lifvlv.`url_name`) AS url_name, ' .
'IF(lifvlv.`meta_title` IS NULL OR lifvlv.`meta_title` = "", NULL, lifvlv.`meta_title`) AS meta_title ' .
'FROM `' . _DB_PREFIX_ . 'feature_value` v ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` vl ' .
'ON (v.`id_feature_value` = vl.`id_feature_value` AND vl.`id_lang` = ' . (int) $idLang . ') ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value` lifvlv ' .
'ON (v.`id_feature_value` = lifvlv.`id_feature_value` AND lifvlv.`id_lang` = ' . (int) $idLang . ') ' .
'WHERE v.`id_feature` = ' . (int) $idFeature . ' ' .
'ORDER BY vl.`value` ASC'
);
foreach ($tempFeatureValues as $feature) {
$this->featureValues[$idLang][$idFeature][$feature['id_feature_value']] = $feature;
}
}
return $this->featureValues[$idLang][$idFeature];
}
}

View File

@@ -0,0 +1,176 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Configuration;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use PrestaShop\Module\FacetedSearch\Product\Search;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use Product;
use Validate;
class Products
{
/**
* Use price tax filter
*
* @var bool
*/
private $psLayeredFilterPriceUsetax;
/**
* Use price rounding
*
* @var bool
*/
private $psLayeredFilterPriceRounding;
/**
* @var AbstractAdapter
*/
private $searchAdapter;
public function __construct(Search $productSearch)
{
$this->searchAdapter = $productSearch->getSearchAdapter();
}
/**
* Get the products associated with the current filters.
*
* @param ProductSearchQuery $query
* @param array $selectedFilters
*
* @return array
*/
public function getProductByFilters(
ProductSearchQuery $query,
array $selectedFilters = []
) {
// Load sorting type and direction, validate it and apply fallback if needed
$orderBy = $query->getSortOrder()->toLegacyOrderBy(false);
$orderWay = $query->getSortOrder()->toLegacyOrderWay();
$orderWay = Validate::isOrderWay($orderWay) ? $orderWay : 'ASC';
$orderBy = Validate::isOrderBy($orderBy) ? $orderBy : 'position';
// Apply it to the filter
$this->searchAdapter->setOrderField($orderBy);
$this->searchAdapter->setOrderDirection($orderWay);
$this->searchAdapter->addGroupBy('id_product');
if (isset($selectedFilters['price']) || $orderBy === 'price') {
$this->searchAdapter->addSelectField('id_product');
$this->searchAdapter->addSelectField('price');
$this->searchAdapter->addSelectField('price_min');
$this->searchAdapter->addSelectField('price_max');
}
// Get full list of matching products
$fullProductList = $this->searchAdapter->execute();
// Count them
$totalProductCount = count($fullProductList);
// Get pagination
$productsPerPage = (int) $query->getResultsPerPage();
$page = (int) $query->getPage();
// Cut them down by pagination
$finalProductList = array_slice(
$fullProductList,
($page - 1) * $productsPerPage,
$productsPerPage
);
// And run post filter
$this->pricePostFiltering($finalProductList, $selectedFilters);
return [
'products' => $finalProductList,
'count' => $totalProductCount,
];
}
/**
* Post filter product depending on the price and a few extra config variables
*
* @param array $matchingProductList
* @param array $selectedFilters
*/
private function pricePostFiltering(&$matchingProductList, $selectedFilters)
{
if (!isset($selectedFilters['price'])) {
return;
}
$priceFilter['min'] = (float) ($selectedFilters['price'][0]);
$priceFilter['max'] = (float) ($selectedFilters['price'][1]);
if ($this->psLayeredFilterPriceUsetax === null) {
$this->psLayeredFilterPriceUsetax = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_USETAX');
}
if ($this->psLayeredFilterPriceRounding === null) {
$this->psLayeredFilterPriceRounding = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_ROUNDING');
}
if ($this->psLayeredFilterPriceUsetax || $this->psLayeredFilterPriceRounding) {
$this->filterPrice(
$matchingProductList,
$this->psLayeredFilterPriceUsetax,
$this->psLayeredFilterPriceRounding,
$priceFilter
);
}
}
/**
* Remove products from the product list in case of price postFiltering
*
* @param array $matchingProductList
* @param bool $psLayeredFilterPriceUsetax
* @param bool $psLayeredFilterPriceRounding
* @param array $priceFilter
*/
private function filterPrice(
&$matchingProductList,
$psLayeredFilterPriceUsetax,
$psLayeredFilterPriceRounding,
$priceFilter
) {
/* for this case, price could be out of range, so we need to compute the real price */
foreach ($matchingProductList as $key => $product) {
if (($product['price_min'] < (int) $priceFilter['min'] && $product['price_max'] > (int) $priceFilter['min'])
|| ($product['price_max'] > (int) $priceFilter['max'] && $product['price_min'] < (int) $priceFilter['max'])
) {
$price = Product::getPriceStatic($product['id_product'], $psLayeredFilterPriceUsetax);
if ($psLayeredFilterPriceRounding) {
$price = (int) $price;
}
if ($price < $priceFilter['min'] || $price > $priceFilter['max']) {
// out of range price, exclude the product
unset($matchingProductList[$key]);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Db;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
/**
* Class responsible for providing filters configured for current search query
*/
class Provider
{
/**
* @var array
*/
private $filters = [];
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Get filters for current search query
*
* @param ProductSearchQuery $query
* @param int $idShop
*
* @return array Filters
*/
public function getFiltersForQuery(ProductSearchQuery $query, int $idShop)
{
if (empty($this->filters)) {
$this->filters = $this->database->executeS(
'SELECT type, id_value, filter_show_limit, filter_type FROM ' . _DB_PREFIX_ . 'layered_category
WHERE controller = \'' . $query->getQueryType() . '\'
AND id_category = ' . ($query->getQueryType() == 'category' ? (int) $query->getIdCategory() : 0) . '
AND id_shop = ' . $idShop . '
GROUP BY `type`, id_value ORDER BY position ASC'
);
}
return $this->filters;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,82 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
namespace PrestaShop\Module\FacetedSearch\Form\Attribute;
use Db;
use PrestaShopDatabaseException;
class FormDataProvider
{
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Fills form data
*
* @param array $params
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
public function getData(array $params)
{
$defaultUrl = [];
$defaultMetaTitle = [];
// if params contains id, gets data for edit form
if (!empty($params['id'])) {
$attributeId = (int) $params['id'];
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value ' .
'WHERE `id_attribute` = ' . $attributeId
);
if (!empty($result) && is_array($result)) {
foreach ($result as $data) {
$defaultUrl[$data['id_lang']] = $data['url_name'];
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
}
}
}
return [
'url_name' => $defaultUrl,
'meta_title' => $defaultMetaTitle,
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
namespace PrestaShop\Module\FacetedSearch\Form\Attribute;
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use PrestaShopBundle\Translation\DataCollectorTranslator;
use PrestaShopBundle\Translation\TranslatorComponent;
use Symfony\Component\Form\FormBuilderInterface;
class FormModifier
{
/**
* @var DataCollectorTranslator|TranslatorComponent
*/
private $translator;
/**
* @param DataCollectorTranslator|TranslatorComponent $translator
*/
public function __construct($translator)
{
$this->translator = $translator;
}
public function modify(FormBuilderInterface $formBuilder)
{
$invalidCharsHint = $this->translator->trans(
'Invalid characters: <>;=#{}_',
[],
'Modules.Facetedsearch.Admin'
);
$urlTip = $this->translator->trans(
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing the word that best represent this attribute. By default, PrestaShop uses the attribute\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$metaTitleTip = $this->translator->trans(
'When the Faceted Search module is enabled, you can get more detailed page titles by choosing the word that best represent this attribute. By default, PrestaShop uses the attribute\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$formBuilder
->add(
'url_name',
TranslatableType::class,
[
'required' => false,
'label' => $this->translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
'help' => $urlTip . ' ' . $invalidCharsHint,
'options' => [
'constraints' => [
new UrlSegment([
'message' => $this->translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
]),
],
],
]
)
->add(
'meta_title',
TranslatableType::class,
[
'required' => false,
'label' => $this->translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
'help' => $metaTitleTip,
]
)
;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
namespace PrestaShop\Module\FacetedSearch\Form\AttributeGroup;
use Db;
use PrestaShopDatabaseException;
class FormDataProvider
{
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Fills form data
*
* @param array $params
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
public function getData(array $params)
{
$defaultUrl = [];
$defaultMetaTitle = [];
$isIndexable = false;
// if params contains id, gets data for edit form
if (!empty($params['id'])) {
$attributeGroupId = (int) $params['id'];
// returns false if request failed.
$queryIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group ' .
'WHERE `id_attribute_group` = ' . $attributeGroupId
);
$isIndexable = (bool) $queryIndexable;
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value ' .
'WHERE `id_attribute_group` = ' . $attributeGroupId
);
if (!empty($result) && is_array($result)) {
foreach ($result as $data) {
$defaultUrl[$data['id_lang']] = $data['url_name'];
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
}
}
}
return [
'url_name' => $defaultUrl,
'meta_title' => $defaultMetaTitle,
'is_indexable' => $isIndexable,
];
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
namespace PrestaShop\Module\FacetedSearch\Form\AttributeGroup;
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
use PrestaShop\PrestaShop\Core\Exception\CoreException;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use PrestaShopBundle\Translation\DataCollectorTranslator;
use PrestaShopBundle\Translation\TranslatorComponent;
use Symfony\Component\Form\FormBuilderInterface;
class FormModifier
{
/**
* @var DataCollectorTranslator|TranslatorComponent
*/
private $translator;
/**
* @param DataCollectorTranslator|TranslatorComponent $translator
*/
public function __construct($translator)
{
$this->translator = $translator;
}
public function modify(FormBuilderInterface $formBuilder)
{
// Dynamically check the class and instanciate it, this avoids the module from requiring PrestaShop 1.7.8 minimum,
// besides this code is not supposed to be called in older versions
if (!class_exists('\PrestaShopBundle\Form\FormBuilderModifier')) {
throw new CoreException('FormBuilderModifier class was not found, it is only available in PrestaShop 1.7.8 and more');
}
$formBuilderModifier = new \PrestaShopBundle\Form\FormBuilderModifier();
$invalidCharsHint = $this->translator->trans(
'Invalid characters: <>;=#{}_',
[],
'Modules.Facetedsearch.Admin'
);
$urlTip = $this->translator->trans(
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing the word that best represent this attribute. By default, PrestaShop uses the attribute\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$metaTitleTip = $this->translator->trans(
'When the Faceted Search module is enabled, you can get more detailed page titles by choosing the word that best represent this attribute. By default, PrestaShop uses the attribute\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$formBuilderModifier->addBefore(
$formBuilder,
'group_type',
'url_name',
TranslatableType::class,
[
'required' => false,
'label' => $this->translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
'help' => $urlTip . ' ' . $invalidCharsHint,
'options' => [
'constraints' => [
new UrlSegment([
'message' => $this->translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
]),
],
],
]
);
$formBuilderModifier->addBefore(
$formBuilder,
'group_type',
'meta_title',
TranslatableType::class,
[
'required' => false,
'label' => $this->translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
'help' => $metaTitleTip,
]
);
$formBuilderModifier->addBefore(
$formBuilder,
'group_type',
'is_indexable',
SwitchType::class,
[
'required' => false,
'label' => $this->translator->trans('Indexable', [], 'Modules.Facetedsearch.Admin'),
'help' => $this->translator->trans(
'Use this attribute in URL generated by the Faceted Search module.',
[],
'Modules.Facetedsearch.Admin'
),
]
);
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\Feature;
use Db;
use PrestaShopDatabaseException;
/**
* Provides form data
*/
class FormDataProvider
{
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Fills form data
*
* @param array $params
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
public function getData(array $params)
{
$defaultUrl = [];
$defaultMetaTitle = [];
$isIndexable = false;
// if params contains id, gets data for edit form
if (!empty($params['id'])) {
$featureId = (int) $params['id'];
// returns false if request failed.
$queryIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
'WHERE `id_feature` = ' . $featureId
);
$isIndexable = (bool) $queryIndexable;
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'WHERE `id_feature` = ' . $featureId
);
if (!empty($result) && is_array($result)) {
foreach ($result as $data) {
$defaultUrl[$data['id_lang']] = $data['url_name'];
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
}
}
}
return [
'url' => $defaultUrl,
'meta_title' => $defaultMetaTitle,
'is_indexable' => $isIndexable,
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\Feature;
use Context;
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use PrestaShopBundle\Translation\DataCollectorTranslator;
use PrestaShopBundle\Translation\TranslatorComponent;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Adds module specific fields to BO form
*/
class FormModifier
{
/**
* @var Context
*/
private $context;
public function __construct(Context $context)
{
$this->context = $context;
}
public function modify(
FormBuilderInterface $formBuilder,
array $data
) {
/** @var DataCollectorTranslator|TranslatorComponent $translator */
$translator = $this->context->getTranslator();
$invalidCharsHint = $translator->trans(
'Invalid characters: <>;=#{}_',
[],
'Modules.Facetedsearch.Admin'
);
$urlTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing ' .
'the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$metaTitleTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed page titles by ' .
'choosing the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$formBuilder
->add(
'url_name',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
'help' => $urlTip . ' ' . $invalidCharsHint,
'options' => [
'constraints' => [
new UrlSegment([
'message' => $translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
]),
],
],
'data' => $data['url'],
]
)
->add(
'meta_title',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
'help' => $metaTitleTip,
'data' => $data['meta_title'],
]
)
->add(
'layered_indexable',
SwitchType::class,
[
'required' => false,
'label' => $translator->trans('Indexable', [], 'Modules.Facetedsearch.Admin'),
'help' => $translator->trans(
'Use this attribute in URL generated by the Faceted Search module.',
[],
'Modules.Facetedsearch.Admin'
),
'data' => $data['is_indexable'],
]
);
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,75 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\FeatureValue;
use Db;
/**
* Provides form data
*/
class FormDataProvider
{
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Fills form data
*
* @param array $params
*
* @return array
*/
public function getData(array $params)
{
$defaultUrl = [];
$defaultMetaTitle = [];
// if params contains id, gets data for edit form
if (!empty($params['id'])) {
$featureValueId = (int) $params['id'];
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value ' .
'WHERE `id_feature_value` = ' . $featureValueId
);
if (!empty($result) && is_array($result)) {
foreach ($result as $data) {
$defaultUrl[$data['id_lang']] = $data['url_name'];
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
}
}
}
return [
'url' => $defaultUrl,
'meta_title' => $defaultMetaTitle,
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\FeatureValue;
use Context;
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use PrestaShopBundle\Translation\DataCollectorTranslator;
use PrestaShopBundle\Translation\TranslatorComponent;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Adds module specific fields to BO form
*/
class FormModifier
{
/**
* @var Context
*/
private $context;
public function __construct(Context $context)
{
$this->context = $context;
}
public function modify(
FormBuilderInterface $formBuilder,
array $data
) {
/** @var DataCollectorTranslator|TranslatorComponent $translator */
$translator = $this->context->getTranslator();
$invalidCharsHint = $translator->trans(
'Invalid characters: <>;=#{}_',
[],
'Modules.Facetedsearch.Admin'
);
$urlTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing ' .
'the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s value, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$metaTitleTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed page titles by ' .
'choosing the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s value, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$formBuilder
->add(
'url_name',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
'help' => $urlTip . ' ' . $invalidCharsHint,
'options' => [
'constraints' => [
new UrlSegment([
'message' => $translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
]),
],
],
'data' => $data['url'],
]
)
->add(
'meta_title',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
'help' => $metaTitleTip,
'data' => $data['meta_title'],
]
);
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,60 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Context;
use Db;
use Ps_Facetedsearch;
abstract class AbstractHook
{
const AVAILABLE_HOOKS = [];
/**
* @var Context
*/
protected $context;
/**
* @var Ps_Facetedsearch
*/
protected $module;
/**
* @var Db
*/
protected $database;
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
$this->context = $module->getContext();
$this->database = $module->getDatabase();
}
/**
* @return array
*/
public function getAvailableHooks()
{
return static::AVAILABLE_HOOKS;
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use PrestaShop\Module\FacetedSearch\Form\Attribute\FormDataProvider;
use PrestaShop\Module\FacetedSearch\Form\Attribute\FormModifier;
use Tools;
class Attribute extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionAttributeGroupDelete',
'actionAttributeSave',
'displayAttributeForm',
'actionAttributePostProcess',
// Hooks for migrated page
'actionAttributeFormBuilderModifier',
'actionAttributeFormDataProviderData',
'actionAfterCreateAttributeFormHandler',
'actionAfterUpdateAttributeFormHandler',
];
/**
* Hook for modifying attribute form formBuilder
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAttributeFormBuilderModifier(array $params)
{
$formModifier = new FormModifier($this->context->getTranslator());
$formModifier->modify($params['form_builder']);
}
/**
* Hook that provides extra data in the form.
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAttributeFormDataProviderData(array $params)
{
$formDataProvider = new FormDataProvider($this->database);
$attributeData = $formDataProvider->getData($params);
// Update data field in params which is passed by reference
$params['data'] = array_merge($params['data'], $attributeData);
}
/**
* Hook after creation form is handled in migrated page.
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAfterCreateAttributeFormHandler(array $params): void
{
$this->save(array_merge(['id_attribute' => $params['id']], $params['form_data']));
}
/**
* Hook after edition form is handled in migrated page.
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAfterUpdateAttributeFormHandler(array $params): void
{
$this->save(array_merge(['id_attribute' => $params['id']], $params['form_data']));
}
/**
* After save attribute
*
* @param array $params
*/
public function actionAttributeSave(array $params)
{
if (empty($params['id_attribute'])) {
return;
}
$formData = [
'id_attribute' => (int) $params['id_attribute'],
];
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$seoUrl = Tools::getValue('url_name_' . $langId);
if (!empty($seoUrl)) {
$formData['url_name'][$langId] = $seoUrl;
}
$metaTitle = Tools::getValue('meta_title_' . $langId);
if (!empty($metaTitle)) {
$formData['meta_title'][$langId] = $metaTitle;
}
}
$this->save($formData);
}
/**
* After delete attribute
*
* @param array $params
*/
public function actionAttributeGroupDelete(array $params)
{
if (empty($params['id_attribute'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process attribute
*
* @param array $params
*/
public function actionAttributePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Attribute form
*
* @param array $params
*/
public function displayAttributeForm(array $params)
{
$values = [];
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang`
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
]);
return $this->module->render('attribute_form.tpl');
}
/**
* This is the common save method, the calling methods just need to format the form data appropriately
* depending on the page being migrated or not.
*
* @param array $formData
*/
private function save(array $formData): void
{
if (empty($formData['id_attribute'])) {
return;
}
$attributeId = (int) $formData['id_attribute'];
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . $attributeId
);
$landIds = array_unique(array_merge(array_keys($formData['meta_title'] ?? []), array_keys($formData['url_name'] ?? [])));
foreach ($landIds as $langId) {
$seoUrl = $formData['url_name'][$langId] ?? null;
$metaTitle = $formData['meta_title'][$langId] ?? null;
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
(`id_attribute`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . $attributeId . ', ' . $langId . ',
\'' . pSQL(Tools::str2url($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,239 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use PrestaShop\Module\FacetedSearch\Form\AttributeGroup\FormDataProvider;
use PrestaShop\Module\FacetedSearch\Form\AttributeGroup\FormModifier;
use Tools;
class AttributeGroup extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionAttributeGroupDelete',
'actionAttributeGroupSave',
'displayAttributeGroupForm',
'displayAttributeGroupPostProcess',
// Hooks for migrated page
'actionAttributeGroupFormBuilderModifier',
'actionAttributeGroupFormDataProviderData',
'actionAfterCreateAttributeGroupFormHandler',
'actionAfterUpdateAttributeGroupFormHandler',
];
/**
* Hook for modifying attribute group form formBuilder
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAttributeGroupFormBuilderModifier(array $params)
{
$formModifier = new FormModifier($this->context->getTranslator());
$formModifier->modify($params['form_builder']);
}
/**
* Hook that provides extra data in the form.
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAttributeGroupFormDataProviderData(array $params)
{
$formDataProvider = new FormDataProvider($this->database);
$attributeGroupData = $formDataProvider->getData($params);
// Update data field in params which is passed by reference
$params['data'] = array_merge($params['data'], $attributeGroupData);
}
/**
* Hook after creation form is handled in migrated page.
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAfterCreateAttributeGroupFormHandler(array $params): void
{
$this->save(array_merge(['id_attribute_group' => $params['id']], $params['form_data']));
}
/**
* Hook after edition form is handled in migrated page.
*
* @since PrestaShop 9.0.0
*
* @param array $params
*/
public function actionAfterUpdateAttributeGroupFormHandler(array $params): void
{
$this->save(array_merge(['id_attribute_group' => $params['id']], $params['form_data']));
}
/**
* After save Attributes group
*
* @param array $params
*/
public function actionAttributeGroupSave(array $params)
{
if (empty($params['id_attribute_group']) || Tools::getValue('layered_indexable') === false) {
return;
}
$formData = [
'id_attribute_group' => (int) $params['id_attribute_group'],
'is_indexable' => (int) Tools::getValue('layered_indexable'),
];
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$seoUrl = Tools::getValue('url_name_' . $langId);
if (!empty($seoUrl)) {
$formData['url_name'][$langId] = $seoUrl;
}
$metaTitle = Tools::getValue('meta_title_' . $langId);
if (!empty($metaTitle)) {
$formData['meta_title'][$langId] = $metaTitle;
}
}
$this->save($formData);
}
/**
* After delete attribute group
*
* @param array $params
*/
public function actionAttributeGroupDelete(array $params)
{
if (empty($params['id_attribute_group'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process attribute group
*
* @param array $params
*/
public function displayAttributeGroupPostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Attribute group form
*
* @param array $params
*
* @return string
*/
public function displayAttributeGroupForm(array $params)
{
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable`
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
'is_indexable' => (bool) $isIndexable,
]);
return $this->module->render('attribute_group_form.tpl');
}
/**
* This is the common save method, the calling methods just need to format the form data appropriately
* depending on the page being migrated or not.
*
* @param array $formData
*/
private function save(array $formData): void
{
if (empty($formData['id_attribute_group'])) {
return;
}
$attributeGroupId = $formData['id_attribute_group'];
// First clean all existing data
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . $attributeGroupId
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . $attributeGroupId
);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group (`id_attribute_group`, `indexable`)
VALUES (' . $attributeGroupId . ', ' . (int) $formData['is_indexable'] . ')'
);
$landIds = array_unique(array_merge(array_keys($formData['meta_title'] ?? []), array_keys($formData['url_name'] ?? [])));
foreach ($landIds as $langId) {
$seoUrl = $formData['url_name'][$langId] ?? null;
$metaTitle = $formData['meta_title'][$langId] ?? null;
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
(`id_attribute_group`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . $attributeGroupId . ', ' . $langId . ',
\'' . pSQL(Tools::str2url($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Configuration;
use Tools;
class Category extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionCategoryAdd',
'actionCategoryDelete',
];
/**
* Category addition
*
* @param array $params
*/
public function actionCategoryAdd(array $params)
{
$this->addCategoryToDefaultFilter((int) $params['category']->id);
// Flush filter block cache in all cases, so a new category shows up
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Category deletion
*
* @param array $params
*/
public function actionCategoryDelete(array $params)
{
$this->removeCategoryFromFilterTemplates((int) $params['category']->id);
}
/**
* Clean and rebuild category filters
*
* @param int $idCategory
*/
private function removeCategoryFromFilterTemplates(int $idCategory)
{
// Get all filter templates
$filterTemplates = $this->database->executeS(
'SELECT * FROM ' . _DB_PREFIX_ . 'layered_filter'
);
$rebuildNeeded = false;
// Go through each template, check if our category is set for this template.
// If yes, remove it and update the template.
foreach ($filterTemplates as $template) {
$filters = Tools::unSerialize($template['filters']);
if (!in_array((int) $idCategory, $filters['categories'])) {
continue;
}
unset($filters['categories'][array_search((int) $idCategory, $filters['categories'])]);
$rebuildNeeded = true;
$this->database->execute(
'UPDATE `' . _DB_PREFIX_ . 'layered_filter`
SET `filters` = "' . pSQL(serialize($filters)) . '",
n_categories = ' . (int) count($filters['categories']) . '
WHERE `id_layered_filter` = ' . (int) $template['id_layered_filter']
);
}
// Rebuild filter table only if a category was removed from a filter
if ($rebuildNeeded) {
$this->module->buildLayeredCategories();
}
// Flush cache all the time, because the category could be cached in a category filter block
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Checks if module is configured to automatically add some filter to new categories.
* If so, it adds the new category.
*
* @param int $idCategory ID of category being created
*/
public function addCategoryToDefaultFilter(int $idCategory)
{
// Get default template
$defaultFilterTemplateId = (int) Configuration::get('PS_LAYERED_DEFAULT_CATEGORY_TEMPLATE');
if (empty($defaultFilterTemplateId)) {
return;
}
// Try to get it's data
$template = $this->module->getFilterTemplate($defaultFilterTemplateId);
if (empty($template)) {
return;
}
// Unserialize filters, add our category
$filters = Tools::unSerialize($template['filters']);
$filters['categories'][] = $idCategory;
// Update it in database
$this->database->execute(
'UPDATE `' . _DB_PREFIX_ . 'layered_filter`
SET `filters` = "' . pSQL(serialize($filters)) . '",
n_categories = ' . (int) count($filters['categories']) . '
WHERE `id_layered_filter` = ' . $defaultFilterTemplateId
);
$this->module->buildLayeredCategories();
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Configuration extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionProductPreferencesPageStockSave',
];
/**
* After save of product stock preferences form
*
* @param array $params
*/
public function actionProductPreferencesPageStockSave(array $params)
{
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Design extends AbstractHook
{
const AVAILABLE_HOOKS = [
'displayLeftColumn',
];
/**
* Force this hook to be called here instance of using WidgetInterface
* because Hook::isHookCallableOn before the instanceof function.
* Which means is_callable always returns true with a __call usage.
*
* @param array $params
*/
public function displayLeftColumn(array $params)
{
return $this->module->fetch('module:ps_facetedsearch/ps_facetedsearch.tpl');
}
}

View File

@@ -0,0 +1,268 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Configuration;
use Language;
use PrestaShop\Module\FacetedSearch\Form\Feature\FormDataProvider;
use PrestaShop\Module\FacetedSearch\Form\Feature\FormModifier;
use PrestaShopDatabaseException;
use Ps_Facetedsearch;
use Tools;
class Feature extends AbstractHook
{
/**
* @var FormModifier
*/
private $formModifier;
/**
* @var FormDataProvider
*/
private $dataProvider;
/**
* @var bool
*/
private $isMigratedPage = false;
public function __construct(Ps_Facetedsearch $module)
{
parent::__construct($module);
$this->formModifier = new FormModifier($module->getContext());
$this->dataProvider = new FormDataProvider($module->getDatabase());
}
const AVAILABLE_HOOKS = [
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
'actionFeatureFormBuilderModifier',
'actionAfterCreateFeatureFormHandler',
'actionAfterUpdateFeatureFormHandler',
];
/**
* Hook for modifying feature form formBuilder
*
* @param array $params
*
* @throws PrestaShopDatabaseException
*/
public function actionFeatureFormBuilderModifier(array $params)
{
$this->isMigratedPage = true;
$this->formModifier->modify($params['form_builder'], $this->dataProvider->getData($params));
}
/**
* Hook after create feature.
*
* @since PrestaShop 1.7.8.0
*
* @param array $params
*/
public function actionAfterCreateFeatureFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after update feature.
*
* @since PrestaShop 1.7.8.0
*
* @param array $params
*/
public function actionAfterUpdateFeatureFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after delete a feature
*
* @param array $params
*/
public function actionFeatureDelete(array $params)
{
if (empty($params['id_feature'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Hook post process feature
*
* @param array $params
*/
public function displayFeaturePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Hook feature form
*
* @param array $params
*/
public function displayFeatureForm(array $params)
{
if ($this->isMigratedPage === true) {
return;
}
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
'WHERE `id_feature` = ' . (int) $params['id_feature']
);
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'WHERE `id_feature` = ' . (int) $params['id_feature']
);
if ($result) {
foreach ($result as $data) {
$values[$data['id_lang']] = [
'url_name' => $data['url_name'],
'meta_title' => $data['meta_title'],
];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
'is_indexable' => (bool) $isIndexable,
]);
return $this->module->render('feature_form.tpl');
}
/**
* After save feature
*
* @param array $params
*/
public function actionFeatureSave(array $params)
{
if (empty($params['id_feature']) || Tools::getValue('layered_indexable') === false) {
return;
}
$featureId = (int) $params['id_feature'];
$formData = [
'layered_indexable' => Tools::getValue('layered_indexable'),
];
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$seoUrl = Tools::getValue('url_name_' . $langId);
$metaTitle = Tools::getValue('meta_title_' . $langId);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$formData['meta_title'][$langId] = $metaTitle;
$formData['url_name'][$langId] = $seoUrl;
}
$this->save($featureId, $formData);
}
/**
* Saves feature form.
*
* @param int $featureId
* @param array $formData
*
* @since PrestaShop 1.7.8.0
*/
private function save($featureId, array $formData)
{
$this->cleanLayeredIndexableTables($featureId);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature
(`id_feature`, `indexable`)
VALUES (' . (int) $featureId . ', ' . (int) $formData['layered_indexable'] . ')'
);
$defaultLangId = (int) Configuration::get('PS_LANG_DEFAULT');
$query = 'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'(`id_feature`, `id_lang`, `url_name`, `meta_title`) ' .
'VALUES (%d, %d, \'%s\', \'%s\')';
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$metaTitle = pSQL($formData['meta_title'][$langId]);
$seoUrl = $formData['url_name'][$langId];
$name = $formData['name'][$langId] ?: $formData['name'][$defaultLangId];
if (!empty($seoUrl)) {
$seoUrl = pSQL(Tools::str2url($seoUrl));
}
$this->database->execute(
sprintf(
$query,
$featureId,
$langId,
$seoUrl,
$metaTitle
)
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Deletes from layered_indexable_feature and layered_indexable_feature_lang_value by feature id
*
* @param int $featureId
*/
private function cleanLayeredIndexableTables($featureId)
{
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . $featureId
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
WHERE `id_feature` = ' . $featureId
);
}
}

View File

@@ -0,0 +1,224 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use PrestaShop\Module\FacetedSearch\Form\FeatureValue\FormDataProvider;
use PrestaShop\Module\FacetedSearch\Form\FeatureValue\FormModifier;
use Ps_Facetedsearch;
use Tools;
class FeatureValue extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionFeatureValueSave',
'actionFeatureValueDelete',
'displayFeatureValueForm',
'displayFeatureValuePostProcess',
'actionFeatureValueFormBuilderModifier',
'actionAfterCreateFeatureValueFormHandler',
'actionAfterUpdateFeatureValueFormHandler',
];
/**
* @var FormModifier
*/
private $formModifier;
/**
* @var FormDataProvider
*/
private $dataProvider;
public function __construct(Ps_Facetedsearch $module)
{
parent::__construct($module);
$this->formModifier = new FormModifier($module->getContext());
$this->dataProvider = new FormDataProvider($module->getDatabase());
}
/**
* Hook for modifying feature form formBuilder
*
* @since PrestaShop 9.0
*
* @param array $params
*/
public function actionFeatureValueFormBuilderModifier(array $params)
{
$this->formModifier->modify($params['form_builder'], $this->dataProvider->getData($params));
}
/**
* Hook after create feature.
*
* @since PrestaShop 9.0
*
* @param array $params
*/
public function actionAfterCreateFeatureValueFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after update feature.
*
* @since PrestaShop 9.0
*
* @param array $params
*/
public function actionAfterUpdateFeatureValueFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* After save feature value
*
* @param array $params
*/
public function actionFeatureValueSave(array $params)
{
if (empty($params['id_feature_value'])) {
return;
}
//Removing all indexed language data for this attribute value id
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
(`id_feature_value`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_feature_value'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::str2url($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete Feature value
*
* @param array $params
*/
public function actionFeatureValueDelete(array $params)
{
if (empty($params['id_feature_value'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process feature value
*
* @param array $params
*/
public function displayFeatureValuePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Display feature value form
*
* @param array $params
*
* @return string
*/
public function displayFeatureValueForm(array $params)
{
$values = [];
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang`
FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
]);
return $this->module->render('feature_value_form.tpl');
}
private function save($featureValueId, array $formData)
{
$featureValueId = (int) $featureValueId;
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . $featureValueId
);
$query = 'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value ' .
'(`id_feature_value`, `id_lang`, `url_name`, `meta_title`) ' .
'VALUES (%d, %d, \'%s\', \'%s\')';
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$metaTitle = pSQL($formData['meta_title'][$langId]);
$seoUrl = $formData['url_name'][$langId];
if (!empty($seoUrl)) {
$seoUrl = pSQL(Tools::str2url($seoUrl));
}
$this->database->execute(
sprintf(
$query,
$featureValueId,
$langId,
$seoUrl,
$metaTitle
)
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Product extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionProductSave',
];
/**
* After save product
*
* @param array $params
*/
public function actionProductSave(array $params)
{
if (empty($params['id_product'])) {
return;
}
$this->module->indexProductPrices((int) $params['id_product']);
$this->module->indexAttributes((int) $params['id_product']);
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Configuration;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\Module\FacetedSearch\Filters\DataAccessor;
use PrestaShop\Module\FacetedSearch\Filters\Provider;
use PrestaShop\Module\FacetedSearch\Product\SearchFactory;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
class ProductSearch extends AbstractHook
{
const AVAILABLE_HOOKS = [
'productSearchProvider',
];
/**
* This method returns the search provider to the controller who requested it.
*
* @param array $params
*
* @return SearchProvider|null
*/
public function productSearchProvider(array $params)
{
/*
* Backward compatibility, required for versions < 8.0
* We need to assign missing queryType to some controllers, which don't report it.
* Remove when module minimum compatibility reaches 8.0.
*/
if (empty($params['query']->getQueryType())) {
$params['query'] = $this->assignMissingQueryType($params['query']);
}
/*
* Check if the type of query (controller) is supported by our module. If not, we
* let the core do the search.
*/
if ($this->module->isControllerSupported($params['query']->getQueryType()) === false) {
return null;
}
// Initialize provider, we will need it right away to check if there are filters setup
$provider = new Provider($this->module->getDatabase());
/*
* If search controller is not specifically enabled, we don't return the instance.
* This condition will be removed when search controller support is fully implemented.
*/
if ($params['query']->getQueryType() === 'search'
&& empty($provider->getFiltersForQuery($params['query'], (int) $this->context->shop->id))) {
return null;
}
/*
* Fix wrong reporting of desired best sales order. BestSalesProductSearchProvider overrides
* the sort set on the query in BestSalesControllerCore.
*/
if ($params['query']->getQueryType() == 'best-sales') {
$params['query']->setSortOrder(new SortOrder('product', 'sales', 'desc'));
}
// Assign assets
if ((bool) Configuration::get('PS_USE_JQUERY_UI_SLIDER')) {
$this->context->controller->addJqueryUi('ui.slider');
}
$this->context->controller->registerStylesheet(
'facetedsearch_front',
'/modules/ps_facetedsearch/views/dist/front.css'
);
$this->context->controller->registerJavascript(
'facetedsearch_front',
'/modules/ps_facetedsearch/views/dist/front.js',
['position' => 'bottom', 'priority' => 100]
);
$urlSerializer = new URLSerializer();
$dataAccessor = new DataAccessor($this->module->getDatabase());
// Return an instance of our searcher, ready to accept requests
return new SearchProvider(
$this->module,
new Converter(
$this->module->getContext(),
$this->module->getDatabase(),
$urlSerializer,
$dataAccessor,
$provider
),
$urlSerializer,
$dataAccessor,
new SearchFactory(),
$provider
);
}
/**
* Assign missing queryType, required for PS versions < 8.0
*
* @param ProductSearchQuery $query
*
* @return ProductSearchQuery
*/
private function assignMissingQueryType(ProductSearchQuery $query)
{
if (!empty($query->getIdCategory())) {
$query->setQueryType('category');
} elseif (!empty($query->getIdManufacturer())) {
$query->setQueryType('manufacturer');
} elseif (!empty($query->getIdSupplier())) {
$query->setQueryType('supplier');
} elseif (!empty($query->getSearchString()) || !empty($query->getSearchTag())) {
$query->setQueryType('search');
}
return $query;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class SpecificPrice extends AbstractHook
{
/**
* @var array
*/
protected $productsBefore = null;
const AVAILABLE_HOOKS = [
'actionObjectSpecificPriceRuleUpdateBefore',
'actionAdminSpecificPriceRuleControllerSaveAfter',
];
/**
* Before saving a specific price rule
*
* @param array $params
*/
public function actionObjectSpecificPriceRuleUpdateBefore(array $params)
{
if (empty($params['object']->id)) {
return;
}
/** @var \SpecificPriceRule */
$specificPrice = $params['object'];
$this->productsBefore = $specificPrice->getAffectedProducts();
}
/**
* After saving a specific price rule
*
* @param array $params
*/
public function actionAdminSpecificPriceRuleControllerSaveAfter(array $params)
{
if (empty($params['return']->id) || empty($this->productsBefore)) {
return;
}
/** @var \SpecificPriceRule */
$specificPrice = $params['return'];
$affectedProducts = array_merge($this->productsBefore, $specificPrice->getAffectedProducts());
foreach ($affectedProducts as $product) {
$this->module->indexProductPrices($product['id_product']);
$this->module->indexAttributes($product['id_product']);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,113 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch;
use Ps_Facetedsearch;
/**
* Class works with Hook\AbstractHook instances in order to reduce ps_facetedsearch.php size.
*
* The dispatch method is called from the __call method in the module class.
*/
class HookDispatcher
{
const CLASSES = [
Hook\Attribute::class,
Hook\AttributeGroup::class,
Hook\Category::class,
Hook\Configuration::class,
Hook\Design::class,
Hook\Feature::class,
Hook\FeatureValue::class,
Hook\Product::class,
Hook\ProductSearch::class,
Hook\SpecificPrice::class,
];
/**
* List of available hooks
*
* @var string[]
*/
private $availableHooks = [];
/**
* Hook classes
*
* @var Hook\AbstractHook[]
*/
private $hooks = [];
/**
* Module
*
* @var Ps_Facetedsearch
*/
private $module;
/**
* Init hooks
*
* @param Ps_Facetedsearch $module
*/
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
foreach (self::CLASSES as $hookClass) {
$hook = new $hookClass($this->module);
$this->availableHooks = array_merge($this->availableHooks, $hook->getAvailableHooks());
$this->hooks[] = $hook;
}
}
/**
* Get available hooks
*
* @return string[]
*/
public function getAvailableHooks()
{
return $this->availableHooks;
}
/**
* Find hook and dispatch it
*
* @param string $hookName
* @param array $params
*
* @return mixed
*/
public function dispatch($hookName, array $params = [])
{
$hookName = preg_replace('~^hook~', '', $hookName);
foreach ($this->hooks as $hook) {
if (method_exists($hook, $hookName)) {
return call_user_func([$hook, $hookName], $params);
}
}
// No hook found, render it as a widget
return $this->module->renderWidget($hookName, $params);
}
}

View File

@@ -0,0 +1,430 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Configuration;
use Context;
use Db;
use FrontController;
use Group;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use Search;
use Shop;
use Tools;
/**
* PrestaShop core does not provide a reasonable (fast) way to get product pool to to search
* without extra performance overhead we don't need. This class contains fast backports of
* Search::find method of every major for this purpose.
*
* This class will be removed when we are able to get product pool from Search class directly.
*/
class CoreSearchBackport
{
/**
* Returns a pool of product IDs to use when filtering products on search controller.
*
* @param ProductSearchQuery $query
*
* @return array Pool of product IDs
*/
public function getProductPool(ProductSearchQuery $query)
{
// Get search expression from query
$expression = Tools::replaceAccentedChars(urldecode($query->getSearchString()));
// No changes in 8.0 to 8.1
if (version_compare(_PS_VERSION_, '8.0.0', '>=')) {
return $this->get80($expression);
} elseif (version_compare(_PS_VERSION_, '1.7.8.0', '>=')) {
return $this->get178($expression);
} elseif (version_compare(_PS_VERSION_, '1.7.7.0', '>=')) {
return $this->get177($expression);
} else {
return $this->get176($expression);
}
}
/**
* Backported from 1.7.6.9
*
* @param string $expr
*
* @return array Pool of product IDs
*/
public function get176($expr)
{
$context = Context::getContext();
$db = Db::getInstance(_PS_USE_SQL_SLAVE_);
$intersect_array = [];
$words = Search::extractKeyWords($expr, $context->language->id, false, $context->language->iso_code);
foreach ($words as $key => $word) {
if (!empty($word) && strlen($word) >= (int) Configuration::get('PS_SEARCH_MINWORDLEN')) {
$sql_param_search = Search::getSearchParamFromWord($word);
$intersect_array[] = 'SELECT DISTINCT si.id_product
FROM ' . _DB_PREFIX_ . 'search_word sw
LEFT JOIN ' . _DB_PREFIX_ . 'search_index si ON sw.id_word = si.id_word
WHERE sw.id_lang = ' . (int) $context->language->id . '
AND sw.id_shop = ' . $context->shop->id . '
AND sw.word LIKE
\'' . $sql_param_search . '\'';
} else {
unset($words[$key]);
}
}
if (!count($words)) {
return [];
}
$sql_groups = '';
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sql_groups = 'AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Configuration::get('PS_UNIDENTIFIED_GROUP'));
}
$results = $db->executeS('
SELECT DISTINCT cp.`id_product`
FROM `' . _DB_PREFIX_ . 'category_product` cp
' . (Group::isFeatureActive() ? 'INNER JOIN `' . _DB_PREFIX_ . 'category_group` cg ON cp.`id_category` = cg.`id_category`' : '') . '
INNER JOIN `' . _DB_PREFIX_ . 'category` c ON cp.`id_category` = c.`id_category`
INNER JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product`
' . Shop::addSqlAssociation('product', 'p', false) . '
WHERE c.`active` = 1
AND product_shop.`active` = 1
AND product_shop.`visibility` IN ("both", "search")
AND product_shop.indexed = 1
' . $sql_groups, true, false);
$eligible_products = [];
foreach ($results as $row) {
$eligible_products[] = $row['id_product'];
}
$eligible_products2 = [];
foreach ($intersect_array as $query) {
foreach ($db->executeS($query, true, false) as $row) {
$eligible_products2[] = $row['id_product'];
}
}
return array_unique(array_intersect($eligible_products, array_unique($eligible_products2)));
}
/**
* Backported from 1.7.7.8
*
* @param string $expr
*
* @return array Pool of product IDs
*/
public function get177($expr)
{
$context = Context::getContext();
$db = Db::getInstance(_PS_USE_SQL_SLAVE_);
$fuzzyLoop = 0;
$eligibleProducts2 = null;
$words = Search::extractKeyWords($expr, $context->language->id, false, $context->language->iso_code);
$fuzzyMaxLoop = (int) Configuration::get('PS_SEARCH_FUZZY_MAX_LOOP');
$psFuzzySearch = (int) Configuration::get('PS_SEARCH_FUZZY');
$psSearchMinWordLength = (int) Configuration::get('PS_SEARCH_MINWORDLEN');
foreach ($words as $key => $word) {
if (empty($word) || strlen($word) < $psSearchMinWordLength) {
unset($words[$key]);
continue;
}
$sql_param_search = Search::getSearchParamFromWord($word);
$sql = 'SELECT DISTINCT si.id_product ' .
'FROM ' . _DB_PREFIX_ . 'search_word sw ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'search_index si ON sw.id_word = si.id_word ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'product_shop product_shop ON (product_shop.`id_product` = si.`id_product`) ' .
'WHERE sw.id_lang = ' . (int) $context->language->id . ' ' .
'AND sw.id_shop = ' . $context->shop->id . ' ' .
'AND product_shop.`active` = 1 ' .
'AND product_shop.`visibility` IN ("both", "search") ' .
'AND product_shop.indexed = 1 ' .
'AND sw.word LIKE ';
while (!($result = $db->executeS($sql . "'" . $sql_param_search . "';", true, false))) {
if (
!$psFuzzySearch
|| $fuzzyLoop++ > $fuzzyMaxLoop
|| !($sql_param_search = Search::findClosestWeightestWord($context, $word))
) {
break;
}
}
if (!$result) {
unset($words[$key]);
continue;
}
$productIds = array_column($result, 'id_product');
if ($eligibleProducts2 === null) {
$eligibleProducts2 = $productIds;
} else {
$eligibleProducts2 = array_intersect($eligibleProducts2, $productIds);
}
}
if (!count($words)) {
return [];
}
$sqlGroups = '';
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = 'AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id);
}
$results = $db->executeS(
'SELECT DISTINCT cp.`id_product` ' .
'FROM `' . _DB_PREFIX_ . 'category_product` cp ' .
(Group::isFeatureActive() ? 'INNER JOIN `' . _DB_PREFIX_ . 'category_group` cg ON cp.`id_category` = cg.`id_category`' : '') . ' ' .
'INNER JOIN `' . _DB_PREFIX_ . 'category` c ON cp.`id_category` = c.`id_category` ' .
'INNER JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product` ' .
Shop::addSqlAssociation('product', 'p', false) . ' ' .
'WHERE c.`active` = 1 ' .
'AND product_shop.`active` = 1 ' .
'AND product_shop.`visibility` IN ("both", "search") ' .
'AND product_shop.indexed = 1 ' . $sqlGroups,
true,
false
);
$eligibleProducts = array_column($results, 'id_product');
return array_unique(array_intersect($eligibleProducts, array_unique($eligibleProducts2)));
}
/**
* Backported from 1.7.8.8
*
* @param string $expr
*
* @return array Pool of product IDs
*/
public function get178($expr)
{
$context = Context::getContext();
$db = Db::getInstance(_PS_USE_SQL_SLAVE_);
$fuzzyLoop = 0;
$eligibleProducts2 = null;
$words = Search::extractKeyWords($expr, $context->language->id, false, $context->language->iso_code);
$fuzzyMaxLoop = (int) Configuration::get('PS_SEARCH_FUZZY_MAX_LOOP');
$psFuzzySearch = (int) Configuration::get('PS_SEARCH_FUZZY');
$psSearchMinWordLength = (int) Configuration::get('PS_SEARCH_MINWORDLEN');
foreach ($words as $key => $word) {
if (empty($word) || strlen($word) < $psSearchMinWordLength) {
unset($words[$key]);
continue;
}
$sql_param_search = Search::getSearchParamFromWord($word);
$sql = 'SELECT DISTINCT si.id_product ' .
'FROM ' . _DB_PREFIX_ . 'search_word sw ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'search_index si ON sw.id_word = si.id_word ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'product_shop product_shop ON (product_shop.`id_product` = si.`id_product`) ' .
'WHERE sw.id_lang = ' . (int) $context->language->id . ' ' .
'AND sw.id_shop = ' . $context->shop->id . ' ' .
'AND product_shop.`active` = 1 ' .
'AND product_shop.`visibility` IN ("both", "search") ' .
'AND product_shop.indexed = 1 ' .
'AND sw.word LIKE ';
while (!($result = $db->executeS($sql . "'" . $sql_param_search . "';", true, false))) {
if (
!$psFuzzySearch
|| $fuzzyLoop++ > $fuzzyMaxLoop
|| !($sql_param_search = Search::findClosestWeightestWord($context, $word))
) {
break;
}
}
if (!$result) {
unset($words[$key]);
continue;
}
$productIds = array_column($result, 'id_product');
if ($eligibleProducts2 === null) {
$eligibleProducts2 = $productIds;
} else {
$eligibleProducts2 = array_intersect($eligibleProducts2, $productIds);
}
}
if (!count($words) || !count($eligibleProducts2)) {
return [];
}
$sqlGroups = '';
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = 'AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id);
}
$results = $db->executeS(
'SELECT DISTINCT cp.`id_product` ' .
'FROM `' . _DB_PREFIX_ . 'category_product` cp ' .
(Group::isFeatureActive() ? 'INNER JOIN `' . _DB_PREFIX_ . 'category_group` cg ON cp.`id_category` = cg.`id_category`' : '') . ' ' .
'INNER JOIN `' . _DB_PREFIX_ . 'category` c ON cp.`id_category` = c.`id_category` ' .
'INNER JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product` ' .
Shop::addSqlAssociation('product', 'p', false) . ' ' .
'WHERE c.`active` = 1 ' .
'AND product_shop.`active` = 1 ' .
'AND product_shop.`visibility` IN ("both", "search") ' .
'AND product_shop.indexed = 1 ' .
'AND cp.id_product IN (' . implode(',', $eligibleProducts2) . ')' . $sqlGroups,
true,
false
);
return array_column($results, 'id_product');
}
/**
* Backported 8.0.1
*
* @param string $expr
*
* @return array Pool of product IDs
*/
public function get80($expr)
{
$context = Context::getContext();
$db = Db::getInstance(_PS_USE_SQL_SLAVE_);
$scoreArray = [];
$fuzzyLoop = 0;
$wordCnt = 0;
$eligibleProducts2Full = [];
$expressions = explode(';', $expr);
$fuzzyMaxLoop = (int) Configuration::get('PS_SEARCH_FUZZY_MAX_LOOP');
$psFuzzySearch = (int) Configuration::get('PS_SEARCH_FUZZY');
$psSearchMinWordLength = (int) Configuration::get('PS_SEARCH_MINWORDLEN');
foreach ($expressions as $expression) {
$eligibleProducts2 = null;
$words = Search::extractKeyWords($expression, $context->language->id, false, $context->language->iso_code);
foreach ($words as $key => $word) {
if (empty($word) || strlen($word) < $psSearchMinWordLength) {
unset($words[$key]);
continue;
}
$sql_param_search = Search::getSearchParamFromWord($word);
$sql = 'SELECT DISTINCT si.id_product ' .
'FROM ' . _DB_PREFIX_ . 'search_word sw ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'search_index si ON sw.id_word = si.id_word ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'product_shop product_shop ON (product_shop.`id_product` = si.`id_product`) ' .
'WHERE sw.id_lang = ' . (int) $context->language->id . ' ' .
'AND sw.id_shop = ' . $context->shop->id . ' ' .
'AND product_shop.`active` = 1 ' .
'AND product_shop.`visibility` IN ("both", "search") ' .
'AND product_shop.indexed = 1 ' .
'AND sw.word LIKE ';
while (!($result = $db->executeS($sql . "'" . $sql_param_search . "';", true, false))) {
if (
!$psFuzzySearch
|| $fuzzyLoop++ > $fuzzyMaxLoop
|| !($sql_param_search = Search::findClosestWeightestWord($context, $word))
) {
break;
}
}
if (!$result) {
unset($words[$key]);
continue;
}
$productIds = array_column($result, 'id_product');
if ($eligibleProducts2 === null) {
$eligibleProducts2 = $productIds;
} else {
$eligibleProducts2 = array_intersect($eligibleProducts2, $productIds);
}
$scoreArray[] = 'sw.word LIKE \'' . $sql_param_search . '\'';
}
$wordCnt += count($words);
if ($eligibleProducts2) {
$eligibleProducts2Full = array_merge($eligibleProducts2Full, $eligibleProducts2);
}
}
$eligibleProducts2Full = array_unique($eligibleProducts2Full);
if (!$wordCnt || !count($eligibleProducts2Full)) {
return [];
}
$sqlScore = '';
if (!empty($scoreArray) && is_array($scoreArray)) {
$sqlScore = ',( ' .
'SELECT SUM(weight) ' .
'FROM ' . _DB_PREFIX_ . 'search_word sw ' .
'LEFT JOIN ' . _DB_PREFIX_ . 'search_index si ON sw.id_word = si.id_word ' .
'WHERE sw.id_lang = ' . (int) $context->language->id . ' ' .
'AND sw.id_shop = ' . $context->shop->id . ' ' .
'AND si.id_product = p.id_product ' .
'AND (' . implode(' OR ', $scoreArray) . ') ' .
') position';
}
$sqlGroups = '';
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$sqlGroups = 'AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id);
}
$results = $db->executeS(
'SELECT DISTINCT cp.`id_product` ' . $sqlScore . ' ' .
'FROM `' . _DB_PREFIX_ . 'category_product` cp ' .
(Group::isFeatureActive() ? 'INNER JOIN `' . _DB_PREFIX_ . 'category_group` cg ON cp.`id_category` = cg.`id_category`' : '') . ' ' .
'INNER JOIN `' . _DB_PREFIX_ . 'category` c ON cp.`id_category` = c.`id_category` ' .
'INNER JOIN `' . _DB_PREFIX_ . 'product` p ON cp.`id_product` = p.`id_product` ' .
Shop::addSqlAssociation('product', 'p', false) . ' ' .
'WHERE c.`active` = 1 ' .
'AND product_shop.`active` = 1 ' .
'AND product_shop.`visibility` IN ("both", "search") ' .
'AND product_shop.indexed = 1 ' .
'AND cp.id_product IN (' . implode(',', $eligibleProducts2Full) . ')' . $sqlGroups . '
ORDER BY position DESC, p.id_product ASC',
true,
false
);
return array_column($results, 'id_product');
}
}

View File

@@ -0,0 +1,499 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Category;
use Configuration;
use Context;
use FrontController;
use Group;
use Hook;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL as MySQLAdapter;
use PrestaShop\Module\FacetedSearch\Definition\Availability;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
class Search
{
const STOCK_MANAGEMENT_FILTER = 'with_stock_management';
const HIGHLIGHTS_FILTER = 'extras';
/**
* @var bool
*/
protected $psStockManagement;
/**
* @var bool
*/
protected $psOrderOutOfStock;
/**
* @var AbstractAdapter
*/
protected $searchAdapter;
/**
* @var Context
*/
protected $context;
/**
* @var ProductSearchQuery
*/
protected $query;
/**
* Search constructor.
*
* @param Context $context
* @param string $adapterType
*/
public function __construct(Context $context, $adapterType = MySQLAdapter::TYPE)
{
$this->context = $context;
switch ($adapterType) {
case MySQLAdapter::TYPE:
default:
$this->searchAdapter = new MySQLAdapter();
}
if ($this->psStockManagement === null) {
$this->psStockManagement = (bool) Configuration::get('PS_STOCK_MANAGEMENT');
}
if ($this->psOrderOutOfStock === null) {
$this->psOrderOutOfStock = (bool) Configuration::get('PS_ORDER_OUT_OF_STOCK');
}
}
/**
* @return AbstractAdapter
*/
public function getSearchAdapter()
{
return $this->searchAdapter;
}
/**
* @return ProductSearchQuery
*/
public function getQuery()
{
return $this->query;
}
/**
* @param ProductSearchQuery $query
*
* @return $this
*/
public function setQuery(ProductSearchQuery $query)
{
$this->query = $query;
return $this;
}
/**
* Init the initial population of the search filter
*
* @param array $selectedFilters
*/
public function initSearch($selectedFilters)
{
// Adds basic filters that are common for every search, like shop and group limitations
$this->addCommonFilters();
// Add filters that the user has selected for current query
$this->addSearchFilters($selectedFilters);
// Adds filters that specific for this controller
$this->addControllerSpecificFilters();
// Add group by to remove duplicate values
$this->getSearchAdapter()->addGroupBy('id_product');
// Move the current search into the "initialPopulation"
// This initialPopulation will be used to generate the base table in the final query
$this->getSearchAdapter()->useFiltersAsInitialPopulation();
}
/**
* Adds filters that the user has specifically selected for current query
*
* @param array $selectedFilters
*/
private function addSearchFilters($selectedFilters)
{
foreach ($selectedFilters as $key => $filterValues) {
if (!count($filterValues)) {
continue;
}
switch ($key) {
case 'id_feature':
$operationsFilter = [];
foreach ($filterValues as $featureId => $filterValue) {
$this->getSearchAdapter()->addOperationsFilter(
'with_features_' . $featureId,
[[['id_feature_value', $filterValue]]]
);
}
break;
case 'id_attribute_group':
$operationsFilter = [];
foreach ($filterValues as $attributeId => $filterValue) {
$this->getSearchAdapter()->addOperationsFilter(
'with_attributes_' . $attributeId,
[[['id_attribute', $filterValue]]]
);
}
break;
case 'category':
$this->addFilter('id_category', $filterValues);
break;
case 'extras':
// Filter for new products
if (in_array('new', $filterValues)) {
$timeCondition = date(
'Y-m-d 00:00:00',
strtotime(
((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') > 0 ?
'-' . ((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') - 1) . ' days' :
'+ 1 days')
)
);
// Reset filter to prevent two same filters if we are on new products page
$this->getSearchAdapter()->addFilter('date_add', ["'" . $timeCondition . "'"], '>');
}
// Filter for discounts - they must work as OR
$operationsFilter = [];
if (in_array('discount', $filterValues)) {
$operationsFilter[] = [
['reduction', [0], '>'],
];
}
if (in_array('sale', $filterValues)) {
$operationsFilter[] = [
['on_sale', [1], '='],
];
}
if (!empty($operationsFilter)) {
$this->getSearchAdapter()->addOperationsFilter(
self::HIGHLIGHTS_FILTER,
$operationsFilter
);
}
break;
case 'availability':
/*
* $filterValues options can have following values:
* 0 - Not available - 0 or less quantity and disabled backorders
* 1 - Available - Positive quantity or enabled backorders
* 2 - In stock - Positive quantity
*/
// If all three values are checked, we show everything
if (count($filterValues) == 3) {
break;
}
// If stock management is deactivated, we show everything
if (!$this->psStockManagement) {
break;
}
$operationsFilter = [];
// Simple cases with 1 option selected
if (count($filterValues) == 1) {
// Not available
if ($filterValues[0] == Availability::NOT_AVAILABLE) {
$operationsFilter[] = [
['quantity', [0], '<='],
['out_of_stock', $this->psOrderOutOfStock ? [0] : [0, 2], '='],
];
// Available
} elseif ($filterValues[0] == Availability::AVAILABLE) {
$operationsFilter[] = [
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
// In stock
} elseif ($filterValues[0] == Availability::IN_STOCK) {
$operationsFilter[] = [
['quantity', [0], '>'],
];
}
// Cases with 2 options selected
} elseif (count($filterValues) == 2) {
// Not available and available, we show everything
if (in_array(Availability::NOT_AVAILABLE, $filterValues) && in_array(Availability::AVAILABLE, $filterValues)) {
break;
// Not available or in stock
} elseif (in_array(Availability::NOT_AVAILABLE, $filterValues) && in_array(Availability::IN_STOCK, $filterValues)) {
$operationsFilter[] = [
['quantity', [0], '<='],
['out_of_stock', $this->psOrderOutOfStock ? [0] : [0, 2], '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
// Available or in stock
} elseif (in_array(Availability::AVAILABLE, $filterValues) && in_array(Availability::IN_STOCK, $filterValues)) {
$operationsFilter[] = [
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
}
}
$this->getSearchAdapter()->addOperationsFilter(
self::STOCK_MANAGEMENT_FILTER,
$operationsFilter
);
break;
case 'manufacturer':
$this->addFilter('id_manufacturer', $filterValues);
break;
case 'condition':
if (count($selectedFilters['condition']) == 3) {
break;
}
$this->addFilter('condition', $filterValues);
break;
case 'weight':
if (!empty($selectedFilters['weight'][0]) || !empty($selectedFilters['weight'][1])) {
$this->getSearchAdapter()->addFilter(
'weight',
[(float) $selectedFilters['weight'][0]],
'>='
);
$this->getSearchAdapter()->addFilter(
'weight',
[(float) $selectedFilters['weight'][1]],
'<='
);
}
break;
case 'price':
if (isset($selectedFilters['price'])
&& (
$selectedFilters['price'][0] !== '' || $selectedFilters['price'][1] !== ''
)
) {
$this->addPriceFilter(
(float) $selectedFilters['price'][0],
(float) $selectedFilters['price'][1]
);
}
break;
}
}
}
/**
* Adds filters that are common for every search
*/
private function addCommonFilters()
{
// Setting proper shop
$this->getSearchAdapter()->addFilter('id_shop', [(int) $this->context->shop->id]);
// Visibility of a product must be in catalog or both (search & catalog)
$this->addFilter('visibility', ['both', 'catalog']);
// User must belong to one of the groups that can access the product
// (Actually it's categories that define access to a product, user must have access to at least
// one category the product is assigned to.)
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$this->addFilter('id_group', empty($groups) ? [Group::getCurrent()->id] : $groups);
}
}
/**
* Adds filters that specific for category page
*/
private function addControllerSpecificFilters()
{
// Category page
if ($this->query->getQueryType() == 'category') {
// We check if some specific filter of this type wasn't added before by the customer
if (!empty($this->getSearchAdapter()->getFilter('id_category'))) {
return;
}
// Get category ID from the query or home category as a fallback
$idCategory = (int) $this->query->getIdCategory();
if (empty($idCategory)) {
$idCategory = (int) Configuration::get('PS_HOME_CATEGORY');
}
$category = new Category((int) $idCategory);
// If we want to display only products from this category AND not it's subcategories,
// we add this one specific category ID, otherwise, we will add everything using nleft and nright
if (Configuration::get('PS_LAYERED_FULL_TREE')) {
$this->getSearchAdapter()->addFilter('nleft', [$category->nleft], '>=');
$this->getSearchAdapter()->addFilter('nright', [$category->nright], '<=');
} else {
$this->addFilter('id_category', [$idCategory]);
}
// If we want to display products, which have this category as their default category
if (Configuration::get('PS_LAYERED_FILTER_BY_DEFAULT_CATEGORY')) {
$this->addFilter('id_category_default', [$idCategory]);
}
}
// Manufacturer controller
if ($this->query->getQueryType() == 'manufacturer') {
$this->getSearchAdapter()->addFilter('id_manufacturer', [$this->query->getIdManufacturer()]);
}
// Supplier controller
if ($this->query->getQueryType() == 'supplier') {
$this->getSearchAdapter()->addFilter('id_supplier', [$this->query->getIdSupplier()]);
}
/*
* New products controller
*
* Comparsion works works on a day basis, not 24 hours.
* If you set 1 day, only products created TODAY will be new.
* If there is a zero set to disable this feature, it creates unreachable condition.
*/
if ($this->query->getQueryType() == 'new-products') {
// We check if some specific filter of this type wasn't added before
if (!empty($this->getSearchAdapter()->getFilter('date_add'))) {
return;
}
$timeCondition = date(
'Y-m-d 00:00:00',
strtotime(
((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') > 0 ?
'-' . ((int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') - 1) . ' days' :
'+ 1 days')
)
);
$this->getSearchAdapter()->addFilter('date_add', ["'" . $timeCondition . "'"], '>');
}
/*
* Bestsellers controller
*
* We are selecting all products from product_sale table.
*/
if ($this->query->getQueryType() == 'best-sales') {
$this->getSearchAdapter()->addFilter('sales', [0], '>');
}
/*
* Prices drop controller
*
* We are selecting products that have a specific price created meeting certain conditions.
*/
if ($this->query->getQueryType() == 'prices-drop') {
// We check if some specific filter of this type wasn't added before
if (!empty($this->getSearchAdapter()->getFilter('reduction'))) {
return;
}
$this->getSearchAdapter()->addFilter('reduction', [0], '>');
}
/*
* Search controller
*
* We are using a fast backport to get a product pool, which is then passed to the query.
* Core search provider does simmilar thing. If nothing is found, we return a value
* (NULL string) that will ensure empty result. It would be better to stop the search
* sooner in the logic, in the future.
*/
if ($this->query->getQueryType() == 'search') {
$productPool = (new CoreSearchBackport())->getProductPool($this->query);
$this->getSearchAdapter()->addFilter(
'id_product',
empty($productPool) ? ['NULL'] : $productPool
);
}
Hook::exec(
'actionFacetedSearchFilters',
[
'search' => $this,
'query' => $this->query,
]
);
}
/**
* Add a filter with the filterValues extracted from the selectedFilters
*
* @param string $filterName
* @param array $filterValues
*/
public function addFilter($filterName, array $filterValues)
{
$values = [];
foreach ($filterValues as $filterValue) {
if (is_array($filterValue)) {
foreach ($filterValue as $subFilterValue) {
$values[] = (int) $subFilterValue;
}
} else {
$values[] = $filterValue;
}
}
if (!empty($values)) {
$this->getSearchAdapter()->addFilter($filterName, $values);
}
}
/**
* Add a price filter
*
* @param float $minPrice
* @param float $maxPrice
*/
private function addPriceFilter($minPrice, $maxPrice)
{
$this->getSearchAdapter()->addFilter('price_min', [$maxPrice], '<=');
$this->getSearchAdapter()->addFilter('price_max', [$minPrice], '>=');
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Context;
class SearchFactory
{
/**
* Returns an instance of Search for this context
*
* @param Context $context
*
* @return Search
*/
public function build(Context $context)
{
return new Search($context);
}
}

View File

@@ -0,0 +1,624 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Configuration;
use Hook;
use PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
use PrestaShop\PrestaShop\Core\Product\Search\FacetsRendererInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
use Ps_Facetedsearch;
use Tools;
class SearchProvider implements FacetsRendererInterface, ProductSearchProviderInterface
{
/**
* @var Ps_Facetedsearch
*/
private $module;
/**
* @var Filters\Converter
*/
private $filtersConverter;
/**
* @var Filters\DataAccessor
*/
private $dataAccessor;
/**
* @var URLSerializer
*/
private $urlSerializer;
/**
* @var SearchFactory
*/
private $searchFactory;
/**
* @var Filters\Provider
*/
private $provider;
public function __construct(
Ps_Facetedsearch $module,
Filters\Converter $converter,
URLSerializer $serializer,
Filters\DataAccessor $dataAccessor,
SearchFactory $searchFactory,
Filters\Provider $provider
) {
$this->module = $module;
$this->filtersConverter = $converter;
$this->urlSerializer = $serializer;
$this->dataAccessor = $dataAccessor;
$this->searchFactory = $searchFactory;
$this->provider = $provider;
}
/**
* @param ProductSearchQuery $query
*
* @return array
*/
private function getAvailableSortOrders($query)
{
$sortSalesDesc = new SortOrder('product', 'sales', 'desc');
// If the query is a search, we want to sort by position in descending order = relevance
// If the query is a category, manufacturer or supplier, we want to sort by position in ascending order
$sortPosAsc = new SortOrder('product', 'position', ($query->getQueryType() == 'search' ? 'desc' : 'asc'));
$sortNameAsc = new SortOrder('product', 'name', 'asc');
$sortNameDesc = new SortOrder('product', 'name', 'desc');
$sortPriceAsc = new SortOrder('product', 'price', 'asc');
$sortPriceDesc = new SortOrder('product', 'price', 'desc');
$sortDateAsc = new SortOrder('product', 'date_add', 'asc');
$sortDateDesc = new SortOrder('product', 'date_add', 'desc');
$sortRefAsc = new SortOrder('product', 'reference', 'asc');
$sortRefDesc = new SortOrder('product', 'reference', 'desc');
$translator = $this->module->getTranslator();
$sortOrders = [
$sortSalesDesc->setLabel(
$translator->trans('Sales, highest to lowest', [], 'Shop.Theme.Catalog')
),
$sortPosAsc->setLabel(
$translator->trans('Relevance', [], 'Shop.Theme.Catalog')
),
$sortNameAsc->setLabel(
$translator->trans('Name, A to Z', [], 'Shop.Theme.Catalog')
),
$sortNameDesc->setLabel(
$translator->trans('Name, Z to A', [], 'Shop.Theme.Catalog')
),
$sortPriceAsc->setLabel(
$translator->trans('Price, low to high', [], 'Shop.Theme.Catalog')
),
$sortPriceDesc->setLabel(
$translator->trans('Price, high to low', [], 'Shop.Theme.Catalog')
),
$sortRefAsc->setLabel(
$translator->trans('Reference, A to Z', [], 'Shop.Theme.Catalog')
),
$sortRefDesc->setLabel(
$translator->trans('Reference, Z to A', [], 'Shop.Theme.Catalog')
),
];
if ($query->getQueryType() == 'new-products') {
$sortOrders[] = $sortDateAsc->setLabel(
$translator->trans('Date added, oldest to newest', [], 'Shop.Theme.Catalog')
);
$sortOrders[] = $sortDateDesc->setLabel(
$translator->trans('Date added, newest to oldest', [], 'Shop.Theme.Catalog')
);
}
return $sortOrders;
}
/**
* Instance of this class was previously passed to frontend controller, so we are now
* ready to accept runQuery requests. The query object contains all the important information
* about what we should get.
*
* @param ProductSearchContext $context
* @param ProductSearchQuery $query
*
* @return ProductSearchResult
*/
public function runQuery(
ProductSearchContext $context,
ProductSearchQuery $query
) {
$result = new ProductSearchResult();
/**
* Get currently selected filters. In the query, it's passed as encoded URL string,
* we make it an array. All filters in the URL that are no longer valid are removed.
*/
$facetedSearchFilters = $this->filtersConverter->createFacetedSearchFiltersFromQuery($query);
// Initialize the search mechanism
$context = $this->module->getContext();
$facetedSearch = $this->searchFactory->build($context);
// Add query information into Search
$facetedSearch->setQuery($query);
// Init the search with the initial population associated with the current filters
$facetedSearch->initSearch($facetedSearchFilters);
// Request combination IDs if we have some attributes to search by.
// If not, we won't use this to let the core select the default combination.
if ($this->shouldPassCombinationIds($facetedSearchFilters)) {
$facetedSearch->getSearchAdapter()->getInitialPopulation()->addSelectField('id_product_attribute');
$facetedSearch->getSearchAdapter()->addSelectField('id_product_attribute');
}
// Load the product searcher, it gets the Adapter through Search object
$filterProductSearch = new Filters\Products($facetedSearch);
// Get the product associated with the current filter
$productsAndCount = $filterProductSearch->getProductByFilters(
$query,
$facetedSearchFilters
);
$result
->setProducts($productsAndCount['products'])
->setTotalProductsCount($productsAndCount['count'])
->setAvailableSortOrders($this->getAvailableSortOrders($query));
// Now let's get the filter blocks associated with the current search.
// This will allow user to further filter this list we found.
$filterBlockSearch = new Filters\Block(
$facetedSearch->getSearchAdapter(),
$context,
$this->module->getDatabase(),
$this->dataAccessor,
$query,
$this->provider
);
// Let's try to get filters from cache, if the controller is supported
$filterHash = $this->generateCacheKeyForQuery($query, $facetedSearchFilters);
if ($this->module->shouldCacheController($query->getQueryType())) {
$filterBlock = $filterBlockSearch->getFromCache($filterHash);
}
// If not, we regenerate it and cache it
if (empty($filterBlock)) {
$filterBlock = $filterBlockSearch->getFilterBlock($productsAndCount['count'], $facetedSearchFilters);
if ($this->module->shouldCacheController($query->getQueryType())) {
$filterBlockSearch->insertIntoCache($filterHash, $filterBlock);
}
}
$facets = $this->filtersConverter->getFacetsFromFilterBlocks(
$filterBlock['filters']
);
$this->labelRangeFilters($facets);
$this->addEncodedFacetsToFilters($facets);
$this->hideUselessFacets($facets, (int) $result->getTotalProductsCount());
$facetCollection = new FacetCollection();
$nextMenu = $facetCollection->setFacets($facets);
$result->setFacetCollection($nextMenu);
$facetFilters = $this->urlSerializer->getActiveFacetFiltersFromFacets($facets);
$result->setEncodedFacets($this->urlSerializer->serialize($facetFilters));
return $result;
}
/**
* Generate unique cache hash to store blocks in cache
*
* @param ProductSearchQuery $query
* @param array $facetedSearchFilters
*
* @return string
*/
private function generateCacheKeyForQuery(ProductSearchQuery $query, array $facetedSearchFilters)
{
$context = $this->module->getContext();
$filterKey = $query->getQueryType();
if ($query->getQueryType() == 'category') {
$filterKey .= $query->getIdCategory();
} elseif ($query->getQueryType() == 'manufacturer') {
$filterKey .= $query->getIdManufacturer();
} elseif ($query->getQueryType() == 'supplier') {
$filterKey .= $query->getIdSupplier();
}
Hook::exec(
'actionFacetedSearchCacheKeyGeneration',
[
'filterKey' => &$filterKey,
'query' => $query,
'facetedSearchFilters' => &$facetedSearchFilters,
]
);
$filterHash = md5(
sprintf(
'%d-%d-%d-%s-%d-%s',
(int) $context->shop->id,
(int) $context->currency->id,
(int) $context->language->id,
$filterKey,
(int) $context->country->id,
serialize($facetedSearchFilters)
)
);
return $filterHash;
}
/**
* Renders an product search result.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return string the HTML of the facets
*/
public function renderFacets(ProductSearchContext $context, ProductSearchResult $result)
{
list($activeFilters, $displayedFacets, $facetsVar) = $this->prepareActiveFiltersForRender($context, $result);
// No need to render without facets
if (empty($facetsVar)) {
return '';
}
$this->module->getContext()->smarty->assign(
[
'show_quantities' => Configuration::get('PS_LAYERED_SHOW_QTIES'),
'facets' => $facetsVar,
'js_enabled' => $this->module->isAjax(),
'displayedFacets' => $displayedFacets,
'activeFilters' => $activeFilters,
'sort_order' => $result->getCurrentSortOrder()->toString(),
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
return $this->module->fetch(
'module:ps_facetedsearch/views/templates/front/catalog/facets.tpl'
);
}
/**
* Renders an product search result of active filters.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return string the HTML of the facets
*/
public function renderActiveFilters(ProductSearchContext $context, ProductSearchResult $result)
{
list($activeFilters) = $this->prepareActiveFiltersForRender($context, $result);
$this->module->getContext()->smarty->assign(
[
'activeFilters' => $activeFilters,
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
return $this->module->fetch(
'module:ps_facetedsearch/views/templates/front/catalog/active-filters.tpl'
);
}
/**
* Prepare active filters for renderer.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return array|null
*/
private function prepareActiveFiltersForRender(ProductSearchContext $context, ProductSearchResult $result)
{
$facetCollection = $result->getFacetCollection();
// not all search providers generate menus
if (empty($facetCollection)) {
return null;
}
$facetsVar = array_map(
[$this, 'prepareFacetForTemplate'],
$facetCollection->getFacets()
);
$displayedFacets = [];
$activeFilters = [];
foreach ($facetsVar as $idx => $facet) {
// Remove undisplayed facets
if (!empty($facet['displayed'])) {
$displayedFacets[] = $facet;
}
// Check if a filter is active
foreach ($facet['filters'] as $filter) {
if ($filter['active']) {
$activeFilters[] = $filter;
}
}
}
return [
$activeFilters,
$displayedFacets,
$facetsVar,
];
}
/**
* Converts a Facet to an array with all necessary
* information for templating.
*
* @param Facet $facet
*
* @return array ready for templating
*/
protected function prepareFacetForTemplate(Facet $facet)
{
$facetsArray = $facet->toArray();
foreach ($facetsArray['filters'] as &$filter) {
$filter['facetLabel'] = $facet->getLabel();
if ($filter['nextEncodedFacets'] || $facet->getWidgetType() === 'slider') {
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
'q' => $filter['nextEncodedFacets'],
'page' => null,
]);
} else {
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
'q' => null,
]);
}
}
unset($filter);
return $facetsArray;
}
/**
* Add a label associated with the facets
*
* @param array $facets
*/
private function labelRangeFilters(array $facets)
{
$context = $this->module->getContext();
foreach ($facets as $facet) {
if (!in_array($facet->getType(), Filters\Converter::RANGE_FILTERS)) {
continue;
}
foreach ($facet->getFilters() as $filter) {
$filterValue = $filter->getValue();
$min = empty($filterValue[0]) ? $facet->getProperty('min') : $filterValue[0];
$max = empty($filterValue[1]) ? $facet->getProperty('max') : $filterValue[1];
if ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
$filter->setLabel(
sprintf(
'%1$s %2$s - %3$s %4$s',
$context->getCurrentLocale()->formatNumber($min),
$unit,
$context->getCurrentLocale()->formatNumber($max),
$unit
)
);
} elseif ($facet->getType() === 'price') {
$filter->setLabel(
sprintf(
'%1$s - %2$s',
$context->getCurrentLocale()->formatPrice($min, $context->currency->iso_code),
$context->getCurrentLocale()->formatPrice($max, $context->currency->iso_code)
)
);
}
}
}
}
/**
* This method generates a URL stub for each filter inside the given facets
* and assigns this stub to the filters.
* The URL stub is called 'nextEncodedFacets' because it is used
* to generate the URL of the search once a filter is activated.
*/
private function addEncodedFacetsToFilters(array $facets)
{
// first get the currently active facetFilter in an array
$originalFacetFilters = $this->urlSerializer->getActiveFacetFiltersFromFacets($facets);
foreach ($facets as $facet) {
$activeFacetFilters = $originalFacetFilters;
// If only one filter can be selected, we keep track of
// the current active filter to disable it before generating the url stub
// and not select two filters in a facet that can have only one active filter.
if (!$facet->isMultipleSelectionAllowed() && !$facet->getProperty('range')) {
foreach ($facet->getFilters() as $filter) {
if ($filter->isActive()) {
// we have a currently active filter is the facet, remove it from the facetFilter array
$activeFacetFilters = $this->urlSerializer->removeFilterFromFacetFilters(
$originalFacetFilters,
$filter,
$facet
);
break;
}
}
}
foreach ($facet->getFilters() as $filter) {
// toggle the current filter
if ($filter->isActive() || $facet->getProperty('range')) {
$facetFilters = $this->urlSerializer->removeFilterFromFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
} else {
$facetFilters = $this->urlSerializer->addFilterToFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
}
// We've toggled the filter, so the call to serialize
// returns the "URL" for the search when user has toggled
// the filter.
$filter->setNextEncodedFacets(
$this->urlSerializer->serialize($facetFilters)
);
}
}
}
/**
* Remove the facet when there's only 1 result.
* Keep facet status when it's a slider.
* Keep facet status if it's a availability or extras facet.
*
* @param array $facets
* @param int $totalProducts
*/
private function hideUselessFacets(array $facets, $totalProducts)
{
foreach ($facets as $facet) {
// If the facet is a slider type, we hide it ONLY if the MIN and MAX value match
if ($facet->getWidgetType() === 'slider') {
$facet->setDisplayed(
$facet->getProperty('min') != $facet->getProperty('max')
);
continue;
}
// Now the rest of facets - we apply this logic
$totalFacetProducts = 0;
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0 && $filter->isDisplayed()) {
$totalFacetProducts += $filter->getMagnitude();
++$usefulFiltersCount;
}
}
// We display the facet in several cases
$facet->setDisplayed(
// If there are two filters available
$usefulFiltersCount > 1
||
// There is only one filter available, but it furhter reduces the product selection
(
count($facet->getFilters()) === 1
&& $totalFacetProducts < $totalProducts
&& $usefulFiltersCount > 0
)
||
// If there is only one filter, but it's availability or extras filter - we want this one to be displayed all the time
($usefulFiltersCount === 1 && ($facet->getType() == 'availability' || $facet->getType() == 'extras'))
);
// Other cases - hidden by default
}
}
/**
* Generate a URL corresponding to the current page but
* with the query string altered.
*
* Params from $extraParams that have a null value are stripped,
* and other params are added. Params not in $extraParams are unchanged.
*/
private function updateQueryString(array $extraParams = [])
{
$uriWithoutParams = explode('?', $_SERVER['REQUEST_URI'])[0];
$url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $uriWithoutParams;
$params = [];
$paramsFromUri = '';
if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
$paramsFromUri = explode('?', $_SERVER['REQUEST_URI'])[1];
}
parse_str($paramsFromUri, $params);
foreach ($extraParams as $key => $value) {
if (null === $value) {
// Force clear param if null value is passed
unset($params[$key]);
} else {
$params[$key] = $value;
}
}
foreach ($params as $key => $param) {
if (null === $param || '' === $param) {
unset($params[$key]);
}
}
$queryString = str_replace('%2F', '/', http_build_query($params, '', '&'));
return $url . ($queryString ? "?$queryString" : '');
}
/**
* Checks if we should return information about combinations to the core
*
* @param array $facetedSearchFilters filters passed in the query and parsed by our module
*
* @return bool if should add attributes to the select
*/
private function shouldPassCombinationIds(array $facetedSearchFilters)
{
return !empty($facetedSearchFilters['id_attribute_group']);
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,245 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class URLSerializer
{
/**
* Add filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, Facet $facet)
{
$facetLabel = $this->getFacetLabel($facet);
$filterLabel = $this->getFilterLabel($facetFilter);
if ($facet->getProperty('range')) {
$facetValue = $facet->getProperty('values');
$facetFilters[$facetLabel] = [
$facetFilter->getProperty('symbol'),
isset($facetValue[0]) ? $facetValue[0] : $facet->getProperty('min'),
isset($facetValue[1]) ? $facetValue[1] : $facet->getProperty('max'),
];
} else {
$facetFilters[$facetLabel][$filterLabel] = $filterLabel;
}
return $facetFilters;
}
/**
* Remove filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function removeFilterFromFacetFilters(array $facetFilters, Filter $facetFilter, $facet)
{
$facetLabel = $this->getFacetLabel($facet);
if ($facet->getProperty('range')) {
unset($facetFilters[$facetLabel]);
} else {
$filterLabel = $this->getFilterLabel($facetFilter);
unset($facetFilters[$facetLabel][$filterLabel]);
if (empty($facetFilters[$facetLabel])) {
unset($facetFilters[$facetLabel]);
}
}
return $facetFilters;
}
/**
* Get active facet filters
*
* @return array
*/
public function getActiveFacetFiltersFromFacets(array $facets)
{
$facetFilters = [];
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $facetFilter) {
if (!$facetFilter->isActive()) {
// Filter is not active
continue;
}
$facetLabel = $this->getFacetLabel($facet);
$filterLabel = $this->getFilterLabel($facetFilter);
if (!$facet->getProperty('range')) {
$facetFilters[$facetLabel][$filterLabel] = $filterLabel;
continue;
}
$facetValue = $facetFilter->getValue();
$facetFilters[$facetLabel] = [
$facetFilter->getProperty('symbol'),
$facetValue[0],
$facetValue[1],
];
}
}
return $facetFilters;
}
/**
* Get Facet label
*
* @param Facet $facet
*
* @return string
*/
private function getFacetLabel(Facet $facet)
{
if ($facet->getProperty(Converter::PROPERTY_URL_NAME) !== null) {
return $facet->getProperty(Converter::PROPERTY_URL_NAME);
}
return $facet->getLabel();
}
/**
* Get Facet Filter label
*
* @param Filter $facetFilter
*
* @return string
*/
private function getFilterLabel(Filter $facetFilter)
{
if ($facetFilter->getProperty(Converter::PROPERTY_URL_NAME) !== null) {
return $facetFilter->getProperty(Converter::PROPERTY_URL_NAME);
}
return $facetFilter->getLabel();
}
/**
* @param array $fragment
*
* @return string
*/
public function serialize(array $fragment)
{
$parts = [];
foreach ($fragment as $key => $values) {
array_unshift($values, $key);
$parts[] = $this->serializeListOfStrings($values, '-');
}
return $this->serializeListOfStrings($parts, '/');
}
/**
* @param string $string
*
* @return array
*/
public function unserialize($string)
{
$fragment = [];
$parts = $this->unserializeListOfStrings($string, '/');
foreach ($parts as $part) {
$values = $this->unserializeListOfStrings($part, '-');
$key = array_shift($values);
$fragment[$key] = $values;
}
return $fragment;
}
/**
* @param string $separator the string separator
* @param string $escape the string escape
* @param array $list
*
* @return string
*/
private function serializeListOfStrings($list, $separator, $escape = '\\')
{
return implode($separator, array_map(function ($item) use ($separator, $escape) {
return strtr(
$item,
[
$separator => $escape . $separator,
]
);
}, $list));
}
/**
* @param string $separator the string separator
* @param string $escape the string escape
* @param string $string the UTF8 string
*
* @return array
*/
private function unserializeListOfStrings($string, $separator, $escape = '\\')
{
$list = [];
$currentString = '';
$escaping = false;
// get UTF-8 chars, inspired from http://stackoverflow.com/questions/9438158/split-utf8-string-into-array-of-chars
$arrayOfCharacters = [];
preg_match_all('/./u', $string, $arrayOfCharacters);
$characters = $arrayOfCharacters[0];
foreach ($characters as $index => $character) {
if ($character === $escape
&& isset($characters[$index + 1])
&& $characters[$index + 1] === $separator
) {
$escaping = true;
continue;
}
if ($character === $separator && $escaping === false) {
$list[] = $currentString;
$currentString = '';
continue;
}
$currentString .= $character;
$escaping = false;
}
if ('' !== $currentString) {
$list[] = $currentString;
}
return $list;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,64 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_0_0(Ps_Facetedsearch $module)
{
// Clear legacy hook names
$oldHooks = [
'categoryAddition',
'categoryUpdate',
'attributeGroupForm',
'afterSaveAttributeGroup',
'afterDeleteAttributeGroup',
'featureForm',
'afterDeleteFeature',
'afterSaveFeature',
'categoryDeletion',
'afterSaveProduct',
'postProcessAttributeGroup',
'postProcessFeature',
'featureValueForm',
'postProcessFeatureValue',
'afterDeleteFeatureValue',
'afterSaveFeatureValue',
'attributeForm',
'postProcessAttribute',
'afterDeleteAttribute',
'afterSaveAttribute',
'productSearchProvider',
'displayLeftColumn',
];
foreach ($oldHooks as $hookName) {
$module->unregisterHook($hookName);
}
// These methods have no return value
// If something failed an exception will be raised and
// the upgrade will stop
$module->rebuildLayeredStructure();
$module->rebuildPriceIndexTable();
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($module->getHookDispatcher()->getAvailableHooks());
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_0_3(Ps_Facetedsearch $module)
{
// Clear legacy hook names
$oldHooks = [
'afterDeleteFeatureValue',
'afterSaveFeatureValue',
'postProcessFeatureValue',
];
foreach ($oldHooks as $hookName) {
$module->unregisterHook($hookName);
}
$newHooks = [
'actionFeatureSave',
'actionFeatureValueDelete',
'displayFeatureValuePostProcess',
];
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_11_0($module)
{
// Get all filter templates
$filterTemplates = Db::getInstance()->executeS(
'SELECT * FROM ' . _DB_PREFIX_ . 'layered_filter'
);
// Add controller info to each of the configuration
if (!empty($filterTemplates)) {
foreach ($filterTemplates as $template) {
$filters = Tools::unSerialize($template['filters']);
$filters['controllers'] = ['category'];
Db::getInstance()->execute(
'UPDATE `' . _DB_PREFIX_ . 'layered_filter`
SET `filters` = "' . pSQL(serialize($filters)) . '"
WHERE `id_layered_filter` = ' . (int) $template['id_layered_filter']
);
}
}
// Add new column to generated filters and fill it with a category controller
Db::getInstance()->execute('ALTER TABLE `' . _DB_PREFIX_ . 'layered_category` ADD `controller` VARCHAR(64) NOT NULL AFTER `id_shop`;');
Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . "layered_category` SET `controller`= 'category';");
// Flush block cache - the cache key changed a bit with this version anyway
$module->invalidateLayeredFilterBlockCache();
return true;
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_12_0($module)
{
// Add availabilility to allowed types
Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'layered_category`
CHANGE `type` `type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'quantity\',\'availability\',\'condition\',\'manufacturer\',\'weight\',\'price\') NOT NULL;');
// Upgrade all generated filters
Db::getInstance()->execute(
'UPDATE `' . _DB_PREFIX_ . 'layered_category` SET type=\'availability\' WHERE type=\'quantity\';');
// Remove the old enum from types
Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'layered_category`
CHANGE `type` `type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'availability\',\'condition\',\'manufacturer\',\'weight\',\'price\') NOT NULL;');
// Flush block cache
$module->invalidateLayeredFilterBlockCache();
return true;
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_13_0(Ps_Facetedsearch $module)
{
$newHooks = [
'actionFeatureValueFormBuilderModifier',
'actionAfterCreateFeatureValueFormHandler',
'actionAfterUpdateFeatureValueFormHandler',
];
// Flush block cache, we changed availability logic a bit
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_14_0(Ps_Facetedsearch $module)
{
// Add availabilility to allowed types
Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'layered_category`
CHANGE `type` `type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'availability\',\'condition\',\'manufacturer\',\'weight\',\'price\',\'extras\') NOT NULL;');
return true;
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_15_0(Ps_Facetedsearch $module)
{
// New hooks for migrated attribute page
$newHooks = [
'actionAttributeFormBuilderModifier',
'actionAttributeFormDataProviderData',
'actionAfterCreateAttributeFormHandler',
'actionAfterUpdateAttributeFormHandler',
'actionAttributeGroupFormBuilderModifier',
'actionAttributeGroupFormDataProviderData',
'actionAfterCreateAttributeGroupFormHandler',
'actionAfterUpdateAttributeGroupFormHandler',
];
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_3_0(Ps_Facetedsearch $module)
{
// These methods have no return value
// If something failed an exception will be raised and
// the upgrade will stop
$module->rebuildPriceIndexTable();
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($module->getHookDispatcher()->getAvailableHooks());
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_4_0(Ps_Facetedsearch $module)
{
$newHooks = [
'actionFeatureFormBuilderModifier',
'actionAfterCreateFeatureFormHandler',
'actionAfterUpdateFeatureFormHandler',
];
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Removes files or directories.
*
* @param array $files An array of files to remove
*
* @return true|string True if everything goes fine, error details otherwise
*/
function removeFromFsDuringUpgrade(array $files)
{
$files = array_reverse($files);
foreach ($files as $file) {
if (is_dir($file)) {
$iterator = new FilesystemIterator($file, FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS);
removeFromFsDuringUpgrade(iterator_to_array($iterator));
if (!rmdir($file) && file_exists($file)) {
return 'Deletion of directory ' . $file . 'failed';
}
} elseif (!unlink($file) && file_exists($file)) {
return 'Deletion of file ' . $file . 'failed';
}
}
return true;
}
/**
* This upgrade file removes the folder vendor/phpunit, when added from a previous release installed on the shop.
*
* @return bool
*/
function upgrade_module_3_4_1($module)
{
$path = __DIR__ . '/../vendor/phpunit';
if (file_exists($path)) {
$result = removeFromFsDuringUpgrade([$path]);
if ($result !== true) {
PrestaShopLogger::addLog('Could not delete PHPUnit from module. ' . $result, 3);
return false;
}
}
return true;
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_6_0($module)
{
Configuration::updateValue('PS_LAYERED_CACHE_ENABLED', 1);
return true;
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_8_0($module)
{
$module->registerHook('actionProductPreferencesPageStockSave');
return Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'layered_price_index`
CHANGE `price_min` `price_min` decimal(20,6) NOT NULL,
CHANGE `price_max` `price_max` decimal(20,6) NOT NULL;');
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_9_0($module)
{
Configuration::updateValue('PS_USE_JQUERY_UI_SLIDER', 1);
return true;
}

Some files were not shown because too many files have changed in this diff Show More