This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Fulltext search index for shop buyables |
||
4 | * |
||
5 | * @author Mark Guinn <[email protected]> |
||
6 | * @date 08.29.2013 |
||
7 | * @package shop_search |
||
8 | */ |
||
9 | class ShopSearch extends Object |
||
0 ignored issues
–
show
|
|||
10 | { |
||
11 | const FACET_TYPE_LINK = 'link'; |
||
12 | const FACET_TYPE_CHECKBOX = 'checkbox'; |
||
13 | const FACET_TYPE_RANGE = 'range'; |
||
14 | |||
15 | /** @var string - class name of adapter class to use */ |
||
16 | private static $adapter_class = 'ShopSearchSimple'; |
||
0 ignored issues
–
show
|
|||
17 | |||
18 | /** @var array - these classes will be added to the index - e.g. Category, Page, etc. */ |
||
19 | private static $searchable = array(); |
||
0 ignored issues
–
show
|
|||
20 | |||
21 | /** @var bool - if true, all buyable models will be added to the index automatically */ |
||
22 | private static $buyables_are_searchable = true; |
||
0 ignored issues
–
show
|
|||
23 | |||
24 | /** @var int - size of paging in the search */ |
||
25 | private static $page_size = 10; |
||
0 ignored issues
–
show
|
|||
26 | |||
27 | /** @var bool */ |
||
28 | private static $suggest_enabled = true; |
||
0 ignored issues
–
show
|
|||
29 | |||
30 | /** @var int - how many suggestions to provide */ |
||
31 | private static $suggest_limit = 5; |
||
0 ignored issues
–
show
|
|||
32 | |||
33 | /** @var bool */ |
||
34 | private static $search_as_you_type_enabled = true; |
||
0 ignored issues
–
show
|
|||
35 | |||
36 | /** @var int - how may sayt (search-as-you-type) entries to provide */ |
||
37 | private static $sayt_limit = 5; |
||
0 ignored issues
–
show
|
|||
38 | |||
39 | /** @var bool - automatically create facets for static attributes */ |
||
40 | private static $auto_facet_attributes = false; |
||
0 ignored issues
–
show
|
|||
41 | |||
42 | /** @var string - optionally, a different template to run ajax results through (sans-Page.ss) */ |
||
43 | private static $ajax_results_template = ''; |
||
0 ignored issues
–
show
|
|||
44 | |||
45 | /** @var string - these allow you to use different querystring params in you need to */ |
||
46 | private static $qs_query = 'q'; |
||
0 ignored issues
–
show
|
|||
47 | private static $qs_filters = 'f'; |
||
0 ignored issues
–
show
|
|||
48 | private static $qs_parent_search = '__ps'; |
||
0 ignored issues
–
show
|
|||
49 | private static $qs_title = '__t'; |
||
0 ignored issues
–
show
|
|||
50 | private static $qs_source = '__src'; // used to log searches from search-as-you-type |
||
0 ignored issues
–
show
|
|||
51 | private static $qs_sort = 'sort'; |
||
0 ignored issues
–
show
|
|||
52 | |||
53 | /** @var array - I'm leaving this particularly bare b/c with config merging it's a pain to remove items */ |
||
54 | private static $sort_options = array( |
||
0 ignored issues
–
show
|
|||
55 | 'score desc' => 'Relevance', |
||
56 | // 'SiteTree_Title asc' => 'Alphabetical (A-Z)', |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
54% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||
57 | // 'SiteTree_Title dsc' => 'Alphabetical (Z-A)', |
||
58 | ); |
||
59 | |||
60 | /** |
||
61 | * @var array - default search facets (price, category, etc) |
||
62 | * Key field name - e.g. Price - can be a VirtualFieldIndex field |
||
63 | * Value facet label - e.g. Search By Category - if the value is a relation or returns an array or |
||
64 | * list all values will be faceted individually |
||
65 | * NOTE: this can also be another array with keys: Label, Type, and Values (for checkbox only) |
||
66 | */ |
||
67 | private static $facets = array(); |
||
0 ignored issues
–
show
|
|||
68 | |||
69 | /** @var array - field definition for Solr only */ |
||
70 | private static $solr_fulltext_fields = array(); |
||
0 ignored issues
–
show
|
|||
71 | |||
72 | /** @var array - field definition for Solr only */ |
||
73 | private static $solr_filter_fields = array(); |
||
0 ignored issues
–
show
|
|||
74 | |||
75 | /** @var string - if present, will create a copy of SiteTree_Title that's suited for alpha sorting */ |
||
76 | private static $solr_title_sort_field = ''; |
||
0 ignored issues
–
show
|
|||
77 | |||
78 | /** |
||
79 | * @var string - If present, everything matching the following regex will be removed from |
||
80 | * keyword search queries before passing to the search adapter. |
||
81 | */ |
||
82 | private static $keyword_filter_regex = '/[^a-zA-Z0-9\s\-]/'; |
||
0 ignored issues
–
show
|
|||
83 | |||
84 | |||
85 | /** |
||
86 | * @return array |
||
87 | */ |
||
88 | 5 | public static function get_searchable_classes() |
|
89 | { |
||
90 | // First get any explicitly declared searchable classes |
||
91 | 5 | $searchable = Config::inst()->get('ShopSearch', 'searchable'); |
|
92 | 5 | if (is_string($searchable) && strlen($searchable) > 0) { |
|
93 | $searchable = array($searchable); |
||
94 | 5 | } elseif (!is_array($searchable)) { |
|
95 | $searchable = array(); |
||
96 | } |
||
97 | |||
98 | // Add in buyables automatically if asked |
||
99 | 5 | if (Config::inst()->get('ShopSearch', 'buyables_are_searchable')) { |
|
100 | $buyables = SS_ClassLoader::instance()->getManifest()->getImplementorsOf('Buyable'); |
||
101 | if (is_array($buyables) && count($buyables) > 0) { |
||
102 | foreach ($buyables as $c) { |
||
103 | $searchable[] = $c; |
||
104 | } |
||
105 | } |
||
106 | } |
||
107 | |||
108 | 5 | return array_unique($searchable); |
|
109 | } |
||
110 | |||
111 | /** |
||
112 | * Returns an array of categories suitable for a dropdown menu |
||
113 | * TODO: cache this |
||
114 | * |
||
115 | * @param int $parentID [optional] |
||
116 | * @param string $prefix [optional] |
||
117 | * @param int $maxDepth [optional] |
||
118 | * @return array |
||
119 | * @static |
||
120 | */ |
||
121 | 2 | public static function get_category_hierarchy($parentID = 0, $prefix = '', $maxDepth = 999) |
|
122 | { |
||
123 | 2 | $out = array(); |
|
124 | 2 | $cats = ProductCategory::get() |
|
125 | 2 | ->filter(array( |
|
126 | 2 | 'ParentID' => $parentID, |
|
127 | 2 | 'ShowInMenus' => 1, |
|
128 | 2 | )) |
|
129 | 2 | ->sort('Sort'); |
|
130 | |||
131 | // If there is a single parent category (usually "Products" or something), we |
||
132 | // probably don't want that in the hierarchy. |
||
133 | 2 | if ($parentID == 0 && $cats->count() == 1) { |
|
134 | return self::get_category_hierarchy($cats->first()->ID, $prefix, $maxDepth); |
||
135 | } |
||
136 | |||
137 | 2 | foreach ($cats as $cat) { |
|
138 | 2 | $out[$cat->ID] = $prefix . $cat->Title; |
|
139 | 2 | if ($maxDepth > 1) { |
|
140 | 2 | $out += self::get_category_hierarchy($cat->ID, $prefix . $cat->Title . ' > ', $maxDepth - 1); |
|
141 | 2 | } |
|
142 | 2 | } |
|
143 | |||
144 | 2 | return $out; |
|
145 | } |
||
146 | |||
147 | /** |
||
148 | * @return ShopSearchAdapter |
||
149 | */ |
||
150 | 5 | public static function adapter() |
|
151 | { |
||
152 | 5 | $adapterClass = Config::inst()->get('ShopSearch', 'adapter_class'); |
|
153 | 5 | return Injector::inst()->get($adapterClass); |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * @return ShopSearch |
||
158 | */ |
||
159 | 5 | public static function inst() |
|
160 | { |
||
161 | 5 | return Injector::inst()->get('ShopSearch'); |
|
162 | } |
||
163 | |||
164 | /** |
||
165 | * The result will contain at least the following: |
||
166 | * Matches - SS_List of results |
||
167 | * TotalMatches - total # of results, unlimited |
||
168 | * Query - query string |
||
169 | * Also saves a log record. |
||
170 | * |
||
171 | * @param array $vars |
||
172 | * @param bool $logSearch [optional] |
||
173 | * @param bool $useFacets [optional] |
||
174 | * @param int $start [optional] |
||
175 | * @param int $limit [optional] |
||
176 | * @return ArrayData |
||
177 | */ |
||
178 | 5 | public function search(array $vars, $logSearch=true, $useFacets=true, $start=-1, $limit=-1) |
|
179 | { |
||
180 | 5 | $qs_q = $this->config()->get('qs_query'); |
|
181 | 5 | $qs_f = $this->config()->get('qs_filters'); |
|
182 | 5 | $qs_ps = $this->config()->get('qs_parent_search'); |
|
183 | 5 | $qs_t = $this->config()->get('qs_title'); |
|
184 | 5 | $qs_sort= $this->config()->get('qs_sort'); |
|
185 | 5 | if ($limit < 0) { |
|
186 | 5 | $limit = $this->config()->get('page_size'); |
|
187 | 5 | } |
|
188 | 5 | if ($start < 0) { |
|
189 | 5 | $start = !empty($vars['start']) ? (int)$vars['start'] : 0; |
|
190 | 5 | } // as far as i can see, fulltextsearch hard codes 'start' |
|
191 | 5 | $facets = $useFacets ? $this->config()->get('facets') : array(); |
|
192 | 5 | if (!is_array($facets)) { |
|
193 | 4 | $facets = array(); |
|
194 | 4 | } |
|
195 | 5 | if (empty($limit)) { |
|
196 | $limit = -1; |
||
197 | } |
||
198 | |||
199 | // figure out and scrub the sort |
||
200 | 5 | $sortOptions = $this->config()->get('sort_options'); |
|
201 | 5 | $sort = !empty($vars[$qs_sort]) ? $vars[$qs_sort] : ''; |
|
202 | 5 | if (!isset($sortOptions[$sort])) { |
|
203 | 5 | $sort = current(array_keys($sortOptions)); |
|
204 | 5 | } |
|
205 | |||
206 | // figure out and scrub the filters |
||
207 | 5 | $filters = !empty($vars[$qs_f]) ? FacetHelper::inst()->scrubFilters($vars[$qs_f]) : array(); |
|
208 | |||
209 | // do the search |
||
210 | 5 | $keywords = !empty($vars[$qs_q]) ? $vars[$qs_q] : ''; |
|
211 | 5 | if ($keywordRegex = $this->config()->get('keyword_filter_regex')) { |
|
212 | 5 | $keywords = preg_replace($keywordRegex, '', $keywords); |
|
213 | 5 | } |
|
214 | 5 | $results = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort); |
|
215 | |||
216 | // massage the results a bit |
||
217 | 5 | if (!empty($keywords) && !$results->hasValue('Query')) { |
|
218 | 4 | $results->Query = $keywords; |
|
219 | 4 | } |
|
220 | 5 | if (!empty($filters) && !$results->hasValue('Filters')) { |
|
221 | 2 | $results->Filters = new ArrayData($filters); |
|
222 | 2 | } |
|
223 | 5 | if (!$results->hasValue('Sort')) { |
|
224 | 5 | $results->Sort = $sort; |
|
225 | 5 | } |
|
226 | 5 | if (!$results->hasValue('TotalMatches')) { |
|
227 | 5 | $results->TotalMatches = $results->Matches->hasMethod('getTotalItems') |
|
228 | 5 | ? $results->Matches->getTotalItems() |
|
229 | 5 | : $results->Matches->count(); |
|
230 | 5 | } |
|
231 | |||
232 | // for some types of facets, update the state |
||
233 | 5 | if ($results->hasValue('Facets')) { |
|
234 | 1 | FacetHelper::inst()->transformHierarchies($results->Facets); |
|
235 | 1 | FacetHelper::inst()->updateFacetState($results->Facets, $filters); |
|
236 | 1 | } |
|
237 | |||
238 | // make a hash of the search so we can know if we've already logged it this session |
||
239 | 5 | $loggedFilters = !empty($filters) ? json_encode($filters) : null; |
|
240 | 5 | $loggedQuery = strtolower($results->Query); |
|
241 | // $searchHash = md5($loggedFilters . $loggedQuery); |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
53% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||
242 | // $sessSearches = Session::get('loggedSearches'); |
||
243 | // if (!is_array($sessSearches)) $sessSearches = array(); |
||
244 | // Debug::dump($searchHash, $sessSearches); |
||
245 | |||
246 | // save the log record |
||
247 | 5 | if ($start == 0 && $logSearch && (!empty($keywords) || !empty($filters))) { // && !in_array($searchHash, $sessSearches)) { |
|
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
65% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||
248 | 5 | $log = SearchLog::create(array( |
|
249 | 5 | 'Query' => $loggedQuery, |
|
250 | 5 | 'Title' => !empty($vars[$qs_t]) ? $vars[$qs_t] : '', |
|
251 | 5 | 'Link' => Controller::curr()->getRequest()->getURL(true), // I'm not 100% happy with this, but can't think of a better way |
|
252 | 5 | 'NumResults' => $results->TotalMatches, |
|
253 | 5 | 'MemberID' => Member::currentUserID(), |
|
254 | 5 | 'Filters' => $loggedFilters, |
|
255 | 5 | 'ParentSearchID'=> !empty($vars[$qs_ps]) ? $vars[$qs_ps] : 0, |
|
256 | 5 | )); |
|
257 | 5 | $log->write(); |
|
258 | 5 | $results->SearchLogID = $log->ID; |
|
259 | 5 | $results->SearchBreadcrumbs = $log->getBreadcrumbs(); |
|
260 | |||
261 | // $sessSearches[] = $searchHash; |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
58% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||
262 | // Session::set('loggedSearches', $sessSearches); |
||
263 | 5 | } |
|
264 | |||
265 | 5 | return $results; |
|
266 | } |
||
267 | |||
268 | /** |
||
269 | * @param string $str |
||
270 | * @return SS_Query |
||
271 | */ |
||
272 | 1 | public function getSuggestQuery($str='') |
|
273 | { |
||
274 | 1 | $hasResults = 'CASE WHEN max("SearchLog"."NumResults") > 0 THEN 1 ELSE 0 END'; |
|
275 | 1 | $searchCount = 'count(distinct "SearchLog"."ID")'; |
|
276 | 1 | $q = new SQLQuery(); |
|
0 ignored issues
–
show
The class
SQLQuery has been deprecated with message: since version 4.0
This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead. ![]() |
|||
277 | 1 | $q = $q->setSelect('"SearchLog"."Query"') |
|
278 | // TODO: what to do with filter? |
||
279 | 1 | ->selectField($searchCount, 'SearchCount') |
|
280 | 1 | ->selectField('max("SearchLog"."Created")', 'LastSearch') |
|
281 | 1 | ->selectField('max("SearchLog"."NumResults")', 'NumResults') |
|
282 | 1 | ->selectField($hasResults, 'HasResults') |
|
283 | 1 | ->setFrom('"SearchLog"') |
|
284 | 1 | ->setGroupBy('"SearchLog"."Query"') |
|
285 | 1 | ->setOrderBy(array( |
|
286 | 1 | "$hasResults DESC", |
|
287 | 1 | "$searchCount DESC" |
|
288 | 1 | )) |
|
289 | 1 | ->setLimit(Config::inst()->get('ShopSearch', 'suggest_limit')) |
|
290 | 1 | ; |
|
291 | |||
292 | 1 | if (strlen($str) > 0) { |
|
293 | 1 | $q = $q->addWhere(sprintf('"SearchLog"."Query" LIKE \'%%%s%%\'', Convert::raw2sql($str))); |
|
294 | 1 | } |
|
295 | |||
296 | 1 | return $q; |
|
297 | } |
||
298 | |||
299 | |||
300 | /** |
||
301 | * @param string $str |
||
302 | * @return array |
||
303 | */ |
||
304 | 1 | public function suggest($str='') |
|
305 | { |
||
306 | 1 | $adapter = self::adapter(); |
|
307 | 1 | if ($adapter->hasMethod('suggest')) { |
|
308 | return $adapter->suggest($str); |
||
0 ignored issues
–
show
The method
suggest() does not seem to exist on object<ShopSearchAdapter> .
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||
309 | } else { |
||
310 | 1 | return $this->getSuggestQuery($str)->execute()->column('Query'); |
|
311 | } |
||
312 | } |
||
313 | |||
314 | |||
315 | /** |
||
316 | * Returns an array that can be made into json and passed to the controller |
||
317 | * containing both term suggestions and a few product matches. |
||
318 | * |
||
319 | * @param array $searchVars |
||
320 | * @return array |
||
321 | */ |
||
322 | public function suggestWithResults(array $searchVars) |
||
323 | { |
||
324 | $qs_q = $this->config()->get('qs_query'); |
||
325 | $qs_f = $this->config()->get('qs_filters'); |
||
326 | $keywords = !empty($searchVars[$qs_q]) ? $searchVars[$qs_q] : ''; |
||
327 | $filters = !empty($searchVars[$qs_f]) ? $searchVars[$qs_f] : array(); |
||
328 | |||
329 | $adapter = self::adapter(); |
||
330 | |||
331 | // get suggestions and product list from the adapter |
||
332 | if ($adapter->hasMethod('suggestWithResults')) { |
||
333 | $results = $adapter->suggestWithResults($keywords, $filters); |
||
334 | } else { |
||
335 | $limit = (int)ShopSearch::config()->sayt_limit; |
||
336 | $search = self::adapter()->searchFromVars($keywords, $filters, array(), 0, $limit, 'Popularity DESC'); |
||
337 | //$search = ShopSearch::inst()->search($searchVars, false, false, 0, $limit); |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
63% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||
338 | |||
339 | $results = array( |
||
340 | 'products' => $search->Matches, |
||
341 | 'suggestions' => $this->suggest($keywords), |
||
342 | ); |
||
343 | } |
||
344 | |||
345 | // the adapter just gave us a list of products, which we need to process a little further |
||
346 | if (!empty($results['products'])) { |
||
347 | // this gets encoded into the product links |
||
348 | $searchVars['total'] = $results['products']->hasMethod('getTotalItems') |
||
349 | ? $results['products']->getTotalItems() |
||
350 | : $results['products']->count(); |
||
351 | |||
352 | $products = array(); |
||
353 | foreach ($results['products'] as $prod) { |
||
354 | if (!$prod || !$prod->exists()) { |
||
355 | continue; |
||
356 | } |
||
357 | $img = $prod->hasMethod('ProductImage') ? $prod->ProductImage() : $prod->Image(); |
||
358 | $thumb = ($img && $img->exists()) ? $img->getThumbnail() : null; |
||
359 | |||
360 | $json = array( |
||
361 | 'link' => $prod->Link() . '?' . ShopSearch::config()->qs_source . '=' . urlencode(base64_encode(json_encode($searchVars))), |
||
362 | 'title' => $prod->Title, |
||
363 | 'desc' => $prod->obj('Content')->Summary(), |
||
364 | 'thumb' => $thumb ? $thumb->Link() : '', |
||
365 | 'price' => $prod->obj('Price')->Nice(), |
||
366 | ); |
||
367 | |||
368 | if ($prod->hasExtension('HasPromotionalPricing') && $prod->hasValidPromotion()) { |
||
369 | $json['original_price'] = $prod->getOriginalPrice()->Nice(); |
||
370 | } |
||
371 | |||
372 | $products[] = $json; |
||
373 | } |
||
374 | |||
375 | // replace the list of product objects with json |
||
376 | $results['products'] = $products; |
||
377 | } |
||
378 | |||
379 | $this->extend('updateSuggestWithResults', $results, $keywords, $filters); |
||
380 | |||
381 | return $results; |
||
382 | } |
||
383 | } |
||
384 |
You can fix this by adding a namespace to your class:
When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.