
Hi Habr! I understand that the story about which I want to tell is quite ordinary. Each programmer working with Open Source has up to ten such cases per day. But I still decided to write about it. It can really help someone, but it can simply improve someone's mood, which is also not bad.
There will be a bit of reverse engineering, a bit of philosophical reflection, and of course a happy ending. Who cares only fix the glitch - you can not read all this nonsense and immediately copy the hack from the end of the article. In any case, welcome under cat.
A little about PrestaShop
It all started with the fact that the authorities set the task to make an online store. The choice was made in favor of PrestaShop 1.6 for the following reasons:
')
- Written in PHP
- Responsive design out of the box
- It looks good with a standard theme (including on mobile devices).
- Coped well with over 50,000 items loaded
- Out of the box there is a convenient and good-looking filter block (in PrestaShop it is called a multi-level navigation block)
With the last point, after a while, a question arose that became the subject of this article.
What is the glitch
When the goods were already loaded and I started setting up filters, it turned out that in certain cases the module behaves incorrectly.
The number of products (see figure), which will be filtered if you mark this position, is shown in front of each value in the filter block. The point is that when choosing different values in different filters, all quantities are recounted in a cunning way, providing convenient feedback and helping to decide on the further selection of products.
Here inadequate behavior crept in. If you mark one property, then everything works and new quantities appear. If you mark two or more properties, then the quantity opposite the positions of the remaining filters is reset to the initial ones (as if no single item has been selected).
Here it should be noted that PrestaShop has concepts of attributes and concepts of product properties. Attributes (attributes) are the characteristics of the product that are involved in the formation of different versions of the same product (for example, shoe size for one particular shoe model). Properties (features) are the characteristics common to all variants of the product. In the formation of options for the product, they do not participate, but simply inform the user about consumer properties.
An error occurs if you mark the position in the filter block by features. In other filters (for example, by manufacturer) this effect does not appear.
Primary assumptions
It became clear that:
- This is a glitch (since everything works with one position, that is, the logic of recalculation into the code is laid)
- This bug is located in the module of multi-level navigation (blocklayered)
- This glitch is most likely due to the incorrect construction of the SQL query condition (I can not explain it, this is more on an intuitive level).
Internet searches yielded nothing, so there was a choice:
- Leave everything as it is, agreeing that this is a feature of the store.
- Get headlong into the code and, by showing enthusiasm, find the cause of the glitch (and fix it).
I made a second decision. Enthusiasm diminished when I opened the blocklayered.php file. It contained more than 3.5 thousand lines of code from which 70% are multi-story SQL queries. The task began to resemble the search for a needle in a haystack. At first I was scared, and even thought badly about the creators of PrestaShop. But then I figured out how I would begin to program the difficult logic of the work of such a module and calmed down a bit. The task is really complicated and, most likely, the complexity of the code is caused by objective reasons. But all the same, when working with the module, I didn’t leave the thought that it is possible to do all this somehow more beautifully.
Tools and tricks
When solving the problem we will use the following tools:
WinSCP is a reliable FTP client with many features. Never failed even on large quantities of files and volumes. All functions are also available from the command line, which makes it useful when writing scripts.
UwAmp - easy to install and configure WAMP-assembly. We will use it to run the code under test locally.
Notepad ++ is an excellent editor for reverse engineering. Work in different encodings and with different ends of lines. Good syntax highlighting. Opening large files. Search strings including files in directories. It works very reliably.
HeidiSQL - GUI for MySQL. Free graphical database tool. Sometimes it is buggy, but in general it is very convenient to work. We use it to examine the contents of the database when analyzing the code.
The basic techniques will be dumping variables and searching the source code for function names and pieces of code. Since the events we are interested in occur even in the ajax requests, we dump the variables into the file. To do this, where you need to insert the following code:
$f=fopen('headfire.txt','a+'); fwrite($f,$very_important_variable); fwrite($f, PHP_EOL); fclose($f);
The headfire in the file name is my alias, I use it, wherever it is necessary to mark my code or file. You must use your own code string. It is important that it is easy to find and difficult to confuse with other files or lines of code.
Begin analysis
Approximately half of the code is responsible for BackOffice. We remove this code immediately and try not to go there.
We start the promotion of the problem by searching for the tpl file responsible for displaying our filters on the page. Search did not take long. The tpl file is in the root of the module and is called
blocklayered.tpl . Looking into it we are convinced that it has a line of output of the quantity, which is buggy here.
<a href="{$value.link}" data-rel="{$value.rel}">{$value.name|escape:html:'UTF-8'}{if $layered_show_qties}<span> ({$value.nbr})</span>{/if}</a>
Out of the corner of the eye, we note that the quantity is displayed by the condition $ layered_show_qties, and the quantity itself has the abbreviation nbr. Maybe this is useful, but maybe not.
The next step is to find the place where the
blocklayered.tpl template is
called . This turns out to be a function
public function generateFiltersBlock($selected_filters);
For verification, we find out that it is called twice - one of the left column hook, the other of the ajax request. It seems to be true. The function itself is small, but it has a function call that prepares the data for the template.
public function getFilterBlock($selected_filters = array())
This function takes up more than 800 lines. It has a bunch of SQL queries. Most likely, all the logic of filter formation is concentrated here. What is remarkable is that in the module it is called 5 times. It seems that it is too expensive to calculate so many queries 5 times in a row. But then you notice the variable
static $cache = null;
and you realize that this is a good old trick with caching in a static variable. And you understand that the code was written by notorious PHP-schnick, who will stop at nothing.
AND, OR and holy water
It is necessary to somehow study the operation of the function. Glitch occurs at the moment when the second tick is lit in the filter. And this is followed by an Ajax request. Therefore, use the dump variable in the file.
Using strong coffee and dancing with tambourines around the gas stove, we find the place where the main request for each filter block is sent and insert the debugging code that outputs this request to the file (
$ sql_query variable):
Pay attention -
$ sql_query - an array. This is evident from the code, so we output it to dump using
print_r with
true flag.
Immediately the first output to the file screams to us about the problem:
Array ( [select] => SELECT p.`id_product`, sa.`quantity`, sa.`out_of_stock` [from] => FROM ps_cat_restriction p [join] => LEFT JOIN `ps_stock_available` sa ON (sa.id_product = p.id_product AND sa.id_product_attribute=0 AND sa.id_shop = 1 AND sa.id_shop_group = 0 ) LEFT JOIN `ps_manufacturer` m ON (m.id_manufacturer = p.id_manufacturer) INNER JOIN `ps_layered_price_index` psi ON (psi.id_product = p.id_product AND psi.id_currency = 1 AND psi.price_min <= 3631136 AND psi.price_max >= 4618 AND psi.id_shop=1) [where] => WHERE 1 AND EXISTS (SELECT * FROM ps_feature_product fp WHERE fp.id_product = p.id_product AND fp.`id_feature_value` = 26634 OR fp.`id_feature_value` = 22096) [group] => [second_query] => )
Pay attention to the condition of
[where] : there is written in one line AND and OR, and OR is between homogeneous conditions and is not highlighted with brackets.
fp.id_product = p.id_product AND fp.`id_feature_value` = 26634 OR fp.`id_feature_value` = 22096
I am convinced that any sensible programmer, noticing that AND and OR are combined in some condition and without brackets, must immediately run after the holy water and sprinkle the monitor, hard drive, and keyboard to it so that this infection does not spread around.
But seriously, even with a quick glance at the condition and based on the nature of the problem, it becomes clear that the error is here - OR forgot to put brackets. It remains only to find this place and fix it. But here, too, a little surprise awaits us.
Last Surprise: Dynamic Dispatch
We are trying to find a place where an erroneous condition is formed. We use for this excerpts from the trace output. A search for
'fp .id_feature_value` leads us to a function:
private static function getId_featureFilterSubQuery($filter_value, $ignore_join = false)
This is what you need. We see the code that forms the condition without brackets and replace it with the correct one. Along the way, I want to note how OR is inserted - they are always inserted, and the last OR is bitten off (moreover, using the barbaric method).
foreach ($filter_value as $filter_val) $query_filters .= 'fp.`id_feature_value` = '.(int)$filter_val.' OR '; $query_filters = rtrim($query_filters, 'OR ').') ';
I think it is ugly. Therefore, I rewrite this piece of code in my style. Below is the source and corrected function code.
And the surprise is that in the module this function is not directly called anywhere. The clumsy name suggests sad thoughts about how it is formed programmatically somewhere and called dynamically. So it is, and in other similar places honest
cases are made. And here they decided to play with dynamic dispatch.
End of story
Glitch fixed. Filters began to work beautifully. Perhaps this error has already been resolved in new releases of PrestaShop. Well, if not and you had a similar problem, I’m glad if I could help you. And yet - do not skimp on the brackets, even if the procedure is obvious.