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.
This commit is contained in:
ArthurHoaro 2016-02-23 19:21:14 +01:00
parent 6c3d6a31f4
commit c51fae92dc
6 changed files with 178 additions and 65 deletions

View 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);
}
/**

View 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)) {

View file

@ -33,6 +33,10 @@ h1 {
margin-bottom: 20px;
}
em {
font-style: italic;
}
/* Buttons */
.bigbutton {
background-color: #c0c0c0;

120
index.php
View file

@ -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,

View 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,
''
))
);
}
}

View file

@ -11,16 +11,16 @@
<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}"
@ -44,19 +44,23 @@
</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"}