aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthur <arthur@hoa.ro>2016-02-28 14:26:46 +0100
committerArthur <arthur@hoa.ro>2016-02-28 14:26:46 +0100
commitc6744a9e89b62ba94563c43ab33f964ec0b11a17 (patch)
tree30e6274a05636d06b10a5749d4786d5d95ccedf0
parent10269bc8c9dfe87eb213c09a44308ce64ae0c12d (diff)
parentc51fae92dc7d3080def81a2797e0d683b3e6d82a (diff)
downloadShaarli-c6744a9e89b62ba94563c43ab33f964ec0b11a17.tar.gz
Shaarli-c6744a9e89b62ba94563c43ab33f964ec0b11a17.tar.zst
Shaarli-c6744a9e89b62ba94563c43ab33f964ec0b11a17.zip
Merge pull request #496 from ArthurHoaro/cross-search
Allow crossed search between terms and tags
-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 1be4d752..7a9d9afb 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 a9264cbb..6712f90e 100644
--- a/index.php
+++ b/index.php
@@ -603,7 +603,7 @@ class pageBuilder
603 if (!empty($_GET['searchtags'])) { 603 if (!empty($_GET['searchtags'])) {
604 $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']); 604 $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']);
605 } 605 }
606 elseif (!empty($_GET['searchterm'])) { 606 if (!empty($_GET['searchterm'])) {
607 $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']); 607 $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']);
608 } 608 }
609 $this->tpl->assign('searchcrits', $searchcrits); 609 $this->tpl->assign('searchcrits', $searchcrits);
@@ -689,11 +689,19 @@ function showRSS()
689 // Read links from database (and filter private links if user it not logged in). 689 // Read links from database (and filter private links if user it not logged in).
690 690
691 // Optionally filter the results: 691 // Optionally filter the results:
692 if (!empty($_GET['searchterm'])) { 692 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
693 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); 693 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
694 if (! empty($searchtags) && ! empty($searchterm)) {
695 $linksToDisplay = $LINKSDB->filter(
696 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
697 array($searchtags, $searchterm)
698 );
694 } 699 }
695 elseif (!empty($_GET['searchtags'])) { 700 elseif ($searchtags) {
696 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); 701 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
702 }
703 elseif ($searchterm) {
704 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
697 } 705 }
698 else { 706 else {
699 $linksToDisplay = $LINKSDB; 707 $linksToDisplay = $LINKSDB;
@@ -787,11 +795,19 @@ function showATOM()
787 ); 795 );
788 796
789 // Optionally filter the results: 797 // Optionally filter the results:
790 if (!empty($_GET['searchterm'])) { 798 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
791 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); 799 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
800 if (! empty($searchtags) && ! empty($searchterm)) {
801 $linksToDisplay = $LINKSDB->filter(
802 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
803 array($searchtags, $searchterm)
804 );
792 } 805 }
793 else if (!empty($_GET['searchtags'])) { 806 elseif ($searchtags) {
794 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); 807 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
808 }
809 elseif ($searchterm) {
810 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
795 } 811 }
796 else { 812 else {
797 $linksToDisplay = $LINKSDB; 813 $linksToDisplay = $LINKSDB;
@@ -1145,11 +1161,19 @@ function renderPage()
1145 if ($targetPage == Router::$PAGE_PICWALL) 1161 if ($targetPage == Router::$PAGE_PICWALL)
1146 { 1162 {
1147 // Optionally filter the results: 1163 // Optionally filter the results:
1148 if (!empty($_GET['searchterm'])) { 1164 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
1149 $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); 1165 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
1166 if (! empty($searchtags) && ! empty($searchterm)) {
1167 $links = $LINKSDB->filter(
1168 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
1169 array($searchtags, $searchterm)
1170 );
1150 } 1171 }
1151 elseif (! empty($_GET['searchtags'])) { 1172 elseif ($searchtags) {
1152 $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); 1173 $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
1174 }
1175 elseif ($searchterm) {
1176 $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
1153 } 1177 }
1154 else { 1178 else {
1155 $links = $LINKSDB; 1179 $links = $LINKSDB;
@@ -1944,29 +1968,46 @@ function importFile()
1944// This function fills all the necessary fields in the $PAGE for the template 'linklist.html' 1968// This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1945function buildLinkList($PAGE,$LINKSDB) 1969function buildLinkList($PAGE,$LINKSDB)
1946{ 1970{
1947 // ---- Filter link database according to parameters 1971 // Filter link database according to parameters.
1948 $search_type = ''; 1972 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
1949 $search_crits = ''; 1973 $searchterm = !empty($_GET['searchterm']) ? escape(trim($_GET['searchterm'])) : '';
1950 $privateonly = !empty($_SESSION['privateonly']) ? true : false; 1974 $privateonly = !empty($_SESSION['privateonly']) ? true : false;
1951 1975
1952 // Fulltext search 1976 // Search tags + fullsearch.
1953 if (isset($_GET['searchterm'])) { 1977 if (! empty($searchtags) && ! empty($searchterm)) {
1954 $search_crits = escape(trim($_GET['searchterm'])); 1978 $linksToDisplay = $LINKSDB->filter(
1955 $search_type = LinkFilter::$FILTER_TEXT; 1979 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
1956 $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); 1980 array($searchtags, $searchterm),
1981 false,
1982 $privateonly
1983 );
1957 } 1984 }
1958 // Search by tag 1985 // Search by tags.
1959 elseif (isset($_GET['searchtags'])) { 1986 elseif (! empty($searchtags)) {
1960 $search_crits = explode(' ', escape(trim($_GET['searchtags']))); 1987 $linksToDisplay = $LINKSDB->filter(
1961 $search_type = LinkFilter::$FILTER_TAG; 1988 LinkFilter::$FILTER_TAG,
1962 $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); 1989 $searchtags,
1990 false,
1991 $privateonly
1992 );
1993 }
1994 // Fulltext search.
1995 elseif (! empty($searchterm)) {
1996 $linksToDisplay = $LINKSDB->filter(
1997 LinkFilter::$FILTER_TEXT,
1998 $searchterm,
1999 false,
2000 $privateonly
2001 );
1963 } 2002 }
1964 // Detect smallHashes in URL. 2003 // Detect smallHashes in URL.
1965 elseif (isset($_SERVER['QUERY_STRING']) 2004 elseif (! empty($_SERVER['QUERY_STRING'])
1966 && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) { 2005 && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])
1967 $search_type = LinkFilter::$FILTER_HASH; 2006 ) {
1968 $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6); 2007 $linksToDisplay = $LINKSDB->filter(
1969 $linksToDisplay = $LINKSDB->filter($search_type, $search_crits); 2008 LinkFilter::$FILTER_HASH,
2009 substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6)
2010 );
1970 2011
1971 if (count($linksToDisplay) == 0) { 2012 if (count($linksToDisplay) == 0) {
1972 $PAGE->render404('The link you are trying to reach does not exist or has been deleted.'); 2013 $PAGE->render404('The link you are trying to reach does not exist or has been deleted.');
@@ -2022,21 +2063,18 @@ function buildLinkList($PAGE,$LINKSDB)
2022 } 2063 }
2023 2064
2024 // Compute paging navigation 2065 // Compute paging navigation
2025 $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm']; 2066 $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags);
2026 $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags']; 2067 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
2027 $previous_page_url = ''; 2068 $previous_page_url = '';
2028 if ($i != count($keys)) { 2069 if ($i != count($keys)) {
2029 $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags; 2070 $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl;
2030 } 2071 }
2031 $next_page_url=''; 2072 $next_page_url='';
2032 if ($page>1) { 2073 if ($page>1) {
2033 $next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags; 2074 $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl;
2034 } 2075 }
2035 2076
2036 $token = ''; 2077 $token = isLoggedIn() ? getToken() : '';
2037 if (isLoggedIn()) {
2038 $token = getToken();
2039 }
2040 2078
2041 // Fill all template fields. 2079 // Fill all template fields.
2042 $data = array( 2080 $data = array(
@@ -2046,8 +2084,8 @@ function buildLinkList($PAGE,$LINKSDB)
2046 'page_current' => $page, 2084 'page_current' => $page,
2047 'page_max' => $pagecount, 2085 'page_max' => $pagecount,
2048 'result_count' => count($linksToDisplay), 2086 'result_count' => count($linksToDisplay),
2049 'search_type' => $search_type, 2087 'search_term' => $searchterm,
2050 'search_crits' => $search_crits, 2088 'search_tags' => $searchtags,
2051 'redirector' => empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'], // Optional redirector URL. 2089 'redirector' => empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'], // Optional redirector URL.
2052 'token' => $token, 2090 'token' => $token,
2053 'links' => $linkDisp, 2091 '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"}