1 /**
2  * @file comments.c comment feed handling
3  *
4  * Copyright (C) 2007-2009 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 <string.h>
22 
23 #include "comments.h"
24 #include "common.h"
25 #include "db.h"
26 #include "debug.h"
27 #include "feed.h"
28 #include "metadata.h"
29 #include "net.h"
30 #include "net_monitor.h"
31 #include "update.h"
32 #include "ui/itemview.h"
33 
34 /* Comment feeds in Liferea are simple flat lists of items attached
35    to a single item. Each item that has a comment feed URL in its
36    metadata list gets its comment feed updated as soon as the user
37    triggers rendering of the item in 3 pane mode.
38 
39    Although rendered differently items and comment items are handled
40    in the same way. */
41 
42 static GHashTable	*commentFeeds = NULL;
43 
44 typedef struct commentFeed
45 {
46 	gulong		itemId;			/**< parent item id */
47 	gchar		*id;			/**< id of the items comments feed (or NULL) */
48 	gchar		*error;			/**< description of error if comments download failed (or NULL)*/
49 
50 	struct updateJob *updateJob;		/**< update job structure used when downloading comments */
51 	updateStatePtr	updateState;		/**< update states (etag, last modified, cookies, last polling times...) used when downloading comments */
52 } *commentFeedPtr;
53 
54 static void
comment_feed_free(commentFeedPtr commentFeed)55 comment_feed_free (commentFeedPtr commentFeed)
56 {
57 	if (commentFeed->updateJob)
58 		update_job_cancel_by_owner (commentFeed);
59 	if (commentFeed->updateState)
60 		update_state_free (commentFeed->updateState);
61 
62 	g_free (commentFeed->error);
63 	g_free (commentFeed->id);
64 	g_free (commentFeed);
65 }
66 
67 static void
comment_feed_free_cb(gpointer key,gpointer value,gpointer user_data)68 comment_feed_free_cb (gpointer key, gpointer value, gpointer user_data)
69 {
70 	comment_feed_free (value);
71 }
72 
73 void
comments_deinit(void)74 comments_deinit (void)
75 {
76 	if (commentFeeds) {
77 		g_hash_table_foreach (commentFeeds, comment_feed_free_cb, NULL);
78 		g_hash_table_destroy (commentFeeds);
79 		commentFeeds = NULL;
80 	}
81 }
82 
83 /**
84  * Hash lookup to find comment feeds with the given id.
85  * Returns the comment feed (or NULL).
86  */
87 static commentFeedPtr
comment_feed_from_id(const gchar * id)88 comment_feed_from_id (const gchar *id)
89 {
90 	if (!commentFeeds)
91 		return NULL;
92 
93 	return (commentFeedPtr) g_hash_table_lookup (commentFeeds, id);
94 }
95 
96 static void
comments_process_update_result(const struct updateResult * const result,gpointer user_data,updateFlags flags)97 comments_process_update_result (const struct updateResult * const result, gpointer user_data, updateFlags flags)
98 {
99 	feedParserCtxtPtr	ctxt;
100 	commentFeedPtr		commentFeed = (commentFeedPtr)user_data;
101 	itemPtr			item;
102 	nodePtr			node;
103 
104 	debug_enter ("comments_process_update_result");
105 
106 	item = item_load (commentFeed->itemId);
107 	g_return_if_fail (item != NULL);
108 
109 	/* note this is to update the feed URL on permanent redirects */
110 	if (result->source && !strcmp (result->source, metadata_list_get (item->metadata, "commentFeedUri"))) {
111 
112 		debug2 (DEBUG_UPDATE, "updating comment feed URL from \"%s\" to \"%s\"",
113 		                      metadata_list_get (item->metadata, "commentFeedUri"),
114 				      result->source);
115 
116 		metadata_list_set (&(item->metadata), "commentFeedUri", result->source);
117 	}
118 
119 	if (401 == result->httpstatus) { /* unauthorized */
120 		commentFeed->error = g_strdup (_("Authorization Error"));
121 	} else if (410 == result->httpstatus) { /* gone */
122 		metadata_list_set (&item->metadata, "commentFeedGone", "true");
123 	} else if (304 == result->httpstatus) {
124 		debug1(DEBUG_UPDATE, "comment feed \"%s\" did not change", result->source);
125 	} else if (result->data) {
126 		debug1(DEBUG_UPDATE, "received update result for comment feed \"%s\"", result->source);
127 
128 		/* parse the new downloaded feed into fake node, subscription and feed */
129 		node = node_new (feed_get_node_type ());
130 		ctxt = feed_create_parser_ctxt ();
131 		ctxt->subscription = subscription_new (result->source, NULL, NULL);
132 		ctxt->feed = feed_new ();
133 		node_set_data (node, ctxt->feed);
134 		node_set_subscription (node, ctxt->subscription);
135 		ctxt->data = result->data;
136 		ctxt->dataLength = result->size;
137 		feed_parse (ctxt);
138 
139 		if (ctxt->failed) {
140 			debug0 (DEBUG_UPDATE, "parsing comment feed failed!");
141 		} else {
142 			itemSetPtr	comments;
143 			GList		*iter;
144 
145 			/* before merging mark all downloaded items as comments */
146 			iter = ctxt->items;
147 			while (iter) {
148 				itemPtr comment = (itemPtr) iter->data;
149 				comment->isComment = TRUE;
150 				comment->parentItemId = commentFeed->itemId;
151 				comment->parentNodeId = g_strdup (item->nodeId);
152 				iter = g_list_next (iter);
153 			}
154 
155 			debug1 (DEBUG_UPDATE, "parsing comment feed successful (%d comments downloaded)", g_list_length(ctxt->items));
156 			comments = db_itemset_load (commentFeed->id);
157 			itemset_merge_items (comments, ctxt->items, ctxt->feed->valid, FALSE);
158 			itemset_free (comments);
159 
160 			/* No comment feed truncating as comment items are automatically
161 			   dropped when the parent items are removed from cache. */
162 		}
163 
164 		node_free (ctxt->subscription->node);
165 		feed_free_parser_ctxt (ctxt);
166 	}
167 
168 	/* update error message */
169 	g_free (commentFeed->error);
170 	commentFeed->error = NULL;
171 
172 	if ((result->httpstatus < 200) || (result->httpstatus >= 400)) {
173 		commentFeed->error = g_strdup (network_strerror (result->returncode, result->httpstatus));
174 	}
175 
176 	/* clean up... */
177 	commentFeed->updateJob = NULL;
178 
179 	/* rerender item with new comments */
180 	itemview_update_item (item);
181 	itemview_update ();
182 
183 	item_unload (item);
184 
185 	debug_exit ("comments_process_update_result");
186 }
187 
188 void
comments_refresh(itemPtr item)189 comments_refresh (itemPtr item)
190 {
191 	commentFeedPtr		commentFeed = NULL;
192 	updateRequestPtr	request;
193 	const gchar		*url;
194 
195 	if (!network_monitor_is_online ())
196 		return;
197 
198 	if (metadata_list_get (item->metadata, "commentFeedGone")) {
199 		debug0 (DEBUG_UPDATE, "Comment feed returned HTTP 410. Not updating anymore!");
200 		return;
201 	}
202 
203 	url = metadata_list_get (item->metadata, "commentFeedUri");
204 	if (url) {
205 		debug2 (DEBUG_UPDATE, "Updating comments for item \"%s\" (comment URL: %s)", item->title, url);
206 
207 		// FIXME: restore update state from DB?
208 
209 		if (item->commentFeedId) {
210 			commentFeed = comment_feed_from_id (item->commentFeedId);
211 		} else {
212 			item->commentFeedId = node_new_id ();
213 			db_item_update (item);
214 		}
215 
216 		if (!commentFeed) {
217 			commentFeed = g_new0 (struct commentFeed, 1);
218 			commentFeed->id = g_strdup (item->commentFeedId);
219 			commentFeed->itemId = item->id;
220 			commentFeed->updateState = update_state_new ();
221 
222 			if (!commentFeeds)
223 				commentFeeds = g_hash_table_new (g_str_hash, g_str_equal);
224 			g_hash_table_insert (commentFeeds, commentFeed->id, commentFeed);
225 		}
226 
227 		request = update_request_new ();
228 		request->options = g_new0 (struct updateOptions, 1);	// FIXME: use copy of parent subscription options
229 		request->source = g_strdup (url);
230 		commentFeed->updateJob = update_execute_request (commentFeed, request, comments_process_update_result, commentFeed, FEED_REQ_PRIORITY_HIGH);
231 
232 		/* Item view refresh to change link from "Update" to "Updating..." */
233 		itemview_update_item (item);
234 		itemview_update ();
235 	}
236 }
237 
238 void
comments_to_xml(xmlNodePtr parentNode,const gchar * id)239 comments_to_xml (xmlNodePtr parentNode, const gchar *id)
240 {
241 	xmlNodePtr	commentsNode;
242 	commentFeedPtr	commentFeed;
243 	itemSetPtr	itemSet;
244 	GList		*iter;
245 
246 	commentFeed = comment_feed_from_id (id);
247 	if (!commentFeed)
248 		return;
249 
250 	commentsNode = xmlNewChild (parentNode, NULL, "comments", NULL);
251 
252 	itemSet = db_itemset_load (id);
253 	g_return_if_fail (itemSet != NULL);
254 
255 	iter = itemSet->ids;
256 	while (iter)
257 	{
258 		itemPtr comment = item_load (GPOINTER_TO_UINT (iter->data));
259 		item_to_xml (comment, commentsNode);
260 		item_unload (comment);
261 		iter = g_list_next (iter);
262 	}
263 
264 	xmlNewTextChild (commentsNode, NULL, "updateState",
265 	                 (commentFeed->updateJob)?"updating":"ok");
266 
267 	if (commentFeed->error)
268 		xmlNewTextChild (commentsNode, NULL, "updateError", commentFeed->error);
269 
270 	itemset_free (itemSet);
271 }
272