aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/FeedBuilder.php279
-rw-r--r--application/LinkDB.php64
-rw-r--r--application/LinkFilter.php14
-rw-r--r--application/LinkUtils.php2
-rw-r--r--application/Router.php14
-rw-r--r--application/Updater.php2
-rw-r--r--application/Utils.php16
-rw-r--r--index.php394
-rw-r--r--plugins/demo_plugin/demo_plugin.php27
-rw-r--r--tests/FeedBuilderTest.php212
-rw-r--r--tests/LinkDBTest.php56
-rw-r--r--tests/LinkFilterTest.php17
-rw-r--r--tests/LinkUtilsTest.php2
-rw-r--r--tests/Updater/UpdaterTest.php4
-rw-r--r--tests/utils/ReferenceLinkDB.php20
-rw-r--r--tpl/configure.html7
-rw-r--r--tpl/feed.atom.html40
-rw-r--r--tpl/feed.rss.html34
18 files changed, 863 insertions, 341 deletions
diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php
new file mode 100644
index 00000000..ddefe6ce
--- /dev/null
+++ b/application/FeedBuilder.php
@@ -0,0 +1,279 @@
1<?php
2
3/**
4 * FeedBuilder class.
5 *
6 * Used to build ATOM and RSS feeds data.
7 */
8class FeedBuilder
9{
10 /**
11 * @var string Constant: RSS feed type.
12 */
13 public static $FEED_RSS = 'rss';
14
15 /**
16 * @var string Constant: ATOM feed type.
17 */
18 public static $FEED_ATOM = 'atom';
19
20 /**
21 * @var string Default language if the locale isn't set.
22 */
23 public static $DEFAULT_LANGUAGE = 'en-en';
24
25 /**
26 * @var int Number of links to display in a feed by default.
27 */
28 public static $DEFAULT_NB_LINKS = 50;
29
30 /**
31 * @var LinkDB instance.
32 */
33 protected $linkDB;
34
35 /**
36 * @var string RSS or ATOM feed.
37 */
38 protected $feedType;
39
40 /**
41 * @var array $_SERVER.
42 */
43 protected $serverInfo;
44
45 /**
46 * @var array $_GET.
47 */
48 protected $userInput;
49
50 /**
51 * @var boolean True if the user is currently logged in, false otherwise.
52 */
53 protected $isLoggedIn;
54
55 /**
56 * @var boolean Use permalinks instead of direct links if true.
57 */
58 protected $usePermalinks;
59
60 /**
61 * @var boolean true to hide dates in feeds.
62 */
63 protected $hideDates;
64
65 /**
66 * @var string PubSub hub URL.
67 */
68 protected $pubsubhubUrl;
69
70 /**
71 * @var string server locale.
72 */
73 protected $locale;
74
75 /**
76 * @var DateTime Latest item date.
77 */
78 protected $latestDate;
79
80 /**
81 * Feed constructor.
82 *
83 * @param LinkDB $linkDB LinkDB instance.
84 * @param string $feedType Type of feed.
85 * @param array $serverInfo $_SERVER.
86 * @param array $userInput $_GET.
87 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
88 */
89 public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn)
90 {
91 $this->linkDB = $linkDB;
92 $this->feedType = $feedType;
93 $this->serverInfo = $serverInfo;
94 $this->userInput = $userInput;
95 $this->isLoggedIn = $isLoggedIn;
96 }
97
98 /**
99 * Build data for feed templates.
100 *
101 * @return array Formatted data for feeds templates.
102 */
103 public function buildData()
104 {
105 // Optionally filter the results:
106 $linksToDisplay = $this->linkDB->filterSearch($this->userInput);
107
108 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
109
110 // Can't use array_keys() because $link is a LinkDB instance and not a real array.
111 $keys = array();
112 foreach ($linksToDisplay as $key => $value) {
113 $keys[] = $key;
114 }
115
116 $pageaddr = escape(index_url($this->serverInfo));
117 $linkDisplayed = array();
118 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
119 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
120 }
121
122 $data['language'] = $this->getTypeLanguage();
123 $data['pubsubhub_url'] = $this->pubsubhubUrl;
124 $data['last_update'] = $this->getLatestDateFormatted();
125 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
126 // Remove leading slash from REQUEST_URI.
127 $data['self_link'] = $pageaddr . escape(ltrim($this->serverInfo['REQUEST_URI'], '/'));
128 $data['index_url'] = $pageaddr;
129 $data['usepermalinks'] = $this->usePermalinks === true;
130 $data['links'] = $linkDisplayed;
131
132 return $data;
133 }
134
135 /**
136 * Build a feed item (one per shaare).
137 *
138 * @param array $link Single link array extracted from LinkDB.
139 * @param string $pageaddr Index URL.
140 *
141 * @return array Link array with feed attributes.
142 */
143 protected function buildItem($link, $pageaddr)
144 {
145 $link['guid'] = $pageaddr .'?'. smallHash($link['linkdate']);
146 // Check for both signs of a note: starting with ? and 7 chars long.
147 if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
148 $link['url'] = $pageaddr . $link['url'];
149 }
150 if ($this->usePermalinks === true) {
151 $permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>';
152 } else {
153 $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>';
154 }
155 $link['description'] = format_description($link['description']) . PHP_EOL .'<br>&#8212; '. $permalink;
156
157 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
158
159 if ($this->feedType == self::$FEED_RSS) {
160 $link['iso_date'] = $date->format(DateTime::RSS);
161 } else {
162 $link['iso_date'] = $date->format(DateTime::ATOM);
163 }
164
165 // Save the more recent item.
166 if (empty($this->latestDate) || $this->latestDate < $date) {
167 $this->latestDate = $date;
168 }
169
170 $taglist = array_filter(explode(' ', $link['tags']), 'strlen');
171 uasort($taglist, 'strcasecmp');
172 $link['taglist'] = $taglist;
173
174 return $link;
175 }
176
177 /**
178 * Assign PubSub hub URL.
179 *
180 * @param string $pubsubhubUrl PubSub hub url.
181 */
182 public function setPubsubhubUrl($pubsubhubUrl)
183 {
184 $this->pubsubhubUrl = $pubsubhubUrl;
185 }
186
187 /**
188 * Set this to true to use permalinks instead of direct links.
189 *
190 * @param boolean $usePermalinks true to force permalinks.
191 */
192 public function setUsePermalinks($usePermalinks)
193 {
194 $this->usePermalinks = $usePermalinks;
195 }
196
197 /**
198 * Set this to true to hide timestamps in feeds.
199 *
200 * @param boolean $hideDates true to enable.
201 */
202 public function setHideDates($hideDates)
203 {
204 $this->hideDates = $hideDates;
205 }
206
207 /**
208 * Set the locale. Used to show feed language.
209 *
210 * @param string $locale The locale (eg. 'fr_FR.UTF8').
211 */
212 public function setLocale($locale)
213 {
214 $this->locale = strtolower($locale);
215 }
216
217 /**
218 * Get the language according to the feed type, based on the locale:
219 *
220 * - RSS format: en-us (default: 'en-en').
221 * - ATOM format: fr (default: 'en').
222 *
223 * @return string The language.
224 */
225 public function getTypeLanguage()
226 {
227 // Use the locale do define the language, if available.
228 if (! empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
229 $length = ($this->feedType == self::$FEED_RSS) ? 5 : 2;
230 return str_replace('_', '-', substr($this->locale, 0, $length));
231 }
232 return ($this->feedType == self::$FEED_RSS) ? 'en-en' : 'en';
233 }
234
235 /**
236 * Format the latest item date found according to the feed type.
237 *
238 * Return an empty string if invalid DateTime is passed.
239 *
240 * @return string Formatted date.
241 */
242 protected function getLatestDateFormatted()
243 {
244 if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
245 return '';
246 }
247
248 $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
249 return $this->latestDate->format($type);
250 }
251
252 /**
253 * Returns the number of link to display according to 'nb' user input parameter.
254 *
255 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
256 * If 'nb' is set to 'all', display all filtered links (max parameter).
257 *
258 * @param int $max maximum number of links to display.
259 *
260 * @return int number of links to display.
261 */
262 public function getNbLinks($max)
263 {
264 if (empty($this->userInput['nb'])) {
265 return self::$DEFAULT_NB_LINKS;
266 }
267
268 if ($this->userInput['nb'] == 'all') {
269 return $max;
270 }
271
272 $intNb = intval($this->userInput['nb']);
273 if (! is_int($intNb) || $intNb == 0) {
274 return self::$DEFAULT_NB_LINKS;
275 }
276
277 return $intNb;
278 }
279}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 1b505620..a62341fc 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -341,17 +341,71 @@ You use the community supported version of the original Shaarli project, by Seba
341 } 341 }
342 342
343 /** 343 /**
344 * Filter links. 344 * Returns the shaare corresponding to a smallHash.
345 * 345 *
346 * @param string $type Type of filter. 346 * @param string $request QUERY_STRING server parameter.
347 * @param mixed $request Search request, string or array. 347 *
348 * @return array $filtered array containing permalink data.
349 *
350 * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link.
351 */
352 public function filterHash($request)
353 {
354 $request = substr($request, 0, 6);
355 $linkFilter = new LinkFilter($this->_links);
356 return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request);
357 }
358
359 /**
360 * Returns the list of articles for a given day.
361 *
362 * @param string $request day to filter. Format: YYYYMMDD.
363 *
364 * @return array list of shaare found.
365 */
366 public function filterDay($request) {
367 $linkFilter = new LinkFilter($this->_links);
368 return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request);
369 }
370
371 /**
372 * Filter links according to search parameters.
373 *
374 * @param array $filterRequest Search request content. Supported keys:
375 * - searchtags: list of tags
376 * - searchterm: term search
348 * @param bool $casesensitive Optional: Perform case sensitive filter 377 * @param bool $casesensitive Optional: Perform case sensitive filter
349 * @param bool $privateonly Optional: Returns private links only if true. 378 * @param bool $privateonly Optional: Returns private links only if true.
350 * 379 *
351 * @return array filtered links 380 * @return array filtered links, all links if no suitable filter was provided.
352 */ 381 */
353 public function filter($type = '', $request = '', $casesensitive = false, $privateonly = false) 382 public function filterSearch($filterRequest = array(), $casesensitive = false, $privateonly = false)
354 { 383 {
384 // Filter link database according to parameters.
385 $searchtags = !empty($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
386 $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
387
388 // Search tags + fullsearch.
389 if (empty($type) && ! empty($searchtags) && ! empty($searchterm)) {
390 $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT;
391 $request = array($searchtags, $searchterm);
392 }
393 // Search by tags.
394 elseif (! empty($searchtags)) {
395 $type = LinkFilter::$FILTER_TAG;
396 $request = $searchtags;
397 }
398 // Fulltext search.
399 elseif (! empty($searchterm)) {
400 $type = LinkFilter::$FILTER_TEXT;
401 $request = $searchterm;
402 }
403 // Otherwise, display without filtering.
404 else {
405 $type = '';
406 $request = '';
407 }
408
355 $linkFilter = new LinkFilter($this->_links); 409 $linkFilter = new LinkFilter($this->_links);
356 return $linkFilter->filter($type, $request, $casesensitive, $privateonly); 410 return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
357 } 411 }
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 3fd803cb..5e0d8015 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -44,7 +44,7 @@ class LinkFilter
44 * Filter links according to parameters. 44 * Filter links according to parameters.
45 * 45 *
46 * @param string $type Type of filter (eg. tags, permalink, etc.). 46 * @param string $type Type of filter (eg. tags, permalink, etc.).
47 * @param string $request Filter content. 47 * @param mixed $request Filter content.
48 * @param bool $casesensitive Optional: Perform case sensitive filter if true. 48 * @param bool $casesensitive Optional: Perform case sensitive filter if true.
49 * @param bool $privateonly Optional: Only returns private links if true. 49 * @param bool $privateonly Optional: Only returns private links if true.
50 * 50 *
@@ -110,6 +110,8 @@ class LinkFilter
110 * @param string $smallHash permalink hash. 110 * @param string $smallHash permalink hash.
111 * 111 *
112 * @return array $filtered array containing permalink data. 112 * @return array $filtered array containing permalink data.
113 *
114 * @throws LinkNotFoundException if the smallhash doesn't match any link.
113 */ 115 */
114 private function filterSmallHash($smallHash) 116 private function filterSmallHash($smallHash)
115 { 117 {
@@ -121,6 +123,11 @@ class LinkFilter
121 return $filtered; 123 return $filtered;
122 } 124 }
123 } 125 }
126
127 if (empty($filtered)) {
128 throw new LinkNotFoundException();
129 }
130
124 return $filtered; 131 return $filtered;
125 } 132 }
126 133
@@ -318,3 +325,8 @@ class LinkFilter
318 return array_filter(explode(' ', trim($tagsOut)), 'strlen'); 325 return array_filter(explode(' ', trim($tagsOut)), 'strlen');
319 } 326 }
320} 327}
328
329class LinkNotFoundException extends Exception
330{
331 protected $message = 'The link you are trying to reach does not exist or has been deleted.';
332}
diff --git a/application/LinkUtils.php b/application/LinkUtils.php
index 26dd6b67..d8dc8b5e 100644
--- a/application/LinkUtils.php
+++ b/application/LinkUtils.php
@@ -9,7 +9,7 @@
9 */ 9 */
10function html_extract_title($html) 10function html_extract_title($html)
11{ 11{
12 if (preg_match('!<title>(.*)</title>!is', $html, $matches)) { 12 if (preg_match('!<title>(.*?)</title>!is', $html, $matches)) {
13 return trim(str_replace("\n", ' ', $matches[1])); 13 return trim(str_replace("\n", ' ', $matches[1]));
14 } 14 }
15 return false; 15 return false;
diff --git a/application/Router.php b/application/Router.php
index 6185f08e..a1e594a0 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -15,6 +15,10 @@ class Router
15 15
16 public static $PAGE_DAILY = 'daily'; 16 public static $PAGE_DAILY = 'daily';
17 17
18 public static $PAGE_FEED_ATOM = 'atom';
19
20 public static $PAGE_FEED_RSS = 'rss';
21
18 public static $PAGE_TOOLS = 'tools'; 22 public static $PAGE_TOOLS = 'tools';
19 23
20 public static $PAGE_CHANGEPASSWORD = 'changepasswd'; 24 public static $PAGE_CHANGEPASSWORD = 'changepasswd';
@@ -49,7 +53,7 @@ class Router
49 * @param array $get $_SERVER['GET']. 53 * @param array $get $_SERVER['GET'].
50 * @param bool $loggedIn true if authenticated user. 54 * @param bool $loggedIn true if authenticated user.
51 * 55 *
52 * @return self::page found. 56 * @return string page found.
53 */ 57 */
54 public static function findPage($query, $get, $loggedIn) 58 public static function findPage($query, $get, $loggedIn)
55 { 59 {
@@ -79,6 +83,14 @@ class Router
79 return self::$PAGE_DAILY; 83 return self::$PAGE_DAILY;
80 } 84 }
81 85
86 if (startsWith($query, 'do='. self::$PAGE_FEED_ATOM)) {
87 return self::$PAGE_FEED_ATOM;
88 }
89
90 if (startsWith($query, 'do='. self::$PAGE_FEED_RSS)) {
91 return self::$PAGE_FEED_RSS;
92 }
93
82 // At this point, only loggedin pages. 94 // At this point, only loggedin pages.
83 if (!$loggedIn) { 95 if (!$loggedIn) {
84 return self::$PAGE_LINKLIST; 96 return self::$PAGE_LINKLIST;
diff --git a/application/Updater.php b/application/Updater.php
index 773a1ffa..58c13c07 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -137,7 +137,7 @@ class Updater
137 */ 137 */
138 public function updateMethodRenameDashTags() 138 public function updateMethodRenameDashTags()
139 { 139 {
140 $linklist = $this->linkDB->filter(); 140 $linklist = $this->linkDB->filterSearch();
141 foreach ($linklist as $link) { 141 foreach ($linklist as $link) {
142 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); 142 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); 143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
diff --git a/application/Utils.php b/application/Utils.php
index 3d819716..5b8ca508 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -63,14 +63,22 @@ function endsWith($haystack, $needle, $case=true)
63 63
64/** 64/**
65 * Htmlspecialchars wrapper 65 * Htmlspecialchars wrapper
66 * Support multidimensional array of strings.
66 * 67 *
67 * @param string $str the string to escape. 68 * @param mixed $input Data to escape: a single string or an array of strings.
68 * 69 *
69 * @return string escaped. 70 * @return string escaped.
70 */ 71 */
71function escape($str) 72function escape($input)
72{ 73{
73 return htmlspecialchars($str, ENT_COMPAT, 'UTF-8', false); 74 if (is_array($input)) {
75 $out = array();
76 foreach($input as $key => $value) {
77 $out[$key] = escape($value);
78 }
79 return $out;
80 }
81 return htmlspecialchars($input, ENT_COMPAT, 'UTF-8', false);
74} 82}
75 83
76/** 84/**
@@ -226,7 +234,7 @@ function space2nbsp($text)
226 * 234 *
227 * @return string formatted description. 235 * @return string formatted description.
228 */ 236 */
229function format_description($description, $redirector) { 237function format_description($description, $redirector = false) {
230 return nl2br(space2nbsp(text2clickable($description, $redirector))); 238 return nl2br(space2nbsp(text2clickable($description, $redirector)));
231} 239}
232 240
diff --git a/index.php b/index.php
index 850b350e..74091f37 100644
--- a/index.php
+++ b/index.php
@@ -154,6 +154,7 @@ if (is_file($GLOBALS['config']['CONFIG_FILE'])) {
154require_once 'application/ApplicationUtils.php'; 154require_once 'application/ApplicationUtils.php';
155require_once 'application/Cache.php'; 155require_once 'application/Cache.php';
156require_once 'application/CachedPage.php'; 156require_once 'application/CachedPage.php';
157require_once 'application/FeedBuilder.php';
157require_once 'application/FileUtils.php'; 158require_once 'application/FileUtils.php';
158require_once 'application/HttpUtils.php'; 159require_once 'application/HttpUtils.php';
159require_once 'application/LinkDB.php'; 160require_once 'application/LinkDB.php';
@@ -483,7 +484,7 @@ if (isset($_POST['login']))
483 if (isset($_POST['returnurl'])) { 484 if (isset($_POST['returnurl'])) {
484 // Prevent loops over login screen. 485 // Prevent loops over login screen.
485 if (strpos($_POST['returnurl'], 'do=login') === false) { 486 if (strpos($_POST['returnurl'], 'do=login') === false) {
486 header('Location: '. escape($_POST['returnurl'])); 487 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
487 exit; 488 exit;
488 } 489 }
489 } 490 }
@@ -637,6 +638,29 @@ class pageBuilder
637 $this->tpl->assign($what,$where); 638 $this->tpl->assign($what,$where);
638 } 639 }
639 640
641 /**
642 * Assign an array of data to the template builder.
643 *
644 * @param array $data Data to assign.
645 *
646 * @return false if invalid data.
647 */
648 public function assignAll($data)
649 {
650 // Lazy initialization
651 if ($this->tpl === false) {
652 $this->initialize();
653 }
654
655 if (empty($data) || !is_array($data)){
656 return false;
657 }
658
659 foreach ($data as $key => $value) {
660 $this->assign($key, $value);
661 }
662 }
663
640 // Render a specific page (using a template). 664 // Render a specific page (using a template).
641 // e.g. pb.renderPage('picwall') 665 // e.g. pb.renderPage('picwall')
642 public function renderPage($page) 666 public function renderPage($page)
@@ -659,232 +683,6 @@ class pageBuilder
659} 683}
660 684
661// ------------------------------------------------------------------------------------------ 685// ------------------------------------------------------------------------------------------
662// Output the last N links in RSS 2.0 format.
663function showRSS()
664{
665 header('Content-Type: application/rss+xml; charset=utf-8');
666
667 // $usepermalink : If true, use permalink instead of final link.
668 // User just has to add 'permalink' in URL parameters. e.g. http://mysite.com/shaarli/?do=rss&permalinks
669 // Also enabled through a config option
670 $usepermalinks = isset($_GET['permalinks']) || !$GLOBALS['config']['ENABLE_RSS_PERMALINKS'];
671
672 // Cache system
673 $query = $_SERVER["QUERY_STRING"];
674 $cache = new CachedPage(
675 $GLOBALS['config']['PAGECACHE'],
676 page_url($_SERVER),
677 startsWith($query,'do=rss') && !isLoggedIn()
678 );
679 $cached = $cache->cachedVersion();
680 if (! empty($cached)) {
681 echo $cached;
682 exit;
683 }
684
685 // If cached was not found (or not usable), then read the database and build the response:
686 $LINKSDB = new LinkDB(
687 $GLOBALS['config']['DATASTORE'],
688 isLoggedIn(),
689 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
690 $GLOBALS['redirector']
691 );
692 // Read links from database (and filter private links if user it not logged in).
693
694 // Optionally filter the results:
695 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
696 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
697 if (! empty($searchtags) && ! empty($searchterm)) {
698 $linksToDisplay = $LINKSDB->filter(
699 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
700 array($searchtags, $searchterm)
701 );
702 }
703 elseif ($searchtags) {
704 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
705 }
706 elseif ($searchterm) {
707 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
708 }
709 else {
710 $linksToDisplay = $LINKSDB;
711 }
712
713 $nblinksToDisplay = 50; // Number of links to display.
714 // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
715 if (!empty($_GET['nb'])) {
716 $nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
717 }
718
719 $pageaddr = escape(index_url($_SERVER));
720 echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
721 echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>';
722 echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n";
723 if (!empty($GLOBALS['config']['PUBSUBHUB_URL']))
724 {
725 echo '<!-- PubSubHubbub Discovery -->';
726 echo '<link rel="hub" href="'.escape($GLOBALS['config']['PUBSUBHUB_URL']).'" xmlns="http://www.w3.org/2005/Atom" />';
727 echo '<link rel="self" href="'.$pageaddr.'?do=rss" xmlns="http://www.w3.org/2005/Atom" />';
728 echo '<!-- End Of PubSubHubbub Discovery -->';
729 }
730 $i=0;
731 $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // No, I can't use array_keys().
732 while ($i<$nblinksToDisplay && $i<count($keys))
733 {
734 $link = $linksToDisplay[$keys[$i]];
735 $guid = $pageaddr.'?'.smallHash($link['linkdate']);
736 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
737 $absurl = $link['url'];
738 if (startsWith($absurl,'?')) $absurl=$pageaddr.$absurl; // make permalink URL absolute
739 if ($usepermalinks===true)
740 echo '<item><title>'.$link['title'].'</title><guid isPermaLink="true">'.$guid.'</guid><link>'.$guid.'</link>';
741 else
742 echo '<item><title>'.$link['title'].'</title><guid isPermaLink="false">'.$guid.'</guid><link>'.$absurl.'</link>';
743 if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) {
744 echo '<pubDate>'.escape($date->format(DateTime::RSS))."</pubDate>\n";
745 }
746 if ($link['tags']!='') // Adding tags to each RSS entry (as mentioned in RSS specification)
747 {
748 foreach(explode(' ',$link['tags']) as $tag) { echo '<category domain="'.$pageaddr.'">'.$tag.'</category>'."\n"; }
749 }
750
751 // Add permalink in description
752 $descriptionlink = '(<a href="'.$guid.'">Permalink</a>)';
753 // If user wants permalinks first, put the final link in description
754 if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)';
755 if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink;
756 echo '<description><![CDATA['.
757 format_description($link['description'], $GLOBALS['redirector']) .
758 $descriptionlink . ']]></description>' . "\n</item>\n";
759 $i++;
760 }
761 echo '</channel></rss><!-- Cached version of '.escape(page_url($_SERVER)).' -->';
762
763 $cache->cache(ob_get_contents());
764 ob_end_flush();
765 exit;
766}
767
768// ------------------------------------------------------------------------------------------
769// Output the last N links in ATOM format.
770function showATOM()
771{
772 header('Content-Type: application/atom+xml; charset=utf-8');
773
774 // $usepermalink : If true, use permalink instead of final link.
775 // User just has to add 'permalink' in URL parameters. e.g. http://mysite.com/shaarli/?do=atom&permalinks
776 $usepermalinks = isset($_GET['permalinks']) || !$GLOBALS['config']['ENABLE_RSS_PERMALINKS'];
777
778 // Cache system
779 $query = $_SERVER["QUERY_STRING"];
780 $cache = new CachedPage(
781 $GLOBALS['config']['PAGECACHE'],
782 page_url($_SERVER),
783 startsWith($query,'do=atom') && !isLoggedIn()
784 );
785 $cached = $cache->cachedVersion();
786 if (!empty($cached)) {
787 echo $cached;
788 exit;
789 }
790
791 // If cached was not found (or not usable), then read the database and build the response:
792 // Read links from database (and filter private links if used it not logged in).
793 $LINKSDB = new LinkDB(
794 $GLOBALS['config']['DATASTORE'],
795 isLoggedIn(),
796 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
797 $GLOBALS['redirector']
798 );
799
800 // Optionally filter the results:
801 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
802 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
803 if (! empty($searchtags) && ! empty($searchterm)) {
804 $linksToDisplay = $LINKSDB->filter(
805 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
806 array($searchtags, $searchterm)
807 );
808 }
809 elseif ($searchtags) {
810 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
811 }
812 elseif ($searchterm) {
813 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
814 }
815 else {
816 $linksToDisplay = $LINKSDB;
817 }
818
819 $nblinksToDisplay = 50; // Number of links to display.
820 // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
821 if (!empty($_GET['nb'])) {
822 $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
823 }
824
825 $pageaddr=escape(index_url($_SERVER));
826 $latestDate = '';
827 $entries='';
828 $i=0;
829 $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // No, I can't use array_keys().
830 while ($i<$nblinksToDisplay && $i<count($keys))
831 {
832 $link = $linksToDisplay[$keys[$i]];
833 $guid = $pageaddr.'?'.smallHash($link['linkdate']);
834 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
835 $iso8601date = $date->format(DateTime::ISO8601);
836 $latestDate = max($latestDate, $iso8601date);
837 $absurl = $link['url'];
838 if (startsWith($absurl,'?')) $absurl=$pageaddr.$absurl; // make permalink URL absolute
839 $entries.='<entry><title>'.$link['title'].'</title>';
840 if ($usepermalinks===true)
841 $entries.='<link href="'.$guid.'" /><id>'.$guid.'</id>';
842 else
843 $entries.='<link href="'.$absurl.'" /><id>'.$guid.'</id>';
844
845 if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) {
846 $entries.='<updated>'.escape($iso8601date).'</updated>';
847 }
848
849 // Add permalink in description
850 $descriptionlink = '(<a href="'.$guid.'">Permalink</a>)';
851 // If user wants permalinks first, put the final link in description
852 if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)';
853 if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink;
854
855 $entries .= '<content type="html"><![CDATA['.
856 format_description($link['description'], $GLOBALS['redirector']) .
857 $descriptionlink . "]]></content>\n";
858 if ($link['tags']!='') // Adding tags to each ATOM entry (as mentioned in ATOM specification)
859 {
860 foreach(explode(' ',$link['tags']) as $tag)
861 { $entries.='<category scheme="'.$pageaddr.'" term="'.$tag.'" />'."\n"; }
862 }
863 $entries.="</entry>\n";
864 $i++;
865 }
866 $feed='<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">';
867 $feed.='<title>'.$GLOBALS['title'].'</title>';
868 if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) $feed.='<updated>'.escape($latestDate).'</updated>';
869 $feed.='<link rel="self" href="'.escape(server_url($_SERVER).$_SERVER["REQUEST_URI"]).'" />';
870 if (!empty($GLOBALS['config']['PUBSUBHUB_URL']))
871 {
872 $feed.='<!-- PubSubHubbub Discovery -->';
873 $feed.='<link rel="hub" href="'.escape($GLOBALS['config']['PUBSUBHUB_URL']).'" />';
874 $feed.='<!-- End Of PubSubHubbub Discovery -->';
875 }
876 $feed.='<author><name>'.$pageaddr.'</name><uri>'.$pageaddr.'</uri></author>';
877 $feed.='<id>'.$pageaddr.'</id>'."\n\n"; // Yes, I know I should use a real IRI (RFC3987), but the site URL will do.
878 $feed.=$entries;
879 $feed.='</feed><!-- Cached version of '.escape(page_url($_SERVER)).' -->';
880 echo $feed;
881
882 $cache->cache(ob_get_contents());
883 ob_end_flush();
884 exit;
885}
886
887// ------------------------------------------------------------------------------------------
888// Daily RSS feed: 1 RSS entry per day giving all the links on that day. 686// Daily RSS feed: 1 RSS entry per day giving all the links on that day.
889// Gives the last 7 days (which have links). 687// Gives the last 7 days (which have links).
890// This RSS feed cannot be filtered. 688// This RSS feed cannot be filtered.
@@ -1018,7 +816,7 @@ function showDaily($pageBuilder)
1018 } 816 }
1019 817
1020 try { 818 try {
1021 $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day); 819 $linksToDisplay = $LINKSDB->filterDay($day);
1022 } catch (Exception $exc) { 820 } catch (Exception $exc) {
1023 error_log($exc); 821 error_log($exc);
1024 $linksToDisplay = array(); 822 $linksToDisplay = array();
@@ -1164,24 +962,7 @@ function renderPage()
1164 if ($targetPage == Router::$PAGE_PICWALL) 962 if ($targetPage == Router::$PAGE_PICWALL)
1165 { 963 {
1166 // Optionally filter the results: 964 // Optionally filter the results:
1167 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : ''; 965 $links = $LINKSDB->filterSearch($_GET);
1168 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
1169 if (! empty($searchtags) && ! empty($searchterm)) {
1170 $links = $LINKSDB->filter(
1171 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
1172 array($searchtags, $searchterm)
1173 );
1174 }
1175 elseif ($searchtags) {
1176 $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $searchtags);
1177 }
1178 elseif ($searchterm) {
1179 $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $searchterm);
1180 }
1181 else {
1182 $links = $LINKSDB;
1183 }
1184
1185 $linksToDisplay = array(); 966 $linksToDisplay = array();
1186 967
1187 // Get only links which have a thumbnail. 968 // Get only links which have a thumbnail.
@@ -1260,6 +1041,49 @@ function renderPage()
1260 showDaily($PAGE); 1041 showDaily($PAGE);
1261 } 1042 }
1262 1043
1044 // ATOM and RSS feed.
1045 if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
1046 $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
1047 header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
1048
1049 // Cache system
1050 $query = $_SERVER['QUERY_STRING'];
1051 $cache = new CachedPage(
1052 $GLOBALS['config']['PAGECACHE'],
1053 page_url($_SERVER),
1054 startsWith($query,'do='. $targetPage) && !isLoggedIn()
1055 );
1056 $cached = $cache->cachedVersion();
1057 if (false && !empty($cached)) {
1058 echo $cached;
1059 exit;
1060 }
1061
1062 // Generate data.
1063 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn());
1064 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
1065 $feedGenerator->setHideDates($GLOBALS['config']['HIDE_TIMESTAMPS'] && !isLoggedIn());
1066 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$GLOBALS['config']['ENABLE_RSS_PERMALINKS']);
1067 if (!empty($GLOBALS['config']['PUBSUBHUB_URL'])) {
1068 $feedGenerator->setPubsubhubUrl($GLOBALS['config']['PUBSUBHUB_URL']);
1069 }
1070 $data = $feedGenerator->buildData();
1071
1072 // Process plugin hook.
1073 $pluginManager = PluginManager::getInstance();
1074 $pluginManager->executeHooks('render_feed', $data, array(
1075 'loggedin' => isLoggedIn(),
1076 'target' => $targetPage,
1077 ));
1078
1079 // Render the template.
1080 $PAGE->assignAll($data);
1081 $PAGE->renderPage('feed.'. $feedType);
1082 $cache->cache(ob_get_contents());
1083 ob_end_flush();
1084 exit;
1085 }
1086
1263 // Display openseach plugin (XML) 1087 // Display openseach plugin (XML)
1264 if ($targetPage == Router::$PAGE_OPENSEARCH) { 1088 if ($targetPage == Router::$PAGE_OPENSEARCH) {
1265 header('Content-Type: application/xml; charset=utf-8'); 1089 header('Content-Type: application/xml; charset=utf-8');
@@ -1511,9 +1335,9 @@ function renderPage()
1511 1335
1512 // Delete a tag: 1336 // Delete a tag:
1513 if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { 1337 if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) {
1514 $needle=trim($_POST['fromtag']); 1338 $needle = trim($_POST['fromtag']);
1515 // True for case-sensitive tag search. 1339 // True for case-sensitive tag search.
1516 $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); 1340 $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true);
1517 foreach($linksToAlter as $key=>$value) 1341 foreach($linksToAlter as $key=>$value)
1518 { 1342 {
1519 $tags = explode(' ',trim($value['tags'])); 1343 $tags = explode(' ',trim($value['tags']));
@@ -1528,9 +1352,9 @@ function renderPage()
1528 1352
1529 // Rename a tag: 1353 // Rename a tag:
1530 if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { 1354 if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) {
1531 $needle=trim($_POST['fromtag']); 1355 $needle = trim($_POST['fromtag']);
1532 // True for case-sensitive tag search. 1356 // True for case-sensitive tag search.
1533 $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); 1357 $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true);
1534 foreach($linksToAlter as $key=>$value) 1358 foreach($linksToAlter as $key=>$value)
1535 { 1359 {
1536 $tags = explode(' ',trim($value['tags'])); 1360 $tags = explode(' ',trim($value['tags']));
@@ -1966,60 +1790,32 @@ function importFile()
1966 } 1790 }
1967} 1791}
1968 1792
1969// ----------------------------------------------------------------------------------------------- 1793/**
1970// Template for the list of links (<div id="linklist">) 1794 * Template for the list of links (<div id="linklist">)
1971// This function fills all the necessary fields in the $PAGE for the template 'linklist.html' 1795 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1796 *
1797 * @param pageBuilder $PAGE pageBuilder instance.
1798 * @param LinkDB $LINKSDB LinkDB instance.
1799 */
1972function buildLinkList($PAGE,$LINKSDB) 1800function buildLinkList($PAGE,$LINKSDB)
1973{ 1801{
1974 // Filter link database according to parameters. 1802 // Used in templates
1975 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : ''; 1803 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : '';
1976 $searchterm = !empty($_GET['searchterm']) ? escape(trim($_GET['searchterm'])) : ''; 1804 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : '';
1977 $privateonly = !empty($_SESSION['privateonly']) ? true : false;
1978
1979 // Search tags + fullsearch.
1980 if (! empty($searchtags) && ! empty($searchterm)) {
1981 $linksToDisplay = $LINKSDB->filter(
1982 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
1983 array($searchtags, $searchterm),
1984 false,
1985 $privateonly
1986 );
1987 }
1988 // Search by tags.
1989 elseif (! empty($searchtags)) {
1990 $linksToDisplay = $LINKSDB->filter(
1991 LinkFilter::$FILTER_TAG,
1992 $searchtags,
1993 false,
1994 $privateonly
1995 );
1996 }
1997 // Fulltext search.
1998 elseif (! empty($searchterm)) {
1999 $linksToDisplay = $LINKSDB->filter(
2000 LinkFilter::$FILTER_TEXT,
2001 $searchterm,
2002 false,
2003 $privateonly
2004 );
2005 }
2006 // Detect smallHashes in URL.
2007 elseif (! empty($_SERVER['QUERY_STRING'])
2008 && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])
2009 ) {
2010 $linksToDisplay = $LINKSDB->filter(
2011 LinkFilter::$FILTER_HASH,
2012 substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6)
2013 );
2014 1805
2015 if (count($linksToDisplay) == 0) { 1806 // Smallhash filter
2016 $PAGE->render404('The link you are trying to reach does not exist or has been deleted.'); 1807 if (! empty($_SERVER['QUERY_STRING'])
1808 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
1809 try {
1810 $linksToDisplay = $LINKSDB->filterHash($_SERVER['QUERY_STRING']);
1811 } catch (LinkNotFoundException $e) {
1812 $PAGE->render404($e->getMessage());
2017 exit; 1813 exit;
2018 } 1814 }
2019 } 1815 } else {
2020 // Otherwise, display without filtering. 1816 // Filter links according search parameters.
2021 else { 1817 $privateonly = !empty($_SESSION['privateonly']);
2022 $linksToDisplay = $LINKSDB->filter('', '', false, $privateonly); 1818 $linksToDisplay = $LINKSDB->filterSearch($_GET, false, $privateonly);
2023 } 1819 }
2024 1820
2025 // ---- Handle paging. 1821 // ---- Handle paging.
@@ -2584,8 +2380,6 @@ function resizeImage($filepath)
2584} 2380}
2585 2381
2586if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database. 2382if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database.
2587if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; }
2588if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; }
2589if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=dailyrss')) { showDailyRSS(); exit; } 2383if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=dailyrss')) { showDailyRSS(); exit; }
2590if (!isset($_SESSION['LINKS_PER_PAGE'])) $_SESSION['LINKS_PER_PAGE']=$GLOBALS['config']['LINKS_PER_PAGE']; 2384if (!isset($_SESSION['LINKS_PER_PAGE'])) $_SESSION['LINKS_PER_PAGE']=$GLOBALS['config']['LINKS_PER_PAGE'];
2591renderPage(); 2385renderPage();
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index f5f028e0..18834e53 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -322,4 +322,29 @@ function hook_demo_plugin_delete_link($data)
322 if (strpos($data['url'], 'youtube.com') !== false) { 322 if (strpos($data['url'], 'youtube.com') !== false) {
323 exit('You can not delete a YouTube link. Don\'t ask.'); 323 exit('You can not delete a YouTube link. Don\'t ask.');
324 } 324 }
325} \ No newline at end of file 325}
326
327/**
328 * Execute render_feed hook.
329 * Called with ATOM and RSS feed.
330 *
331 * Special data keys:
332 * - _PAGE_: current page
333 * - _LOGGEDIN_: true/false
334 *
335 * @param array $data data passed to plugin
336 *
337 * @return array altered $data.
338 */
339function hook_demo_plugin_render_feed($data)
340{
341 foreach ($data['links'] as &$link) {
342 if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) {
343 $link['description'] .= ' - ATOM Feed' ;
344 }
345 elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) {
346 $link['description'] .= ' - RSS Feed';
347 }
348 }
349 return $data;
350}
diff --git a/tests/FeedBuilderTest.php b/tests/FeedBuilderTest.php
new file mode 100644
index 00000000..069b1581
--- /dev/null
+++ b/tests/FeedBuilderTest.php
@@ -0,0 +1,212 @@
1<?php
2
3require_once 'application/FeedBuilder.php';
4require_once 'application/LinkDB.php';
5
6/**
7 * FeedBuilderTest class.
8 *
9 * Unit tests for FeedBuilder.
10 */
11class FeedBuilderTest extends PHPUnit_Framework_TestCase
12{
13 /**
14 * @var string locale Basque (Spain).
15 */
16 public static $LOCALE = 'eu_ES';
17
18 /**
19 * @var string language in RSS format.
20 */
21 public static $RSS_LANGUAGE = 'eu-es';
22
23 /**
24 * @var string language in ATOM format.
25 */
26 public static $ATOM_LANGUAGUE = 'eu';
27
28 protected static $testDatastore = 'sandbox/datastore.php';
29
30 public static $linkDB;
31
32 public static $serverInfo;
33
34 /**
35 * Called before every test method.
36 */
37 public static function setUpBeforeClass()
38 {
39 $refLinkDB = new ReferenceLinkDB();
40 $refLinkDB->write(self::$testDatastore);
41 self::$linkDB = new LinkDB(self::$testDatastore, true, false);
42 self::$serverInfo = array(
43 'HTTPS' => 'Off',
44 'SERVER_NAME' => 'host.tld',
45 'SERVER_PORT' => '80',
46 'SCRIPT_NAME' => '/index.php',
47 'REQUEST_URI' => '/index.php?do=feed',
48 );
49 }
50
51 /**
52 * Test GetTypeLanguage().
53 */
54 public function testGetTypeLanguage()
55 {
56 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false);
57 $feedBuilder->setLocale(self::$LOCALE);
58 $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
59 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false);
60 $feedBuilder->setLocale(self::$LOCALE);
61 $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
62 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false);
63 $this->assertEquals('en', $feedBuilder->getTypeLanguage());
64 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false);
65 $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
66 }
67
68 /**
69 * Test buildData with RSS feed.
70 */
71 public function testRSSBuildData()
72 {
73 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_RSS, self::$serverInfo, null, false);
74 $feedBuilder->setLocale(self::$LOCALE);
75 $data = $feedBuilder->buildData();
76 // Test headers (RSS)
77 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']);
78 $this->assertEmpty($data['pubsubhub_url']);
79 $this->assertEquals('Tue, 10 Mar 2015 11:46:51 +0100', $data['last_update']);
80 $this->assertEquals(true, $data['show_dates']);
81 $this->assertEquals('http://host.tld/index.php?do=feed', $data['self_link']);
82 $this->assertEquals('http://host.tld/', $data['index_url']);
83 $this->assertFalse($data['usepermalinks']);
84 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
85
86 // Test first link (note link)
87 $link = array_shift($data['links']);
88 $this->assertEquals('20150310_114651', $link['linkdate']);
89 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
90 $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
91 $this->assertEquals('Tue, 10 Mar 2015 11:46:51 +0100', $link['iso_date']);
92 $this->assertContains('Stallman has a beard', $link['description']);
93 $this->assertContains('Permalink', $link['description']);
94 $this->assertContains('http://host.tld/?WDWyig', $link['description']);
95 $this->assertEquals(1, count($link['taglist']));
96 $this->assertEquals('stuff', $link['taglist'][0]);
97
98 // Test URL with external link.
99 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $data['links']['20150310_114633']['url']);
100
101 // Test multitags.
102 $this->assertEquals(5, count($data['links']['20141125_084734']['taglist']));
103 $this->assertEquals('css', $data['links']['20141125_084734']['taglist'][0]);
104 }
105
106 /**
107 * Test buildData with ATOM feed (test only specific to ATOM).
108 */
109 public function testAtomBuildData()
110 {
111 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false);
112 $feedBuilder->setLocale(self::$LOCALE);
113 $data = $feedBuilder->buildData();
114 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
115 $link = array_shift($data['links']);
116 $this->assertEquals('2015-03-10T11:46:51+01:00', $link['iso_date']);
117 }
118
119 /**
120 * Test buildData with search criteria.
121 */
122 public function testBuildDataFiltered()
123 {
124 $criteria = array(
125 'searchtags' => 'stuff',
126 'searchterm' => 'beard',
127 );
128 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false);
129 $feedBuilder->setLocale(self::$LOCALE);
130 $data = $feedBuilder->buildData();
131 $this->assertEquals(1, count($data['links']));
132 $link = array_shift($data['links']);
133 $this->assertEquals('20150310_114651', $link['linkdate']);
134 }
135
136 /**
137 * Test buildData with nb limit.
138 */
139 public function testBuildDataCount()
140 {
141 $criteria = array(
142 'nb' => '1',
143 );
144 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false);
145 $feedBuilder->setLocale(self::$LOCALE);
146 $data = $feedBuilder->buildData();
147 $this->assertEquals(1, count($data['links']));
148 $link = array_shift($data['links']);
149 $this->assertEquals('20150310_114651', $link['linkdate']);
150 }
151
152 /**
153 * Test buildData with permalinks on.
154 */
155 public function testBuildDataPermalinks()
156 {
157 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false);
158 $feedBuilder->setLocale(self::$LOCALE);
159 $feedBuilder->setUsePermalinks(true);
160 $data = $feedBuilder->buildData();
161 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
162 $this->assertTrue($data['usepermalinks']);
163 // First link is a permalink
164 $link = array_shift($data['links']);
165 $this->assertEquals('20150310_114651', $link['linkdate']);
166 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
167 $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
168 $this->assertContains('Direct link', $link['description']);
169 $this->assertContains('http://host.tld/?WDWyig', $link['description']);
170 // Second link is a direct link
171 $link = array_shift($data['links']);
172 $this->assertEquals('20150310_114633', $link['linkdate']);
173 $this->assertEquals('http://host.tld/?kLHmZg', $link['guid']);
174 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
175 $this->assertContains('Direct link', $link['description']);
176 $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
177 }
178
179 /**
180 * Test buildData with hide dates settings.
181 */
182 public function testBuildDataHideDates()
183 {
184 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false);
185 $feedBuilder->setLocale(self::$LOCALE);
186 $feedBuilder->setHideDates(true);
187 $data = $feedBuilder->buildData();
188 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
189 $this->assertFalse($data['show_dates']);
190
191 // Show dates while logged in
192 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, true);
193 $feedBuilder->setLocale(self::$LOCALE);
194 $feedBuilder->setHideDates(true);
195 $data = $feedBuilder->buildData();
196 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
197 $this->assertTrue($data['show_dates']);
198 }
199
200 /**
201 * Test buildData with hide dates settings.
202 */
203 public function testBuildDataPubsubhub()
204 {
205 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false);
206 $feedBuilder->setLocale(self::$LOCALE);
207 $feedBuilder->setPubsubhubUrl('http://pubsubhub.io');
208 $data = $feedBuilder->buildData();
209 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
210 $this->assertEquals('http://pubsubhub.io', $data['pubsubhub_url']);
211 }
212}
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php
index b6a273b3..52d31400 100644
--- a/tests/LinkDBTest.php
+++ b/tests/LinkDBTest.php
@@ -17,8 +17,20 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
17{ 17{
18 // datastore to test write operations 18 // datastore to test write operations
19 protected static $testDatastore = 'sandbox/datastore.php'; 19 protected static $testDatastore = 'sandbox/datastore.php';
20
21 /**
22 * @var ReferenceLinkDB instance.
23 */
20 protected static $refDB = null; 24 protected static $refDB = null;
25
26 /**
27 * @var LinkDB public LinkDB instance.
28 */
21 protected static $publicLinkDB = null; 29 protected static $publicLinkDB = null;
30
31 /**
32 * @var LinkDB private LinkDB instance.
33 */
22 protected static $privateLinkDB = null; 34 protected static $privateLinkDB = null;
23 35
24 /** 36 /**
@@ -335,9 +347,10 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
335 public function testFilterString() 347 public function testFilterString()
336 { 348 {
337 $tags = 'dev cartoon'; 349 $tags = 'dev cartoon';
350 $request = array('searchtags' => $tags);
338 $this->assertEquals( 351 $this->assertEquals(
339 2, 352 2,
340 count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) 353 count(self::$privateLinkDB->filterSearch($request, true, false))
341 ); 354 );
342 } 355 }
343 356
@@ -347,9 +360,10 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
347 public function testFilterArray() 360 public function testFilterArray()
348 { 361 {
349 $tags = array('dev', 'cartoon'); 362 $tags = array('dev', 'cartoon');
363 $request = array('searchtags' => $tags);
350 $this->assertEquals( 364 $this->assertEquals(
351 2, 365 2,
352 count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) 366 count(self::$privateLinkDB->filterSearch($request, true, false))
353 ); 367 );
354 } 368 }
355 369
@@ -360,14 +374,48 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
360 public function testHiddenTags() 374 public function testHiddenTags()
361 { 375 {
362 $tags = '.hidden'; 376 $tags = '.hidden';
377 $request = array('searchtags' => $tags);
363 $this->assertEquals( 378 $this->assertEquals(
364 1, 379 1,
365 count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) 380 count(self::$privateLinkDB->filterSearch($request, true, false))
366 ); 381 );
367 382
368 $this->assertEquals( 383 $this->assertEquals(
369 0, 384 0,
370 count(self::$publicLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) 385 count(self::$publicLinkDB->filterSearch($request, true, false))
371 ); 386 );
372 } 387 }
388
389 /**
390 * Test filterHash() with a valid smallhash.
391 */
392 public function testFilterHashValid()
393 {
394 $request = smallHash('20150310_114651');
395 $this->assertEquals(
396 1,
397 count(self::$publicLinkDB->filterHash($request))
398 );
399 }
400
401 /**
402 * Test filterHash() with an invalid smallhash.
403 *
404 * @expectedException LinkNotFoundException
405 */
406 public function testFilterHashInValid1()
407 {
408 $request = 'blabla';
409 self::$publicLinkDB->filterHash($request);
410 }
411
412 /**
413 * Test filterHash() with an empty smallhash.
414 *
415 * @expectedException LinkNotFoundException
416 */
417 public function testFilterHashInValid()
418 {
419 self::$publicLinkDB->filterHash('');
420 }
373} 421}
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php
index ef1cc10a..1620bb78 100644
--- a/tests/LinkFilterTest.php
+++ b/tests/LinkFilterTest.php
@@ -12,8 +12,6 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
12 */ 12 */
13 protected static $linkFilter; 13 protected static $linkFilter;
14 14
15 protected static $NB_LINKS_REFDB = 7;
16
17 /** 15 /**
18 * Instanciate linkFilter with ReferenceLinkDB data. 16 * Instanciate linkFilter with ReferenceLinkDB data.
19 */ 17 */
@@ -29,7 +27,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
29 public function testFilter() 27 public function testFilter()
30 { 28 {
31 $this->assertEquals( 29 $this->assertEquals(
32 self::$NB_LINKS_REFDB, 30 ReferenceLinkDB::$NB_LINKS_TOTAL,
33 count(self::$linkFilter->filter('', '')) 31 count(self::$linkFilter->filter('', ''))
34 ); 32 );
35 33
@@ -40,12 +38,12 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
40 ); 38 );
41 39
42 $this->assertEquals( 40 $this->assertEquals(
43 self::$NB_LINKS_REFDB, 41 ReferenceLinkDB::$NB_LINKS_TOTAL,
44 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) 42 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, ''))
45 ); 43 );
46 44
47 $this->assertEquals( 45 $this->assertEquals(
48 self::$NB_LINKS_REFDB, 46 ReferenceLinkDB::$NB_LINKS_TOTAL,
49 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '')) 47 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, ''))
50 ); 48 );
51 } 49 }
@@ -167,13 +165,12 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
167 165
168 /** 166 /**
169 * No link for this hash 167 * No link for this hash
168 *
169 * @expectedException LinkNotFoundException
170 */ 170 */
171 public function testFilterUnknownSmallHash() 171 public function testFilterUnknownSmallHash()
172 { 172 {
173 $this->assertEquals( 173 self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah');
174 0,
175 count(self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah'))
176 );
177 } 174 }
178 175
179 /** 176 /**
@@ -383,7 +380,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
383 )) 380 ))
384 ); 381 );
385 $this->assertEquals( 382 $this->assertEquals(
386 self::$NB_LINKS_REFDB, 383 ReferenceLinkDB::$NB_LINKS_TOTAL,
387 count(self::$linkFilter->filter( 384 count(self::$linkFilter->filter(
388 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 385 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
389 '' 386 ''
diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php
index c2257590..609a80cb 100644
--- a/tests/LinkUtilsTest.php
+++ b/tests/LinkUtilsTest.php
@@ -15,6 +15,8 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase
15 $title = 'Read me please.'; 15 $title = 'Read me please.';
16 $html = '<html><meta>stuff</meta><title>'. $title .'</title></html>'; 16 $html = '<html><meta>stuff</meta><title>'. $title .'</title></html>';
17 $this->assertEquals($title, html_extract_title($html)); 17 $this->assertEquals($title, html_extract_title($html));
18 $html = '<html><title>'. $title .'</title>blabla<title>another</title></html>';
19 $this->assertEquals($title, html_extract_title($html));
18 } 20 }
19 21
20 /** 22 /**
diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php
index d865066b..a29d9067 100644
--- a/tests/Updater/UpdaterTest.php
+++ b/tests/Updater/UpdaterTest.php
@@ -236,9 +236,9 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
236 $refDB = new ReferenceLinkDB(); 236 $refDB = new ReferenceLinkDB();
237 $refDB->write(self::$testDatastore); 237 $refDB->write(self::$testDatastore);
238 $linkDB = new LinkDB(self::$testDatastore, true, false); 238 $linkDB = new LinkDB(self::$testDatastore, true, false);
239 $this->assertEmpty($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude')); 239 $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
240 $updater = new Updater(array(), self::$configFields, $linkDB, true); 240 $updater = new Updater(array(), self::$configFields, $linkDB, true);
241 $updater->updateMethodRenameDashTags(); 241 $updater->updateMethodRenameDashTags();
242 $this->assertNotEmpty($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude')); 242 $this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
243 } 243 }
244} 244}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index 61faef05..dc4f5dfa 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -4,6 +4,8 @@
4 */ 4 */
5class ReferenceLinkDB 5class ReferenceLinkDB
6{ 6{
7 public static $NB_LINKS_TOTAL = 7;
8
7 private $_links = array(); 9 private $_links = array();
8 private $_publicCount = 0; 10 private $_publicCount = 0;
9 private $_privateCount = 0; 11 private $_privateCount = 0;
@@ -14,6 +16,15 @@ class ReferenceLinkDB
14 function __construct() 16 function __construct()
15 { 17 {
16 $this->addLink( 18 $this->addLink(
19 'Link title: @website',
20 '?WDWyig',
21 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this.',
22 0,
23 '20150310_114651',
24 'stuff'
25 );
26
27 $this->addLink(
17 'Free as in Freedom 2.0 @website', 28 'Free as in Freedom 2.0 @website',
18 'https://static.fsf.org/nosvn/faif-2.0.pdf', 29 'https://static.fsf.org/nosvn/faif-2.0.pdf',
19 'Richard Stallman and the Free Software Revolution. Read this.', 30 'Richard Stallman and the Free Software Revolution. Read this.',
@@ -23,15 +34,6 @@ class ReferenceLinkDB
23 ); 34 );
24 35
25 $this->addLink( 36 $this->addLink(
26 'Link title: @website',
27 'local',
28 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this.',
29 0,
30 '20150310_114651',
31 'stuff'
32 );
33
34 $this->addLink(
35 'MediaGoblin', 37 'MediaGoblin',
36 'http://mediagoblin.org/', 38 'http://mediagoblin.org/',
37 'A free software media publishing platform', 39 'A free software media publishing platform',
diff --git a/tpl/configure.html b/tpl/configure.html
index 9c725a51..77c8b7d9 100644
--- a/tpl/configure.html
+++ b/tpl/configure.html
@@ -22,9 +22,12 @@
22 <input type="checkbox" name="privateLinkByDefault" id="privateLinkByDefault" {if="!empty($GLOBALS['privateLinkByDefault'])"}checked{/if}/><label for="privateLinkByDefault">&nbsp;All new links are private by default</label></td> 22 <input type="checkbox" name="privateLinkByDefault" id="privateLinkByDefault" {if="!empty($GLOBALS['privateLinkByDefault'])"}checked{/if}/><label for="privateLinkByDefault">&nbsp;All new links are private by default</label></td>
23 </tr> 23 </tr>
24 <tr> 24 <tr>
25 <td valign="top"><b>Enable RSS Permalinks</b></td> 25 <td valign="top"><b>RSS direct links</b></td>
26 <td> 26 <td>
27 <input type="checkbox" name="enableRssPermalinks" id="enableRssPermalinks" {if="!empty($GLOBALS['config']['ENABLE_RSS_PERMALINKS'])"}checked{/if}/><label for="enableRssPermalinks">&nbsp;Switches the RSS feed URLs between full URLs and shortlinks. Enabling it will show a permalink in the description, and the feed item will be linked to the absolute URL. Disabling it swaps this behaviour around (permalink in title and link in description). RSS Permalinks are currently <b>{if="$GLOBALS['config']['ENABLE_RSS_PERMALINKS']"}enabled{else}disabled{/if}</b></label> 27 <input type="checkbox" name="enableRssPermalinks" id="enableRssPermalinks" {if="!empty($GLOBALS['config']['ENABLE_RSS_PERMALINKS'])"}checked{/if}/>
28 <label for="enableRssPermalinks">
29 &nbsp;Disable it to use permalinks in RSS feed instead of direct links to your shaared links. Currently <b>{if="$GLOBALS['config']['ENABLE_RSS_PERMALINKS']"}enabled{else}disabled{/if}.</b>
30 </label>
28 </td> 31 </td>
29 </tr> 32 </tr>
30 <tr> 33 <tr>
diff --git a/tpl/feed.atom.html b/tpl/feed.atom.html
new file mode 100644
index 00000000..2ebb162a
--- /dev/null
+++ b/tpl/feed.atom.html
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<feed xmlns="http://www.w3.org/2005/Atom">
3 <title>{$pagetitle}</title>
4 <subtitle>Shaared links</subtitle>
5 {if="$show_dates"}
6 <updated>{$last_update}</updated>
7 {/if}
8 <link rel="self" href="{$self_link}#" />
9 {if="!empty($pubsubhub_url)"}
10 <!-- PubSubHubbub Discovery -->
11 <link rel="hub" href="{$pubsubhub_url}#" />
12 <!-- End Of PubSubHubbub Discovery -->
13 {/if}
14 <author>
15 <name>{$index_url}</name>
16 <uri>{$index_url}</uri>
17 </author>
18 <id>{$index_url}</id>
19 <generator>Shaarli</generator>
20 {loop="links"}
21 <entry>
22 <title>{$value.title}</title>
23 {if="$usepermalinks"}
24 <link href="{$value.guid}#" />
25 {else}
26 <link href="{$value.url}#" />
27 {/if}
28 <id>{$value.guid}</id>
29 {if="$show_dates"}
30 <updated>{$value.iso_date}</updated>
31 {/if}
32 <content type="html" xml:lang="{$language}">
33 <![CDATA[{$value.description}]]>
34 </content>
35 {loop="$value.taglist"}
36 <category scheme="{$index_url}?searchtags=" term="{$value|strtolower}" label="{$value}" />
37 {/loop}
38 </entry>
39 {/loop}
40</feed>
diff --git a/tpl/feed.rss.html b/tpl/feed.rss.html
new file mode 100644
index 00000000..26de7f19
--- /dev/null
+++ b/tpl/feed.rss.html
@@ -0,0 +1,34 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
3 <channel>
4 <title>{$pagetitle}</title>
5 <link>{$index_url}</link>
6 <description>Shaared links</description>
7 <language>{$language}</language>
8 <copyright>{$index_url}</copyright>
9 <generator>Shaarli</generator>
10 <atom:link rel="self" href="{$self_link}" />
11 {if="!empty($pubsubhub_url)"}
12 <!-- PubSubHubbub Discovery -->
13 <atom:link rel="hub" href="{$pubsubhub_url}" />
14 {/if}
15 {loop="links"}
16 <item>
17 <title>{$value.title}</title>
18 <guid isPermaLink="{if="$usepermalinks"}true{else}false{/if}">{$value.guid}</guid>
19 {if="$usepermalinks"}
20 <link>{$value.guid}</link>
21 {else}
22 <link>{$value.url}</link>
23 {/if}
24 {if="$show_dates"}
25 <pubDate>{$value.iso_date}</pubDate>
26 {/if}
27 <description><![CDATA[{$value.description}]]></description>
28 {loop="$value.taglist"}
29 <category domain="{$index_url}?searchtags=">{$value}</category>
30 {/loop}
31 </item>
32 {/loop}
33 </channel>
34</rss>