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