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