Merge pull request #424 from ArthurHoaro/search

Link filter refactoring
This commit is contained in:
Arthur 2016-01-06 19:57:42 +01:00
commit 88c15abb2a
8 changed files with 683 additions and 419 deletions

View file

@ -62,6 +62,11 @@ class LinkDB implements Iterator, Countable, ArrayAccess
// link redirector set in user settings. // link redirector set in user settings.
private $_redirector; private $_redirector;
/**
* @var LinkFilter instance.
*/
private $linkFilter;
/** /**
* Creates a new LinkDB * Creates a new LinkDB
* *
@ -80,6 +85,7 @@ function __construct($datastore, $isLoggedIn, $hidePublicLinks, $redirector = ''
$this->_redirector = $redirector; $this->_redirector = $redirector;
$this->_checkDB(); $this->_checkDB();
$this->_readDB(); $this->_readDB();
$this->linkFilter = new LinkFilter($this->_links);
} }
/** /**
@ -334,114 +340,18 @@ public function getLinkFromUrl($url)
} }
/** /**
* Returns the list of links corresponding to a full-text search * Filter links.
* *
* Searches: * @param string $type Type of filter.
* - in the URLs, title and description; * @param mixed $request Search request, string or array.
* - are case-insensitive. * @param bool $casesensitive Optional: Perform case sensitive filter
* @param bool $privateonly Optional: Returns private links only if true.
* *
* Example: * @return array filtered links
* print_r($mydb->filterFulltext('hollandais'));
*
* mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
* - allows to perform searches on Unicode text
* - see https://github.com/shaarli/Shaarli/issues/75 for examples
*/ */
public function filterFulltext($searchterms) public function filter($type, $request, $casesensitive = false, $privateonly = false) {
{ $requestFilter = is_array($request) ? implode(' ', $request) : $request;
// FIXME: explode(' ',$searchterms) and perform a AND search. return $this->linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly);
// FIXME: accept double-quotes to search for a string "as is"?
$filtered = array();
$search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8');
$keys = array('title', 'description', 'url', 'tags');
foreach ($this->_links as $link) {
$found = false;
foreach ($keys as $key) {
if (strpos(mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'),
$search) !== false) {
$found = true;
}
}
if ($found) {
$filtered[$link['linkdate']] = $link;
}
}
krsort($filtered);
return $filtered;
}
/**
* Returns the list of links associated with a given list of tags
*
* You can specify one or more tags, separated by space or a comma, e.g.
* print_r($mydb->filterTags('linux programming'));
*/
public function filterTags($tags, $casesensitive=false)
{
// Same as above, we use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
// FIXME: is $casesensitive ever true?
$t = str_replace(
',', ' ',
($casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'))
);
$searchtags = explode(' ', $t);
$filtered = array();
foreach ($this->_links as $l) {
$linktags = explode(
' ',
($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8'))
);
if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) {
$filtered[$l['linkdate']] = $l;
}
}
krsort($filtered);
return $filtered;
}
/**
* Returns the list of articles for a given day, chronologically sorted
*
* Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
* print_r($mydb->filterDay('20120125'));
*/
public function filterDay($day)
{
if (! checkDateFormat('Ymd', $day)) {
throw new Exception('Invalid date format');
}
$filtered = array();
foreach ($this->_links as $l) {
if (startsWith($l['linkdate'], $day)) {
$filtered[$l['linkdate']] = $l;
}
}
ksort($filtered);
return $filtered;
}
/**
* Returns the article corresponding to a smallHash
*/
public function filterSmallHash($smallHash)
{
$filtered = array();
foreach ($this->_links as $l) {
if ($smallHash == smallHash($l['linkdate'])) {
// Yes, this is ugly and slow
$filtered[$l['linkdate']] = $l;
return $filtered;
}
}
return $filtered;
} }
/** /**

259
application/LinkFilter.php Normal file
View file

@ -0,0 +1,259 @@
<?php
/**
* Class LinkFilter.
*
* Perform search and filter operation on link data list.
*/
class LinkFilter
{
/**
* @var string permalinks.
*/
public static $FILTER_HASH = 'permalink';
/**
* @var string text search.
*/
public static $FILTER_TEXT = 'fulltext';
/**
* @var string tag filter.
*/
public static $FILTER_TAG = 'tags';
/**
* @var string filter by day.
*/
public static $FILTER_DAY = 'FILTER_DAY';
/**
* @var array all available links.
*/
private $links;
/**
* @param array $links initialization.
*/
public function __construct($links)
{
$this->links = $links;
}
/**
* Filter links according to parameters.
*
* @param string $type Type of filter (eg. tags, permalink, etc.).
* @param string $request Filter content.
* @param bool $casesensitive Optional: Perform case sensitive filter if true.
* @param bool $privateonly Optional: Only returns private links if true.
*
* @return array filtered link list.
*/
public function filter($type, $request, $casesensitive = false, $privateonly = false)
{
switch($type) {
case self::$FILTER_HASH:
return $this->filterSmallHash($request);
break;
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);
}
}
/**
* Unknown filter, but handle private only.
*
* @param bool $privateonly returns private link only if true.
*
* @return array filtered links.
*/
private function noFilter($privateonly = false)
{
if (! $privateonly) {
krsort($this->links);
return $this->links;
}
$out = array();
foreach ($this->links as $value) {
if ($value['private']) {
$out[$value['linkdate']] = $value;
}
}
krsort($out);
return $out;
}
/**
* Returns the shaare corresponding to a smallHash.
*
* @param string $smallHash permalink hash.
*
* @return array $filtered array containing permalink data.
*/
private function filterSmallHash($smallHash)
{
$filtered = array();
foreach ($this->links as $l) {
if ($smallHash == smallHash($l['linkdate'])) {
// Yes, this is ugly and slow
$filtered[$l['linkdate']] = $l;
return $filtered;
}
}
return $filtered;
}
/**
* Returns the list of links corresponding to a full-text search
*
* Searches:
* - in the URLs, title and description;
* - are case-insensitive.
*
* Example:
* print_r($mydb->filterFulltext('hollandais'));
*
* mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
* - allows to perform searches on Unicode text
* - see https://github.com/shaarli/Shaarli/issues/75 for examples
*
* @param string $searchterms search query.
* @param bool $privateonly return only private links if true.
*
* @return array search results.
*/
private function filterFulltext($searchterms, $privateonly = false)
{
// FIXME: explode(' ',$searchterms) and perform a AND search.
// FIXME: accept double-quotes to search for a string "as is"?
$filtered = array();
$search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8');
$explodedSearch = explode(' ', trim($search));
$keys = array('title', 'description', 'url', 'tags');
// Iterate over every stored link.
foreach ($this->links as $link) {
$found = false;
// ignore non private links when 'privatonly' is on.
if (! $link['private'] && $privateonly === true) {
continue;
}
// Iterate over searchable link fields.
foreach ($keys as $key) {
// Search full expression.
if (strpos(
mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'),
$search
) !== false) {
$found = true;
}
if ($found) {
break;
}
}
if ($found) {
$filtered[$link['linkdate']] = $link;
}
}
krsort($filtered);
return $filtered;
}
/**
* Returns the list of links associated with a given list of tags
*
* You can specify one or more tags, separated by space or a comma, e.g.
* print_r($mydb->filterTags('linux programming'));
*
* @param string $tags list of tags separated by commas or blank spaces.
* @param bool $casesensitive ignore case if false.
* @param bool $privateonly returns private links only.
*
* @return array filtered links.
*/
public function filterTags($tags, $casesensitive = false, $privateonly = false)
{
$searchtags = $this->tagsStrToArray($tags, $casesensitive);
$filtered = array();
foreach ($this->links as $l) {
// ignore non private links when 'privatonly' is on.
if (! $l['private'] && $privateonly === true) {
continue;
}
$linktags = $this->tagsStrToArray($l['tags'], $casesensitive);
if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) {
$filtered[$l['linkdate']] = $l;
}
}
krsort($filtered);
return $filtered;
}
/**
* Returns the list of articles for a given day, chronologically sorted
*
* Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
* print_r($mydb->filterDay('20120125'));
*
* @param string $day day to filter.
*
* @return array all link matching given day.
*
* @throws Exception if date format is invalid.
*/
public function filterDay($day)
{
if (! checkDateFormat('Ymd', $day)) {
throw new Exception('Invalid date format');
}
$filtered = array();
foreach ($this->links as $l) {
if (startsWith($l['linkdate'], $day)) {
$filtered[$l['linkdate']] = $l;
}
}
ksort($filtered);
return $filtered;
}
/**
* Convert a list of tags (str) to an array. Also
* - handle case sensitivity.
* - accepts spaces commas as separator.
* - remove private tags for loggedout users.
*
* @param string $tags string containing a list of tags.
* @param bool $casesensitive will convert everything to lowercase if false.
*
* @return array filtered tags string.
*/
public function tagsStrToArray($tags, $casesensitive)
{
// We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
$tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
$tagsOut = str_replace(',', ' ', $tagsOut);
return explode(' ', trim($tagsOut));
}
}

View file

@ -72,10 +72,12 @@ function sanitizeLink(&$link)
/** /**
* Checks if a string represents a valid date * Checks if a string represents a valid date
* @param string $format The expected DateTime format of the string
* @param string $string A string-formatted date
*
* @return bool whether the string is a valid date
* *
* @param string a string-formatted date
* @param format the expected DateTime format of the string
* @return whether the string is a valid date
* @see http://php.net/manual/en/class.datetime.php * @see http://php.net/manual/en/class.datetime.php
* @see http://php.net/manual/en/datetime.createfromformat.php * @see http://php.net/manual/en/datetime.createfromformat.php
*/ */

210
index.php
View file

@ -151,6 +151,7 @@
require_once 'application/FileUtils.php'; require_once 'application/FileUtils.php';
require_once 'application/HttpUtils.php'; require_once 'application/HttpUtils.php';
require_once 'application/LinkDB.php'; require_once 'application/LinkDB.php';
require_once 'application/LinkFilter.php';
require_once 'application/TimeZone.php'; require_once 'application/TimeZone.php';
require_once 'application/Url.php'; require_once 'application/Url.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
@ -730,18 +731,23 @@ function showRSS()
// Read links from database (and filter private links if user it not logged in). // Read links from database (and filter private links if user it not logged in).
// Optionally filter the results: // Optionally filter the results:
$linksToDisplay=array(); if (!empty($_GET['searchterm'])) {
if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); }
else $linksToDisplay = $LINKSDB; elseif (!empty($_GET['searchtags'])) {
$linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
$nblinksToDisplay = 50; // Number of links to display. }
if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. else {
{ $linksToDisplay = $LINKSDB;
$nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ;
} }
$pageaddr=escape(index_url($_SERVER)); $nblinksToDisplay = 50; // Number of links to display.
// In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
if (!empty($_GET['nb'])) {
$nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
}
$pageaddr = escape(index_url($_SERVER));
echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>'; echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>';
echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n"; echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n";
@ -821,15 +827,20 @@ function showATOM()
); );
// Optionally filter the results: // Optionally filter the results:
$linksToDisplay=array(); if (!empty($_GET['searchterm'])) {
if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); }
else $linksToDisplay = $LINKSDB; else if (!empty($_GET['searchtags'])) {
$linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
}
else {
$linksToDisplay = $LINKSDB;
}
$nblinksToDisplay = 50; // Number of links to display. $nblinksToDisplay = 50; // Number of links to display.
if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
{ if (!empty($_GET['nb'])) {
$nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ; $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
} }
$pageaddr=escape(index_url($_SERVER)); $pageaddr=escape(index_url($_SERVER));
@ -1024,7 +1035,7 @@ function showDaily($pageBuilder)
} }
try { try {
$linksToDisplay = $LINKSDB->filterDay($day); $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day);
} catch (Exception $exc) { } catch (Exception $exc) {
error_log($exc); error_log($exc);
$linksToDisplay = array(); $linksToDisplay = array();
@ -1149,13 +1160,17 @@ function renderPage()
if ($targetPage == Router::$PAGE_PICWALL) if ($targetPage == Router::$PAGE_PICWALL)
{ {
// Optionally filter the results: // Optionally filter the results:
$links=array(); if (!empty($_GET['searchterm'])) {
if (!empty($_GET['searchterm'])) $links = $LINKSDB->filterFulltext($_GET['searchterm']); $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
elseif (!empty($_GET['searchtags'])) $links = $LINKSDB->filterTags(trim($_GET['searchtags'])); }
else $links = $LINKSDB; elseif (! empty($_GET['searchtags'])) {
$links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
}
else {
$links = $LINKSDB;
}
$body=''; $linksToDisplay = array();
$linksToDisplay=array();
// Get only links which have a thumbnail. // Get only links which have a thumbnail.
foreach($links as $link) foreach($links as $link)
@ -1282,13 +1297,15 @@ function renderPage()
} }
if (isset($params['searchtags'])) { if (isset($params['searchtags'])) {
$tags = explode(' ',$params['searchtags']); $tags = explode(' ', $params['searchtags']);
$tags=array_diff($tags, array($_GET['removetag'])); // Remove value from array $tags. // Remove value from array $tags.
if (count($tags)==0) { $tags = array_diff($tags, array($_GET['removetag']));
unset($params['searchtags']);
} else {
$params['searchtags'] = implode(' ',$tags); $params['searchtags'] = implode(' ',$tags);
if (empty($params['searchtags'])) {
unset($params['searchtags']);
} }
unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different)
} }
header('Location: ?'.http_build_query($params)); header('Location: ?'.http_build_query($params));
@ -1468,7 +1485,8 @@ function renderPage()
// Delete a tag: // Delete a tag:
if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) {
$needle=trim($_POST['fromtag']); $needle=trim($_POST['fromtag']);
$linksToAlter = $LINKSDB->filterTags($needle,true); // True for case-sensitive tag search. // True for case-sensitive tag search.
$linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true);
foreach($linksToAlter as $key=>$value) foreach($linksToAlter as $key=>$value)
{ {
$tags = explode(' ',trim($value['tags'])); $tags = explode(' ',trim($value['tags']));
@ -1484,7 +1502,8 @@ function renderPage()
// Rename a tag: // Rename a tag:
if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) {
$needle=trim($_POST['fromtag']); $needle=trim($_POST['fromtag']);
$linksToAlter = $LINKSDB->filterTags($needle,true); // true for case-sensitive tag search. // True for case-sensitive tag search.
$linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true);
foreach($linksToAlter as $key=>$value) foreach($linksToAlter as $key=>$value)
{ {
$tags = explode(' ',trim($value['tags'])); $tags = explode(' ',trim($value['tags']));
@ -1865,81 +1884,78 @@ function importFile()
function buildLinkList($PAGE,$LINKSDB) function buildLinkList($PAGE,$LINKSDB)
{ {
// ---- Filter link database according to parameters // ---- Filter link database according to parameters
$linksToDisplay=array(); $search_type = '';
$search_type=''; $search_crits = '';
$search_crits=''; $privateonly = !empty($_SESSION['privateonly']) ? true : false;
if (isset($_GET['searchterm'])) // Fulltext search
{ // Fulltext search
$linksToDisplay = $LINKSDB->filterFulltext(trim($_GET['searchterm'])); if (isset($_GET['searchterm'])) {
$search_crits=escape(trim($_GET['searchterm'])); $search_crits = escape(trim($_GET['searchterm']));
$search_type='fulltext'; $search_type = LinkFilter::$FILTER_TEXT;
$linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
} }
elseif (isset($_GET['searchtags'])) // Search by tag // Search by tag
{ elseif (isset($_GET['searchtags'])) {
$linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); $search_crits = explode(' ', escape(trim($_GET['searchtags'])));
$search_crits=explode(' ',escape(trim($_GET['searchtags']))); $search_type = LinkFilter::$FILTER_TAG;
$search_type='tags'; $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
} }
elseif (isset($_SERVER['QUERY_STRING']) && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/',$_SERVER['QUERY_STRING'])) // Detect smallHashes in URL // Detect smallHashes in URL.
{ elseif (isset($_SERVER['QUERY_STRING'])
$linksToDisplay = $LINKSDB->filterSmallHash(substr(trim($_SERVER["QUERY_STRING"], '/'),0,6)); && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) {
if (count($linksToDisplay)==0) $search_type = LinkFilter::$FILTER_HASH;
{ $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6);
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); $linksToDisplay = $LINKSDB->filter($search_type, $search_crits);
echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.';
if (count($linksToDisplay) == 0) {
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
echo '<h1>404 Not found.</h1>Oh crap.
The link you are trying to reach does not exist or has been deleted.';
echo '<br>Would you mind <a href="?">clicking here</a>?'; echo '<br>Would you mind <a href="?">clicking here</a>?';
exit; exit;
} }
$search_type='permalink';
} }
else // Otherwise, display without filtering.
$linksToDisplay = $LINKSDB; // Otherwise, display without filtering. else {
$linksToDisplay = $LINKSDB->filter('', '', false, $privateonly);
// Option: Show only private links
if (!empty($_SESSION['privateonly']))
{
$tmp = array();
foreach($linksToDisplay as $linkdate=>$link)
{
if ($link['private']!=0) $tmp[$linkdate]=$link;
}
$linksToDisplay=$tmp;
} }
// ---- Handle paging. // ---- Handle paging.
/* Can someone explain to me why you get the following error when using array_keys() on an object which implements the interface ArrayAccess??? $keys = array();
"Warning: array_keys() expects parameter 1 to be array, object given in ... " foreach ($linksToDisplay as $key => $value) {
If my class implements ArrayAccess, why won't array_keys() accept it ? ( $keys=array_keys($linksToDisplay); ) $keys[] = $key;
*/ }
$keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks PHP.
// If there is only a single link, we change on-the-fly the title of the page. // If there is only a single link, we change on-the-fly the title of the page.
if (count($linksToDisplay)==1) $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title']; if (count($linksToDisplay) == 1) {
$GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title'];
}
// Select articles according to paging. // Select articles according to paging.
$pagecount = ceil(count($keys)/$_SESSION['LINKS_PER_PAGE']); $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
$pagecount = ($pagecount==0 ? 1 : $pagecount); $pagecount = $pagecount == 0 ? 1 : $pagecount;
$page=( empty($_GET['page']) ? 1 : intval($_GET['page'])); $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
$page = ( $page<1 ? 1 : $page ); $page = $page < 1 ? 1 : $page;
$page = ( $page>$pagecount ? $pagecount : $page ); $page = $page > $pagecount ? $pagecount : $page;
$i = ($page-1)*$_SESSION['LINKS_PER_PAGE']; // Start index. // Start index.
$end = $i+$_SESSION['LINKS_PER_PAGE']; $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
$linkDisp=array(); // Links to display $end = $i + $_SESSION['LINKS_PER_PAGE'];
$linkDisp = array();
while ($i<$end && $i<count($keys)) while ($i<$end && $i<count($keys))
{ {
$link = $linksToDisplay[$keys[$i]]; $link = $linksToDisplay[$keys[$i]];
$link['description'] = format_description($link['description'], $GLOBALS['redirector']); $link['description'] = format_description($link['description'], $GLOBALS['redirector']);
$classLi = $i%2!=0 ? '' : 'publicLinkHightLight'; $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
$link['class'] = ($link['private']==0 ? $classLi : 'private'); $link['class'] = $link['private'] == 0 ? $classLi : 'private';
$link['timestamp']=linkdate2timestamp($link['linkdate']); $link['timestamp'] = linkdate2timestamp($link['linkdate']);
$taglist = explode(' ',$link['tags']); $taglist = explode(' ', $link['tags']);
uasort($taglist, 'strcasecmp'); uasort($taglist, 'strcasecmp');
$link['taglist']=$taglist; $link['taglist'] = $taglist;
$link['shorturl'] = smallHash($link['linkdate']); $link['shorturl'] = smallHash($link['linkdate']);
if ($link["url"][0] === '?' && // Check for both signs of a note: starting with ? and 7 chars long. I doubt that you'll post any links that look like this. // Check for both signs of a note: starting with ? and 7 chars long.
strlen($link["url"]) === 7) { if ($link['url'][0] === '?' &&
$link["url"] = index_url($_SERVER) . $link["url"]; strlen($link['url']) === 7) {
$link['url'] = index_url($_SERVER) . $link['url'];
} }
$linkDisp[$keys[$i]] = $link; $linkDisp[$keys[$i]] = $link;
@ -1947,13 +1963,21 @@ function buildLinkList($PAGE,$LINKSDB)
} }
// Compute paging navigation // Compute paging navigation
$searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] ); $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm'];
$searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] ); $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags'];
$paging=''; $previous_page_url = '';
$previous_page_url=''; if ($i!=count($keys)) $previous_page_url='?page='.($page+1).$searchterm.$searchtags; if ($i != count($keys)) {
$next_page_url='';if ($page>1) $next_page_url='?page='.($page-1).$searchterm.$searchtags; $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags;
}
$next_page_url='';
if ($page>1) {
$next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags;
}
$token = ''; if (isLoggedIn()) $token=getToken(); $token = '';
if (isLoggedIn()) {
$token = getToken();
}
// Fill all template fields. // Fill all template fields.
$data = array( $data = array(

View file

@ -301,217 +301,6 @@ public function testAllTags()
); );
} }
/**
* Filter links using a tag
*/
public function testFilterOneTag()
{
$this->assertEquals(
3,
sizeof(self::$publicLinkDB->filterTags('web', false))
);
$this->assertEquals(
4,
sizeof(self::$privateLinkDB->filterTags('web', false))
);
}
/**
* Filter links using a tag - case-sensitive
*/
public function testFilterCaseSensitiveTag()
{
$this->assertEquals(
0,
sizeof(self::$privateLinkDB->filterTags('mercurial', true))
);
$this->assertEquals(
1,
sizeof(self::$privateLinkDB->filterTags('Mercurial', true))
);
}
/**
* Filter links using a tag combination
*/
public function testFilterMultipleTags()
{
$this->assertEquals(
1,
sizeof(self::$publicLinkDB->filterTags('dev cartoon', false))
);
$this->assertEquals(
2,
sizeof(self::$privateLinkDB->filterTags('dev cartoon', false))
);
}
/**
* Filter links using a non-existent tag
*/
public function testFilterUnknownTag()
{
$this->assertEquals(
0,
sizeof(self::$publicLinkDB->filterTags('null', false))
);
}
/**
* Return links for a given day
*/
public function testFilterDay()
{
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterDay('20121206'))
);
$this->assertEquals(
3,
sizeof(self::$privateLinkDB->filterDay('20121206'))
);
}
/**
* 404 - day not found
*/
public function testFilterUnknownDay()
{
$this->assertEquals(
0,
sizeof(self::$publicLinkDB->filterDay('19700101'))
);
$this->assertEquals(
0,
sizeof(self::$privateLinkDB->filterDay('19700101'))
);
}
/**
* Use an invalid date format
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid date format/
*/
public function testFilterInvalidDayWithChars()
{
self::$privateLinkDB->filterDay('Rainy day, dream away');
}
/**
* Use an invalid date format
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid date format/
*/
public function testFilterInvalidDayDigits()
{
self::$privateLinkDB->filterDay('20');
}
/**
* Retrieve a link entry with its hash
*/
public function testFilterSmallHash()
{
$links = self::$privateLinkDB->filterSmallHash('IuWvgA');
$this->assertEquals(
1,
sizeof($links)
);
$this->assertEquals(
'MediaGoblin',
$links['20130614_184135']['title']
);
}
/**
* No link for this hash
*/
public function testFilterUnknownSmallHash()
{
$this->assertEquals(
0,
sizeof(self::$privateLinkDB->filterSmallHash('Iblaah'))
);
}
/**
* Full-text search - result from a link's URL
*/
public function testFilterFullTextURL()
{
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('ars.userfriendly.org'))
);
}
/**
* Full-text search - result from a link's title only
*/
public function testFilterFullTextTitle()
{
// use miscellaneous cases
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('userfriendly -'))
);
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('UserFriendly -'))
);
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('uSeRFrIendlY -'))
);
// use miscellaneous case and offset
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('RFrIendL'))
);
}
/**
* Full-text search - result from the link's description only
*/
public function testFilterFullTextDescription()
{
$this->assertEquals(
1,
sizeof(self::$publicLinkDB->filterFullText('media publishing'))
);
}
/**
* Full-text search - result from the link's tags only
*/
public function testFilterFullTextTags()
{
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('gnu'))
);
}
/**
* Full-text search - result set from mixed sources
*/
public function testFilterFullTextMixed()
{
$this->assertEquals(
2,
sizeof(self::$publicLinkDB->filterFullText('free software'))
);
}
/** /**
* Test real_url without redirector. * Test real_url without redirector.
*/ */
@ -534,4 +323,28 @@ public function testLinkRealUrlWithRedirector()
$this->assertStringStartsWith($redirector, $link['real_url']); $this->assertStringStartsWith($redirector, $link['real_url']);
} }
} }
/**
* Test filter with string.
*/
public function testFilterString()
{
$tags = 'dev cartoon';
$this->assertEquals(
2,
count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
);
}
/**
* Test filter with string.
*/
public function testFilterArray()
{
$tags = array('dev', 'cartoon');
$this->assertEquals(
2,
count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
);
}
} }

242
tests/LinkFilterTest.php Normal file
View file

@ -0,0 +1,242 @@
<?php
require_once 'application/LinkFilter.php';
/**
* Class LinkFilterTest.
*/
class LinkFilterTest extends PHPUnit_Framework_TestCase
{
/**
* @var LinkFilter instance.
*/
protected static $linkFilter;
/**
* Instanciate linkFilter with ReferenceLinkDB data.
*/
public static function setUpBeforeClass()
{
$refDB = new ReferenceLinkDB();
self::$linkFilter = new LinkFilter($refDB->getLinks());
}
/**
* Blank filter.
*/
public function testFilter()
{
$this->assertEquals(
6,
count(self::$linkFilter->filter('', ''))
);
// Private only.
$this->assertEquals(
2,
count(self::$linkFilter->filter('', '', false, true))
);
}
/**
* Filter links using a tag
*/
public function testFilterOneTag()
{
$this->assertEquals(
4,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false))
);
// Private only.
$this->assertEquals(
1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, true))
);
}
/**
* Filter links using a tag - case-sensitive
*/
public function testFilterCaseSensitiveTag()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true))
);
$this->assertEquals(
1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true))
);
}
/**
* Filter links using a tag combination
*/
public function testFilterMultipleTags()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false))
);
}
/**
* Filter links using a non-existent tag
*/
public function testFilterUnknownTag()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false))
);
}
/**
* Return links for a given day
*/
public function testFilterDay()
{
$this->assertEquals(
3,
count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206'))
);
}
/**
* 404 - day not found
*/
public function testFilterUnknownDay()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101'))
);
}
/**
* Use an invalid date format
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid date format/
*/
public function testFilterInvalidDayWithChars()
{
self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away');
}
/**
* Use an invalid date format
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid date format/
*/
public function testFilterInvalidDayDigits()
{
self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20');
}
/**
* Retrieve a link entry with its hash
*/
public function testFilterSmallHash()
{
$links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA');
$this->assertEquals(
1,
count($links)
);
$this->assertEquals(
'MediaGoblin',
$links['20130614_184135']['title']
);
}
/**
* No link for this hash
*/
public function testFilterUnknownSmallHash()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah'))
);
}
/**
* Full-text search - result from a link's URL
*/
public function testFilterFullTextURL()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
);
}
/**
* Full-text search - result from a link's title only
*/
public function testFilterFullTextTitle()
{
// use miscellaneous cases
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -'))
);
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -'))
);
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
);
// use miscellaneous case and offset
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL'))
);
}
/**
* Full-text search - result from the link's description only
*/
public function testFilterFullTextDescription()
{
$this->assertEquals(
1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'media publishing'))
);
}
/**
* Full-text search - result from the link's tags only
*/
public function testFilterFullTextTags()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'gnu'))
);
// Private only.
$this->assertEquals(
1,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, true))
);
}
/**
* Full-text search - result set from mixed sources
*/
public function testFilterFullTextMixed()
{
$this->assertEquals(
2,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software'))
);
}
}

View file

@ -124,4 +124,9 @@ public function countPrivateLinks()
{ {
return $this->_privateCount; return $this->_privateCount;
} }
public function getLinks()
{
return $this->_links;
}
} }

View file

@ -7,15 +7,24 @@
<body> <body>
<div id="pageheader"> <div id="pageheader">
{include="page.header"} {include="page.header"}
<div id="headerform" class="search"> <div id="headerform" class="search">
<form method="GET" class="searchform" name="searchform"> <form method="GET" class="searchform" name="searchform">
<input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" value=""> <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text"
{if="!empty($search_crits) && $search_type=='fulltext'"}
value="{$search_crits}"
{/if}
>
<input type="submit" value="Search" class="bigbutton"> <input type="submit" value="Search" class="bigbutton">
</form> </form>
<form method="GET" class="tagfilter" name="tagfilter"> <form method="GET" class="tagfilter" name="tagfilter">
<input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" value="" <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}
autocomplete="off" class="awesomplete" data-multiple data-minChars="1" autocomplete="off" class="awesomplete" data-multiple data-minChars="1"
data-list="{loop="$tags"}{$key}, {/loop}"> data-list="{loop="$tags"}{$key}, {/loop}">
>
<input type="submit" value="Search" class="bigbutton"> <input type="submit" value="Search" class="bigbutton">
</form> </form>
{loop="$plugins_header.fields_toolbar"} {loop="$plugins_header.fields_toolbar"}
@ -44,7 +53,7 @@
<div id="searchcriteria">{$result_count} results for tags <i> <div id="searchcriteria">{$result_count} results for tags <i>
{loop="search_crits"} {loop="search_crits"}
<span class="linktag" title="Remove tag"> <span class="linktag" title="Remove tag">
<a href="?removetag={$value}">{$value} <span class="remove">x</span></a> <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
</span> </span>
{/loop}</i></div> {/loop}</i></div>
{/if} {/if}