1 /**
2 * @file subscription.c common subscription handling
3 *
4 * Copyright (C) 2003-2015 Lars Windolf <lars.windolf@gmx.de>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21 #include "subscription.h"
22
23 #include <math.h>
24 #include <string.h>
25
26 #include "auth.h"
27 #include "common.h"
28 #include "conf.h"
29 #include "db.h"
30 #include "debug.h"
31 #include "favicon.h"
32 #include "feedlist.h"
33 #include "metadata.h"
34 #include "net.h"
35 #include "ui/auth_dialog.h"
36 #include "ui/itemview.h"
37 #include "ui/liferea_shell.h"
38 #include "ui/feed_list_node.h"
39
40 /* The allowed feed protocol prefixes (see http://25hoursaday.com/draft-obasanjo-feed-URI-scheme-02.html) */
41 #define FEED_PROTOCOL_PREFIX "feed://"
42 #define FEED_PROTOCOL_PREFIX2 "feed:"
43
44 subscriptionPtr
subscription_new(const gchar * source,const gchar * filter,updateOptionsPtr options)45 subscription_new (const gchar *source,
46 const gchar *filter,
47 updateOptionsPtr options)
48 {
49 subscriptionPtr subscription;
50
51 subscription = g_new0 (struct subscription, 1);
52 subscription->type = feed_get_subscription_type ();
53 subscription->updateOptions = options;
54
55 if (!subscription->updateOptions)
56 subscription->updateOptions = g_new0 (struct updateOptions, 1);
57
58 subscription->updateState = g_new0 (struct updateState, 1);
59 subscription->updateInterval = -1;
60 subscription->defaultInterval = -1;
61
62 if (source) {
63 gboolean feedPrefix = FALSE;
64 gchar *uri = g_strdup (source);
65 g_strstrip (uri); /* strip confusing whitespaces */
66
67 /* strip feed protocol prefix variant 1 */
68 if (uri == strstr (uri, FEED_PROTOCOL_PREFIX)) {
69 gchar *tmp = uri;
70 uri = g_strdup (uri + strlen (FEED_PROTOCOL_PREFIX));
71 g_free (tmp);
72 feedPrefix = TRUE;
73 }
74
75 /* strip feed protocol prefix variant 2 */
76 if (uri == strstr (uri, FEED_PROTOCOL_PREFIX2)) {
77 gchar *tmp = uri;
78 uri = g_strdup (uri + strlen (FEED_PROTOCOL_PREFIX2));
79 g_free (tmp);
80 feedPrefix = TRUE;
81 }
82
83 /* ensure protocol prefix (but only for feed:[//] URIs to avoid
84 breaking local file and command line subscriptions) */
85 if (feedPrefix && !strstr (uri, "://")) {
86 gchar *tmp = uri;
87 uri = g_strdup_printf ("http://%s", uri);
88 g_free (tmp);
89 }
90
91 subscription_set_source (subscription, uri);
92 g_free (uri);
93 }
94
95 if (filter)
96 subscription_set_filter (subscription, filter);
97
98 return subscription;
99 }
100
101 /* Checks whether updating a feed makes sense. */
102 static gboolean
subscription_can_be_updated(subscriptionPtr subscription)103 subscription_can_be_updated (subscriptionPtr subscription)
104 {
105 if (subscription->updateJob) {
106 liferea_shell_set_status_bar (_("Subscription \"%s\" is already being updated!"), node_get_title (subscription->node));
107 return FALSE;
108 }
109
110 if (subscription->discontinued) {
111 liferea_shell_set_status_bar (_("The subscription \"%s\" was discontinued. Liferea won't update it anymore!"), node_get_title (subscription->node));
112 return FALSE;
113 }
114
115 if (!subscription_get_source (subscription)) {
116 g_warning ("Feed source is NULL! This should never happen - cannot update!");
117 return FALSE;
118 }
119 return TRUE;
120 }
121
122 void
subscription_reset_update_counter(subscriptionPtr subscription,GTimeVal * now)123 subscription_reset_update_counter (subscriptionPtr subscription, GTimeVal *now)
124 {
125 if (!subscription)
126 return;
127
128 subscription->updateState->lastPoll.tv_sec = now->tv_sec;
129 debug1 (DEBUG_UPDATE, "Resetting last poll counter to %ld.", subscription->updateState->lastPoll.tv_sec);
130 }
131
132 static void
subscription_favicon_downloaded(gpointer user_data)133 subscription_favicon_downloaded (gpointer user_data)
134 {
135 nodePtr node = (nodePtr)user_data;
136
137 node_load_icon (node);
138 feed_list_node_update (node->id);
139 }
140
141 void
subscription_update_favicon(subscriptionPtr subscription)142 subscription_update_favicon (subscriptionPtr subscription)
143 {
144 debug1 (DEBUG_UPDATE, "trying to download favicon.ico for \"%s\"", node_get_title (subscription->node));
145 liferea_shell_set_status_bar (_("Updating favicon for \"%s\""), node_get_title (subscription->node));
146 g_get_current_time (&subscription->updateState->lastFaviconPoll);
147 favicon_download (subscription,
148 node_get_base_url (subscription->node),
149 subscription_get_source (subscription),
150 subscription->updateOptions, // FIXME: correct?
151 subscription_favicon_downloaded,
152 (gpointer)subscription->node);
153 }
154
155 /**
156 * Updates the error status of the given subscription
157 *
158 * @param subscription the subscription
159 * @param httpstatus the new HTTP status code
160 * @param resultcode the update result code
161 * @param filterError filter error string (or NULL)
162 */
163 static void
subscription_update_error_status(subscriptionPtr subscription,gint httpstatus,gint resultcode,gchar * filterError)164 subscription_update_error_status (subscriptionPtr subscription,
165 gint httpstatus,
166 gint resultcode,
167 gchar *filterError)
168 {
169 gboolean errorFound = FALSE;
170
171 if (subscription->filterError)
172 g_free (subscription->filterError);
173 if (subscription->httpError)
174 g_free (subscription->httpError);
175 if (subscription->updateError)
176 g_free (subscription->updateError);
177
178 subscription->filterError = g_strdup (filterError);
179 subscription->updateError = NULL;
180 subscription->httpError = NULL;
181 subscription->httpErrorCode = httpstatus;
182
183 if (((httpstatus >= 200) && (httpstatus < 400)) && /* HTTP codes starting with 2 and 3 mean no error */
184 (NULL == subscription->filterError))
185 return;
186
187 if ((200 != httpstatus) || (resultcode != 0)) {
188 subscription->httpError = g_strdup (network_strerror (resultcode, httpstatus));
189 errorFound = TRUE;
190 }
191
192 /* if none of the above error descriptions matched... */
193 if (!errorFound)
194 subscription->updateError = g_strdup (_("There was a problem while reading this subscription. Please check the URL and console output."));
195 }
196
197 static void
subscription_process_update_result(const struct updateResult * const result,gpointer user_data,guint32 flags)198 subscription_process_update_result (const struct updateResult * const result, gpointer user_data, guint32 flags)
199 {
200 subscriptionPtr subscription = (subscriptionPtr)user_data;
201 nodePtr node = subscription->node;
202 gboolean processing = FALSE;
203 GTimeVal now;
204 gint next_update = 0;
205 gint update_time_sources = 0;
206 gint maxage = -1;
207 gint syn_update = -1;
208 gint ttl = subscription->updateState->timeToLive = -1;
209
210 /* 1. preprocessing */
211
212 g_assert (subscription->updateJob);
213 /* update the subscription URL on permanent redirects */
214 if ((301 == result->returncode || 308 == result->returncode) && result->source && !g_str_equal (result->source, subscription->updateJob->request->source)) {
215 debug2 (DEBUG_UPDATE, "The URL of \"%s\" has changed permanently and was updated to \"%s\"", node_get_title(node), result->source);
216 subscription_set_source (subscription, result->source);
217 liferea_shell_set_status_bar (_("The URL of \"%s\" has changed permanently and was updated"), node_get_title(node));
218 }
219
220 if (401 == result->httpstatus) { /* unauthorized */
221 auth_dialog_new (subscription, flags);
222 } else if (410 == result->httpstatus) { /* gone */
223 subscription->discontinued = TRUE;
224 node->available = TRUE;
225 liferea_shell_set_status_bar (_("\"%s\" is discontinued. Liferea won't updated it anymore!"), node_get_title (node));
226 } else if (304 == result->httpstatus) {
227 node->available = TRUE;
228 liferea_shell_set_status_bar (_("\"%s\" has not changed since last update"), node_get_title(node));
229 } else {
230 processing = TRUE;
231 }
232
233
234 subscription_update_error_status (subscription, result->httpstatus, result->returncode, result->filterErrors);
235
236 subscription->updateJob = NULL;
237
238 /* 2. call subscription type specific processing */
239 if (processing)
240 SUBSCRIPTION_TYPE (subscription)->process_update_result (subscription, result, flags);
241
242 /* 3. set default update interval */
243 update_state_set_cache_maxage (subscription->updateState, update_state_get_cache_maxage (result->updateState));
244 maxage = subscription->updateState->maxAgeMinutes;
245
246 if (0 < subscription->updateState->synFrequency &&
247 0 < subscription->updateState->synPeriod) {
248 syn_update = ceil ( (float) (subscription->updateState->synPeriod / subscription->updateState->synFrequency) );
249 } else if (0 < subscription->updateState->synPeriod) {
250 syn_update = subscription->updateState->synPeriod;
251 }
252 if (0 < subscription->updateState->timeToLive) {
253 ttl = subscription->updateState->timeToLive;
254 }
255
256 if (0 < maxage ) { update_time_sources++; next_update += maxage; }
257 if (0 < syn_update) { update_time_sources++; next_update += syn_update; }
258 if (0 < ttl ) { update_time_sources++; next_update += ttl; }
259
260 if (0 < update_time_sources) {
261 /* enforce a 5 minute minimum update interval.
262 round up to nearest 5-minute block to coalesce updates (battery optimization). */
263 next_update = ceil ((float) (next_update / update_time_sources));
264 next_update -= next_update % 5;
265 if (5 > next_update) {
266 next_update = 5;
267 }
268 } else {
269 next_update = -1;
270 }
271
272 debug1 (DEBUG_UPDATE, "The next suggested update time is in %d minutes.", next_update);
273 subscription_set_default_update_interval (subscription, next_update);
274
275 /* 4. call favicon updating after subscription processing
276 to ensure we have valid baseUrl for feed nodes... */
277 g_get_current_time (&now);
278 if (favicon_update_needed (subscription->node->id, subscription->updateState, &now))
279 subscription_update_favicon (subscription);
280
281 /* 5. generic postprocessing */
282 update_state_set_lastmodified (subscription->updateState, update_state_get_lastmodified (result->updateState));
283 update_state_set_cookies (subscription->updateState, update_state_get_cookies (result->updateState));
284 update_state_set_etag (subscription->updateState, update_state_get_etag (result->updateState));
285 g_get_current_time (&subscription->updateState->lastPoll);
286
287 // FIXME: use new-items signal in itemview class
288 itemview_update_node_info (subscription->node);
289 itemview_update ();
290
291 db_subscription_update (subscription);
292 db_node_update (subscription->node);
293
294 if (processing && subscription->node->newCount > 0) {
295 feedlist_new_items (node->newCount);
296 feedlist_node_was_updated (node);
297 }
298 }
299
300 void
subscription_update(subscriptionPtr subscription,guint flags)301 subscription_update (subscriptionPtr subscription, guint flags)
302 {
303 updateRequestPtr request;
304 GTimeVal now;
305
306 if (!subscription)
307 return;
308
309 if (subscription->updateJob)
310 return;
311
312 debug1 (DEBUG_UPDATE, "Scheduling %s to be updated", node_get_title (subscription->node));
313
314 if (subscription_can_be_updated (subscription)) {
315 liferea_shell_set_status_bar (_("Updating \"%s\""), node_get_title (subscription->node));
316
317 g_get_current_time (&now);
318 subscription_reset_update_counter (subscription, &now);
319
320 request = update_request_new ();
321 request->updateState = update_state_copy (subscription->updateState);
322 request->options = update_options_copy (subscription->updateOptions);
323 request->source = g_strdup (subscription_get_source (subscription));
324 if (subscription_get_filter (subscription))
325 request->filtercmd = g_strdup (subscription_get_filter (subscription));
326
327 if (SUBSCRIPTION_TYPE (subscription)->prepare_update_request (subscription, request))
328 subscription->updateJob = update_execute_request (subscription, request, subscription_process_update_result, subscription, flags);
329 else
330 update_request_free (request);
331 }
332 }
333
334 void
subscription_auto_update(subscriptionPtr subscription)335 subscription_auto_update (subscriptionPtr subscription)
336 {
337 gint interval;
338 guint flags = 0;
339 GTimeVal now;
340
341 if (!subscription)
342 return;
343
344 interval = subscription_get_update_interval (subscription);
345 if (-1 == interval)
346 conf_get_int_value (DEFAULT_UPDATE_INTERVAL, &interval);
347
348 if (-2 >= interval || 0 == interval)
349 return; /* don't update this subscription */
350
351 g_get_current_time (&now);
352
353 if (subscription->updateState->lastPoll.tv_sec + interval*60 <= now.tv_sec)
354 subscription_update (subscription, flags);
355 }
356
357 void
subscription_cancel_update(subscriptionPtr subscription)358 subscription_cancel_update (subscriptionPtr subscription)
359 {
360 if (!subscription->updateJob)
361 return;
362
363 update_job_cancel_by_owner (subscription);
364 subscription->updateJob = NULL;
365 }
366
367 gint
subscription_get_update_interval(subscriptionPtr subscription)368 subscription_get_update_interval (subscriptionPtr subscription)
369 {
370 return subscription->updateInterval;
371 }
372
373 void
subscription_set_update_interval(subscriptionPtr subscription,gint interval)374 subscription_set_update_interval (subscriptionPtr subscription, gint interval)
375 {
376 if (0 == interval) {
377 interval = -1; /* This is evil, I know, but when this method
378 is called to set the update interval to 0
379 we mean "never updating". The updating logic
380 expects -1 for "never updating" and 0 for
381 updating according to the global update
382 interval... */
383 }
384 subscription->updateInterval = interval;
385 feedlist_schedule_save ();
386 }
387
388 guint
subscription_get_default_update_interval(subscriptionPtr subscription)389 subscription_get_default_update_interval (subscriptionPtr subscription)
390 {
391 return subscription->defaultInterval;
392 }
393
394 void
subscription_set_default_update_interval(subscriptionPtr subscription,guint interval)395 subscription_set_default_update_interval (subscriptionPtr subscription, guint interval)
396 {
397 subscription->defaultInterval = interval;
398 }
399
400 static const gchar *
subscription_get_orig_source(subscriptionPtr subscription)401 subscription_get_orig_source (subscriptionPtr subscription)
402 {
403 return subscription->origSource;
404 }
405
406 const gchar *
subscription_get_source(subscriptionPtr subscription)407 subscription_get_source (subscriptionPtr subscription)
408 {
409 return subscription->source;
410 }
411
412 const gchar *
subscription_get_homepage(subscriptionPtr subscription)413 subscription_get_homepage (subscriptionPtr subscription)
414 {
415 return metadata_list_get (subscription->metadata, "homepage");
416 }
417
418 const gchar *
subscription_get_filter(subscriptionPtr subscription)419 subscription_get_filter (subscriptionPtr subscription)
420 {
421 return subscription->filtercmd;
422 }
423
424 static void
subscription_set_orig_source(subscriptionPtr subscription,const gchar * source)425 subscription_set_orig_source (subscriptionPtr subscription, const gchar *source)
426 {
427 g_free (subscription->origSource);
428 subscription->origSource = g_strchomp (g_strdup (source));
429 feedlist_schedule_save ();
430 }
431
432 void
subscription_set_source(subscriptionPtr subscription,const gchar * source)433 subscription_set_source (subscriptionPtr subscription, const gchar *source)
434 {
435 g_free (subscription->source);
436 subscription->source = g_strchomp (g_strdup (source));
437 feedlist_schedule_save ();
438
439 update_state_set_cookies (subscription->updateState, NULL);
440
441 if (NULL == subscription_get_orig_source (subscription))
442 subscription_set_orig_source (subscription, source);
443 }
444
445 void
subscription_set_homepage(subscriptionPtr subscription,const gchar * newHtmlUrl)446 subscription_set_homepage (subscriptionPtr subscription, const gchar *newHtmlUrl)
447 {
448 gchar *htmlUrl = NULL;
449
450 if (newHtmlUrl) {
451 if (strstr (newHtmlUrl, "://")) {
452 /* absolute URI can be used directly */
453 htmlUrl = g_strchomp (g_strdup (newHtmlUrl));
454 } else {
455 /* relative URI part needs to be expanded */
456 gchar *tmp, *source;
457
458 source = g_strdup (subscription_get_source (subscription));
459 tmp = strrchr (source, '/');
460 if (tmp)
461 *(tmp+1) = '\0';
462
463 htmlUrl = common_build_url (newHtmlUrl, source);
464 g_free (source);
465 }
466
467 metadata_list_set (&subscription->metadata, "homepage", htmlUrl);
468 g_free (htmlUrl);
469 }
470 }
471
472 void
subscription_set_filter(subscriptionPtr subscription,const gchar * filter)473 subscription_set_filter (subscriptionPtr subscription, const gchar *filter)
474 {
475 g_free (subscription->filtercmd);
476 subscription->filtercmd = g_strdup (filter);
477 feedlist_schedule_save ();
478 }
479
480 void
subscription_set_auth_info(subscriptionPtr subscription,const gchar * username,const gchar * password)481 subscription_set_auth_info (subscriptionPtr subscription,
482 const gchar *username,
483 const gchar *password)
484 {
485 g_assert (NULL != subscription->updateOptions);
486
487 g_free (subscription->updateOptions->username);
488 g_free (subscription->updateOptions->password);
489
490 subscription->updateOptions->username = g_strdup (username);
491 subscription->updateOptions->password = g_strdup (password);
492
493 liferea_auth_info_store (subscription);
494 }
495
496 subscriptionPtr
subscription_import(xmlNodePtr xml,gboolean trusted)497 subscription_import (xmlNodePtr xml, gboolean trusted)
498 {
499 subscriptionPtr subscription;
500 xmlChar *source, *homepage, *filter, *intervalStr, *tmp;
501
502 subscription = subscription_new (NULL, NULL, NULL);
503
504 source = xmlGetProp (xml, BAD_CAST "xmlUrl");
505 if (!source)
506 source = xmlGetProp (xml, BAD_CAST "xmlurl"); /* e.g. for AmphetaDesk */
507
508 if (source) {
509 if (!trusted && source[0] == '|') {
510 /* FIXME: Display warning dialog asking if the command
511 is safe? */
512 tmp = g_strdup_printf ("unsafe command: %s", source);
513 xmlFree (source);
514 source = tmp;
515 }
516
517 subscription_set_source (subscription, source);
518 xmlFree (source);
519
520 homepage = xmlGetProp (xml, BAD_CAST "htmlUrl");
521 if (homepage && xmlStrcmp (homepage, ""))
522 subscription_set_homepage (subscription, homepage);
523 xmlFree (homepage);
524
525 if ((filter = xmlGetProp (xml, BAD_CAST "filtercmd"))) {
526 if (!trusted) {
527 /* FIXME: Display warning dialog asking if the command
528 is safe? */
529 tmp = g_strdup_printf ("unsafe command: %s", filter);
530 xmlFree (filter);
531 filter = tmp;
532 }
533
534 subscription_set_filter (subscription, filter);
535 xmlFree (filter);
536 }
537
538 intervalStr = xmlGetProp (xml, BAD_CAST "updateInterval");
539 subscription_set_update_interval (subscription, common_parse_long (intervalStr, -1));
540 xmlFree (intervalStr);
541
542 /* no proxy flag */
543 tmp = xmlGetProp (xml, BAD_CAST "dontUseProxy");
544 if (tmp && !xmlStrcmp (tmp, BAD_CAST "true"))
545 subscription->updateOptions->dontUseProxy = TRUE;
546 xmlFree (tmp);
547
548 /* authentication options */
549 subscription->updateOptions->username = xmlGetProp (xml, BAD_CAST "username");
550 subscription->updateOptions->password = xmlGetProp (xml, BAD_CAST "password");
551 }
552
553 return subscription;
554 }
555
556 void
subscription_export(subscriptionPtr subscription,xmlNodePtr xml,gboolean trusted)557 subscription_export (subscriptionPtr subscription, xmlNodePtr xml, gboolean trusted)
558 {
559 gchar *interval = g_strdup_printf ("%d", subscription_get_update_interval (subscription));
560
561 xmlNewProp (xml, BAD_CAST "xmlUrl", BAD_CAST subscription_get_source (subscription));
562
563 if (subscription_get_homepage (subscription))
564 xmlNewProp (xml, BAD_CAST"htmlUrl", BAD_CAST subscription_get_homepage (subscription));
565 else
566 xmlNewProp (xml, BAD_CAST"htmlUrl", BAD_CAST "");
567
568 if (subscription_get_filter (subscription))
569 xmlNewProp (xml, BAD_CAST"filtercmd", BAD_CAST subscription_get_filter (subscription));
570
571 if(trusted) {
572 xmlNewProp (xml, BAD_CAST"updateInterval", BAD_CAST interval);
573
574 if (subscription->updateOptions->dontUseProxy)
575 xmlNewProp (xml, BAD_CAST"dontUseProxy", BAD_CAST"true");
576
577 if (!liferea_auth_has_active_store ()) {
578 if (subscription->updateOptions->username)
579 xmlNewProp (xml, BAD_CAST"username", subscription->updateOptions->username);
580 if (subscription->updateOptions->password)
581 xmlNewProp (xml, BAD_CAST"password", subscription->updateOptions->password);
582 }
583 }
584
585 g_free (interval);
586 }
587
588 void
subscription_to_xml(subscriptionPtr subscription,xmlNodePtr xml)589 subscription_to_xml (subscriptionPtr subscription, xmlNodePtr xml)
590 {
591 gchar *tmp;
592
593 xmlNewTextChild (xml, NULL, "feedSource", subscription_get_source (subscription));
594 xmlNewTextChild (xml, NULL, "feedOrigSource", subscription_get_orig_source (subscription));
595
596 tmp = g_strdup_printf ("%d", subscription_get_default_update_interval (subscription));
597 xmlNewTextChild (xml, NULL, "feedUpdateInterval", tmp);
598 g_free (tmp);
599
600 tmp = g_strdup_printf ("%d", subscription->discontinued?1:0);
601 xmlNewTextChild (xml, NULL, "feedDiscontinued", tmp);
602 g_free (tmp);
603
604 if (subscription->updateError)
605 xmlNewTextChild (xml, NULL, "updateError", subscription->updateError);
606 if (subscription->httpError) {
607 xmlNewTextChild (xml, NULL, "httpError", subscription->httpError);
608
609 tmp = g_strdup_printf ("%d", subscription->httpErrorCode);
610 xmlNewTextChild (xml, NULL, "httpErrorCode", tmp);
611 g_free (tmp);
612 }
613 if (subscription->filterError)
614 xmlNewTextChild (xml, NULL, "filterError", subscription->filterError);
615
616 metadata_add_xml_nodes (subscription->metadata, xml);
617 }
618
619 void
subscription_free(subscriptionPtr subscription)620 subscription_free (subscriptionPtr subscription)
621 {
622 if (!subscription)
623 return;
624
625 g_free (subscription->updateError);
626 g_free (subscription->filterError);
627 g_free (subscription->httpError);
628 g_free (subscription->source);
629 g_free (subscription->origSource);
630 g_free (subscription->filtercmd);
631
632 update_job_cancel_by_owner (subscription);
633 update_options_free (subscription->updateOptions);
634 update_state_free (subscription->updateState);
635 metadata_list_free (subscription->metadata);
636
637 g_free (subscription);
638 }
639