1 /**
2 * @file opml_source.c OPML Planet/Blogroll feed list source
3 *
4 * Copyright (C) 2006-2016 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 "fl_sources/opml_source.h"
22
23 #include <unistd.h>
24
25 #include "common.h"
26 #include "debug.h"
27 #include "export.h"
28 #include "feed.h"
29 #include "feedlist.h"
30 #include "folder.h"
31 #include "node.h"
32 #include "xml.h"
33 #include "ui/icons.h"
34 #include "ui/liferea_dialog.h"
35 #include "ui/ui_common.h"
36
37 /** default OPML update interval = once a day */
38 #define OPML_SOURCE_UPDATE_INTERVAL 60*60*24
39
40 /* OPML subscription list helper functions */
41
42 typedef struct mergeCtxt {
43 nodePtr rootNode; /**< root node of the OPML feed list source */
44 nodePtr parent; /**< currently processed feed list node */
45 xmlNodePtr xmlNode; /**< currently processed XML node of old OPML doc */
46 } *mergeCtxtPtr;
47
48 static void
opml_source_merge_feed(xmlNodePtr match,gpointer user_data)49 opml_source_merge_feed (xmlNodePtr match, gpointer user_data)
50 {
51 mergeCtxtPtr mergeCtxt = (mergeCtxtPtr)user_data;
52 xmlChar *url, *title;
53 gchar *expr;
54 nodePtr node = NULL;
55
56 url = xmlGetProp (match, "xmlUrl");
57 title = xmlGetProp (match, "title");
58 if (!title)
59 title = xmlGetProp (match, "description");
60 if (!title)
61 title = xmlGetProp (match, "text");
62 if (!title && !url)
63 return;
64
65 if (url)
66 expr = g_strdup_printf ("//outline[@xmlUrl = '%s']", url);
67 else
68 expr = g_strdup_printf ("//outline[@title = '%s']", title);
69
70 if (!xpath_find (mergeCtxt->xmlNode, expr)) {
71 debug2(DEBUG_UPDATE, "adding %s (%s)", title, url);
72 if (url) {
73 node = node_new (feed_get_node_type ());
74 node_set_data (node, feed_new ());
75 node_set_subscription (node, subscription_new (url, NULL, NULL));
76 } else {
77 node = node_new (folder_get_node_type ());
78 }
79 node_set_title (node, title);
80 node_set_parent (node, mergeCtxt->rootNode, -1);
81 feedlist_node_imported (node);
82
83 subscription_update (node->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH);
84 }
85
86 /* Recursion if this is a folder */
87 if (!url) {
88 if (!node) {
89 /* if the folder node wasn't created above it
90 must already exist and we search it in the
91 parents children list */
92 GSList *iter = mergeCtxt->parent->children;
93 while (iter) {
94 if (g_str_equal (title, node_get_title (iter->data)))
95 node = iter->data;
96 iter = g_slist_next (iter);
97 }
98 }
99
100 if (node) {
101 mergeCtxtPtr mc = g_new0 (struct mergeCtxt, 1);
102 mc->rootNode = mergeCtxt->rootNode;
103 mc->parent = node;
104 mc->xmlNode = mergeCtxt->xmlNode; // FIXME: must be correct child!
105 xpath_foreach_match (match, "./outline", opml_source_merge_feed, (gpointer)mc);
106 g_free (mc);
107 } else {
108 g_print ("opml_source_merge_feed(): bad! bad! very bad!");
109 }
110 }
111
112 g_free (expr);
113 xmlFree (title);
114 xmlFree (url);
115 }
116
117 static void
opml_source_check_for_removal(nodePtr node,gpointer user_data)118 opml_source_check_for_removal (nodePtr node, gpointer user_data)
119 {
120 gchar *expr = NULL;
121
122 if (IS_FEED (node)) {
123 expr = g_strdup_printf ("//outline[ @xmlUrl='%s' ]", subscription_get_source (node->subscription));
124 } else if (IS_FOLDER (node)) {
125 node_foreach_child_data (node, opml_source_check_for_removal, user_data);
126 expr = g_strdup_printf ("//outline[ (@title='%s') or (@text='%s') or (@description='%s')]", node->title, node->title, node->title);
127 } else {
128 g_print ("opml_source_check_for_removal(): This should never happen...");
129 return;
130 }
131
132 if (!xpath_find ((xmlNodePtr)user_data, expr)) {
133 debug1 (DEBUG_UPDATE, "removing %s...", node_get_title (node));
134 feedlist_node_removed (node);
135 } else {
136 debug1 (DEBUG_UPDATE, "keeping %s...", node_get_title (node));
137 }
138 g_free (expr);
139 }
140
141 /* OPML subscription type implementation */
142
143 static gboolean
opml_subscription_prepare_update_request(subscriptionPtr subscription,struct updateRequest * request)144 opml_subscription_prepare_update_request (subscriptionPtr subscription, struct updateRequest *request)
145 {
146 /* Nothing to do here for simple OPML subscriptions */
147 return TRUE;
148 }
149
150 static void
opml_subscription_process_update_result(subscriptionPtr subscription,const struct updateResult * const result,updateFlags flags)151 opml_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags)
152 {
153 nodePtr node = subscription->node;
154 mergeCtxtPtr mergeCtxt;
155 xmlDocPtr doc, oldDoc;
156 xmlNodePtr root, title;
157
158 debug1 (DEBUG_UPDATE, "OPML download finished data=%d", result->data);
159
160 node->available = FALSE;
161
162 if (result->data) {
163 doc = xml_parse (result->data, result->size, NULL);
164 if (doc) {
165 gchar *filename;
166
167 root = xmlDocGetRootElement (doc);
168
169 /* Go through all existing nodes and remove those whose
170 URLs are not in new feed list. Also removes those URLs
171 from the list that have corresponding existing nodes. */
172 node_foreach_child_data (node, opml_source_check_for_removal, (gpointer)root);
173
174 opml_source_export (node); /* save new feed list tree to disk
175 to ensure correct document in
176 next step */
177
178 /* Merge up-to-date OPML feed list. */
179 filename = opml_source_get_feedlist (node);
180 oldDoc = xmlParseFile (filename);
181 g_free (filename);
182
183 mergeCtxt = g_new0 (struct mergeCtxt, 1);
184 mergeCtxt->rootNode = node;
185 mergeCtxt->parent = node;
186 mergeCtxt->xmlNode = xmlDocGetRootElement (oldDoc);
187
188 if (g_str_equal (node_get_title (node), OPML_SOURCE_DEFAULT_TITLE)) {
189 title = xpath_find (root, "/opml/head/title");
190 if (title) {
191 xmlChar *titleStr = xmlNodeListGetString(title->doc, title->xmlChildrenNode, 1);
192 if (titleStr) {
193 node_set_title (node, titleStr);
194 xmlFree (titleStr);
195 }
196 }
197 }
198
199 xpath_foreach_match (root, "/opml/body/outline",
200 opml_source_merge_feed,
201 (gpointer)mergeCtxt);
202 g_free (mergeCtxt);
203 xmlFreeDoc (oldDoc);
204 xmlFreeDoc (doc);
205
206 opml_source_export (node); /* save new feed list tree to disk */
207
208 node->available = TRUE;
209 } else {
210 g_print ("Cannot parse downloaded OPML document!");
211 }
212 }
213
214 node_foreach_child_data (node, node_update_subscription, GUINT_TO_POINTER (0));
215 }
216
217 /* subscription type definition */
218
219 static struct subscriptionType opmlSubscriptionType = {
220 opml_subscription_prepare_update_request,
221 opml_subscription_process_update_result
222 };
223
224 /* OPML source type implementation */
225
226 static void ui_opml_source_get_source_url (void);
227
228 gchar *
opml_source_get_feedlist(nodePtr node)229 opml_source_get_feedlist (nodePtr node)
230 {
231 return common_create_cache_filename ("plugins", node->id, "opml");
232 }
233
234 void
opml_source_import(nodePtr node)235 opml_source_import (nodePtr node)
236 {
237 gchar *filename;
238
239 debug_enter ("opml_source_import");
240
241 /* We only ship an icon for opml, not for other sources */
242 if (g_str_equal (NODE_SOURCE_TYPE (node)->id, "fl_opml"))
243 node->icon = icon_create_from_file ("fl_opml.png");
244
245 debug1 (DEBUG_CACHE, "starting import of opml source instance (id=%s)", node->id);
246 filename = opml_source_get_feedlist (node);
247 if (g_file_test (filename, G_FILE_TEST_EXISTS)) {
248 import_OPML_feedlist (filename, node, FALSE, TRUE);
249 } else {
250 g_print ("cannot open \"%s\"", filename);
251 node->available = FALSE;
252 }
253 g_free (filename);
254
255 subscription_set_update_interval (node->subscription, OPML_SOURCE_UPDATE_INTERVAL);
256
257 node->subscription->type = &opmlSubscriptionType;
258
259 debug_exit ("opml_source_import");
260 }
261
262 void
opml_source_export(nodePtr node)263 opml_source_export (nodePtr node)
264 {
265 gchar *filename;
266
267 debug_enter ("opml_source_export");
268
269 /* Although the OPML structure won't change, it needs to
270 be saved so that the feed ids are saved to disk after
271 the first import or updates of the source OPML. */
272
273 g_assert (node == node->source->root);
274
275 filename = opml_source_get_feedlist (node);
276 export_OPML_feedlist (filename, node, TRUE);
277 g_free (filename);
278
279 debug1 (DEBUG_CACHE, "adding OPML source: title=%s", node_get_title(node));
280
281 debug_exit ("opml_source_export");
282 }
283
284 void
opml_source_remove(nodePtr node)285 opml_source_remove (nodePtr node)
286 {
287 gchar *filename;
288
289 /* step 1: delete all child nodes */
290 node_foreach_child (node, feedlist_node_removed);
291 g_assert (!node->children);
292
293 /* step 2: delete source instance OPML cache file */
294 filename = opml_source_get_feedlist (node);
295 unlink (filename);
296 g_free (filename);
297 }
298
299 static void
opml_source_auto_update(nodePtr node)300 opml_source_auto_update (nodePtr node)
301 {
302 GTimeVal now;
303
304 g_get_current_time (&now);
305
306 /* do daily updates for the feed list and feed updates according to the default interval */
307 if (node->subscription->updateState->lastPoll.tv_sec + OPML_SOURCE_UPDATE_INTERVAL <= now.tv_sec)
308 node_source_update (node);
309 }
310
opml_source_init(void)311 static void opml_source_init(void) { }
312
opml_source_deinit(void)313 static void opml_source_deinit(void) { }
314
315 /* node source type definition */
316
317 static struct nodeSourceType nst = {
318 .id = "fl_opml",
319 .name = N_("Planet, BlogRoll, OPML"),
320 .sourceSubscriptionType = &opmlSubscriptionType,
321 .capabilities = NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION,
322 .source_type_init = opml_source_init,
323 .source_type_deinit = opml_source_deinit,
324 .source_new = ui_opml_source_get_source_url,
325 .source_delete = opml_source_remove,
326 .source_import = opml_source_import,
327 .source_export = opml_source_export,
328 .source_get_feedlist = opml_source_get_feedlist,
329 .source_auto_update = opml_source_auto_update,
330 .free = NULL,
331 .item_set_flag = NULL,
332 .item_mark_read = NULL,
333 .add_folder = NULL,
334 .add_subscription = NULL,
335 .remove_node = NULL,
336 .convert_to_local = NULL
337 };
338
339 nodeSourceTypePtr
opml_source_get_type(void)340 opml_source_get_type (void)
341 {
342 nst.feedSubscriptionType = feed_get_subscription_type ();
343
344 return &nst;
345 }
346
347 /* GUI callbacks */
348
349 static void
on_opml_source_selected(GtkDialog * dialog,gint response_id,gpointer user_data)350 on_opml_source_selected (GtkDialog *dialog,
351 gint response_id,
352 gpointer user_data)
353 {
354 nodePtr node;
355
356 if (response_id == GTK_RESPONSE_OK) {
357 node = node_new (node_source_get_node_type ());
358 node_set_title (node, OPML_SOURCE_DEFAULT_TITLE);
359 node_source_new (node, opml_source_get_type (), gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET (dialog), "location_entry"))));
360 feedlist_node_added (node);
361 node_source_update (node);
362 }
363
364 gtk_widget_destroy (GTK_WIDGET (dialog));
365 }
366
367 static void
on_opml_file_selected(const gchar * filename,gpointer user_data)368 on_opml_file_selected (const gchar *filename, gpointer user_data)
369 {
370 GtkWidget *dialog = GTK_WIDGET (user_data);
371
372 if (filename && dialog)
373 gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (dialog, "location_entry")), g_strdup(filename));
374 }
375
376 static void
on_opml_file_choose_clicked(GtkButton * button,gpointer user_data)377 on_opml_file_choose_clicked (GtkButton *button, gpointer user_data)
378 {
379 ui_choose_file (_("Choose OPML File"), _("_Open"), FALSE, on_opml_file_selected, NULL, NULL, "*.opml|*.xml", _("OPML Files"), user_data);
380 }
381
382 static void
ui_opml_source_get_source_url(void)383 ui_opml_source_get_source_url (void)
384 {
385 GtkWidget *dialog, *button;
386
387 dialog = liferea_dialog_new ("opml_source");
388 button = liferea_dialog_lookup (dialog, "select_button");
389
390 g_signal_connect (G_OBJECT (dialog), "response",
391 G_CALLBACK (on_opml_source_selected),
392 NULL);
393
394 g_signal_connect (G_OBJECT (button), "clicked",
395 G_CALLBACK (on_opml_file_choose_clicked),
396 dialog);
397 }
398