From 78ac0574b936779186c291e3c273e23dda625d22 Mon Sep 17 00:00:00 2001
From: Erik Thuning <boooink@gmail.com>
Date: Wed, 15 Sep 2021 15:53:52 +0200
Subject: [PATCH] Major search overhaul

---
 include/Entity.php     |  24 +---
 include/Product.php    | 153 +++++++++++++++++--------
 include/SearchPage.php | 247 ++++++++++++++++-------------------------
 include/User.php       |  57 +++++++---
 include/functions.php  |  21 ++--
 5 files changed, 260 insertions(+), 242 deletions(-)

diff --git a/include/Entity.php b/include/Entity.php
index 4edcd23..37c5f8e 100644
--- a/include/Entity.php
+++ b/include/Entity.php
@@ -1,27 +1,9 @@
 <?php
-class Entity {
+abstract class Entity {
     protected function __construct() {
         
     }
-    
-    protected function specify_search($searchterms, $searchfields) {
-        if(array_key_exists('fritext', $searchterms)) {
-            $freeterm = $searchterms['fritext'];
-            unset($searchterms['fritext']);
-            foreach($searchfields as $field) {
-                if(array_key_exists($field, $searchterms)) {
-                    $term = $searchterms[$field];
-                    if(is_array($term)) {
-                        $term[] = $freeterm;
-                    } else {
-                        $searchterms[$field] = array($term, $freeterm);
-                    }
-                } else {
-                    $searchterms[$field] = $freeterm;
-                }
-            }
-        }
-        return $searchterms;
-    }
+
+    abstract public function matches($term, $ldap);
 }
 ?>
diff --git a/include/Product.php b/include/Product.php
index b09a121..13b9322 100644
--- a/include/Product.php
+++ b/include/Product.php
@@ -110,58 +110,117 @@ class Product extends Entity {
         return true;
     }
 
-    public function matches($terms, $matchAll=false) {
-        print('DEBUG $terms in matches: ');
-        var_dump($terms);
-        print('<br><br>');
-        $terms = $this->specify_search($terms, array('brand',
-                                                     'name',
-                                                     'serial',
-                                                     'invoice',
-                                                     'status',
-                                                     'tag'));
-        print('DEBUG $terms POST TRANSLATION: ');
-        var_dump($terms);
-        print('<br><br>');
+    /*
+      Return a list of field-value mappings containing all matching search terms.
+     */
+    public function matches($terms, $ldap) {
         $matches = array();
-        foreach($terms as $field => $values) {
-            if(property_exists($this, $field)) {
-                if(match($values, $this->$field)) {
-                    $matches[$field] = $this->$field;
-                } else {
-                    if($matchAll) {
-                        return array();
+
+        // Create a list mapping all basic fields to getters
+        $fields = array('brand' => 'get_brand',
+                        'name' => 'get_name',
+                        'invoice' => 'get_invoice',
+                        'serial' => 'get_serial');
+
+        foreach($terms as $term) {
+            $key = $term->get_key();
+            $matched = false;
+            switch($key) {
+                case 'brand':
+                case 'name':
+                case 'invoice':
+                case 'serial':
+                    // If $key is a standard field, check against its value
+                    $getter = $fields[$key];
+                    $value = $this->$getter();
+                    if(match($term, $value)) {
+                        //Record a successful match
+                        $matches[$key] = $value;
+                        $matched = true;
                     }
-                }
-            } else if(array_key_exists($field, $this->get_info())) {
-                if(match($values, $this->get_info()[$field])) {
-                    $matches[$field] = $this->get_info()[$field];
-                } else {
-                    if($matchAll) {
-                        return array();
-                    }
-                }
-            } else if($field == 'tag') {
-                foreach($this->get_tags() as $tag) {
-                    if(match($values, $tag)) {
-                        if(!array_key_exists('tags', $matches)) {
-                            $matches['tags'] = array();
-                        }
-                        $matches['tags'][] = $tag;
-                    } else {
-                        if($matchAll) {
-                            return array();
+                    break;
+                case 'tag':
+                    // If $key is tag, iterate over the tags
+                    $matched_tags = $this->match_tags($term);
+                    if($matched_tags) {
+                        // Record a successful match
+                        $matched = true;
+                        if(!isset($matches['tags'])) {
+                            // This is the first list of matching tags
+                            $matches['tags'] = $matched_tags;
+                        } else {
+                            // Merge these results with existing results
+                            $matches['tags'] = array_unique(
+                                array_merge($matches['tags'],
+                                            $matched_tags));
                         }
                     }
-                }
-            } else if($field == 'status') {
-                if(match($values, $this->get_status())) {
-                    $matches['status'] = $this->get_status();
-                } else {
-                    if($matchAll) {
-                        return array();
+                    break;
+                case 'fritext':
+                    // if $key is fritext:
+                    // First check basic fields
+                    foreach($fields as $field => $getter) {
+                        $value = $this->$getter();
+                        if(match($term, $value)) {
+                            $matches[$field] = $value;
+                            $matched = true;
+                        }
                     }
-                }
+                    // Then tags
+                    $matched_tags = $this->match_tags($term);
+                    if($matched_tags) {
+                        $matched = true;
+                        if(!isset($matches['tags'])) {
+                            $matches['tags'] = $matched_tags;
+                        } else {
+                            $matches['tags'] = array_unique(
+                                array_merge($matches['tags'],
+                                            $matched_tags));
+                        }
+                    }
+                    // Then custom fields
+                    foreach($this->get_info() as $field => $value) {
+                        if(match($term, $value)) {
+                            //Record a successful match
+                            $matches[$field] = $value;
+                            $matched = true;
+                        }
+                    }
+                    break;
+                default:
+                    // Handle custom fields
+                    $info = $this->get_info();
+                    if(isset($info[$key])) {
+                        // If $key is a valid custom field on this product
+                        $value = $info[$key];
+                        if(match($term, $value)) {
+                            //Record a successful match
+                            $matches[$key] = $value;
+                            $matched = true;
+                        }
+                    }
+                    break;
+            }
+            // If a mandatory match failed, the entire search has failed
+            // and we return an empty result.
+            if($term->is_mandatory() && !$matched) {
+                return array();
+            }
+            // If a negative match succeeded, the entire search has failed
+            // and we return an empty result.
+            if($term->is_negative() && $matched) {
+                return array();
+            }
+        }
+        return $matches;
+    }
+
+    private function match_tags($term) {
+        $tags = $this->get_tags();
+        $matches = array();
+        foreach($tags as $tag) {
+            if(match($term, $tag)) {
+                $matches[] = $tag;
             }
         }
         return $matches;
diff --git a/include/SearchPage.php b/include/SearchPage.php
index 859605f..2e9340b 100644
--- a/include/SearchPage.php
+++ b/include/SearchPage.php
@@ -28,11 +28,7 @@ class SearchPage extends Page {
         if(!$this->terms) {
             return $out;
         }
-        print('<br>');
         foreach(array('user', 'product') as $type) {
-            // print('=== DEBUG $type: ');
-            // print_r($type);
-            // print(' ===<br><br>');
             $result = $this->search($type, $this->terms);
             if($result) {
                 $out[$type] = $result;
@@ -42,148 +38,32 @@ class SearchPage extends Page {
     }
 
     private function search($type, $terms) {
-
-        /*
-        ==================================================
-
-        ORIGINAL CODE || BACKUP || FOR REFERENCE
-
-        $items = get_items($type);
-        $out = array();
-        foreach($items as $item) {
-            $result = $item->matches($terms);
-            if($result) {
-                $out[] = array($item, $result);
+        $matches = array();
+        foreach(get_items($type) as $item) {
+            if($result = $item->matches($terms, $this->ldap)) {
+                $matches[] = array($item, $result);
             }
         }
-        return $out;
-    
-        ==================================================
-        */
-
-        $mustMatchArray = array(); 
-        $cannotMatchArray = array(); 
-        $mayMatchArray = array();
-        foreach($terms as $key => $value) {
-            if(!is_array($value)) {
-                $value = array($value);
-            }
-            foreach($value as $term) {
-                switch ($term[0]) {
-                    case "+":
-                        if (!array_key_exists($key, $mustMatchArray)) {
-                            $mustMatchArray[$key] = array();
-                        }
-                        $mustMatchArray[$key][] = substr($term, 1);
-                        break;
-                    case "!":
-                    case "-":
-                        if (!array_key_exists($key, $cannotMatchArray)) {
-                            $cannotMatchArray[$key] = array();
-                        }
-                        $cannotMatchArray[$key][] = substr($term, 1);
-                        break;
-                    case "~":
-                        if (!array_key_exists($key, $mayMatchArray)) {
-                            $mayMatchArray[$key] = array();
-                        }
-                        $mayMatchArray[$key][] = substr($term, 1);
-                        break;
-                    default:
-                        if (!array_key_exists($key, $mayMatchArray)) {
-                            $mayMatchArray[$key] = array();
-                        }
-                        $mayMatchArray[$key][] = $term;
-                        break;
-                }
-            }
-        }
-
-        $items = get_items($type);
-        $sanitizedItems = array();
-        foreach($items as $item) {
-            $result = $item->matches($mustMatchArray, True);
-            if($result) {
-                $sanitizedItems[] = array($item, $result);
-            }
-
-            // $mustMatchCheck = array();
-            // foreach($mustMatchArray as $mustMatchTerm) {
-
-            //     $matchResult = $item->matches($mustMatchTerm);
-            //     if($matchResult) {
-            //         $mustMatchCheck[] = True;
-            //     } else {
-            //         $mustMatchCheck[] = False;
-            //     }
-            // }
-            // if(in_array(False, $mustMatchCheck, True) === False) {
-            //     $sanitizedItems[] = array($item, $matchResult);
-            // }
-
-            
-
-            // $mustExcludeCheck = array();
-            // foreach($mustExcludeArray as $mustExcludeTerm) {
-            //     if($item->matches($mustExcludeTerm)) {
-            //         $mustExcludeCheck[] = False;
-            //     } else {
-            //         $mustExcludeCheck[] = True;
-            //     }
-            // }
-
-            // if (in_array(False, $mustIncludeCheck, True) === False) {
-            //     if(in_array(False, $mustExcludeCheck, True) === True) {
-
-            //         //  === IF TRUE DO NOTHING ===
-
-            //     } else {
-            //         foreach ($canIncludeArray as $canIncludeTerm) {                    
-            //             $result = $item->matches($canIncludeTerm);
-            //             if($result) {
-            //                 $out[] = array($item, $result);
-            //             }
-            //         }
-            //     }
-            // }
-        }
-        print('DEBUG $sanitizedItems: ');
-        var_dump($sanitizedItems);
-        print('<br><br>');
-        // $out = array();
-        // foreach($sanitizedItems as $sanitizedItem) {
-        //     print('DEBUG $sanitizedItem: ');
-        //     var_dump($sanitizedItem);
-        //     print('<br><br>');
-
-        //     if($sanitizedItem->matches($cannotMatchArray)) {
-
-        //         //  === IF TRUE DO NOTHING ===
-
-        //     }
-        //     else {
-        //         $result = $sanitizedItem->matches($mayMatchArray);
-        //         if($result) {
-        //             $out[] = array($sanitizedItem, $result);
-        //         }
-        //     }
-        // }
-
-        // return array();
-        // return $out;
-        return $sanitizedItems;
+        return $matches;
     }
 
     private function translate_terms($terms) {
         $matches = array();
+
+        // If there is a q-query
+        // and it contains a : character
         if(isset($terms['q']) && preg_match('/([^:]+):(.*)/',
                                             $terms['q'],
                                             $matches)) {
+            // remove the q key
             unset($terms['q']);
+            // insert the term, using whatever came before
+            // the : as the key and whatever came after as the value
             $terms[$matches[1]] = $matches[2];
         }
         $translated = array();
-        foreach($terms as $key => $value) {
+        // Translate all keys into a standard format
+        foreach($terms as $key => $values) {
             $newkey = $key;
             switch($key) {
                 case 'q':
@@ -204,17 +84,38 @@ class SearchPage extends Page {
                     $newkey = 'serial';
                     break;
                 case 'tagg':
+                case 'tags':
                     $newkey = 'tag';
                     break;
+                case 'anteckning':
+                    $newkey = 'note';
+                    break;
+                case 'e-post':
+                case 'epost':
+                case 'mail':
+                    $newkey = 'email';
+                    break;
                 case 'status':
-                    $value = $this->translate_values($value);
+                    // Translate all status values into a standard format
+                    $values = $this->translate_values($values);
                     break;
             }
-            if(!array_key_exists($newkey, $translated)) {
-                $translated[$newkey] = $value;
-            } else {
-                $temp = $translated[$newkey];
-                $translated[$newkey] = array_merge((array)$temp, (array)$value);
+            // Wrap the value in an array if it isn't one
+            if(!is_array($values)) {
+                $values = array($values);
+            }
+            // Make a SearchTerm object from each term
+            foreach($values as $value) {
+                // Check for flags
+                $flag = SearchTerm::OPTIONAL;
+                if(in_array($value[0], array(SearchTerm::MANDATORY,
+                                             SearchTerm::OPTIONAL,
+                                             SearchTerm::NEGATIVE))) {
+                    $flag = $value[0];
+                    $value = substr($value, 1);
+                }
+                // Collect the new SearchTerm
+                $translated[] = new SearchTerm($key, $value, $flag);
             }
         }
         return $translated;
@@ -270,16 +171,15 @@ class SearchPage extends Page {
         $terms = '';
         if($this->terms) {
             $hidden = '';
-            foreach($this->terms as $key => $value) {
-                if(!is_array($value)) {
-                    $value = array($value);
-                }
-                foreach($value as $item) {
-                    $terms .= replace(array('term' => ucfirst($key).": $item",
-                                            'key' => $key,
-                                            'value' => $item),
-                                      $this->fragments['search_term']);
-                }
+            foreach($this->terms as $term) {
+                $key = $term->get_key();
+                $flag = $term->get_flag();
+                $query = $term->get_query();
+                $fullterm = ucfirst($key).": ".$flag.$query;
+                $terms .= replace(array('term' => $fullterm,
+                                        'key' => $key,
+                                        'value' => $flag.$query),
+                                  $this->fragments['search_term']);
             }
         }
         $products = 'Inga artiklar hittade.';
@@ -327,4 +227,53 @@ class SearchPage extends Page {
              . $data . '<br/>';
     }
 }
+
+class SearchTerm {
+    public const MANDATORY = '+';
+    public const OPTIONAL = '~';
+    public const NEGATIVE = '-';
+
+    private $key;
+    private $query;
+    private $flag;
+
+    public function __construct($key, $query, $flag=SearchTerm::OPTIONAL) {
+        $this->key = $key;
+        $this->query = $query;
+        $this->flag = $flag;
+    }
+
+    public function get_key() {
+        return $this->key;
+    }
+
+    public function get_query() {
+        return $this->query;
+    }
+
+    public function get_flag() {
+        return $this->flag;
+    }
+
+    public function is_optional() {
+        if($this->flag == SearchTerm::OPTIONAL) {
+            return true;
+        }
+        return false;
+    }
+
+    public function is_mandatory() {
+        if($this->flag == SearchTerm::MANDATORY) {
+            return true;
+        }
+        return false;
+    }
+
+    public function is_negative() {
+        if($this->flag == SearchTerm::NEGATIVE) {
+            return true;
+        }
+        return false;
+    }
+}
 ?>
diff --git a/include/User.php b/include/User.php
index 34d5699..d77475f 100644
--- a/include/User.php
+++ b/include/User.php
@@ -45,37 +45,60 @@ class User extends Entity {
         return true;
     }
 
-    public function matches($terms, $ldap, $matchAll=false) {
-        $terms = $this->specify_search($terms, array('name',
-                                                     'email',
-                                                     'notes'));
+    public function matches($terms, $ldap) {
         $matches = array();
-        foreach($terms as $field => $values) {
-            switch($field) {
+        foreach($terms as $term) {
+            // Iterate over the terms
+            $matched = false;
+            $key = $term->get_key();
+            switch($key) {
                 case 'name':
-                    if(match($values, $this->name)) {
-                        $matches['name'] = $this->name;
+                    // If the key is name, check username and displayname
+                    $name = $this->get_name();
+                    if(match($term, $name)) {
+                        $matches['name'] = $name;
+                        $matched = true;
                     }
                     $dname = $this->get_displayname($ldap);
-                    if(match($values, $dname)) {
+                    if(match($term, $dname)) {
                         $matches['displayname'] = $dname;
+                        $matched = true;
+                    }
+                    break;
+                case 'note':
+                    // If the key is note, check it.
+                    $note = $this->get_note();
+                    if($note && match($term, $note)) {
+                        $matches['note'] = $note;
+                        $matched = true;
                     }
                     break;
                 case 'email':
                     $email = $this->get_email($ldap, false);
-                    if($email && match($values, $email)) {
+                    if($email && match($term, $email)) {
                         $matches['email'] = $email;
+                        $matched = true;
                     }
                     break;
-                case 'notes':
-                    if(match($values, $this->notes)) {
-                        $matches['notes'] = $this->notes;
+                case 'fritext':
+                    //Check everything if the key is fritext
+                    $name = $this->get_name();
+                    if(match($term, $name)) {
+                        $matches['name'] = $name;
+                        $matched = true;
+                    }
+                    $dname = $this->get_displayname($ldap);
+                    if(match($term, $dname)) {
+                        $matches['displayname'] = $dname;
+                        $matched = true;
                     }
-                    break;
             }
-        }
-        if($matchAll && array_diff_assoc($terms, $matches)) {
-            return array();
+            if($term->is_mandatory() && !$matched) {
+                return array();
+            }
+            if($term->is_negative() && $matched) {
+                return array();
+            }
         }
         return $matches;
     }
diff --git a/include/functions.php b/include/functions.php
index 099b4bb..b85f1b3 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -253,18 +253,23 @@ function suggest_content($fieldname) {
     return $out;
 }
 
-function match($searchterms, $subject) {
-    if(!is_array($searchterms)) {
-        $searchterms = array($searchterms);
-    }
-    foreach($searchterms as $term) {
-        if(fnmatch('*'.$term.'*', $subject, FNM_CASEFOLD)) {
-            return true;
-        }
+function match($term, $subject) {
+    if(fnmatch('*'.$term->get_query().'*', $subject, FNM_CASEFOLD)) {
+        return true;
     }
     return false;
 }
 
+function match_tags($searchterm, $tags) {
+    $found = array();
+    foreach($tags as $tag) {
+        if(fnmatch('*'.$tag.'*', $searchterm, FNM_CASEFOLD)) {
+            $found[] = $tag;
+        }
+    }
+    return $found;
+}
+
 function format_date($date) {
     if($date) {
         return gmdate('Y-m-d', $date);