]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Allow crossed search between terms and tags 496/head
authorArthurHoaro <arthur@hoa.ro>
Tue, 23 Feb 2016 18:21:14 +0000 (19:21 +0100)
committerArthurHoaro <arthur@hoa.ro>
Sun, 28 Feb 2016 13:17:40 +0000 (14:17 +0100)
  * Partial fix of #449
  * Current use case: search term + click on tag.
  * LinkFilter now returns all links if no filter is given.
  * Unit tests.

application/LinkDB.php
application/LinkFilter.php
inc/shaarli.css
index.php
tests/LinkFilterTest.php
tpl/linklist.html

index 9488ac4582532f770da99072c52f0f02b12bea0a..1b505620959c3a6a06061dabd4ea6f982b39f3fd 100644 (file)
@@ -353,8 +353,7 @@ You use the community supported version of the original Shaarli project, by Seba
     public function filter($type = '', $request = '', $casesensitive = false, $privateonly = false)
     {
         $linkFilter = new LinkFilter($this->_links);
-        $requestFilter = is_array($request) ? implode(' ', $request) : $request;
-        return $linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly);
+        return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
     }
 
     /**
index 17594e8f4ea5250d368b675932c62e65ce1b08a9..3fd803cb81f4d79c08538a8fc3a7079d8f0b9644 100644 (file)
@@ -55,16 +55,25 @@ class LinkFilter
         switch($type) {
             case self::$FILTER_HASH:
                 return $this->filterSmallHash($request);
-                break;
+            case self::$FILTER_TAG | self::$FILTER_TEXT:
+                if (!empty($request)) {
+                    $filtered = $this->links;
+                    if (isset($request[0])) {
+                        $filtered = $this->filterTags($request[0], $casesensitive, $privateonly);
+                    }
+                    if (isset($request[1])) {
+                        $lf = new LinkFilter($filtered);
+                        $filtered = $lf->filterFulltext($request[1], $privateonly);
+                    }
+                    return $filtered;
+                }
+                return $this->noFilter($privateonly);
             case self::$FILTER_TEXT:
                 return $this->filterFulltext($request, $privateonly);
-                break;
             case self::$FILTER_TAG:
                 return $this->filterTags($request, $casesensitive, $privateonly);
-                break;
             case self::$FILTER_DAY:
                 return $this->filterDay($request);
-                break;
             default:
                 return $this->noFilter($privateonly);
         }
@@ -138,6 +147,10 @@ class LinkFilter
      */
     private function filterFulltext($searchterms, $privateonly = false)
     {
+        if (empty($searchterms)) {
+            return $this->links;
+        }
+
         $filtered = array();
         $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
         $exactRegex = '/"([^"]+)"/';
@@ -219,6 +232,12 @@ class LinkFilter
      */
     public function filterTags($tags, $casesensitive = false, $privateonly = false)
     {
+        // Implode if array for clean up.
+        $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags;
+        if (empty($tags)) {
+            return $this->links;
+        }
+
         $searchtags = self::tagsStrToArray($tags, $casesensitive);
         $filtered = array();
         if (empty($searchtags)) {
index 8a7409b23df05dc4160eefb559d0cd9b5a9db31c..2e41988e8e0f5a90030dd23494e29f31fc15b4c9 100644 (file)
@@ -33,6 +33,10 @@ h1 {
     margin-bottom: 20px;
 }
 
+em {
+    font-style: italic;
+}
+
 /* Buttons */
 .bigbutton {
     background-color: #c0c0c0;
index 5bd9cac4e39e15df6e4901b35a4929da7f316616..c2bec1db39afc5bcc82d5ad8e3b417ffed611643 100644 (file)
--- a/index.php
+++ b/index.php
@@ -623,7 +623,7 @@ class pageBuilder
         if (!empty($_GET['searchtags'])) {
             $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']);
         }
-        elseif (!empty($_GET['searchterm'])) {
+        if (!empty($_GET['searchterm'])) {
             $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']);
         }
         $this->tpl->assign('searchcrits', $searchcrits);
@@ -709,11 +709,19 @@ function showRSS()
     // Read links from database (and filter private links if user it not logged in).
 
     // Optionally filter the results:
-    if (!empty($_GET['searchterm'])) {
-        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+    $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
+    $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
+    if (! empty($searchtags) && ! empty($searchterm)) {
+        $linksToDisplay = $LINKSDB->filter(
+            LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+            array($searchtags, $searchterm)
+        );
     }
-    elseif (!empty($_GET['searchtags'])) {
-        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+    elseif ($searchtags) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
+    }
+    elseif ($searchterm) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
     }
     else {
         $linksToDisplay = $LINKSDB;
@@ -807,11 +815,19 @@ function showATOM()
     );
 
     // Optionally filter the results:
-    if (!empty($_GET['searchterm'])) {
-        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+    $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
+    $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
+    if (! empty($searchtags) && ! empty($searchterm)) {
+        $linksToDisplay = $LINKSDB->filter(
+            LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+            array($searchtags, $searchterm)
+        );
     }
-    else if (!empty($_GET['searchtags'])) {
-        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+    elseif ($searchtags) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
+    }
+    elseif ($searchterm) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
     }
     else {
         $linksToDisplay = $LINKSDB;
@@ -1165,11 +1181,19 @@ function renderPage()
     if ($targetPage == Router::$PAGE_PICWALL)
     {
         // Optionally filter the results:
-        if (!empty($_GET['searchterm'])) {
-            $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+        $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
+        $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
+        if (! empty($searchtags) && ! empty($searchterm)) {
+            $links = $LINKSDB->filter(
+                LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+                array($searchtags, $searchterm)
+            );
         }
-        elseif (! empty($_GET['searchtags'])) {
-            $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+        elseif ($searchtags) {
+            $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
+        }
+        elseif ($searchterm) {
+            $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
         }
         else {
             $links = $LINKSDB;
@@ -1963,29 +1987,46 @@ function importFile()
 // This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
 function buildLinkList($PAGE,$LINKSDB)
 {
-    // ---- Filter link database according to parameters
-    $search_type = '';
-    $search_crits = '';
+    // Filter link database according to parameters.
+    $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
+    $searchterm = !empty($_GET['searchterm']) ? escape(trim($_GET['searchterm'])) : '';
     $privateonly = !empty($_SESSION['privateonly']) ? true : false;
 
-    // Fulltext search
-    if (isset($_GET['searchterm'])) {
-        $search_crits = escape(trim($_GET['searchterm']));
-        $search_type = LinkFilter::$FILTER_TEXT;
-        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
+    // Search tags + fullsearch.
+    if (! empty($searchtags) && ! empty($searchterm)) {
+        $linksToDisplay = $LINKSDB->filter(
+            LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+            array($searchtags, $searchterm),
+            false,
+            $privateonly
+        );
     }
-    // Search by tag
-    elseif (isset($_GET['searchtags'])) {
-        $search_crits = explode(' ', escape(trim($_GET['searchtags'])));
-        $search_type = LinkFilter::$FILTER_TAG;
-        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
+    // Search by tags.
+    elseif (! empty($searchtags)) {
+        $linksToDisplay = $LINKSDB->filter(
+            LinkFilter::$FILTER_TAG,
+            $searchtags,
+            false,
+            $privateonly
+        );
+    }
+    // Fulltext search.
+    elseif (! empty($searchterm)) {
+        $linksToDisplay = $LINKSDB->filter(
+            LinkFilter::$FILTER_TEXT,
+            $searchterm,
+            false,
+            $privateonly
+        );
     }
     // Detect smallHashes in URL.
-    elseif (isset($_SERVER['QUERY_STRING'])
-        && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) {
-        $search_type = LinkFilter::$FILTER_HASH;
-        $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6);
-        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits);
+    elseif (! empty($_SERVER['QUERY_STRING'])
+        && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])
+    ) {
+        $linksToDisplay = $LINKSDB->filter(
+            LinkFilter::$FILTER_HASH,
+            substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6)
+        );
 
         if (count($linksToDisplay) == 0) {
             $PAGE->render404('The link you are trying to reach does not exist or has been deleted.');
@@ -2041,21 +2082,18 @@ function buildLinkList($PAGE,$LINKSDB)
     }
 
     // Compute paging navigation
-    $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm'];
-    $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags'];
+    $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags);
+    $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
     $previous_page_url = '';
     if ($i != count($keys)) {
-        $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags;
+        $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl;
     }
     $next_page_url='';
     if ($page>1) {
-        $next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags;
+        $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl;
     }
 
-    $token = '';
-    if (isLoggedIn()) {
-        $token = getToken();
-    }
+    $token = isLoggedIn() ? getToken() : '';
 
     // Fill all template fields.
     $data = array(
@@ -2065,8 +2103,8 @@ function buildLinkList($PAGE,$LINKSDB)
         'page_current' => $page,
         'page_max' => $pagecount,
         'result_count' => count($linksToDisplay),
-        'search_type' => $search_type,
-        'search_crits' => $search_crits,
+        'search_term' => $searchterm,
+        'search_tags' => $searchtags,
         'redirector' => empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'],  // Optional redirector URL.
         'token' => $token,
         'links' => $linkDisp,
index 31fd4cf499ac0606d32ced23d261b6ac1c18ad0a..ef1cc10a9789c78ea80dc85ee375eeaca17a5f0f 100644 (file)
@@ -12,6 +12,8 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
      */
     protected static $linkFilter;
 
+    protected static $NB_LINKS_REFDB = 7;
+
     /**
      * Instanciate linkFilter with ReferenceLinkDB data.
      */
@@ -27,7 +29,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
     public function testFilter()
     {
         $this->assertEquals(
-            7,
+            self::$NB_LINKS_REFDB,
             count(self::$linkFilter->filter('', ''))
         );
 
@@ -36,6 +38,16 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
             2,
             count(self::$linkFilter->filter('', '', false, true))
         );
+
+        $this->assertEquals(
+            self::$NB_LINKS_REFDB,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, ''))
+        );
+
+        $this->assertEquals(
+            self::$NB_LINKS_REFDB,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, ''))
+        );
     }
 
     /**
@@ -341,4 +353,41 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
             count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
         );
     }
+
+    /**
+     * Test crossed search (terms + tags).
+     */
+    public function testFilterCrossedSearch()
+    {
+        $terms = '"Free Software " stallman "read this" @website stuff';
+        $tags = 'free';
+        $this->assertEquals(
+            1,
+            count(self::$linkFilter->filter(
+                LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+                array($tags, $terms)
+            ))
+        );
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(
+                LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+                array('', $terms)
+            ))
+        );
+        $this->assertEquals(
+            1,
+            count(self::$linkFilter->filter(
+                LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+                array($tags, '')
+            ))
+        );
+        $this->assertEquals(
+            self::$NB_LINKS_REFDB,
+            count(self::$linkFilter->filter(
+                LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
+                ''
+            ))
+        );
+    }
 }
index ca91699ee81ff1ede7c4015d4803656dbe787cad..c0d420065562d7cb6b662cb6e0e03922bedd5f19 100644 (file)
     <div id="headerform" class="search">
         <form method="GET" class="searchform" name="searchform">
             <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text"
-               {if="!empty($search_crits) && $search_type=='fulltext'"}
-                    value="{$search_crits}"
+               {if="!empty($search_term)"}
+                    value="{$search_term}"
                {/if}
             >
             <input type="submit" value="Search" class="bigbutton">
         </form>
         <form method="GET" class="tagfilter" name="tagfilter">
             <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag"
-                {if="!empty($search_crits) && $search_type=='tags'"}
-                    value="{function="implode(' ', $search_crits)"}"
+                {if="!empty($search_tags)"}
+                    value="{$search_tags}"
                 {/if}
                 autocomplete="off" class="awesomplete" data-multiple data-minChars="1"
                 data-list="{loop="$tags"}{$key}, {/loop}"
     </div>
 
     {if="count($links)==0"}
-        <div id="searchcriteria">Nothing found.</i></div>
-    {else}
-        {if="$search_type=='fulltext'"}
-            <div id="searchcriteria">{$result_count} results for <i>{$search_crits}</i></div>
-        {/if}
-        {if="$search_type=='tags'"}
-            <div id="searchcriteria">{$result_count} results for tags <i>
-            {loop="search_crits"}
-                <span class="linktag" title="Remove tag">
-                    <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
-                </span>
-            {/loop}</i></div>
-        {/if}
+        <div id="searchcriteria">Nothing found.</div>
+    {elseif="!empty($search_term) or !empty($search_tags)"}
+        <div id="searchcriteria">
+            {$result_count} results
+            {if="!empty($search_term)"}
+                for <em>{$search_term}</em>
+            {/if}
+            {if="!empty($search_tags)"}
+                {$exploded_tags=explode(' ', $search_tags)}
+                tagged
+                {loop="$exploded_tags"}
+                    <span class="linktag" title="Remove tag">
+                        <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
+                    </span>
+                {/loop}
+            {/if}
+        </div>
     {/if}
     <ul>
         {loop="links"}