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);