add support for double quotes in the search string
authorDenis Ovsienko <denis@ovsienko.info>
Tue, 6 Dec 2016 16:28:36 +0000 (16:28 +0000)
committerDenis Ovsienko <denis@ovsienko.info>
Tue, 6 Dec 2016 16:28:36 +0000 (16:28 +0000)
Make it possible to search for substrings that include space(s), for
example, searching for "mail server" (with the quotes) will no longer
return those records that contain only "mail" or only "server" or "mail
backup server". However, the query will still match "e-mail server" as
before. Add some tests.

* parseSearchTerms(): a new function to implement the syntax more
  complex than possible with just explode()
* getSearchResultByField(): use the above
* searchHandler(): ditto

ChangeLog
tests/PureFunctionTest.php
wwwroot/inc/database.php
wwwroot/inc/functions.php
wwwroot/inc/interface.php

index f179ec19eec9795565de34f16d87212e1de7aa2c..7f71c594cf746f7d49f7ce43ee0d5a3d56ee87a4 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -19,6 +19,7 @@
        update: add 1000Base-BX40 and 1000Base-BX80 port types (#1645)
        update: upgrade CodeMirror from 3.22 to 3.24
        update: include file timestamps in the mouse hint (#1663)
+       update: add support for double quotes in the search string
 0.20.11 2016-02-07
        bugfix: Tag Picker does not display the full tag list as a tree (#1403)
        bugfix: race conditions in 802.Q deploy (switch remained synced after recalc)
index 680736e9680e78f3e30778029a9d7c69dca9f26a..f179e943a984ac41ef4666602cfa313c1e9a10a0 100644 (file)
@@ -263,6 +263,22 @@ class PureFunctionTest extends PHPUnit_Framework_TestCase
                        array ('validTagName', '$tag', FALSE),
                        array ('validTagName', '2015-', FALSE),
                        array ('validTagName', 'iqn.domain.', FALSE),
+
+                       array ('parseSearchTerms', '', array ()),
+                       array ('parseSearchTerms', 'sixty', array ('sixty')),
+                       array ('parseSearchTerms', '"sixty "', array ('sixty')),
+                       array ('parseSearchTerms', 'seventy сімдесят', array ('seventy', 'сімдесят')),
+                       array ('parseSearchTerms', '"seventy" сімдесят', array ('seventy', 'сімдесят')),
+                       array ('parseSearchTerms', 'seventy " сімдесят"', array ('seventy', 'сімдесят')),
+                       array ('parseSearchTerms', '"seventy" "сімдесят"', array ('seventy', 'сімдесят')),
+                       array ('parseSearchTerms', 'eighty вісімдесят восемьдесят', array ('eighty', 'вісімдесят', 'восемьдесят')),
+                       array ('parseSearchTerms', '" seventy one "', array ('seventy one')),
+                       array ('parseSearchTerms', '"seventy one" сімдесят', array ('seventy one', 'сімдесят')),
+                       array ('parseSearchTerms', 'seventy "сімдесят один"', array ('seventy', 'сімдесят один')),
+                       array ('parseSearchTerms', '"eighty one" вісімдесят восемьдесят', array ('eighty one', 'вісімдесят', 'восемьдесят')),
+                       array ('parseSearchTerms', 'eighty "вісімдесят один" восемьдесят', array ('eighty', 'вісімдесят один', 'восемьдесят')),
+                       array ('parseSearchTerms', 'eighty вісімдесят "восемьдесят один"', array ('eighty', 'вісімдесят', 'восемьдесят один')),
+                       array ('parseSearchTerms', '"eighty one" "вісімдесят один" "восемьдесят один"', array ('eighty one', 'вісімдесят один', 'восемьдесят один')),
                );
        }
 
@@ -1177,6 +1193,12 @@ class PureFunctionTest extends PHPUnit_Framework_TestCase
                        array ('groupBy', array ('', 'test')),
                        array ('groupBy', array (0, 'test')),
                        array ('groupBy', array (array (array ('id' => 1, 'name' => 'one'), 2), 'name')),
+
+                       array ('parseSearchTerms', array ('one two three"')),
+                       array ('parseSearchTerms', array ('"one two three')),
+                       array ('parseSearchTerms', array ('one "two" "three')),
+                       array ('parseSearchTerms', array ('one "" three')),
+                       array ('parseSearchTerms', array ('""')),
                );
        }
 
index e413ffd848d11a3e95a982634ea48e2330607de0..ab89da6ce9b972a5863360931b5b6052dc47bef4 100644 (file)
@@ -3047,7 +3047,8 @@ function getSearchResultByField ($tablename, $retcolumns, $scancolumn, $terms, $
        $query = 'SELECT ' . implode (', ', $retcolumns) . " FROM ${tablename} WHERE ";
        $qparams = array();
        $pfx = '';
-       foreach (explode (' ', $terms) as $term)
+       $pterms = $exactness == 3 ? explode (' ', $terms) : parseSearchTerms ($terms);
+       foreach ($pterms as $term)
        {
                switch ($exactness)
                {
index 26691b8a0a37981e95e0e3bbc9c1afacbfb4953c..ba57d5884f41a527ab4292ed8e035c4187aceba6 100644 (file)
@@ -5015,6 +5015,75 @@ function buildSearchRedirectURL ($result_type, $record)
        return buildRedirectURL ($next_page, isset ($next_tab) ? $next_tab : 'default', $params);
 }
 
+// This works like explode() with space as a separator with the added difference
+// that anything in double quotes is returned as a single word.
+function parseSearchTerms ($terms)
+{
+       $ret = array();
+       if (mb_substr_count ($terms, '"') % 2 != 0)
+               throw new InvalidArgException ('terms', $terms, 'contains odd number of quotes');
+       $state = 'whitespace';
+       $buffer = '';
+       $len = mb_strlen ($terms);
+       for ($i = 0; $i < $len; $i++)
+       {
+               $c = mb_substr ($terms, $i, 1);
+               switch ($state)
+               {
+               case 'whitespace':
+                       switch ($c)
+                       {
+                       case ' ':
+                               break; // nom-nom
+                       case '"':
+                               $buffer = '';
+                               $state = 'quoted_string';
+                               break;
+                       default:
+                               $buffer = $c;
+                               $state = 'word';
+                       }
+                       break;
+
+               case 'word':
+                       switch ($c)
+                       {
+                       case '"':
+                               throw new InvalidArgException ('terms', $terms, 'punctuation error');
+                       case ' ':
+                               $ret[] = $buffer;
+                               $buffer = '';
+                               $state = 'whitespace';
+                               break;
+                       default:
+                               $buffer .= $c;
+                       }
+                       break;
+
+               case 'quoted_string':
+                       switch ($c)
+                       {
+                       case '"':
+                               if (trim ($buffer) == '')
+                                       throw new InvalidArgException ('terms', $terms, 'punctuation error');
+                               $ret[] = trim ($buffer);
+                               $buffer = '';
+                               $state = 'whitespace';
+                               // FIXME: this does not detect missing whitespace that would be reasonable
+                               // to expect between the closing quote and the next token, if any.
+                               break;
+                       default:
+                               $buffer .= $c;
+                       }
+                       break;
+               }
+       }
+       if ($buffer != '')
+               $ret[] = $buffer;
+
+       return $ret;
+}
+
 // Take a parse tree and figure out if it is a valid payload or not.
 // Depending on that return either NULL or an array filled with the load
 // of that expression.
index 2b0382d84849b202d9af9e5859594ea33943e007..877d68382414a0c89c7e57dad5b69ccee8aac158 100644 (file)
@@ -3643,6 +3643,19 @@ function searchHandler()
                showError ('Search string cannot be empty.');
                redirectUser (buildRedirectURL ('index', 'default'));
        }
+
+       try
+       {
+               parseSearchTerms ($terms);
+               // Discard the return value as searchEntitiesByText() and its retriever
+               // functions expect the original string as the parameter.
+       }
+       catch (InvalidArgException $iae)
+       {
+               showError ($iae->getMessage());
+               redirectUser (buildRedirectURL ('index', 'default'));
+       }
+
        renderSearchResults ($terms, searchEntitiesByText ($terms));
 }