This commit is contained in:
2025-04-01 00:38:54 +02:00
parent d4d4c0c09d
commit 87da06293a
22351 changed files with 5168854 additions and 7538 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,67 @@
GitHub contributors:
--------------------------------
- 123monsite-regis
- Alex Even
- Alex Sampaio
- André
- Bastien Bieri
- Clotaire 202 ecommerce
- Damien Metzger
- David Gonzalez
- Edvinas Gurevicius
- Francois Gaillard
- François-Marie de Jouvencel
- GoT
- Gregory Roussac
- Gytis Škėma
- Hashem
- Hendrik Luup
- Jerome Nadaud
- Jonathan Lelievre
- Julien Bourdeau
- Julius Zukauskas
- Jérôme Nadaud
- Krystian Podemski
- MathiasReker
- Mathieu Ferment
- Matthieu Rolland
- Maxime Biloé
- Michel ANTOINE
- Mickaël Andrieu
- Nico
- Pablo Borowicz
- Pavel Novitsky
- PeNov
- Pierre RAMBAUD
- PrestaSafe
- Progi1984
- Quetzacoalt91
- Robert Keresnyei
- Rokas Zygmantas
- Rémi Gaillard
- Sacha Froment
- Samir Shah
- Stomp9
- Thibaud Chauviere
- Thierry Marianne
- Thomas
- Thomas Nabord
- Veebipoed.ee
- Xavier
- Zebx
- alex4102
- djfm
- fojt-cz
- gRoussac
- indesign47
- iqit-commerce
- iqit-commerce (Marcin Sz)
- joce
- jocelyn fournier
- jolelievre
- kermes
- marionf
- matks
- matrix
- raph
- sadlyblue

View File

@@ -0,0 +1,47 @@
Academic Free License ("AFL") v. 3.0
This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
Licensed under the Academic Free License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
a) to reproduce the Original Work in copies, either alone or as part of a collective work;
b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
c) to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.

View File

@@ -0,0 +1,60 @@
# Faceted search module
[![Build Status](https://travis-ci.com/PrestaShop/ps_facetedsearch.svg?branch=master)](https://travis-ci.com/PrestaShop/ps_facetedsearch)
[![Latest Stable Version](https://poser.pugx.org/PrestaShop/ps_facetedsearch/v)](//packagist.org/packages/PrestaShop/ps_facetedsearch)
[![Total Downloads](https://poser.pugx.org/PrestaShop/ps_facetedsearch/downloads)](//packagist.org/packages/PrestaShop/ps_facetedsearch)
[![GitHub license](https://img.shields.io/github/license/PrestaShop/ps_facetedsearch)](https://github.com/PrestaShop/ps_facetedsearch/LICENSE.md)
## About
Filter your catalog to help visitors picture the category tree and browse your store easily.
## Compatibility
PrestaShop: 1.7.6.0 or later
## Multistore compatibility
This module is partially compatible with the multistore feature. Some of its options might not be available.
## Reporting issues
You can report issues with this module in the main PrestaShop repository. [Click here to report an issue][report-issue].
## Requirements
Required only for development:
- npm
- composer
## Installation
Install all dependencies. Be careful, you need NodeJs 14+.
```
npm install
composer install
```
## Usage
```
npm run dev # Watch js/css files for changes
npm run build # Build for production
```
## Contributing
PrestaShop modules are open source extensions to the [PrestaShop e-commerce platform][prestashop]. Everyone is welcome and even encouraged to contribute with their own improvements!
Just make sure to follow our [contribution guidelines][contribution-guidelines].
## License
This module is released under the [Academic Free License 3.0][AFL-3.0]
[report-issue]: https://github.com/PrestaShop/PrestaShop/issues/new/choose
[prestashop]: https://www.prestashop.com/
[contribution-guidelines]: https://devdocs.prestashop.com/1.7/contribute/contribution-guidelines/project-modules/
[AFL-3.0]: https://opensource.org/licenses/AFL-3.0

View File

@@ -0,0 +1 @@
.bootstrap .filter_list .filter_list_item{display:table;width:100%;padding:5px 0;margin-bottom:4px;background-color:#fff;box-shadow:rgba(0,0,0,.3) 0 0 3px,rgba(0,0,0,.1) 0 -2px 0 inset;border-radius:3px;cursor:pointer}.bootstrap .filter_panel{min-height:20px;padding:7px 7px 0px 7px;margin-bottom:20px;background-color:#ebebeb;border:1px solid #d9d9d9;border-radius:3px;box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.bootstrap .filter_panel header{margin-bottom:7px}/*# sourceMappingURL=blocklayered.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["blocklayered.scss"],"names":[],"mappings":"AAmBE,0CACC,aAAA,CACA,UAAA,CACA,aAAA,CACA,iBAAA,CACA,qBAAA,CAEA,+DAAA,CAKA,iBAAA,CACC,cAAA,CAGF,yBACC,eAAA,CACA,uBAAA,CACA,kBAAA,CACA,wBAAA,CACA,wBAAA,CACA,iBAAA,CAEA,0CAAA,CAEC,gCACC,iBAAA","file":"blocklayered.css"}

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 @@
#search_filters .facet .title{display:flex}#search_filters .facet .title .collapse-icons{margin-left:auto}#search_filters .facet .facet-title{width:calc(100% - 30px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#search_filters .facet .facet-label{width:100%;text-align:left}#search_filters .facet .facet-label .custom-checkbox,#search_filters .facet .facet-label .custom-radio{top:-7px;margin-right:0}#search_filters .facet .facet-label .color{margin-left:0}#search_filters .facet .facet-label a{width:calc(100% - 30px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}/*# sourceMappingURL=facet.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["facet.scss"],"names":[],"mappings":"AA2BI,8BACE,YAAA,CACA,8CACE,gBAAA,CAIJ,oCAfF,uBAAA,CACA,eAAA,CACA,sBAAA,CACA,kBAAA,CAgBE,oCACE,UAAA,CACA,eAAA,CACA,uGAEE,QAAA,CACA,cAAA,CAEF,2CACE,aAAA,CAGF,sCA/BJ,uBAAA,CACA,eAAA,CACA,sBAAA,CACA,kBAAA","file":"facet.css"}

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 @@
.faceted-overlay{left:0;top:0;width:100%;height:100%;position:fixed;background-color:rgba(25,25,25,.5);z-index:100}.faceted-overlay .overlay__inner{left:0;top:0;width:100%;height:100%;position:absolute}.faceted-overlay .overlay__content{left:50%;position:absolute;top:50%;transform:translate(-50%, -50%)}.faceted-overlay .spinner{width:75px;height:75px;display:inline-block;border-width:2px;border-color:rgba(255,255,255,.05);border-top-color:#fff;animation:spin 1s infinite linear;border-radius:100%;border-style:solid}@keyframes spin{100%{transform:rotate(360deg)}}/*# sourceMappingURL=overlay.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["overlay.scss"],"names":[],"mappings":"AAkBA,iBACE,MAAA,CACA,KAAA,CACA,UAAA,CACA,WAAA,CACA,cAAA,CACA,kCAAA,CACA,WAAA,CAEA,iCACE,MAAA,CACA,KAAA,CACA,UAAA,CACA,WAAA,CACA,iBAAA,CAGF,mCACE,QAAA,CACA,iBAAA,CACA,OAAA,CACA,+BAAA,CAGF,0BACE,UAAA,CACA,WAAA,CACA,oBAAA,CACA,gBAAA,CACA,kCAAA,CACA,qBAAA,CACA,iCAAA,CACA,kBAAA,CACA,kBAAA,CAIJ,gBACE,KACE,wBAAA,CAAA","file":"overlay.css"}

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 @@
#search_filters .ui-slider-horizontal .ui-slider-handle{margin-left:-1px;cursor:pointer}#search_filters .ui-widget-header{background:#555}#search_filters .ui-slider .ui-slider-handle{top:-0.45em;width:.4em;background:#fff;border:1px solid #555}#search_filters .ui-slider-horizontal{height:.4em}/*# sourceMappingURL=slider.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["slider.scss"],"names":[],"mappings":"AAoBI,wDACE,gBAAA,CACA,cAAA,CAGJ,kCACE,eAAA,CAGA,6CACE,WAAA,CACA,UAAA,CACA,eAAA,CACA,qBAAA,CAGJ,sCACE,WAAA","file":"slider.css"}

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;

View File

@@ -0,0 +1,47 @@
{
"name": "prestashop/ps_facetedsearch",
"description": "PrestaShop module ps_facetedsearch",
"homepage": "https://github.com/PrestaShop/ps_facetedsearch",
"license": "AFL-3.0",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
}
],
"require": {
"php": ">=7.1",
"doctrine/collections": "^1.4"
},
"require-dev": {
"prestashop/php-dev-tools": "^3.4",
"phpunit/phpunit": "~5.7",
"mockery/mockery": "^1.2"
},
"config": {
"platform": {
"php": "7.1.0"
},
"preferred-install": "dist",
"prepend-autoloader": false
},
"type": "prestashop-module",
"autoload": {
"psr-4": {
"PrestaShop\\Module\\FacetedSearch\\Controller\\": "src/Controller/",
"PrestaShop\\Module\\FacetedSearch\\": "src/",
"PrestaShop\\Module\\FacetedSearch\\Tests\\": "tests/php/FacetedSearch"
},
"classmap": [
"ps_facetedsearch.php"
]
},
"scripts": {
"test": [
"@php -d date.timezone=UTC ./vendor/bin/phpunit -c tests/php/phpunit.xml"
],
"lint": [
"php-cs-fixer fix --no-interaction --dry-run --diff"
]
}
}

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.0]]></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,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Nawigacja fasetowa]]></displayName>
<version><![CDATA[4.0.0]]></version>
<description><![CDATA[Filtruj sw&oacute;j katalog, aby ułatwić odwiedzającym zobrazowanie drzewa kategorii i łatwe przeglądanie Twojego sklepu.]]></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,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');
}
}
}

View File

@@ -0,0 +1,29 @@
.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;
}
.bootstrap .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);
}
.bootstrap .filter_panel header {
margin-bottom: 7px;
}

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;

View File

@@ -0,0 +1,35 @@
<?php
/*
* 2007-2015 PrestaShop
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/afl-3.0.php
* 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 http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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,85 @@
/*
* HTML5 Sortable jQuery Plugin
* http://farhadi.ir/projects/html5sortable
*
* Copyright 2012, Ali Farhadi
* Released under the MIT license.
*/
(function($) {
var dragging, placeholders = $();
$.fn.sortable = function(options) {
var method = String(options);
options = $.extend({
connectWith: false
}, options);
return this.each(function() {
if (/^enable|disable|destroy$/.test(method)) {
var items = $(this).children($(this).data('items')).attr('draggable', method == 'enable');
if (method == 'destroy') {
items.add(this).removeData('connectWith items')
.off('dragstart.h5s dragend.h5s selectstart.h5s dragover.h5s dragenter.h5s drop.h5s');
}
return;
}
var isHandle, index, items = $(this).children(options.items);
var placeholder = $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="sortable-placeholder">');
items.find(options.handle).mousedown(function() {
isHandle = true;
}).mouseup(function() {
isHandle = false;
});
$(this).data('items', options.items)
placeholders = placeholders.add(placeholder);
if (options.connectWith) {
$(options.connectWith).add(this).data('connectWith', options.connectWith);
}
items.attr('draggable', 'true').on('dragstart.h5s', function(e) {
if (options.handle && !isHandle) {
return false;
}
isHandle = false;
var dt = e.originalEvent.dataTransfer;
dt.effectAllowed = 'move';
dt.setData('Text', 'dummy');
index = (dragging = $(this)).addClass('sortable-dragging').index();
}).on('dragend.h5s', function() {
if (!dragging) {
return;
}
dragging.removeClass('sortable-dragging').show();
placeholders.detach();
if (index != dragging.index()) {
dragging.parent().trigger('sortupdate', {item: dragging, start_index: index, end_index: dragging.index()});
}
dragging = null;
}).not('a[href], img').on('selectstart.h5s', function() {
this.dragDrop && this.dragDrop();
return false;
}).end().add([this, placeholder]).on('dragover.h5s dragenter.h5s drop.h5s', function(e) {
if (!items.is(dragging) && options.connectWith !== $(dragging).parent().data('connectWith')) {
return true;
}
if (e.type == 'drop') {
e.stopPropagation();
placeholders.filter(':visible').after(dragging);
dragging.trigger('dragend.h5s');
return false;
}
e.preventDefault();
e.originalEvent.dataTransfer.dropEffect = 'move';
if (items.is(this)) {
if (options.forcePlaceholderSize) {
placeholder.height(dragging.outerHeight());
}
dragging.hide();
$(this)[placeholder.index() < $(this).index() ? 'after' : 'before'](placeholder);
placeholders.not(placeholder).detach();
} else if (!placeholders.is(this) && !$(this).children(options.items).length) {
placeholders.detach();
$(this).append(placeholder);
}
return false;
});
});
};
})(jQuery);

View File

@@ -0,0 +1,219 @@
function checkForm()
{
var is_category_selected = false;
var is_filter_selected = false;
$('#categories-treeview input[type=checkbox]').each(
function()
{
if ($(this).prop('checked'))
{
is_category_selected = true;
return false;
}
}
);
$('.filter_list_item input[type=checkbox]').each(
function()
{
if ($(this).prop('checked'))
{
is_filter_selected = true;
return false;
}
}
);
if (!is_category_selected)
{
alert(translations['no_selected_categories']);
$('#categories-treeview input[type=checkbox]').first().focus();
return false;
}
if (!is_filter_selected)
{
alert(translations['no_selected_filters']);
$('#filter_list_item input[type=checkbox]').first().focus();
return false;
}
return true;
}
$(document).ready(
function()
{
$('.ajaxcall').click(
function()
{
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;
var type = $(this).attr('rel');
$.ajax(
{
url: this.href+'&ajax=1',
context: this,
dataType: 'json',
cache: 'false',
success: function(res)
{
this.running = false;
this.restartAllowed = true;
$('#indexing-warning').hide();
$(this).html(this.legend);
if (type == 'price')
$('#ajax-message-ok span').html(translations['url_indexation_finished']);
else
$('#ajax-message-ok span').html(translations['attribute_indexation_finished']);
$('#ajax-message-ok').show();
return;
},
error: function(res)
{
this.restartAllowed = true;
$('#indexing-warning').hide();
if (type == 'price')
$('#ajax-message-ko span').html(translations['url_indexation_failed']);
else
$('#ajax-message-ko span').html(translations['attribute_indexation_failed']);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.running = false;
}
}
);
return false;
});
$('.ajaxcall-recurcive').each(
function(it, elm)
{
$(elm).click(
function()
{
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: function(res)
{
this.running = false;
if (res.result)
{
this.cursor = 0;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(translations['price_indexation_finished']);
$('#ajax-message-ok').show();
return;
}
this.cursor = parseInt(res.cursor);
$(this).html(this.legend+translations['price_indexation_in_progress'].replace('%s', res.count));
$(this).click();
},
error: function(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();
}
$('.sortable').sortable(
{
forcePlaceholderSize: true
});
$('.filter_list_item input[type=checkbox]').click(
function()
{
var current_selected_filters_count = parseInt($('#selected_filters').html());
if ($(this).prop('checked'))
$('#selected_filters').html(current_selected_filters_count+1);
else
$('#selected_filters').html(current_selected_filters_count-1);
}
);
if (typeof filters !== 'undefined')
{
filters = JSON.parse(filters);
for (filter in filters)
{
$('#'+filter).attr("checked","checked");
$('#selected_filters').html(parseInt($('#selected_filters').html())+1);
$('select[name="'+filter+'_filter_type"]').val(filters[filter].filter_type);
$('select[name="'+filter+'_filter_show_limit"]').val(filters[filter].filter_show_limit);
}
}
}
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
{
"name": "ps_facetedsearch",
"description": "Displays a block allowing multiple filters.",
"private": true,
"directories": {
"test": "tests"
},
"scripts": {
"test": "./node_modules/.bin/mocha --require @babel/register --reporter spec \"./tests/**/*.spec.js\"",
"lint": "eslint --ext .js,.vue .",
"lint-fix": "eslint --fix --ext .js,.vue .",
"build": "webpack-cli --mode production",
"dev": "webpack-cli --mode development --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/PrestaShop/ps_facetedsearch.git"
},
"keywords": [],
"author": "PrestaShop",
"license": "AFL-3.0",
"bugs": {
"url": "https://github.com/PrestaShop/ps_facetedsearch/issues"
},
"babel": {
"presets": [
"@babel/preset-env"
]
},
"homepage": "https://github.com/PrestaShop/ps_facetedsearch#readme",
"devDependencies": {
"@babel/eslint-parser": "^7.25.8",
"@babel/cli": "^7.24.8",
"@babel/core": "^7.24.9",
"@babel/node": "^7.24.8",
"@babel/preset-env": "^7.25.8",
"@babel/register": "^7.23.7",
"babel-loader": "^9.1.3",
"chai": "^4.3.10",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.2",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prestashop": "^0.2.1",
"eslint-plugin-import": "^2.29.1",
"mini-css-extract-plugin": "^1.0.0",
"mocha": "^10.4.0",
"node-sass": "^9.0.0",
"sass-loader": "^14.2.1",
"style-loader": "^2.0.0",
"webpack": "^5.93.0",
"webpack-cli": "^4.10.0"
},
"dependencies": {
"jquery-ui-touch-punch": "^0.2.3",
"lodash.escaperegexp": "^4.1.2"
}
}

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,847 @@
<?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() . ')';
}
$query = 'SELECT ' . implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p';
foreach ($joinConditions as $joinAliasInfos) {
foreach ($joinAliasInfos as $tableAlias => $joinInfos) {
$query .= ' ' . $joinInfos['joinType'] . ' ' . _DB_PREFIX_ . $joinInfos['tableName'] . ' ' .
$tableAlias . ' ON ' . $joinInfos['joinCondition'];
}
}
if (!empty($whereConditions)) {
$query .= ' WHERE ' . implode(' AND ', $whereConditions);
}
if ($groupFields) {
$query .= ' GROUP BY ' . implode(', ', $groupFields);
}
if ($orderField) {
$query .= ' ORDER BY ' . $orderField . ' ' . strtoupper($this->getOrderDirection());
if ($orderField !== 'p.id_product') {
$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)
{
$orderField = $this->getOrderField();
// If we have set an initial population, add this field into initial population selects
if ($this->getInitialPopulation() !== null && !empty($orderField)) {
$this->getInitialPopulation()->addSelectField($orderField);
}
// Do not try to process the orderField if it already has an alias, or if it's a group function
if (empty($orderField) || strpos($orderField, '.') !== false
|| strpos($orderField, '(') !== false) {
return $orderField;
}
// Alter order by field if it's a price column
if ($orderField === 'price') {
$orderField = $this->getOrderDirection() === 'asc' ? 'price_min' : 'price_max';
}
// Add table mapping or p. prefix depending on field type
$orderField = $this->computeFieldName($orderField, $filterToTableMapping, true);
// Alter order by field and add some products to the end of the list, if required
$orderField = $this->computeShowLast($orderField, $filterToTableMapping);
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 ($filterArray['color'] != '') {
$filter->setProperty(self::PROPERTY_COLOR, $filterArray['color']);
} elseif (file_exists(_PS_COL_IMG_DIR_ . $id . '.jpg')) {
$filter->setProperty(self::PROPERTY_TEXTURE, _THEME_COL_DIR_ . $id . '.jpg');
}
}
$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,147 @@
<?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',
'actionCategoryUpdate',
];
/**
* 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 update
*
* @param array $params
*/
public function actionCategoryUpdate(array $params)
{
/*
* The category status might (active, inactive) have changed,
* we have to update the layered cache table structure.
*/
if (isset($params['category']) && !$params['category']->active) {
$this->removeCategoryFromFilterTemplates((int) $params['category']->id);
}
}
/**
* 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,490 @@
<?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 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
);
}
}
/**
* 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], '>=');
}
}

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