aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/feed
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2019-07-27 12:30:33 +0200
committerArthurHoaro <arthur@hoa.ro>2019-07-27 12:30:33 +0200
commit1b8ed48a0d3964186f4d66d443783f4d250e7147 (patch)
tree23597f312507ba0c1b461755b9aa086106374a4d /application/feed
parent1aa24ed8d2974cda98733f74b36844b02942cc11 (diff)
parented3365325d231e044dedc32608fde87b1b39fa34 (diff)
downloadShaarli-1b8ed48a0d3964186f4d66d443783f4d250e7147.tar.gz
Shaarli-1b8ed48a0d3964186f4d66d443783f4d250e7147.tar.zst
Shaarli-1b8ed48a0d3964186f4d66d443783f4d250e7147.zip
Merge tag 'v0.11.0' into latest
Release v0.11.0
Diffstat (limited to 'application/feed')
-rw-r--r--application/feed/Cache.php38
-rw-r--r--application/feed/CachedPage.php62
-rw-r--r--application/feed/FeedBuilder.php299
3 files changed, 399 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..d809bdd9
--- /dev/null
+++ b/application/feed/CachedPage.php
@@ -0,0 +1,62 @@
1<?php
2
3namespace Shaarli\Feed;
4
5/**
6 * Simple cache system, mainly for the RSS/ATOM feeds
7 */
8class CachedPage
9{
10 // Directory containing page caches
11 private $cacheDir;
12
13 // Should this URL be cached (boolean)?
14 private $shouldBeCached;
15
16 // Name of the cache file for this URL
17 private $filename;
18
19 /**
20 * Creates a new CachedPage
21 *
22 * @param string $cacheDir page cache directory
23 * @param string $url page URL
24 * @param bool $shouldBeCached whether this page needs to be cached
25 */
26 public function __construct($cacheDir, $url, $shouldBeCached)
27 {
28 // TODO: check write access to the cache directory
29 $this->cacheDir = $cacheDir;
30 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
31 $this->shouldBeCached = $shouldBeCached;
32 }
33
34 /**
35 * Returns the cached version of a page, if it exists and should be cached
36 *
37 * @return string a cached version of the page if it exists, null otherwise
38 */
39 public function cachedVersion()
40 {
41 if (!$this->shouldBeCached) {
42 return null;
43 }
44 if (is_file($this->filename)) {
45 return file_get_contents($this->filename);
46 }
47 return null;
48 }
49
50 /**
51 * Puts a page in the cache
52 *
53 * @param string $pageContent XML content to cache
54 */
55 public function cache($pageContent)
56 {
57 if (!$this->shouldBeCached) {
58 return;
59 }
60 file_put_contents($this->filename, $pageContent);
61 }
62}
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
new file mode 100644
index 00000000..7c859474
--- /dev/null
+++ b/application/feed/FeedBuilder.php
@@ -0,0 +1,299 @@
1<?php
2namespace Shaarli\Feed;
3
4use DateTime;
5
6/**
7 * FeedBuilder class.
8 *
9 * Used to build ATOM and RSS feeds data.
10 */
11class FeedBuilder
12{
13 /**
14 * @var string Constant: RSS feed type.
15 */
16 public static $FEED_RSS = 'rss';
17
18 /**
19 * @var string Constant: ATOM feed type.
20 */
21 public static $FEED_ATOM = 'atom';
22
23 /**
24 * @var string Default language if the locale isn't set.
25 */
26 public static $DEFAULT_LANGUAGE = 'en-en';
27
28 /**
29 * @var int Number of links to display in a feed by default.
30 */
31 public static $DEFAULT_NB_LINKS = 50;
32
33 /**
34 * @var \Shaarli\Bookmark\LinkDB instance.
35 */
36 protected $linkDB;
37
38 /**
39 * @var string RSS or ATOM feed.
40 */
41 protected $feedType;
42
43 /**
44 * @var array $_SERVER
45 */
46 protected $serverInfo;
47
48 /**
49 * @var array $_GET
50 */
51 protected $userInput;
52
53 /**
54 * @var boolean True if the user is currently logged in, false otherwise.
55 */
56 protected $isLoggedIn;
57
58 /**
59 * @var boolean Use permalinks instead of direct links if true.
60 */
61 protected $usePermalinks;
62
63 /**
64 * @var boolean true to hide dates in feeds.
65 */
66 protected $hideDates;
67
68 /**
69 * @var string server locale.
70 */
71 protected $locale;
72
73 /**
74 * @var DateTime Latest item date.
75 */
76 protected $latestDate;
77
78 /**
79 * Feed constructor.
80 *
81 * @param \Shaarli\Bookmark\LinkDB $linkDB LinkDB instance.
82 * @param string $feedType Type of feed.
83 * @param array $serverInfo $_SERVER.
84 * @param array $userInput $_GET.
85 * @param boolean $isLoggedIn True if the user is currently logged in,
86 * 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 // Prepend the root URL for notes
151 if (is_note($link['url'])) {
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'], $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}