Issues (158)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/ShopSearch.php (30 issues)

Upgrade to new PHP Analysis Engine

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
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
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
The property $adapter_class is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $searchable is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $buyables_are_searchable is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
23
24
    /** @var int - size of paging in the search */
25
    private static $page_size = 10;
0 ignored issues
show
The property $page_size is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
26
27
    /** @var bool */
28
    private static $suggest_enabled = true;
0 ignored issues
show
The property $suggest_enabled is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
30
    /** @var int - how many suggestions to provide */
31
    private static $suggest_limit = 5;
0 ignored issues
show
The property $suggest_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
32
33
    /** @var bool */
34
    private static $search_as_you_type_enabled = true;
0 ignored issues
show
The property $search_as_you_type_enabled is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
35
36
    /** @var int - how may sayt (search-as-you-type) entries to provide */
37
    private static $sayt_limit = 5;
0 ignored issues
show
The property $sayt_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
38
39
    /** @var bool - automatically create facets for static attributes */
40
    private static $auto_facet_attributes = false;
0 ignored issues
show
The property $auto_facet_attributes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $ajax_results_template is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $qs_query is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
47
    private static $qs_filters       = 'f';
0 ignored issues
show
The property $qs_filters is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
48
    private static $qs_parent_search = '__ps';
0 ignored issues
show
The property $qs_parent_search is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
49
    private static $qs_title         = '__t';
0 ignored issues
show
The property $qs_title is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
50
    private static $qs_source        = '__src'; // used to log searches from search-as-you-type
0 ignored issues
show
The property $qs_source is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
51
    private static $qs_sort          = 'sort';
0 ignored issues
show
The property $qs_sort is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $sort_options is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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.

Loading history...
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
The property $facets is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
68
69
    /** @var array - field definition for Solr only */
70
    private static $solr_fulltext_fields = array();
0 ignored issues
show
The property $solr_fulltext_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
71
72
    /** @var array - field definition for Solr only */
73
    private static $solr_filter_fields = array();
0 ignored issues
show
The property $solr_filter_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $solr_title_sort_field is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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
The property $keyword_filter_regex is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
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.

Loading history...
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.

Loading history...
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.

Loading history...
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
Deprecated Code introduced by
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.

Loading history...
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.

Loading history...
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.

Loading history...
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