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