diff options
-rw-r--r-- | application/LinkDB.php | 120 | ||||
-rw-r--r-- | application/LinkFilter.php | 259 | ||||
-rw-r--r-- | application/Utils.php | 12 | ||||
-rw-r--r-- | index.php | 210 | ||||
-rw-r--r-- | tests/LinkDBTest.php | 229 | ||||
-rw-r--r-- | tests/LinkFilterTest.php | 242 | ||||
-rw-r--r-- | tests/utils/ReferenceLinkDB.php | 5 | ||||
-rw-r--r-- | tpl/linklist.html | 19 |
8 files changed, 680 insertions, 416 deletions
diff --git a/application/LinkDB.php b/application/LinkDB.php index 51fa926d..16848519 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -63,6 +63,11 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
63 | private $_redirector; | 63 | private $_redirector; |
64 | 64 | ||
65 | /** | 65 | /** |
66 | * @var LinkFilter instance. | ||
67 | */ | ||
68 | private $linkFilter; | ||
69 | |||
70 | /** | ||
66 | * Creates a new LinkDB | 71 | * Creates a new LinkDB |
67 | * | 72 | * |
68 | * Checks if the datastore exists; else, attempts to create a dummy one. | 73 | * Checks if the datastore exists; else, attempts to create a dummy one. |
@@ -80,6 +85,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
80 | $this->_redirector = $redirector; | 85 | $this->_redirector = $redirector; |
81 | $this->_checkDB(); | 86 | $this->_checkDB(); |
82 | $this->_readDB(); | 87 | $this->_readDB(); |
88 | $this->linkFilter = new LinkFilter($this->_links); | ||
83 | } | 89 | } |
84 | 90 | ||
85 | /** | 91 | /** |
@@ -334,114 +340,18 @@ You use the community supported version of the original Shaarli project, by Seba | |||
334 | } | 340 | } |
335 | 341 | ||
336 | /** | 342 | /** |
337 | * Returns the list of links corresponding to a full-text search | 343 | * Filter links. |
338 | * | 344 | * |
339 | * Searches: | 345 | * @param string $type Type of filter. |
340 | * - in the URLs, title and description; | 346 | * @param mixed $request Search request, string or array. |
341 | * - are case-insensitive. | 347 | * @param bool $casesensitive Optional: Perform case sensitive filter |
342 | * | 348 | * @param bool $privateonly Optional: Returns private links only if true. |
343 | * Example: | ||
344 | * print_r($mydb->filterFulltext('hollandais')); | ||
345 | * | ||
346 | * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8') | ||
347 | * - allows to perform searches on Unicode text | ||
348 | * - see https://github.com/shaarli/Shaarli/issues/75 for examples | ||
349 | */ | ||
350 | public function filterFulltext($searchterms) | ||
351 | { | ||
352 | // FIXME: explode(' ',$searchterms) and perform a AND search. | ||
353 | // FIXME: accept double-quotes to search for a string "as is"? | ||
354 | $filtered = array(); | ||
355 | $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8'); | ||
356 | $keys = array('title', 'description', 'url', 'tags'); | ||
357 | |||
358 | foreach ($this->_links as $link) { | ||
359 | $found = false; | ||
360 | |||
361 | foreach ($keys as $key) { | ||
362 | if (strpos(mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'), | ||
363 | $search) !== false) { | ||
364 | $found = true; | ||
365 | } | ||
366 | } | ||
367 | |||
368 | if ($found) { | ||
369 | $filtered[$link['linkdate']] = $link; | ||
370 | } | ||
371 | } | ||
372 | krsort($filtered); | ||
373 | return $filtered; | ||
374 | } | ||
375 | |||
376 | /** | ||
377 | * Returns the list of links associated with a given list of tags | ||
378 | * | 349 | * |
379 | * You can specify one or more tags, separated by space or a comma, e.g. | 350 | * @return array filtered links |
380 | * print_r($mydb->filterTags('linux programming')); | ||
381 | */ | 351 | */ |
382 | public function filterTags($tags, $casesensitive=false) | 352 | public function filter($type, $request, $casesensitive = false, $privateonly = false) { |
383 | { | 353 | $requestFilter = is_array($request) ? implode(' ', $request) : $request; |
384 | // Same as above, we use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | 354 | return $this->linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly); |
385 | // FIXME: is $casesensitive ever true? | ||
386 | $t = str_replace( | ||
387 | ',', ' ', | ||
388 | ($casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8')) | ||
389 | ); | ||
390 | |||
391 | $searchtags = explode(' ', $t); | ||
392 | $filtered = array(); | ||
393 | |||
394 | foreach ($this->_links as $l) { | ||
395 | $linktags = explode( | ||
396 | ' ', | ||
397 | ($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8')) | ||
398 | ); | ||
399 | |||
400 | if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) { | ||
401 | $filtered[$l['linkdate']] = $l; | ||
402 | } | ||
403 | } | ||
404 | krsort($filtered); | ||
405 | return $filtered; | ||
406 | } | ||
407 | |||
408 | |||
409 | /** | ||
410 | * Returns the list of articles for a given day, chronologically sorted | ||
411 | * | ||
412 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | ||
413 | * print_r($mydb->filterDay('20120125')); | ||
414 | */ | ||
415 | public function filterDay($day) | ||
416 | { | ||
417 | if (! checkDateFormat('Ymd', $day)) { | ||
418 | throw new Exception('Invalid date format'); | ||
419 | } | ||
420 | |||
421 | $filtered = array(); | ||
422 | foreach ($this->_links as $l) { | ||
423 | if (startsWith($l['linkdate'], $day)) { | ||
424 | $filtered[$l['linkdate']] = $l; | ||
425 | } | ||
426 | } | ||
427 | ksort($filtered); | ||
428 | return $filtered; | ||
429 | } | ||
430 | |||
431 | /** | ||
432 | * Returns the article corresponding to a smallHash | ||
433 | */ | ||
434 | public function filterSmallHash($smallHash) | ||
435 | { | ||
436 | $filtered = array(); | ||
437 | foreach ($this->_links as $l) { | ||
438 | if ($smallHash == smallHash($l['linkdate'])) { | ||
439 | // Yes, this is ugly and slow | ||
440 | $filtered[$l['linkdate']] = $l; | ||
441 | return $filtered; | ||
442 | } | ||
443 | } | ||
444 | return $filtered; | ||
445 | } | 355 | } |
446 | 356 | ||
447 | /** | 357 | /** |
diff --git a/application/LinkFilter.php b/application/LinkFilter.php new file mode 100644 index 00000000..cf647371 --- /dev/null +++ b/application/LinkFilter.php | |||
@@ -0,0 +1,259 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Class LinkFilter. | ||
5 | * | ||
6 | * Perform search and filter operation on link data list. | ||
7 | */ | ||
8 | class LinkFilter | ||
9 | { | ||
10 | /** | ||
11 | * @var string permalinks. | ||
12 | */ | ||
13 | public static $FILTER_HASH = 'permalink'; | ||
14 | |||
15 | /** | ||
16 | * @var string text search. | ||
17 | */ | ||
18 | public static $FILTER_TEXT = 'fulltext'; | ||
19 | |||
20 | /** | ||
21 | * @var string tag filter. | ||
22 | */ | ||
23 | public static $FILTER_TAG = 'tags'; | ||
24 | |||
25 | /** | ||
26 | * @var string filter by day. | ||
27 | */ | ||
28 | public static $FILTER_DAY = 'FILTER_DAY'; | ||
29 | |||
30 | /** | ||
31 | * @var array all available links. | ||
32 | */ | ||
33 | private $links; | ||
34 | |||
35 | /** | ||
36 | * @param array $links initialization. | ||
37 | */ | ||
38 | public function __construct($links) | ||
39 | { | ||
40 | $this->links = $links; | ||
41 | } | ||
42 | |||
43 | /** | ||
44 | * Filter links according to parameters. | ||
45 | * | ||
46 | * @param string $type Type of filter (eg. tags, permalink, etc.). | ||
47 | * @param string $request Filter content. | ||
48 | * @param bool $casesensitive Optional: Perform case sensitive filter if true. | ||
49 | * @param bool $privateonly Optional: Only returns private links if true. | ||
50 | * | ||
51 | * @return array filtered link list. | ||
52 | */ | ||
53 | public function filter($type, $request, $casesensitive = false, $privateonly = false) | ||
54 | { | ||
55 | switch($type) { | ||
56 | case self::$FILTER_HASH: | ||
57 | return $this->filterSmallHash($request); | ||
58 | break; | ||
59 | case self::$FILTER_TEXT: | ||
60 | return $this->filterFulltext($request, $privateonly); | ||
61 | break; | ||
62 | case self::$FILTER_TAG: | ||
63 | return $this->filterTags($request, $casesensitive, $privateonly); | ||
64 | break; | ||
65 | case self::$FILTER_DAY: | ||
66 | return $this->filterDay($request); | ||
67 | break; | ||
68 | default: | ||
69 | return $this->noFilter($privateonly); | ||
70 | } | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Unknown filter, but handle private only. | ||
75 | * | ||
76 | * @param bool $privateonly returns private link only if true. | ||
77 | * | ||
78 | * @return array filtered links. | ||
79 | */ | ||
80 | private function noFilter($privateonly = false) | ||
81 | { | ||
82 | if (! $privateonly) { | ||
83 | krsort($this->links); | ||
84 | return $this->links; | ||
85 | } | ||
86 | |||
87 | $out = array(); | ||
88 | foreach ($this->links as $value) { | ||
89 | if ($value['private']) { | ||
90 | $out[$value['linkdate']] = $value; | ||
91 | } | ||
92 | } | ||
93 | |||
94 | krsort($out); | ||
95 | return $out; | ||
96 | } | ||
97 | |||
98 | /** | ||
99 | * Returns the shaare corresponding to a smallHash. | ||
100 | * | ||
101 | * @param string $smallHash permalink hash. | ||
102 | * | ||
103 | * @return array $filtered array containing permalink data. | ||
104 | */ | ||
105 | private function filterSmallHash($smallHash) | ||
106 | { | ||
107 | $filtered = array(); | ||
108 | foreach ($this->links as $l) { | ||
109 | if ($smallHash == smallHash($l['linkdate'])) { | ||
110 | // Yes, this is ugly and slow | ||
111 | $filtered[$l['linkdate']] = $l; | ||
112 | return $filtered; | ||
113 | } | ||
114 | } | ||
115 | return $filtered; | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * Returns the list of links corresponding to a full-text search | ||
120 | * | ||
121 | * Searches: | ||
122 | * - in the URLs, title and description; | ||
123 | * - are case-insensitive. | ||
124 | * | ||
125 | * Example: | ||
126 | * print_r($mydb->filterFulltext('hollandais')); | ||
127 | * | ||
128 | * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8') | ||
129 | * - allows to perform searches on Unicode text | ||
130 | * - see https://github.com/shaarli/Shaarli/issues/75 for examples | ||
131 | * | ||
132 | * @param string $searchterms search query. | ||
133 | * @param bool $privateonly return only private links if true. | ||
134 | * | ||
135 | * @return array search results. | ||
136 | */ | ||
137 | private function filterFulltext($searchterms, $privateonly = false) | ||
138 | { | ||
139 | // FIXME: explode(' ',$searchterms) and perform a AND search. | ||
140 | // FIXME: accept double-quotes to search for a string "as is"? | ||
141 | $filtered = array(); | ||
142 | $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8'); | ||
143 | $explodedSearch = explode(' ', trim($search)); | ||
144 | $keys = array('title', 'description', 'url', 'tags'); | ||
145 | |||
146 | // Iterate over every stored link. | ||
147 | foreach ($this->links as $link) { | ||
148 | $found = false; | ||
149 | |||
150 | // ignore non private links when 'privatonly' is on. | ||
151 | if (! $link['private'] && $privateonly === true) { | ||
152 | continue; | ||
153 | } | ||
154 | |||
155 | // Iterate over searchable link fields. | ||
156 | foreach ($keys as $key) { | ||
157 | // Search full expression. | ||
158 | if (strpos( | ||
159 | mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'), | ||
160 | $search | ||
161 | ) !== false) { | ||
162 | $found = true; | ||
163 | } | ||
164 | |||
165 | if ($found) { | ||
166 | break; | ||
167 | } | ||
168 | } | ||
169 | |||
170 | if ($found) { | ||
171 | $filtered[$link['linkdate']] = $link; | ||
172 | } | ||
173 | } | ||
174 | |||
175 | krsort($filtered); | ||
176 | return $filtered; | ||
177 | } | ||
178 | |||
179 | /** | ||
180 | * Returns the list of links associated with a given list of tags | ||
181 | * | ||
182 | * You can specify one or more tags, separated by space or a comma, e.g. | ||
183 | * print_r($mydb->filterTags('linux programming')); | ||
184 | * | ||
185 | * @param string $tags list of tags separated by commas or blank spaces. | ||
186 | * @param bool $casesensitive ignore case if false. | ||
187 | * @param bool $privateonly returns private links only. | ||
188 | * | ||
189 | * @return array filtered links. | ||
190 | */ | ||
191 | public function filterTags($tags, $casesensitive = false, $privateonly = false) | ||
192 | { | ||
193 | $searchtags = $this->tagsStrToArray($tags, $casesensitive); | ||
194 | $filtered = array(); | ||
195 | |||
196 | foreach ($this->links as $l) { | ||
197 | // ignore non private links when 'privatonly' is on. | ||
198 | if (! $l['private'] && $privateonly === true) { | ||
199 | continue; | ||
200 | } | ||
201 | |||
202 | $linktags = $this->tagsStrToArray($l['tags'], $casesensitive); | ||
203 | |||
204 | if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) { | ||
205 | $filtered[$l['linkdate']] = $l; | ||
206 | } | ||
207 | } | ||
208 | krsort($filtered); | ||
209 | return $filtered; | ||
210 | } | ||
211 | |||
212 | /** | ||
213 | * Returns the list of articles for a given day, chronologically sorted | ||
214 | * | ||
215 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | ||
216 | * print_r($mydb->filterDay('20120125')); | ||
217 | * | ||
218 | * @param string $day day to filter. | ||
219 | * | ||
220 | * @return array all link matching given day. | ||
221 | * | ||
222 | * @throws Exception if date format is invalid. | ||
223 | */ | ||
224 | public function filterDay($day) | ||
225 | { | ||
226 | if (! checkDateFormat('Ymd', $day)) { | ||
227 | throw new Exception('Invalid date format'); | ||
228 | } | ||
229 | |||
230 | $filtered = array(); | ||
231 | foreach ($this->links as $l) { | ||
232 | if (startsWith($l['linkdate'], $day)) { | ||
233 | $filtered[$l['linkdate']] = $l; | ||
234 | } | ||
235 | } | ||
236 | ksort($filtered); | ||
237 | return $filtered; | ||
238 | } | ||
239 | |||
240 | /** | ||
241 | * Convert a list of tags (str) to an array. Also | ||
242 | * - handle case sensitivity. | ||
243 | * - accepts spaces commas as separator. | ||
244 | * - remove private tags for loggedout users. | ||
245 | * | ||
246 | * @param string $tags string containing a list of tags. | ||
247 | * @param bool $casesensitive will convert everything to lowercase if false. | ||
248 | * | ||
249 | * @return array filtered tags string. | ||
250 | */ | ||
251 | public function tagsStrToArray($tags, $casesensitive) | ||
252 | { | ||
253 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | ||
254 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); | ||
255 | $tagsOut = str_replace(',', ' ', $tagsOut); | ||
256 | |||
257 | return explode(' ', trim($tagsOut)); | ||
258 | } | ||
259 | } | ||
diff --git a/application/Utils.php b/application/Utils.php index f84f70e4..aeaef9ff 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -72,12 +72,14 @@ function sanitizeLink(&$link) | |||
72 | 72 | ||
73 | /** | 73 | /** |
74 | * Checks if a string represents a valid date | 74 | * Checks if a string represents a valid date |
75 | |||
76 | * @param string $format The expected DateTime format of the string | ||
77 | * @param string $string A string-formatted date | ||
78 | * | ||
79 | * @return bool whether the string is a valid date | ||
75 | * | 80 | * |
76 | * @param string a string-formatted date | 81 | * @see http://php.net/manual/en/class.datetime.php |
77 | * @param format the expected DateTime format of the string | 82 | * @see http://php.net/manual/en/datetime.createfromformat.php |
78 | * @return whether the string is a valid date | ||
79 | * @see http://php.net/manual/en/class.datetime.php | ||
80 | * @see http://php.net/manual/en/datetime.createfromformat.php | ||
81 | */ | 83 | */ |
82 | function checkDateFormat($format, $string) | 84 | function checkDateFormat($format, $string) |
83 | { | 85 | { |
@@ -151,6 +151,7 @@ require_once 'application/CachedPage.php'; | |||
151 | require_once 'application/FileUtils.php'; | 151 | require_once 'application/FileUtils.php'; |
152 | require_once 'application/HttpUtils.php'; | 152 | require_once 'application/HttpUtils.php'; |
153 | require_once 'application/LinkDB.php'; | 153 | require_once 'application/LinkDB.php'; |
154 | require_once 'application/LinkFilter.php'; | ||
154 | require_once 'application/TimeZone.php'; | 155 | require_once 'application/TimeZone.php'; |
155 | require_once 'application/Url.php'; | 156 | require_once 'application/Url.php'; |
156 | require_once 'application/Utils.php'; | 157 | require_once 'application/Utils.php'; |
@@ -730,18 +731,23 @@ function showRSS() | |||
730 | // Read links from database (and filter private links if user it not logged in). | 731 | // Read links from database (and filter private links if user it not logged in). |
731 | 732 | ||
732 | // Optionally filter the results: | 733 | // Optionally filter the results: |
733 | $linksToDisplay=array(); | 734 | if (!empty($_GET['searchterm'])) { |
734 | if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); | 735 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); |
735 | else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 736 | } |
736 | else $linksToDisplay = $LINKSDB; | 737 | elseif (!empty($_GET['searchtags'])) { |
738 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); | ||
739 | } | ||
740 | else { | ||
741 | $linksToDisplay = $LINKSDB; | ||
742 | } | ||
737 | 743 | ||
738 | $nblinksToDisplay = 50; // Number of links to display. | 744 | $nblinksToDisplay = 50; // Number of links to display. |
739 | if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. | 745 | // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. |
740 | { | 746 | if (!empty($_GET['nb'])) { |
741 | $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ; | 747 | $nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1); |
742 | } | 748 | } |
743 | 749 | ||
744 | $pageaddr=escape(index_url($_SERVER)); | 750 | $pageaddr = escape(index_url($_SERVER)); |
745 | echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; | 751 | echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; |
746 | echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>'; | 752 | echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>'; |
747 | echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n"; | 753 | echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n"; |
@@ -821,15 +827,20 @@ function showATOM() | |||
821 | ); | 827 | ); |
822 | 828 | ||
823 | // Optionally filter the results: | 829 | // Optionally filter the results: |
824 | $linksToDisplay=array(); | 830 | if (!empty($_GET['searchterm'])) { |
825 | if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); | 831 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); |
826 | else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 832 | } |
827 | else $linksToDisplay = $LINKSDB; | 833 | else if (!empty($_GET['searchtags'])) { |
834 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); | ||
835 | } | ||
836 | else { | ||
837 | $linksToDisplay = $LINKSDB; | ||
838 | } | ||
828 | 839 | ||
829 | $nblinksToDisplay = 50; // Number of links to display. | 840 | $nblinksToDisplay = 50; // Number of links to display. |
830 | if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. | 841 | // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. |
831 | { | 842 | if (!empty($_GET['nb'])) { |
832 | $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ; | 843 | $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1); |
833 | } | 844 | } |
834 | 845 | ||
835 | $pageaddr=escape(index_url($_SERVER)); | 846 | $pageaddr=escape(index_url($_SERVER)); |
@@ -1024,7 +1035,7 @@ function showDaily($pageBuilder) | |||
1024 | } | 1035 | } |
1025 | 1036 | ||
1026 | try { | 1037 | try { |
1027 | $linksToDisplay = $LINKSDB->filterDay($day); | 1038 | $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day); |
1028 | } catch (Exception $exc) { | 1039 | } catch (Exception $exc) { |
1029 | error_log($exc); | 1040 | error_log($exc); |
1030 | $linksToDisplay = array(); | 1041 | $linksToDisplay = array(); |
@@ -1149,13 +1160,17 @@ function renderPage() | |||
1149 | if ($targetPage == Router::$PAGE_PICWALL) | 1160 | if ($targetPage == Router::$PAGE_PICWALL) |
1150 | { | 1161 | { |
1151 | // Optionally filter the results: | 1162 | // Optionally filter the results: |
1152 | $links=array(); | 1163 | if (!empty($_GET['searchterm'])) { |
1153 | if (!empty($_GET['searchterm'])) $links = $LINKSDB->filterFulltext($_GET['searchterm']); | 1164 | $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); |
1154 | elseif (!empty($_GET['searchtags'])) $links = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 1165 | } |
1155 | else $links = $LINKSDB; | 1166 | elseif (! empty($_GET['searchtags'])) { |
1167 | $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); | ||
1168 | } | ||
1169 | else { | ||
1170 | $links = $LINKSDB; | ||
1171 | } | ||
1156 | 1172 | ||
1157 | $body=''; | 1173 | $linksToDisplay = array(); |
1158 | $linksToDisplay=array(); | ||
1159 | 1174 | ||
1160 | // Get only links which have a thumbnail. | 1175 | // Get only links which have a thumbnail. |
1161 | foreach($links as $link) | 1176 | foreach($links as $link) |
@@ -1282,13 +1297,15 @@ function renderPage() | |||
1282 | } | 1297 | } |
1283 | 1298 | ||
1284 | if (isset($params['searchtags'])) { | 1299 | if (isset($params['searchtags'])) { |
1285 | $tags = explode(' ',$params['searchtags']); | 1300 | $tags = explode(' ', $params['searchtags']); |
1286 | $tags=array_diff($tags, array($_GET['removetag'])); // Remove value from array $tags. | 1301 | // Remove value from array $tags. |
1287 | if (count($tags)==0) { | 1302 | $tags = array_diff($tags, array($_GET['removetag'])); |
1303 | $params['searchtags'] = implode(' ',$tags); | ||
1304 | |||
1305 | if (empty($params['searchtags'])) { | ||
1288 | unset($params['searchtags']); | 1306 | unset($params['searchtags']); |
1289 | } else { | ||
1290 | $params['searchtags'] = implode(' ',$tags); | ||
1291 | } | 1307 | } |
1308 | |||
1292 | unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) | 1309 | unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) |
1293 | } | 1310 | } |
1294 | header('Location: ?'.http_build_query($params)); | 1311 | header('Location: ?'.http_build_query($params)); |
@@ -1468,7 +1485,8 @@ function renderPage() | |||
1468 | // Delete a tag: | 1485 | // Delete a tag: |
1469 | if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { | 1486 | if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { |
1470 | $needle=trim($_POST['fromtag']); | 1487 | $needle=trim($_POST['fromtag']); |
1471 | $linksToAlter = $LINKSDB->filterTags($needle,true); // True for case-sensitive tag search. | 1488 | // True for case-sensitive tag search. |
1489 | $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); | ||
1472 | foreach($linksToAlter as $key=>$value) | 1490 | foreach($linksToAlter as $key=>$value) |
1473 | { | 1491 | { |
1474 | $tags = explode(' ',trim($value['tags'])); | 1492 | $tags = explode(' ',trim($value['tags'])); |
@@ -1484,7 +1502,8 @@ function renderPage() | |||
1484 | // Rename a tag: | 1502 | // Rename a tag: |
1485 | if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { | 1503 | if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { |
1486 | $needle=trim($_POST['fromtag']); | 1504 | $needle=trim($_POST['fromtag']); |
1487 | $linksToAlter = $LINKSDB->filterTags($needle,true); // true for case-sensitive tag search. | 1505 | // True for case-sensitive tag search. |
1506 | $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); | ||
1488 | foreach($linksToAlter as $key=>$value) | 1507 | foreach($linksToAlter as $key=>$value) |
1489 | { | 1508 | { |
1490 | $tags = explode(' ',trim($value['tags'])); | 1509 | $tags = explode(' ',trim($value['tags'])); |
@@ -1865,81 +1884,78 @@ function importFile() | |||
1865 | function buildLinkList($PAGE,$LINKSDB) | 1884 | function buildLinkList($PAGE,$LINKSDB) |
1866 | { | 1885 | { |
1867 | // ---- Filter link database according to parameters | 1886 | // ---- Filter link database according to parameters |
1868 | $linksToDisplay=array(); | 1887 | $search_type = ''; |
1869 | $search_type=''; | 1888 | $search_crits = ''; |
1870 | $search_crits=''; | 1889 | $privateonly = !empty($_SESSION['privateonly']) ? true : false; |
1871 | if (isset($_GET['searchterm'])) // Fulltext search | 1890 | |
1872 | { | 1891 | // Fulltext search |
1873 | $linksToDisplay = $LINKSDB->filterFulltext(trim($_GET['searchterm'])); | 1892 | if (isset($_GET['searchterm'])) { |
1874 | $search_crits=escape(trim($_GET['searchterm'])); | 1893 | $search_crits = escape(trim($_GET['searchterm'])); |
1875 | $search_type='fulltext'; | 1894 | $search_type = LinkFilter::$FILTER_TEXT; |
1876 | } | 1895 | $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); |
1877 | elseif (isset($_GET['searchtags'])) // Search by tag | 1896 | } |
1878 | { | 1897 | // Search by tag |
1879 | $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags'])); | 1898 | elseif (isset($_GET['searchtags'])) { |
1880 | $search_crits=explode(' ',escape(trim($_GET['searchtags']))); | 1899 | $search_crits = explode(' ', escape(trim($_GET['searchtags']))); |
1881 | $search_type='tags'; | 1900 | $search_type = LinkFilter::$FILTER_TAG; |
1882 | } | 1901 | $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly); |
1883 | elseif (isset($_SERVER['QUERY_STRING']) && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/',$_SERVER['QUERY_STRING'])) // Detect smallHashes in URL | 1902 | } |
1884 | { | 1903 | // Detect smallHashes in URL. |
1885 | $linksToDisplay = $LINKSDB->filterSmallHash(substr(trim($_SERVER["QUERY_STRING"], '/'),0,6)); | 1904 | elseif (isset($_SERVER['QUERY_STRING']) |
1886 | if (count($linksToDisplay)==0) | 1905 | && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) { |
1887 | { | 1906 | $search_type = LinkFilter::$FILTER_HASH; |
1888 | header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); | 1907 | $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6); |
1889 | echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.'; | 1908 | $linksToDisplay = $LINKSDB->filter($search_type, $search_crits); |
1909 | |||
1910 | if (count($linksToDisplay) == 0) { | ||
1911 | header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); | ||
1912 | echo '<h1>404 Not found.</h1>Oh crap. | ||
1913 | The link you are trying to reach does not exist or has been deleted.'; | ||
1890 | echo '<br>Would you mind <a href="?">clicking here</a>?'; | 1914 | echo '<br>Would you mind <a href="?">clicking here</a>?'; |
1891 | exit; | 1915 | exit; |
1892 | } | 1916 | } |
1893 | $search_type='permalink'; | ||
1894 | } | 1917 | } |
1895 | else | 1918 | // Otherwise, display without filtering. |
1896 | $linksToDisplay = $LINKSDB; // Otherwise, display without filtering. | 1919 | else { |
1897 | 1920 | $linksToDisplay = $LINKSDB->filter('', '', false, $privateonly); | |
1898 | |||
1899 | // Option: Show only private links | ||
1900 | if (!empty($_SESSION['privateonly'])) | ||
1901 | { | ||
1902 | $tmp = array(); | ||
1903 | foreach($linksToDisplay as $linkdate=>$link) | ||
1904 | { | ||
1905 | if ($link['private']!=0) $tmp[$linkdate]=$link; | ||
1906 | } | ||
1907 | $linksToDisplay=$tmp; | ||
1908 | } | 1921 | } |
1909 | 1922 | ||
1910 | // ---- Handle paging. | 1923 | // ---- Handle paging. |
1911 | /* Can someone explain to me why you get the following error when using array_keys() on an object which implements the interface ArrayAccess??? | 1924 | $keys = array(); |
1912 | "Warning: array_keys() expects parameter 1 to be array, object given in ... " | 1925 | foreach ($linksToDisplay as $key => $value) { |
1913 | If my class implements ArrayAccess, why won't array_keys() accept it ? ( $keys=array_keys($linksToDisplay); ) | 1926 | $keys[] = $key; |
1914 | */ | 1927 | } |
1915 | $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks PHP. | ||
1916 | 1928 | ||
1917 | // If there is only a single link, we change on-the-fly the title of the page. | 1929 | // If there is only a single link, we change on-the-fly the title of the page. |
1918 | if (count($linksToDisplay)==1) $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title']; | 1930 | if (count($linksToDisplay) == 1) { |
1931 | $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title']; | ||
1932 | } | ||
1919 | 1933 | ||
1920 | // Select articles according to paging. | 1934 | // Select articles according to paging. |
1921 | $pagecount = ceil(count($keys)/$_SESSION['LINKS_PER_PAGE']); | 1935 | $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); |
1922 | $pagecount = ($pagecount==0 ? 1 : $pagecount); | 1936 | $pagecount = $pagecount == 0 ? 1 : $pagecount; |
1923 | $page=( empty($_GET['page']) ? 1 : intval($_GET['page'])); | 1937 | $page= empty($_GET['page']) ? 1 : intval($_GET['page']); |
1924 | $page = ( $page<1 ? 1 : $page ); | 1938 | $page = $page < 1 ? 1 : $page; |
1925 | $page = ( $page>$pagecount ? $pagecount : $page ); | 1939 | $page = $page > $pagecount ? $pagecount : $page; |
1926 | $i = ($page-1)*$_SESSION['LINKS_PER_PAGE']; // Start index. | 1940 | // Start index. |
1927 | $end = $i+$_SESSION['LINKS_PER_PAGE']; | 1941 | $i = ($page-1) * $_SESSION['LINKS_PER_PAGE']; |
1928 | $linkDisp=array(); // Links to display | 1942 | $end = $i + $_SESSION['LINKS_PER_PAGE']; |
1943 | $linkDisp = array(); | ||
1929 | while ($i<$end && $i<count($keys)) | 1944 | while ($i<$end && $i<count($keys)) |
1930 | { | 1945 | { |
1931 | $link = $linksToDisplay[$keys[$i]]; | 1946 | $link = $linksToDisplay[$keys[$i]]; |
1932 | $link['description'] = format_description($link['description'], $GLOBALS['redirector']); | 1947 | $link['description'] = format_description($link['description'], $GLOBALS['redirector']); |
1933 | $classLi = $i%2!=0 ? '' : 'publicLinkHightLight'; | 1948 | $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight'; |
1934 | $link['class'] = ($link['private']==0 ? $classLi : 'private'); | 1949 | $link['class'] = $link['private'] == 0 ? $classLi : 'private'; |
1935 | $link['timestamp']=linkdate2timestamp($link['linkdate']); | 1950 | $link['timestamp'] = linkdate2timestamp($link['linkdate']); |
1936 | $taglist = explode(' ',$link['tags']); | 1951 | $taglist = explode(' ', $link['tags']); |
1937 | uasort($taglist, 'strcasecmp'); | 1952 | uasort($taglist, 'strcasecmp'); |
1938 | $link['taglist']=$taglist; | 1953 | $link['taglist'] = $taglist; |
1939 | $link['shorturl'] = smallHash($link['linkdate']); | 1954 | $link['shorturl'] = smallHash($link['linkdate']); |
1940 | 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. | 1955 | // Check for both signs of a note: starting with ? and 7 chars long. |
1941 | strlen($link["url"]) === 7) { | 1956 | if ($link['url'][0] === '?' && |
1942 | $link["url"] = index_url($_SERVER) . $link["url"]; | 1957 | strlen($link['url']) === 7) { |
1958 | $link['url'] = index_url($_SERVER) . $link['url']; | ||
1943 | } | 1959 | } |
1944 | 1960 | ||
1945 | $linkDisp[$keys[$i]] = $link; | 1961 | $linkDisp[$keys[$i]] = $link; |
@@ -1947,13 +1963,21 @@ function buildLinkList($PAGE,$LINKSDB) | |||
1947 | } | 1963 | } |
1948 | 1964 | ||
1949 | // Compute paging navigation | 1965 | // Compute paging navigation |
1950 | $searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] ); | 1966 | $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm']; |
1951 | $searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] ); | 1967 | $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags']; |
1952 | $paging=''; | 1968 | $previous_page_url = ''; |
1953 | $previous_page_url=''; if ($i!=count($keys)) $previous_page_url='?page='.($page+1).$searchterm.$searchtags; | 1969 | if ($i != count($keys)) { |
1954 | $next_page_url='';if ($page>1) $next_page_url='?page='.($page-1).$searchterm.$searchtags; | 1970 | $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags; |
1971 | } | ||
1972 | $next_page_url=''; | ||
1973 | if ($page>1) { | ||
1974 | $next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags; | ||
1975 | } | ||
1955 | 1976 | ||
1956 | $token = ''; if (isLoggedIn()) $token=getToken(); | 1977 | $token = ''; |
1978 | if (isLoggedIn()) { | ||
1979 | $token = getToken(); | ||
1980 | } | ||
1957 | 1981 | ||
1958 | // Fill all template fields. | 1982 | // Fill all template fields. |
1959 | $data = array( | 1983 | $data = array( |
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 7b22b270..3b1a2057 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php | |||
@@ -302,236 +302,49 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
302 | } | 302 | } |
303 | 303 | ||
304 | /** | 304 | /** |
305 | * Filter links using a tag | 305 | * Test real_url without redirector. |
306 | */ | ||
307 | public function testFilterOneTag() | ||
308 | { | ||
309 | $this->assertEquals( | ||
310 | 3, | ||
311 | sizeof(self::$publicLinkDB->filterTags('web', false)) | ||
312 | ); | ||
313 | |||
314 | $this->assertEquals( | ||
315 | 4, | ||
316 | sizeof(self::$privateLinkDB->filterTags('web', false)) | ||
317 | ); | ||
318 | } | ||
319 | |||
320 | /** | ||
321 | * Filter links using a tag - case-sensitive | ||
322 | */ | ||
323 | public function testFilterCaseSensitiveTag() | ||
324 | { | ||
325 | $this->assertEquals( | ||
326 | 0, | ||
327 | sizeof(self::$privateLinkDB->filterTags('mercurial', true)) | ||
328 | ); | ||
329 | |||
330 | $this->assertEquals( | ||
331 | 1, | ||
332 | sizeof(self::$privateLinkDB->filterTags('Mercurial', true)) | ||
333 | ); | ||
334 | } | ||
335 | |||
336 | /** | ||
337 | * Filter links using a tag combination | ||
338 | */ | ||
339 | public function testFilterMultipleTags() | ||
340 | { | ||
341 | $this->assertEquals( | ||
342 | 1, | ||
343 | sizeof(self::$publicLinkDB->filterTags('dev cartoon', false)) | ||
344 | ); | ||
345 | |||
346 | $this->assertEquals( | ||
347 | 2, | ||
348 | sizeof(self::$privateLinkDB->filterTags('dev cartoon', false)) | ||
349 | ); | ||
350 | } | ||
351 | |||
352 | /** | ||
353 | * Filter links using a non-existent tag | ||
354 | */ | ||
355 | public function testFilterUnknownTag() | ||
356 | { | ||
357 | $this->assertEquals( | ||
358 | 0, | ||
359 | sizeof(self::$publicLinkDB->filterTags('null', false)) | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | /** | ||
364 | * Return links for a given day | ||
365 | */ | ||
366 | public function testFilterDay() | ||
367 | { | ||
368 | $this->assertEquals( | ||
369 | 2, | ||
370 | sizeof(self::$publicLinkDB->filterDay('20121206')) | ||
371 | ); | ||
372 | |||
373 | $this->assertEquals( | ||
374 | 3, | ||
375 | sizeof(self::$privateLinkDB->filterDay('20121206')) | ||
376 | ); | ||
377 | } | ||
378 | |||
379 | /** | ||
380 | * 404 - day not found | ||
381 | */ | ||
382 | public function testFilterUnknownDay() | ||
383 | { | ||
384 | $this->assertEquals( | ||
385 | 0, | ||
386 | sizeof(self::$publicLinkDB->filterDay('19700101')) | ||
387 | ); | ||
388 | |||
389 | $this->assertEquals( | ||
390 | 0, | ||
391 | sizeof(self::$privateLinkDB->filterDay('19700101')) | ||
392 | ); | ||
393 | } | ||
394 | |||
395 | /** | ||
396 | * Use an invalid date format | ||
397 | * @expectedException Exception | ||
398 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
399 | */ | ||
400 | public function testFilterInvalidDayWithChars() | ||
401 | { | ||
402 | self::$privateLinkDB->filterDay('Rainy day, dream away'); | ||
403 | } | ||
404 | |||
405 | /** | ||
406 | * Use an invalid date format | ||
407 | * @expectedException Exception | ||
408 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
409 | */ | ||
410 | public function testFilterInvalidDayDigits() | ||
411 | { | ||
412 | self::$privateLinkDB->filterDay('20'); | ||
413 | } | ||
414 | |||
415 | /** | ||
416 | * Retrieve a link entry with its hash | ||
417 | */ | ||
418 | public function testFilterSmallHash() | ||
419 | { | ||
420 | $links = self::$privateLinkDB->filterSmallHash('IuWvgA'); | ||
421 | |||
422 | $this->assertEquals( | ||
423 | 1, | ||
424 | sizeof($links) | ||
425 | ); | ||
426 | |||
427 | $this->assertEquals( | ||
428 | 'MediaGoblin', | ||
429 | $links['20130614_184135']['title'] | ||
430 | ); | ||
431 | |||
432 | } | ||
433 | |||
434 | /** | ||
435 | * No link for this hash | ||
436 | */ | ||
437 | public function testFilterUnknownSmallHash() | ||
438 | { | ||
439 | $this->assertEquals( | ||
440 | 0, | ||
441 | sizeof(self::$privateLinkDB->filterSmallHash('Iblaah')) | ||
442 | ); | ||
443 | } | ||
444 | |||
445 | /** | ||
446 | * Full-text search - result from a link's URL | ||
447 | */ | ||
448 | public function testFilterFullTextURL() | ||
449 | { | ||
450 | $this->assertEquals( | ||
451 | 2, | ||
452 | sizeof(self::$publicLinkDB->filterFullText('ars.userfriendly.org')) | ||
453 | ); | ||
454 | } | ||
455 | |||
456 | /** | ||
457 | * Full-text search - result from a link's title only | ||
458 | */ | 306 | */ |
459 | public function testFilterFullTextTitle() | 307 | public function testLinkRealUrlWithoutRedirector() |
460 | { | 308 | { |
461 | // use miscellaneous cases | 309 | $db = new LinkDB(self::$testDatastore, false, false); |
462 | $this->assertEquals( | 310 | foreach($db as $link) { |
463 | 2, | 311 | $this->assertEquals($link['url'], $link['real_url']); |
464 | sizeof(self::$publicLinkDB->filterFullText('userfriendly -')) | 312 | } |
465 | ); | ||
466 | $this->assertEquals( | ||
467 | 2, | ||
468 | sizeof(self::$publicLinkDB->filterFullText('UserFriendly -')) | ||
469 | ); | ||
470 | $this->assertEquals( | ||
471 | 2, | ||
472 | sizeof(self::$publicLinkDB->filterFullText('uSeRFrIendlY -')) | ||
473 | ); | ||
474 | |||
475 | // use miscellaneous case and offset | ||
476 | $this->assertEquals( | ||
477 | 2, | ||
478 | sizeof(self::$publicLinkDB->filterFullText('RFrIendL')) | ||
479 | ); | ||
480 | } | 313 | } |
481 | 314 | ||
482 | /** | 315 | /** |
483 | * Full-text search - result from the link's description only | 316 | * Test real_url with redirector. |
484 | */ | 317 | */ |
485 | public function testFilterFullTextDescription() | 318 | public function testLinkRealUrlWithRedirector() |
486 | { | 319 | { |
487 | $this->assertEquals( | 320 | $redirector = 'http://redirector.to?'; |
488 | 1, | 321 | $db = new LinkDB(self::$testDatastore, false, false, $redirector); |
489 | sizeof(self::$publicLinkDB->filterFullText('media publishing')) | 322 | foreach($db as $link) { |
490 | ); | 323 | $this->assertStringStartsWith($redirector, $link['real_url']); |
324 | } | ||
491 | } | 325 | } |
492 | 326 | ||
493 | /** | 327 | /** |
494 | * Full-text search - result from the link's tags only | 328 | * Test filter with string. |
495 | */ | 329 | */ |
496 | public function testFilterFullTextTags() | 330 | public function testFilterString() |
497 | { | 331 | { |
332 | $tags = 'dev cartoon'; | ||
498 | $this->assertEquals( | 333 | $this->assertEquals( |
499 | 2, | 334 | 2, |
500 | sizeof(self::$publicLinkDB->filterFullText('gnu')) | 335 | count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) |
501 | ); | 336 | ); |
502 | } | 337 | } |
503 | 338 | ||
504 | /** | 339 | /** |
505 | * Full-text search - result set from mixed sources | 340 | * Test filter with string. |
506 | */ | 341 | */ |
507 | public function testFilterFullTextMixed() | 342 | public function testFilterArray() |
508 | { | 343 | { |
344 | $tags = array('dev', 'cartoon'); | ||
509 | $this->assertEquals( | 345 | $this->assertEquals( |
510 | 2, | 346 | 2, |
511 | sizeof(self::$publicLinkDB->filterFullText('free software')) | 347 | count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) |
512 | ); | 348 | ); |
513 | } | 349 | } |
514 | |||
515 | /** | ||
516 | * Test real_url without redirector. | ||
517 | */ | ||
518 | public function testLinkRealUrlWithoutRedirector() | ||
519 | { | ||
520 | $db = new LinkDB(self::$testDatastore, false, false); | ||
521 | foreach($db as $link) { | ||
522 | $this->assertEquals($link['url'], $link['real_url']); | ||
523 | } | ||
524 | } | ||
525 | |||
526 | /** | ||
527 | * Test real_url with redirector. | ||
528 | */ | ||
529 | public function testLinkRealUrlWithRedirector() | ||
530 | { | ||
531 | $redirector = 'http://redirector.to?'; | ||
532 | $db = new LinkDB(self::$testDatastore, false, false, $redirector); | ||
533 | foreach($db as $link) { | ||
534 | $this->assertStringStartsWith($redirector, $link['real_url']); | ||
535 | } | ||
536 | } | ||
537 | } | 350 | } |
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php new file mode 100644 index 00000000..5107ab72 --- /dev/null +++ b/tests/LinkFilterTest.php | |||
@@ -0,0 +1,242 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'application/LinkFilter.php'; | ||
4 | |||
5 | /** | ||
6 | * Class LinkFilterTest. | ||
7 | */ | ||
8 | class LinkFilterTest extends PHPUnit_Framework_TestCase | ||
9 | { | ||
10 | /** | ||
11 | * @var LinkFilter instance. | ||
12 | */ | ||
13 | protected static $linkFilter; | ||
14 | |||
15 | /** | ||
16 | * Instanciate linkFilter with ReferenceLinkDB data. | ||
17 | */ | ||
18 | public static function setUpBeforeClass() | ||
19 | { | ||
20 | $refDB = new ReferenceLinkDB(); | ||
21 | self::$linkFilter = new LinkFilter($refDB->getLinks()); | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * Blank filter. | ||
26 | */ | ||
27 | public function testFilter() | ||
28 | { | ||
29 | $this->assertEquals( | ||
30 | 6, | ||
31 | count(self::$linkFilter->filter('', '')) | ||
32 | ); | ||
33 | |||
34 | // Private only. | ||
35 | $this->assertEquals( | ||
36 | 2, | ||
37 | count(self::$linkFilter->filter('', '', false, true)) | ||
38 | ); | ||
39 | } | ||
40 | |||
41 | /** | ||
42 | * Filter links using a tag | ||
43 | */ | ||
44 | public function testFilterOneTag() | ||
45 | { | ||
46 | $this->assertEquals( | ||
47 | 4, | ||
48 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false)) | ||
49 | ); | ||
50 | |||
51 | // Private only. | ||
52 | $this->assertEquals( | ||
53 | 1, | ||
54 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, true)) | ||
55 | ); | ||
56 | } | ||
57 | |||
58 | /** | ||
59 | * Filter links using a tag - case-sensitive | ||
60 | */ | ||
61 | public function testFilterCaseSensitiveTag() | ||
62 | { | ||
63 | $this->assertEquals( | ||
64 | 0, | ||
65 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true)) | ||
66 | ); | ||
67 | |||
68 | $this->assertEquals( | ||
69 | 1, | ||
70 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true)) | ||
71 | ); | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * Filter links using a tag combination | ||
76 | */ | ||
77 | public function testFilterMultipleTags() | ||
78 | { | ||
79 | $this->assertEquals( | ||
80 | 2, | ||
81 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false)) | ||
82 | ); | ||
83 | } | ||
84 | |||
85 | /** | ||
86 | * Filter links using a non-existent tag | ||
87 | */ | ||
88 | public function testFilterUnknownTag() | ||
89 | { | ||
90 | $this->assertEquals( | ||
91 | 0, | ||
92 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false)) | ||
93 | ); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Return links for a given day | ||
98 | */ | ||
99 | public function testFilterDay() | ||
100 | { | ||
101 | $this->assertEquals( | ||
102 | 3, | ||
103 | count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) | ||
104 | ); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * 404 - day not found | ||
109 | */ | ||
110 | public function testFilterUnknownDay() | ||
111 | { | ||
112 | $this->assertEquals( | ||
113 | 0, | ||
114 | count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101')) | ||
115 | ); | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * Use an invalid date format | ||
120 | * @expectedException Exception | ||
121 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
122 | */ | ||
123 | public function testFilterInvalidDayWithChars() | ||
124 | { | ||
125 | self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away'); | ||
126 | } | ||
127 | |||
128 | /** | ||
129 | * Use an invalid date format | ||
130 | * @expectedException Exception | ||
131 | * @expectedExceptionMessageRegExp /Invalid date format/ | ||
132 | */ | ||
133 | public function testFilterInvalidDayDigits() | ||
134 | { | ||
135 | self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20'); | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Retrieve a link entry with its hash | ||
140 | */ | ||
141 | public function testFilterSmallHash() | ||
142 | { | ||
143 | $links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA'); | ||
144 | |||
145 | $this->assertEquals( | ||
146 | 1, | ||
147 | count($links) | ||
148 | ); | ||
149 | |||
150 | $this->assertEquals( | ||
151 | 'MediaGoblin', | ||
152 | $links['20130614_184135']['title'] | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | /** | ||
157 | * No link for this hash | ||
158 | */ | ||
159 | public function testFilterUnknownSmallHash() | ||
160 | { | ||
161 | $this->assertEquals( | ||
162 | 0, | ||
163 | count(self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah')) | ||
164 | ); | ||
165 | } | ||
166 | |||
167 | /** | ||
168 | * Full-text search - result from a link's URL | ||
169 | */ | ||
170 | public function testFilterFullTextURL() | ||
171 | { | ||
172 | $this->assertEquals( | ||
173 | 2, | ||
174 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org')) | ||
175 | ); | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Full-text search - result from a link's title only | ||
180 | */ | ||
181 | public function testFilterFullTextTitle() | ||
182 | { | ||
183 | // use miscellaneous cases | ||
184 | $this->assertEquals( | ||
185 | 2, | ||
186 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -')) | ||
187 | ); | ||
188 | $this->assertEquals( | ||
189 | 2, | ||
190 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -')) | ||
191 | ); | ||
192 | $this->assertEquals( | ||
193 | 2, | ||
194 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -')) | ||
195 | ); | ||
196 | |||
197 | // use miscellaneous case and offset | ||
198 | $this->assertEquals( | ||
199 | 2, | ||
200 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL')) | ||
201 | ); | ||
202 | } | ||
203 | |||
204 | /** | ||
205 | * Full-text search - result from the link's description only | ||
206 | */ | ||
207 | public function testFilterFullTextDescription() | ||
208 | { | ||
209 | $this->assertEquals( | ||
210 | 1, | ||
211 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'media publishing')) | ||
212 | ); | ||
213 | } | ||
214 | |||
215 | /** | ||
216 | * Full-text search - result from the link's tags only | ||
217 | */ | ||
218 | public function testFilterFullTextTags() | ||
219 | { | ||
220 | $this->assertEquals( | ||
221 | 2, | ||
222 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'gnu')) | ||
223 | ); | ||
224 | |||
225 | // Private only. | ||
226 | $this->assertEquals( | ||
227 | 1, | ||
228 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, true)) | ||
229 | ); | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * Full-text search - result set from mixed sources | ||
234 | */ | ||
235 | public function testFilterFullTextMixed() | ||
236 | { | ||
237 | $this->assertEquals( | ||
238 | 2, | ||
239 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) | ||
240 | ); | ||
241 | } | ||
242 | } | ||
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index 47b51829..011317ef 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php | |||
@@ -124,4 +124,9 @@ class ReferenceLinkDB | |||
124 | { | 124 | { |
125 | return $this->_privateCount; | 125 | return $this->_privateCount; |
126 | } | 126 | } |
127 | |||
128 | public function getLinks() | ||
129 | { | ||
130 | return $this->_links; | ||
131 | } | ||
127 | } | 132 | } |
diff --git a/tpl/linklist.html b/tpl/linklist.html index 666748a7..6ce59dd8 100644 --- a/tpl/linklist.html +++ b/tpl/linklist.html | |||
@@ -7,15 +7,24 @@ | |||
7 | <body> | 7 | <body> |
8 | <div id="pageheader"> | 8 | <div id="pageheader"> |
9 | {include="page.header"} | 9 | {include="page.header"} |
10 | |||
10 | <div id="headerform" class="search"> | 11 | <div id="headerform" class="search"> |
11 | <form method="GET" class="searchform" name="searchform"> | 12 | <form method="GET" class="searchform" name="searchform"> |
12 | <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" value=""> | 13 | <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" |
14 | {if="!empty($search_crits) && $search_type=='fulltext'"} | ||
15 | value="{$search_crits}" | ||
16 | {/if} | ||
17 | > | ||
13 | <input type="submit" value="Search" class="bigbutton"> | 18 | <input type="submit" value="Search" class="bigbutton"> |
14 | </form> | 19 | </form> |
15 | <form method="GET" class="tagfilter" name="tagfilter"> | 20 | <form method="GET" class="tagfilter" name="tagfilter"> |
16 | <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" value="" | 21 | <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" |
17 | autocomplete="off" class="awesomplete" data-multiple data-minChars="1" | 22 | {if="!empty($search_crits) && $search_type=='tags'"} |
18 | data-list="{loop="$tags"}{$key}, {/loop}"> | 23 | value="{function="implode(' ', $search_crits)"}" |
24 | {/if} | ||
25 | autocomplete="off" class="awesomplete" data-multiple data-minChars="1" | ||
26 | data-list="{loop="$tags"}{$key}, {/loop}"> | ||
27 | > | ||
19 | <input type="submit" value="Search" class="bigbutton"> | 28 | <input type="submit" value="Search" class="bigbutton"> |
20 | </form> | 29 | </form> |
21 | {loop="$plugins_header.fields_toolbar"} | 30 | {loop="$plugins_header.fields_toolbar"} |
@@ -44,7 +53,7 @@ | |||
44 | <div id="searchcriteria">{$result_count} results for tags <i> | 53 | <div id="searchcriteria">{$result_count} results for tags <i> |
45 | {loop="search_crits"} | 54 | {loop="search_crits"} |
46 | <span class="linktag" title="Remove tag"> | 55 | <span class="linktag" title="Remove tag"> |
47 | <a href="?removetag={$value}">{$value} <span class="remove">x</span></a> | 56 | <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> |
48 | </span> | 57 | </span> |
49 | {/loop}</i></div> | 58 | {/loop}</i></div> |
50 | {/if} | 59 | {/if} |