This commit introduces new functionality: When `search_type` is `web`,
search behavior matches aurweb's HTML search page.

New parameters: `search_type`
Valid search_type values: `rpc` (default), `web`

The `rpc` search type uses the existing legacy search method.

The `web` search type clones the web-based aurweb search page's behavior
as closely as possible (see note at the bottom of this message).

The following example searches for packages that contain `blah` AND `abc`:
https://aur.archlinux.org/rpc/?v=5&type=search&search_type=web&arg=blah%20abc

The legacy method still searches for packages that contain 'blah abc':
https://aur.archlinux.org/rpc/?v=5&type=search&arg=blah%20abc
https://aur.archlinux.org/rpc/?v=5&type=search&search_type=rpc&arg=blah%20abc

An invalid `search_type` returns a json error message about it being
unsupported.

Additionally, `search_type` is only parsed and considered during a search of
type `name` or `name-desc`.

Note: This change was written as a solution to
https://bugs.archlinux.org/task/49133.

PS: + Some spacing issues fixed in comments.

Signed-off-by: Kevin Morris <kevr.gt...@gmail.com>
---
 doc/rpc.txt               |  7 ++++
 web/lib/aurjson.class.php | 75 +++++++++++++++++++++++++++++++++++----
 2 files changed, 76 insertions(+), 6 deletions(-)

diff --git a/doc/rpc.txt b/doc/rpc.txt
index 3148ebea..eb97547d 100644
--- a/doc/rpc.txt
+++ b/doc/rpc.txt
@@ -21,6 +21,11 @@ The _by_ parameter can be skipped and defaults to 
`name-desc`.
 If a maintainer search is performed and the search argument is left empty, a
 list of orphan packages is returned.
 
+The _search_type_ parameter can be skipped and defaults to `rpc`.
+
+_search_type_ is only ever used during a `name` or `name-desc` search and
+supports the following values: `rpc`, `web`.
+
 Package Details
 ---------------
 
@@ -39,6 +44,8 @@ Examples
   `/rpc/?v=5&type=search&by=makedepends&arg=boost`
 `search` with callback::
   `/rpc/?v=5&type=search&arg=foobar&callback=jsonp1192244621103`
+`search` with `web` _search_type_ for packages containing `cookie` AND `milk`::
+  `/rpc/?v=5&type=search&search_type=web&arg=cookie%20milk`
 `info`::
   `/rpc/?v=5&type=info&arg[]=foobar`
 `info` with multiple packages::
diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php
index 0ac586fe..0e94b578 100644
--- a/web/lib/aurjson.class.php
+++ b/web/lib/aurjson.class.php
@@ -23,6 +23,9 @@ class AurJSON {
        private static $exposed_depfields = array(
                'depends', 'makedepends', 'checkdepends', 'optdepends'
        );
+       private static $exposed_search_types = array(
+               'rpc', 'web'
+       );
        private static $fields_v1 = array(
                'Packages.ID', 'Packages.Name',
                'PackageBases.ID AS PackageBaseID',
@@ -140,7 +143,7 @@ class AurJSON {
        }
 
        /*
-        * Check if an IP needs to be  rate limited.
+        * Check if an IP needs to be rate limited.
         *
         * @param $ip IP of the current request
         *
@@ -192,7 +195,7 @@ class AurJSON {
                $value = get_cache_value('ratelimit-ws:' . $ip, $status);
                if (!$status || ($status && $value < $deletion_time)) {
                        if (set_cache_value('ratelimit-ws:' . $ip, $time, 
$window_length) &&
-                           set_cache_value('ratelimit:' . $ip, 1, 
$window_length)) {
+                               set_cache_value('ratelimit:' . $ip, 1, 
$window_length)) {
                                return;
                        }
                } else {
@@ -472,6 +475,33 @@ class AurJSON {
                return array('ids' => $id_args, 'names' => $name_args);
        }
 
+       /*
+        * Prepare a WHERE statement for each keyword: append $func($keyword)
+        * separated by $delim. Each keyword is sanitized as wildcards before
+        * it's passed to $func.
+        *
+        * @param $delim Delimiter to use in the middle of two keywords.
+        * @param $keywords Array of keywords to prepare.
+        * @param $func A function that returns a string. This value is 
concatenated.
+        *
+        * @return A WHERE condition statement of keywords separated by $delim.
+        */
+       private function join_where($delim, $keywords, $func) {
+               // Applied to each item to concatenate our entire statement.
+               $reduce_func = function($carry, $item) use(&$func) {
+                       array_push($carry, $func($item));
+                       return $carry;
+               };
+
+               // Manual array_reduce with a local lambda.
+               $acc = array(); // Initial
+               foreach ($keywords as &$keyword) {
+                       $acc += $reduce_func($acc, $keyword);
+               }
+
+               return join(" $delim ", $acc);
+       }
+
        /*
         * Performs a fulltext mysql search of the package database.
         *
@@ -480,6 +510,7 @@ class AurJSON {
         * @return mixed Returns an array of package matches.
         */
        private function search($http_data) {
+
                $keyword_string = $http_data['arg'];
 
                if (isset($http_data['by'])) {
@@ -488,17 +519,49 @@ class AurJSON {
                        $search_by = 'name-desc';
                }
 
+               if (isset($http_data['search_type'])) {
+                       $search_type = $http_data['search_type'];
+               } else {
+                       // Default search_type: rpc
+                       $search_type = 'rpc';
+               }
+
+               if (!in_array($search_type, self::$exposed_search_types)) {
+                       return $this->json_error('Unsupported search type.');
+               }
+
                if ($search_by === 'name' || $search_by === 'name-desc') {
                        if (strlen($keyword_string) < 2) {
                                return $this->json_error('Query arg too 
small.');
                        }
-                       $keyword_string = $this->dbh->quote("%" . 
addcslashes($keyword_string, '%_') . "%");
 
                        if ($search_by === 'name') {
-                               $where_condition = "(Packages.Name LIKE 
$keyword_string)";
+                               if ($search_type === 'rpc') {
+                                       $keyword_string = $this->dbh->quote(
+                                               "%" . 
addcslashes($keyword_string, '%_') . "%");
+                                       $where_condition = "(Packages.Name LIKE 
$keyword_string)";
+                               } else {
+                                       $keywords = explode(' ', 
$keyword_string);
+                                       $func = function($keyword) {
+                                               $str = $this->dbh->quote("%" . 
addcslashes($keyword, '%_') . "%");
+                                               return "(Packages.Name LIKE 
$str)";
+                                       };
+                                       $where_condition = 
$this->join_where("AND", $keywords, $func);
+                               }
                        } else if ($search_by === 'name-desc') {
-                               $where_condition = "(Packages.Name LIKE 
$keyword_string OR ";
-                               $where_condition .= "Description LIKE 
$keyword_string)";
+                               if ($search_type === 'rpc') {
+                                       $keyword_string = $this->dbh->quote(
+                                               "%" . 
addcslashes($keyword_string, '%_') . "%");
+                                       $where_condition = "(Packages.Name LIKE 
$keyword_string ";
+                                       $where_condition .= "OR Description 
LIKE $keyword_string)";
+                               } else {
+                                       $keywords = explode(' ', 
$keyword_string);
+                                       $func = function($keyword) {
+                                               $str = $this->dbh->quote("%" . 
addcslashes($keyword, '%_') . "%");
+                                               return "(Packages.Name LIKE 
$str OR Description LIKE $str)";
+                                       };
+                                       $where_condition = 
$this->join_where("AND", $keywords, $func);
+                               }
                        }
                } else if ($search_by === 'maintainer') {
                        if (empty($keyword_string)) {
-- 
2.20.1

Reply via email to