aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/feed
diff options
context:
space:
mode:
Diffstat (limited to 'application/feed')
-rw-r--r--application/feed/Cache.php38
-rw-r--r--application/feed/CachedPage.php61
-rw-r--r--application/feed/FeedBuilder.php299
3 files changed, 398 insertions, 0 deletions
diff --git a/application/feed/Cache.php b/application/feed/Cache.php
new file mode 100644
index 00000000..e5d43e61
--- /dev/null
+++ b/application/feed/Cache.php
@@ -0,0 +1,38 @@
1<?php
2/**
3 * Cache utilities
4 */
5
6/**
7 * Purges all cached pages
8 *
9 * @param string $pageCacheDir page cache directory
10 *
11 * @return mixed an error string if the directory is missing
12 */
13function purgeCachedPages($pageCacheDir)
14{
15 if (! is_dir($pageCacheDir)) {
16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error);
18 return $error;
19 }
20
21 array_map('unlink', glob($pageCacheDir.'/*.cache'));
22}
23
24/**
25 * Invalidates caches when the database is changed or the user logs out.
26 *
27 * @param string $pageCacheDir page cache directory
28 */
29function invalidateCaches($pageCacheDir)
30{
31 // Purge cache attached to session.
32 if (isset($_SESSION['tags'])) {
33 unset($_SESSION['tags']);
34 }
35
36 // Purge page cache shared by sessions.
37 purgeCachedPages($pageCacheDir);
38}
diff --git a/application/feed/CachedPage.php b/application/feed/CachedPage.php
new file mode 100644
index 00000000..1c51ac73
--- /dev/null
+++ b/application/feed/CachedPage.php
@@ -0,0 +1,61 @@
1<?php
2
3namespace Shaarli\Feed;
4/**
5 * Simple cache system, mainly for the RSS/ATOM feeds
6 */
7class CachedPage
8{
9 // Directory containing page caches
10 private $cacheDir;
11
12 // Should this URL be cached (boolean)?
13 private $shouldBeCached;
14
15 // Name of the cache file for this URL
16 private $filename;
17
18 /**
19 * Creates a new CachedPage
20 *
21 * @param string $cacheDir page cache directory
22 * @param string $url page URL
23 * @param bool $shouldBeCached whether this page needs to be cached
24 */
25 public function __construct($cacheDir, $url, $shouldBeCached)
26 {
27 // TODO: check write access to the cache directory
28 $this->cacheDir = $cacheDir;
29 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
30 $this->shouldBeCached = $shouldBeCached;
31 }
32
33 /**
34 * Returns the cached version of a page, if it exists and should be cached
35 *
36 * @return string a cached version of the page if it exists, null otherwise
37 */
38 public function cachedVersion()
39 {
40 if (!$this->shouldBeCached) {
41 return null;
42 }
43 if (is_file($this->filename)) {
44 return file_get_contents($this->filename);
45 }
46 return null;
47 }
48
49 /**
50 * Puts a page in the cache
51 *
52 * @param string $pageContent XML content to cache
53 */
54 public function cache($pageContent)
55 {
56 if (!$this->shouldBeCached) {
57 return;
58 }
59 file_put_contents($this->filename, $pageContent);
60 }
61}
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
new file mode 100644
index 00000000..dcfd2c89
--- /dev/null
+++ b/application/feed/FeedBuilder.php
@@ -0,0 +1,299 @@
1<?php
2namespace Shaarli\Feed;
3
4use DateTime;
5use LinkDB;
6
7/**
8 * FeedBuilder class.
9 *
10 * Used to build ATOM and RSS feeds data.
11 */
12class FeedBuilder
13{
14 /**
15 * @var string Constant: RSS feed type.
16 */
17 public static $FEED_RSS = 'rss';
18
19 /**
20 * @var string Constant: ATOM feed type.
21 */
22 public static $FEED_ATOM = 'atom';
23
24 /**
25 * @var string Default language if the locale isn't set.
26 */
27 public static $DEFAULT_LANGUAGE = 'en-en';
28
29 /**
30 * @var int Number of links to display in a feed by default.
31 */
32 public static $DEFAULT_NB_LINKS = 50;
33
34 /**
35 * @var LinkDB instance.
36 */
37 protected $linkDB;
38
39 /**
40 * @var string RSS or ATOM feed.
41 */
42 protected $feedType;
43
44 /**
45 * @var array $_SERVER
46 */
47 protected $serverInfo;
48
49 /**
50 * @var array $_GET
51 */
52 protected $userInput;
53
54 /**
55 * @var boolean True if the user is currently logged in, false otherwise.
56 */
57 protected $isLoggedIn;
58
59 /**
60 * @var boolean Use permalinks instead of direct links if true.
61 */
62 protected $usePermalinks;
63
64 /**
65 * @var boolean true to hide dates in feeds.
66 */
67 protected $hideDates;
68
69 /**
70 * @var string server locale.
71 */
72 protected $locale;
73
74 /**
75 * @var DateTime Latest item date.
76 */
77 protected $latestDate;
78
79 /**
80 * Feed constructor.
81 *
82 * @param LinkDB $linkDB LinkDB instance.
83 * @param string $feedType Type of feed.
84 * @param array $serverInfo $_SERVER.
85 * @param array $userInput $_GET.
86 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
87 */
88 public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn)
89 {
90 $this->linkDB = $linkDB;
91 $this->feedType = $feedType;
92 $this->serverInfo = $serverInfo;
93 $this->userInput = $userInput;
94 $this->isLoggedIn = $isLoggedIn;
95 }
96
97 /**
98 * Build data for feed templates.
99 *
100 * @return array Formatted data for feeds templates.
101 */
102 public function buildData()
103 {
104 // Search for untagged links
105 if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
106 $this->userInput['searchtags'] = false;
107 }
108
109 // Optionally filter the results:
110 $linksToDisplay = $this->linkDB->filterSearch($this->userInput);
111
112 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
113
114 // Can't use array_keys() because $link is a LinkDB instance and not a real array.
115 $keys = array();
116 foreach ($linksToDisplay as $key => $value) {
117 $keys[] = $key;
118 }
119
120 $pageaddr = escape(index_url($this->serverInfo));
121 $linkDisplayed = array();
122 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
123 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
124 }
125
126 $data['language'] = $this->getTypeLanguage();
127 $data['last_update'] = $this->getLatestDateFormatted();
128 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
129 // Remove leading slash from REQUEST_URI.
130 $data['self_link'] = escape(server_url($this->serverInfo))
131 . escape($this->serverInfo['REQUEST_URI']);
132 $data['index_url'] = $pageaddr;
133 $data['usepermalinks'] = $this->usePermalinks === true;
134 $data['links'] = $linkDisplayed;
135
136 return $data;
137 }
138
139 /**
140 * Build a feed item (one per shaare).
141 *
142 * @param array $link Single link array extracted from LinkDB.
143 * @param string $pageaddr Index URL.
144 *
145 * @return array Link array with feed attributes.
146 */
147 protected function buildItem($link, $pageaddr)
148 {
149 $link['guid'] = $pageaddr . '?' . $link['shorturl'];
150 // Check for both signs of a note: starting with ? and 7 chars long.
151 if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
152 $link['url'] = $pageaddr . $link['url'];
153 }
154 if ($this->usePermalinks === true) {
155 $permalink = '<a href="' . $link['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>';
156 } else {
157 $permalink = '<a href="' . $link['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>';
158 }
159 $link['description'] = format_description($link['description'], '', false, $pageaddr);
160 $link['description'] .= PHP_EOL . '<br>&#8212; ' . $permalink;
161
162 $pubDate = $link['created'];
163 $link['pub_iso_date'] = $this->getIsoDate($pubDate);
164
165 // atom:entry elements MUST contain exactly one atom:updated element.
166 if (!empty($link['updated'])) {
167 $upDate = $link['updated'];
168 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
169 } else {
170 $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);
171 }
172
173 // Save the more recent item.
174 if (empty($this->latestDate) || $this->latestDate < $pubDate) {
175 $this->latestDate = $pubDate;
176 }
177 if (!empty($upDate) && $this->latestDate < $upDate) {
178 $this->latestDate = $upDate;
179 }
180
181 $taglist = array_filter(explode(' ', $link['tags']), 'strlen');
182 uasort($taglist, 'strcasecmp');
183 $link['taglist'] = $taglist;
184
185 return $link;
186 }
187
188 /**
189 * Set this to true to use permalinks instead of direct links.
190 *
191 * @param boolean $usePermalinks true to force permalinks.
192 */
193 public function setUsePermalinks($usePermalinks)
194 {
195 $this->usePermalinks = $usePermalinks;
196 }
197
198 /**
199 * Set this to true to hide timestamps in feeds.
200 *
201 * @param boolean $hideDates true to enable.
202 */
203 public function setHideDates($hideDates)
204 {
205 $this->hideDates = $hideDates;
206 }
207
208 /**
209 * Set the locale. Used to show feed language.
210 *
211 * @param string $locale The locale (eg. 'fr_FR.UTF8').
212 */
213 public function setLocale($locale)
214 {
215 $this->locale = strtolower($locale);
216 }
217
218 /**
219 * Get the language according to the feed type, based on the locale:
220 *
221 * - RSS format: en-us (default: 'en-en').
222 * - ATOM format: fr (default: 'en').
223 *
224 * @return string The language.
225 */
226 public function getTypeLanguage()
227 {
228 // Use the locale do define the language, if available.
229 if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
230 $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2;
231 return str_replace('_', '-', substr($this->locale, 0, $length));
232 }
233 return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en';
234 }
235
236 /**
237 * Format the latest item date found according to the feed type.
238 *
239 * Return an empty string if invalid DateTime is passed.
240 *
241 * @return string Formatted date.
242 */
243 protected function getLatestDateFormatted()
244 {
245 if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
246 return '';
247 }
248
249 $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
250 return $this->latestDate->format($type);
251 }
252
253 /**
254 * Get ISO date from DateTime according to feed type.
255 *
256 * @param DateTime $date Date to format.
257 * @param string|bool $format Force format.
258 *
259 * @return string Formatted date.
260 */
261 protected function getIsoDate(DateTime $date, $format = false)
262 {
263 if ($format !== false) {
264 return $date->format($format);
265 }
266 if ($this->feedType == self::$FEED_RSS) {
267 return $date->format(DateTime::RSS);
268 }
269 return $date->format(DateTime::ATOM);
270 }
271
272 /**
273 * Returns the number of link to display according to 'nb' user input parameter.
274 *
275 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
276 * If 'nb' is set to 'all', display all filtered links (max parameter).
277 *
278 * @param int $max maximum number of links to display.
279 *
280 * @return int number of links to display.
281 */
282 public function getNbLinks($max)
283 {
284 if (empty($this->userInput['nb'])) {
285 return self::$DEFAULT_NB_LINKS;
286 }
287
288 if ($this->userInput['nb'] == 'all') {
289 return $max;
290 }
291
292 $intNb = intval($this->userInput['nb']);
293 if (!is_int($intNb) || $intNb == 0) {
294 return self::$DEFAULT_NB_LINKS;
295 }
296
297 return $intNb;
298 }
299}