aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2016-02-23 19:21:14 +0100
committerArthurHoaro <arthur@hoa.ro>2016-02-28 14:17:40 +0100
commitc51fae92dc7d3080def81a2797e0d683b3e6d82a (patch)
tree394f9419589c1983d9be9834b89c8be2e8ef237c
parent6c3d6a31f413862941fe514e7167c04fe71ba1a7 (diff)
downloadShaarli-c51fae92dc7d3080def81a2797e0d683b3e6d82a.tar.gz
Shaarli-c51fae92dc7d3080def81a2797e0d683b3e6d82a.tar.zst
Shaarli-c51fae92dc7d3080def81a2797e0d683b3e6d82a.zip
Allow crossed search between terms and tags
* Partial fix of #449 * Current use case: search term + click on tag. * LinkFilter now returns all links if no filter is given. * Unit tests.
-rw-r--r--application/LinkDB.php3
-rw-r--r--application/LinkFilter.php27
-rw-r--r--inc/shaarli.css4
-rw-r--r--index.php120
-rw-r--r--tests/LinkFilterTest.php51
-rw-r--r--tpl/linklist.html38
6 files changed, 178 insertions, 65 deletions
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 9488ac45..1b505620 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -353,8 +353,7 @@ You use the community supported version of the original Shaarli project, by Seba
353 public function filter($type = '', $request = '', $casesensitive = false, $privateonly = false) 353 public function filter($type = '', $request = '', $casesensitive = false, $privateonly = false)
354 { 354 {
355 $linkFilter = new LinkFilter($this->_links); 355 $linkFilter = new LinkFilter($this->_links);
356 $requestFilter = is_array($request) ? implode(' ', $request) : $request; 356 return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
357 return $linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly);
358 } 357 }
359 358
360 /** 359 /**
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 17594e8f..3fd803cb 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -55,16 +55,25 @@ class LinkFilter
55 switch($type) { 55 switch($type) {
56 case self::$FILTER_HASH: 56 case self::$FILTER_HASH:
57 return $this->filterSmallHash($request); 57 return $this->filterSmallHash($request);
58 break; 58 case self::$FILTER_TAG | self::$FILTER_TEXT:
59 if (!empty($request)) {
60 $filtered = $this->links;
61 if (isset($request[0])) {
62 $filtered = $this->filterTags($request[0], $casesensitive, $privateonly);
63 }
64 if (isset($request[1])) {
65 $lf = new LinkFilter($filtered);
66 $filtered = $lf->filterFulltext($request[1], $privateonly);
67 }
68 return $filtered;
69 }
70 return $this->noFilter($privateonly);
59 case self::$FILTER_TEXT: 71 case self::$FILTER_TEXT:
60 return $this->filterFulltext($request, $privateonly); 72 return $this->filterFulltext($request, $privateonly);
61 break;
62 case self::$FILTER_TAG: 73 case self::$FILTER_TAG:
63 return $this->filterTags($request, $casesensitive, $privateonly); 74 return $this->filterTags($request, $casesensitive, $privateonly);
64 break;
65 case self::$FILTER_DAY: 75 case self::$FILTER_DAY:
66 return $this->filterDay($request); 76 return $this->filterDay($request);
67 break;
68 default: 77 default:
69 return $this->noFilter($privateonly); 78 return $this->noFilter($privateonly);
70 } 79 }
@@ -138,6 +147,10 @@ class LinkFilter
138 */ 147 */
139 private function filterFulltext($searchterms, $privateonly = false) 148 private function filterFulltext($searchterms, $privateonly = false)
140 { 149 {
150 if (empty($searchterms)) {
151 return $this->links;
152 }
153
141 $filtered = array(); 154 $filtered = array();
142 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); 155 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
143 $exactRegex = '/"([^"]+)"/'; 156 $exactRegex = '/"([^"]+)"/';
@@ -219,6 +232,12 @@ class LinkFilter
219 */ 232 */
220 public function filterTags($tags, $casesensitive = false, $privateonly = false) 233 public function filterTags($tags, $casesensitive = false, $privateonly = false)
221 { 234 {
235 // Implode if array for clean up.
236 $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags;
237 if (empty($tags)) {
238 return $this->links;
239 }
240
222 $searchtags = self::tagsStrToArray($tags, $casesensitive); 241 $searchtags = self::tagsStrToArray($tags, $casesensitive);
223 $filtered = array(); 242 $filtered = array();
224 if (empty($searchtags)) { 243 if (empty($searchtags)) {
diff --git a/inc/shaarli.css b/inc/shaarli.css
index 8a7409b2..2e41988e 100644
--- a/inc/shaarli.css
+++ b/inc/shaarli.css
@@ -33,6 +33,10 @@ h1 {
33 margin-bottom: 20px; 33 margin-bottom: 20px;
34} 34}
35 35
36em {
37 font-style: italic;
38}
39
36/* Buttons */ 40/* Buttons */
37.bigbutton { 41.bigbutton {
38 background-color: #c0c0c0; 42 background-color: #c0c0c0;
diff --git a/index.php b/index.php
index 5bd9cac4..c2bec1db 100644
--- a/index.php
+++ b/index.php
@@ -623,7 +623,7 @@ class pageBuilder
623 if (!empty($_GET['searchtags'])) { 623 if (!empty($_GET['searchtags'])) {
624 $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']); 624 $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']);
625 } 625 }
626 elseif (!empty($_GET['searchterm'])) { 626 if (!empty($_GET['searchterm'])) {
627 $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']); 627 $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']);
628 } 628 }
629 $this->tpl->assign('searchcrits', $searchcrits); 629 $this->tpl->assign('searchcrits', $searchcrits);
@@ -709,11 +709,19 @@ function showRSS()
709 // Read links from database (and filter private links if user it not logged in). 709 // Read links from database (and filter private links if user it not logged in).
710 710
711 // Optionally filter the results: 711 // Optionally filter the results:
712 if (!empty($_GET['searchterm'])) { 712 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
713 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); 713 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
714 if (! empty($searchtags) && ! empty($searchterm)) {
715 $linksToDisplay = $LINKSDB->filter(
716 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
717 array($searchtags, $searchterm)
718 );
714 } 719 }
715 elseif (!empty($_GET['searchtags'])) { 720 elseif ($searchtags) {
716 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); 721 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
722 }
723 elseif ($searchterm) {
724 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
717 } 725 }
718 else { 726 else {
719 $linksToDisplay = $LINKSDB; 727 $linksToDisplay = $LINKSDB;
@@ -807,11 +815,19 @@ function showATOM()
807 ); 815 );
808 816
809 // Optionally filter the results: 817 // Optionally filter the results:
810 if (!empty($_GET['searchterm'])) { 818 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
811 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); 819 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
820 if (! empty($searchtags) && ! empty($searchterm)) {
821 $linksToDisplay = $LINKSDB->filter(
822 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
823 array($searchtags, $searchterm)
824 );
812 } 825 }
813 else if (!empty($_GET['searchtags'])) { 826 elseif ($searchtags) {
814 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); 827 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
828 }
829 elseif ($searchterm) {
830 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
815 } 831 }
816 else { 832 else {
817 $linksToDisplay = $LINKSDB; 833 $linksToDisplay = $LINKSDB;
@@ -1165,11 +1181,19 @@ function renderPage()
1165 if ($targetPage == Router::$PAGE_PICWALL) 1181 if ($targetPage == Router::$PAGE_PICWALL)
1166 { 1182 {
1167 // Optionally filter the results: 1183 // Optionally filter the results:
1168 if (!empty($_GET['searchterm'])) { 1184 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
1169 $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); 1185 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
1186 if (! empty($searchtags) && ! empty($searchterm)) {
1187 $links = $LINKSDB->filter(
1188 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
1189 array($searchtags, $searchterm)
1190 );
1170 } 1191 }
1171 elseif (! empty($_GET['searchtags'])) { 1192 elseif ($searchtags) {
1172 $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); 1193 $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
1194 }
1195 elseif ($searchterm) {
1196 $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
1173 } 1197 }
1174 else { 1198 else {
1175 $links = $LINKSDB; 1199 $links = $LINKSDB;
@@ -1963,29 +1987,46 @@ function importFile()
1963// This function fills all the necessary fields in the $PAGE for the template 'linklist.html' 1987// This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1964function buildLinkList($PAGE,$LINKSDB) 1988function buildLinkList($PAGE,$LINKSDB)
1965{ 1989{
1966 // ---- Filter link database according to parameters 1990 // Filter link database according to parameters.
1967 $search_type = ''; 1991 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
1968 $search_crits = ''; 1992 $searchterm = !empty($_GET['searchterm']) ? escape(trim($_GET['searchterm'])) : '';
1969 $privateonly = !empty($_SESSION['privateonly']) ? true : false; 1993 $privateonly = !empty($_SESSION['privateonly']) ? true : false;
1970 1994
1971 // Fulltext search 1995 // Search tags + fullsearch.
1972 if (isset($_GET['searchterm'])) { 1996 if (! empty($searchtags) && ! empty($searchterm)) {
1973 $search_crits = escape(trim($_GET['searchterm'])); 1997 $linksToDisplay = $LINKSDB->filter(
1974 $search_type = LinkFilter::$FILTER_TEXT; 1998 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
1975 $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); 1999 array($searchtags, $searchterm),
2000 false,
2001 $privateonly
2002 );
1976 } 2003 }
1977 // Search by tag 2004 // Search by tags.
1978 elseif (isset($_GET['searchtags'])) { 2005 elseif (! empty($searchtags)) {
1979 $search_crits = explode(' ', escape(trim($_GET['searchtags']))); 2006 $linksToDisplay = $LINKSDB->filter(
1980 $search_type = LinkFilter::$FILTER_TAG; 2007 LinkFilter::$FILTER_TAG,
1981 $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); 2008 $searchtags,
2009 false,
2010 $privateonly
2011 );
2012 }
2013 // Fulltext search.
2014 elseif (! empty($searchterm)) {
2015 $linksToDisplay = $LINKSDB->filter(
2016 LinkFilter::$FILTER_TEXT,
2017 $searchterm,
2018 false,
2019 $privateonly
2020 );
1982 } 2021 }
1983 // Detect smallHashes in URL. 2022 // Detect smallHashes in URL.
1984 elseif (isset($_SERVER['QUERY_STRING']) 2023 elseif (! empty($_SERVER['QUERY_STRING'])
1985 && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) { 2024 && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])
1986 $search_type = LinkFilter::$FILTER_HASH; 2025 ) {
1987 $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6); 2026 $linksToDisplay = $LINKSDB->filter(
1988 $linksToDisplay = $LINKSDB->filter($search_type, $search_crits); 2027 LinkFilter::$FILTER_HASH,
2028 substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6)
2029 );
1989 2030
1990 if (count($linksToDisplay) == 0) { 2031 if (count($linksToDisplay) == 0) {
1991 $PAGE->render404('The link you are trying to reach does not exist or has been deleted.'); 2032 $PAGE->render404('The link you are trying to reach does not exist or has been deleted.');
@@ -2041,21 +2082,18 @@ function buildLinkList($PAGE,$LINKSDB)
2041 } 2082 }
2042 2083
2043 // Compute paging navigation 2084 // Compute paging navigation
2044 $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm']; 2085 $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags);
2045 $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags']; 2086 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
2046 $previous_page_url = ''; 2087 $previous_page_url = '';
2047 if ($i != count($keys)) { 2088 if ($i != count($keys)) {
2048 $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags; 2089 $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl;
2049 } 2090 }
2050 $next_page_url=''; 2091 $next_page_url='';
2051 if ($page>1) { 2092 if ($page>1) {
2052 $next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags; 2093 $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl;
2053 } 2094 }
2054 2095
2055 $token = ''; 2096 $token = isLoggedIn() ? getToken() : '';
2056 if (isLoggedIn()) {
2057 $token = getToken();
2058 }
2059 2097
2060 // Fill all template fields. 2098 // Fill all template fields.
2061 $data = array( 2099 $data = array(
@@ -2065,8 +2103,8 @@ function buildLinkList($PAGE,$LINKSDB)
2065 'page_current' => $page, 2103 'page_current' => $page,
2066 'page_max' => $pagecount, 2104 'page_max' => $pagecount,
2067 'result_count' => count($linksToDisplay), 2105 'result_count' => count($linksToDisplay),
2068 'search_type' => $search_type, 2106 'search_term' => $searchterm,
2069 'search_crits' => $search_crits, 2107 'search_tags' => $searchtags,
2070 'redirector' => empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'], // Optional redirector URL. 2108 'redirector' => empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'], // Optional redirector URL.
2071 'token' => $token, 2109 'token' => $token,
2072 'links' => $linkDisp, 2110 'links' => $linkDisp,
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php
index 31fd4cf4..ef1cc10a 100644
--- a/tests/LinkFilterTest.php
+++ b/tests/LinkFilterTest.php
@@ -12,6 +12,8 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
12 */ 12 */
13 protected static $linkFilter; 13 protected static $linkFilter;
14 14
15 protected static $NB_LINKS_REFDB = 7;
16
15 /** 17 /**
16 * Instanciate linkFilter with ReferenceLinkDB data. 18 * Instanciate linkFilter with ReferenceLinkDB data.
17 */ 19 */
@@ -27,7 +29,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
27 public function testFilter() 29 public function testFilter()
28 { 30 {
29 $this->assertEquals( 31 $this->assertEquals(
30 7, 32 self::$NB_LINKS_REFDB,
31 count(self::$linkFilter->filter('', '')) 33 count(self::$linkFilter->filter('', ''))
32 ); 34 );
33 35
@@ -36,6 +38,16 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
36 2, 38 2,
37 count(self::$linkFilter->filter('', '', false, true)) 39 count(self::$linkFilter->filter('', '', false, true))
38 ); 40 );
41
42 $this->assertEquals(
43 self::$NB_LINKS_REFDB,
44 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, ''))
45 );
46
47 $this->assertEquals(
48 self::$NB_LINKS_REFDB,
49 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, ''))
50 );
39 } 51 }
40 52
41 /** 53 /**
@@ -341,4 +353,41 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
341 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) 353 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
342 ); 354 );
343 } 355 }
356
357 /**
358 * Test crossed search (terms + tags).
359 */
360 public function testFilterCrossedSearch()
361 {
362 $terms = '"Free Software " stallman "read this" @website stuff';
363 $tags = 'free';
364 $this->assertEquals(
365 1,
366 count(self::$linkFilter->filter(
367 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
368 array($tags, $terms)
369 ))
370 );
371 $this->assertEquals(
372 2,
373 count(self::$linkFilter->filter(
374 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
375 array('', $terms)
376 ))
377 );
378 $this->assertEquals(
379 1,
380 count(self::$linkFilter->filter(
381 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
382 array($tags, '')
383 ))
384 );
385 $this->assertEquals(
386 self::$NB_LINKS_REFDB,
387 count(self::$linkFilter->filter(
388 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
389 ''
390 ))
391 );
392 }
344} 393}
diff --git a/tpl/linklist.html b/tpl/linklist.html
index ca91699e..c0d42006 100644
--- a/tpl/linklist.html
+++ b/tpl/linklist.html
@@ -11,16 +11,16 @@
11 <div id="headerform" class="search"> 11 <div id="headerform" class="search">
12 <form method="GET" class="searchform" name="searchform"> 12 <form method="GET" class="searchform" name="searchform">
13 <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" 13 <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text"
14 {if="!empty($search_crits) && $search_type=='fulltext'"} 14 {if="!empty($search_term)"}
15 value="{$search_crits}" 15 value="{$search_term}"
16 {/if} 16 {/if}
17 > 17 >
18 <input type="submit" value="Search" class="bigbutton"> 18 <input type="submit" value="Search" class="bigbutton">
19 </form> 19 </form>
20 <form method="GET" class="tagfilter" name="tagfilter"> 20 <form method="GET" class="tagfilter" name="tagfilter">
21 <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" 21 <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag"
22 {if="!empty($search_crits) && $search_type=='tags'"} 22 {if="!empty($search_tags)"}
23 value="{function="implode(' ', $search_crits)"}" 23 value="{$search_tags}"
24 {/if} 24 {/if}
25 autocomplete="off" class="awesomplete" data-multiple data-minChars="1" 25 autocomplete="off" class="awesomplete" data-multiple data-minChars="1"
26 data-list="{loop="$tags"}{$key}, {/loop}" 26 data-list="{loop="$tags"}{$key}, {/loop}"
@@ -44,19 +44,23 @@
44 </div> 44 </div>
45 45
46 {if="count($links)==0"} 46 {if="count($links)==0"}
47 <div id="searchcriteria">Nothing found.</i></div> 47 <div id="searchcriteria">Nothing found.</div>
48 {else} 48 {elseif="!empty($search_term) or !empty($search_tags)"}
49 {if="$search_type=='fulltext'"} 49 <div id="searchcriteria">
50 <div id="searchcriteria">{$result_count} results for <i>{$search_crits}</i></div> 50 {$result_count} results
51 {/if} 51 {if="!empty($search_term)"}
52 {if="$search_type=='tags'"} 52 for <em>{$search_term}</em>
53 <div id="searchcriteria">{$result_count} results for tags <i> 53 {/if}
54 {loop="search_crits"} 54 {if="!empty($search_tags)"}
55 <span class="linktag" title="Remove tag"> 55 {$exploded_tags=explode(' ', $search_tags)}
56 <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> 56 tagged
57 </span> 57 {loop="$exploded_tags"}
58 {/loop}</i></div> 58 <span class="linktag" title="Remove tag">
59 {/if} 59 <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
60 </span>
61 {/loop}
62 {/if}
63 </div>
60 {/if} 64 {/if}
61 <ul> 65 <ul>
62 {loop="links"} 66 {loop="links"}