1 /**
2  * @file export.c  OPML feed list import & export
3  *
4  * Copyright (C) 2004-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
5  * Copyright (C) 2004-2015 Lars Windolf <lars.windolf@gmx.de>
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20  */
21 
22 #include "export.h"
23 
24 #include <errno.h>
25 #include <glib.h>
26 #include <libxml/tree.h>
27 #include <sys/stat.h>
28 
29 #include "auth.h"
30 #include "common.h"
31 #include "db.h"
32 #include "debug.h"
33 #include "favicon.h"
34 #include "feedlist.h"
35 #include "folder.h"
36 #include "node.h"
37 #include "xml.h"
38 #include "ui/ui_common.h"
39 #include "ui/feed_list_node.h"
40 
41 struct exportData {
42 	gboolean	trusted; /**< Include all the extra Liferea-specific tags */
43 	xmlNodePtr	cur;
44 };
45 
46 static void export_node_children (nodePtr node, xmlNodePtr cur, gboolean trusted);
47 
48 /* Used for exporting, this adds a folder or feed's node to the XML tree */
49 static void
export_append_node_tag(nodePtr node,gpointer userdata)50 export_append_node_tag (nodePtr node, gpointer userdata)
51 {
52 	xmlNodePtr 	cur = ((struct exportData*)userdata)->cur;
53 	gboolean	internal = ((struct exportData*)userdata)->trusted;
54 	xmlNodePtr	childNode;
55 	gchar		*tmp;
56 
57 	/* When exporting external OPML do not export every node type... */
58 	if (!(internal || (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_EXPORT)))
59 		return;
60 
61 	childNode = xmlNewChild (cur, NULL, BAD_CAST"outline", NULL);
62 
63 	/* 1. write generic node attributes */
64 	xmlNewProp (childNode, BAD_CAST"title", BAD_CAST node_get_title(node));
65 	xmlNewProp (childNode, BAD_CAST"text", BAD_CAST node_get_title(node)); /* The OPML spec requires "text" */
66 	xmlNewProp (childNode, BAD_CAST"description", BAD_CAST node_get_title(node));
67 
68 	if (node_type_to_str (node))
69 		xmlNewProp (childNode, BAD_CAST"type", BAD_CAST node_type_to_str (node));
70 
71 	/* Don't add the following tags if we are exporting to other applications */
72 	if (internal) {
73 		xmlNewProp (childNode, BAD_CAST"id", BAD_CAST node_get_id (node));
74 
75 		switch (node->sortColumn) {
76 			case NODE_VIEW_SORT_BY_TITLE:
77 				xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"title");
78 				break;
79 			case NODE_VIEW_SORT_BY_TIME:
80 				xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"time");
81 				break;
82 			case NODE_VIEW_SORT_BY_PARENT:
83 				xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"parent");
84 				break;
85 			case NODE_VIEW_SORT_BY_STATE:
86 				xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"state");
87 				break;
88 			default:
89 				g_assert_not_reached();
90 				break;
91 		}
92 
93 		if (FALSE == node->sortReversed)
94 			xmlNewProp (childNode, BAD_CAST"sortReversed", BAD_CAST"false");
95 
96 		if (node->loadItemLink)
97 			xmlNewProp (childNode, BAD_CAST"loadItemLink", BAD_CAST"true");
98 
99 		/* Do not export the default view mode setting to avoid making
100 		   it permanent. Do not use node_get_view_mode () here to ensure
101 		   that the comparison works as node_get_view_mode () returns
102 		   the effective mode! */
103 		if (NODE_VIEW_MODE_DEFAULT != node->viewMode) {
104 			tmp = g_strdup_printf ("%u", node_get_view_mode(node));
105 			xmlNewProp (childNode, BAD_CAST"viewMode", BAD_CAST tmp);
106 			g_free (tmp);
107 		}
108 	}
109 
110 	/* 2. add node type specific stuff */
111 	NODE_TYPE (node)->export (node, childNode, internal);
112 
113 	/* 3. add children */
114 	if (internal) {
115 		if (feed_list_node_is_expanded (node->id))
116 			xmlNewProp (childNode, BAD_CAST"expanded", BAD_CAST"true");
117 		else
118 			xmlNewProp (childNode, BAD_CAST"collapsed", BAD_CAST"true");
119 	}
120 
121 	if (IS_FOLDER (node))
122 		export_node_children (node, childNode, internal);
123 }
124 
125 static void
export_node_children(nodePtr node,xmlNodePtr cur,gboolean trusted)126 export_node_children (nodePtr node, xmlNodePtr cur, gboolean trusted)
127 {
128 	struct exportData	params;
129 
130 	params.cur = cur;
131 	params.trusted = trusted;
132 	node_foreach_child_data (node, export_append_node_tag, &params);
133 }
134 
135 gboolean
export_OPML_feedlist(const gchar * filename,nodePtr node,gboolean trusted)136 export_OPML_feedlist (const gchar *filename, nodePtr node, gboolean trusted)
137 {
138 	xmlDocPtr 	doc;
139 	xmlNodePtr 	cur, opmlNode;
140 	gboolean	error = FALSE;
141 	gchar		*backupFilename;
142 	int		old_umask = 0;
143 
144 	debug_enter ("export_OPML_feedlist");
145 
146 	backupFilename = g_strdup_printf ("%s~", filename);
147 
148 	doc = xmlNewDoc ("1.0");
149 	if (doc) {
150 		opmlNode = xmlNewDocNode (doc, NULL, BAD_CAST"opml", NULL);
151 		if (opmlNode) {
152 			xmlNewProp (opmlNode, BAD_CAST"version", BAD_CAST"1.0");
153 
154 			/* create head */
155 			cur = xmlNewChild (opmlNode, NULL, BAD_CAST"head", NULL);
156 			if (cur)
157 				xmlNewTextChild (cur, NULL, BAD_CAST"title", BAD_CAST"Liferea Feed List Export");
158 
159 			/* create body with feed list */
160 			cur = xmlNewChild (opmlNode, NULL, BAD_CAST"body", NULL);
161 			if (cur)
162 				export_node_children (node, cur, trusted);
163 
164 			xmlDocSetRootElement (doc, opmlNode);
165 		} else {
166 			g_warning ("could not create XML feed node for feed cache document!");
167 			error = TRUE;
168 		}
169 
170 		if (!trusted)
171 			old_umask = umask (022);	/* give read permissions for other, per-default we wouldn't give it... */
172 
173 		xmlSetDocCompressMode (doc, 0);
174 
175 		if (-1 == xmlSaveFormatFile (backupFilename, doc, TRUE)) {
176 			g_warning ("Could not export to OPML file!");
177 			error = TRUE;
178 		}
179 
180 		if (!trusted)
181 			umask (old_umask);
182 
183 		xmlFreeDoc (doc);
184 
185 		if (!error) {
186 			// FIXME: Use g_rename() once we've reached Glib 2.6+
187 			if (rename (backupFilename, filename) < 0) {
188 				g_warning (_("Error renaming %s to %s: %s\n"), backupFilename, filename, g_strerror (errno));
189 				error = TRUE;
190 			}
191 		}
192 	} else {
193 		g_warning ("Could not create XML document!");
194 		error = TRUE;
195 	}
196 
197 	g_free (backupFilename);
198 
199 	debug_exit ("export_OPML_feedlist");
200 	return !error;
201 }
202 
203 static void
import_parse_outline(xmlNodePtr cur,nodePtr parentNode,gboolean trusted)204 import_parse_outline (xmlNodePtr cur, nodePtr parentNode, gboolean trusted)
205 {
206 	gchar		*title, *typeStr, *tmp, *sortStr;
207 	xmlNodePtr	child;
208 	nodePtr		node;
209 	nodeTypePtr	type = NULL;
210 	gboolean	needsUpdate = FALSE;
211 
212 	debug_enter("import_parse_outline");
213 
214 	/* 1. determine node type */
215 	typeStr = xmlGetProp (cur, BAD_CAST"type");
216 	if (typeStr) {
217 		type = node_str_to_type (typeStr);
218 		xmlFree (typeStr);
219 	}
220 
221 	/* if we didn't find a type attribute we use heuristics */
222 	if (!type) {
223 		/* check for a source URL */
224 		tmp = xmlGetProp (cur, BAD_CAST"xmlUrl");
225 		if (!tmp)
226 			tmp = xmlGetProp (cur, BAD_CAST"xmlurl");	/* AmphetaDesk */
227 		if (!tmp)
228 			tmp = xmlGetProp (cur, BAD_CAST"xmlURL");	/* LiveJournal */
229 
230 		if (tmp) {
231 			debug0 (DEBUG_CACHE, "-> URL found assuming type feed");
232 			type = feed_get_node_type();
233 			xmlFree (tmp);
234 		} else {
235 			/* if the outline has no type and URL it just has to be a folder */
236 			type = folder_get_node_type();
237 			debug0 (DEBUG_CACHE, "-> must be a folder");
238 		}
239 	}
240 
241 	g_assert (NULL != type);
242 
243 	/* Check if adding this type is allowed */
244 	// FIXME: Prevent news bins outside root source
245 	// FIXME: Prevent search folders outside root source
246 
247 	/* 2. do general node parsing */
248 	node = node_new (type);
249 	node_set_parent (node, parentNode, -1);
250 
251 	/* The id should only be used from feedlist.opml. Otherwise,
252 	   it could cause corruption if the same id was imported
253 	   multiple times. */
254 	if (trusted) {
255 		gchar *id = NULL;
256 		id = xmlGetProp (cur, BAD_CAST"id");
257 
258 		/* If, for some reason, the OPML has been corrupted
259 		   and there are two copies asking for a certain ID
260 		   then give the second one a new ID. */
261 		if (node_is_used_id (id)) {
262 			xmlFree (id);
263 			id = NULL;
264 		}
265 
266 		if (id) {
267 			node_set_id (node, id);
268 			xmlFree (id);
269 		} else {
270 			needsUpdate = TRUE;
271 		}
272 	} else {
273 		needsUpdate = TRUE;
274 	}
275 
276 	/* title */
277 	title = xmlGetProp (cur, BAD_CAST"title");
278 	if (!title || !xmlStrcmp (title, BAD_CAST"")) {
279 		if (title)
280 			xmlFree (title);
281 		title = xmlGetProp (cur, BAD_CAST"text");
282 	}
283 
284 	if (title) {
285 		node_set_title (node, title);
286 		xmlFree (title);
287 	}
288 
289 	/* sorting order */
290 	sortStr = xmlGetProp (cur, BAD_CAST"sortColumn");
291 	if (sortStr) {
292 		if (!xmlStrcmp (sortStr, "title"))
293 			node->sortColumn = NODE_VIEW_SORT_BY_TITLE;
294 		else if (!xmlStrcmp (sortStr, "parent"))
295 			node->sortColumn = NODE_VIEW_SORT_BY_PARENT;
296 		else if (!xmlStrcmp (sortStr, "state"))
297 			node->sortColumn = NODE_VIEW_SORT_BY_STATE;
298 		else
299 			node->sortColumn = NODE_VIEW_SORT_BY_TIME;
300 		xmlFree (sortStr);
301 	}
302 	sortStr = xmlGetProp (cur, BAD_CAST"sortReversed");
303 	if (sortStr) {
304 		if(!xmlStrcmp (sortStr, BAD_CAST"false"))
305 			node->sortReversed = FALSE;
306 		xmlFree (sortStr);
307 	}
308 
309 	/* auto item link loading flag */
310 	tmp = xmlGetProp (cur, BAD_CAST"loadItemLink");
311 	if (tmp) {
312 		if (!xmlStrcmp (tmp, BAD_CAST"true"))
313 		node->loadItemLink = TRUE;
314 		xmlFree (tmp);
315 	}
316 
317 	/* viewing mode */
318 	tmp = xmlGetProp (cur, BAD_CAST"viewMode");
319 	if (tmp) {
320 		node_set_view_mode (node, atoi (tmp));
321 		xmlFree (tmp);
322 	}
323 
324 	/* expansion state */
325 	if (xmlHasProp (cur, BAD_CAST"expanded"))
326 		node->expanded = TRUE;
327 	else if (xmlHasProp (cur, BAD_CAST"collapsed"))
328 		node->expanded = FALSE;
329 	else
330 		node->expanded = TRUE;
331 
332 	/* 3. Try to load the favicon (needs to be done before adding to the feed list) */
333 	node_load_icon (node);
334 
335 	/* 4. add to GUI parent */
336 	feedlist_node_imported (node);
337 
338 	/* 5. import child nodes */
339 	if (IS_FOLDER (node)) {
340 		child = cur->xmlChildrenNode;
341 		while (child) {
342 			if (!xmlStrcmp (child->name, BAD_CAST"outline"))
343 				import_parse_outline (child, node, trusted);
344 			child = child->next;
345 		}
346 	}
347 
348 	/* 6. do node type specific parsing */
349 	NODE_TYPE (node)->import (node, parentNode, cur, trusted);
350 
351 	if (node->subscription) {
352 		/* Handle OPML auth info (imported from subscription_import() */
353 		if(node->subscription->updateOptions->username) {
354 			/* Write to password store (for migration) */
355 			liferea_auth_info_store (node->subscription);
356 		} else {
357 			/* If no auth options in OPML try to import them from the key store */
358 			liferea_auth_info_query (node->id);
359 		}
360 
361 		/* 7. update immediately if necessary */
362 		if (needsUpdate) {
363 			debug1 (DEBUG_CACHE, "seems to be an import, setting new id: %s and doing first download...", node_get_id(node));
364 			subscription_update (node->subscription, 0);
365 		}
366 	}
367 
368 	/* 8. Always update the node info in the DB to ensure a proper
369 	   node entry and parent node information. Search folders would
370 	   silentely fail to work without node entry. */
371 	db_node_update (node);
372 
373 	debug_exit ("import_parse_outline");
374 }
375 
376 static void
import_parse_body(xmlNodePtr n,nodePtr parentNode,gboolean trusted)377 import_parse_body (xmlNodePtr n, nodePtr parentNode, gboolean trusted)
378 {
379 	xmlNodePtr cur;
380 
381 	cur = n->xmlChildrenNode;
382 	while (cur) {
383 		if (!xmlStrcmp (cur->name, BAD_CAST"outline"))
384 			import_parse_outline (cur, parentNode, trusted);
385 		cur = cur->next;
386 	}
387 }
388 
389 static void
import_parse_OPML(xmlNodePtr n,nodePtr parentNode,gboolean trusted)390 import_parse_OPML (xmlNodePtr n, nodePtr parentNode, gboolean trusted)
391 {
392 	xmlNodePtr cur;
393 
394 	cur = n->xmlChildrenNode;
395 	while (cur) {
396 		/* we ignore the head */
397 		if (!xmlStrcmp (cur->name, BAD_CAST"body")) {
398 			import_parse_body (cur, parentNode, trusted);
399 		}
400 		cur = cur->next;
401 	}
402 }
403 
404 gboolean
import_OPML_feedlist(const gchar * filename,nodePtr parentNode,gboolean showErrors,gboolean trusted)405 import_OPML_feedlist (const gchar *filename, nodePtr parentNode, gboolean showErrors, gboolean trusted)
406 {
407 	xmlDocPtr 	doc;
408 	xmlNodePtr 	cur;
409 	gboolean	error = FALSE;
410 
411 	debug1 (DEBUG_CACHE, "Importing OPML file: %s", filename);
412 
413 	/* read the feed list */
414 	doc = xmlParseFile (filename);
415 	if (!doc) {
416 		if (showErrors)
417 			ui_show_error_box (_("XML error while reading OPML file! Could not import \"%s\"!"), filename);
418 		else
419 			g_warning (_("XML error while reading OPML file! Could not import \"%s\"!"), filename);
420 		error = TRUE;
421 	} else {
422 		cur = xmlDocGetRootElement (doc);
423 		if (!cur) {
424 			if (showErrors)
425 				ui_show_error_box (_("Empty document! OPML document \"%s\" should not be empty when importing."), filename);
426 			else
427 				g_warning (_("Empty document! OPML document \"%s\" should not be empty when importing."), filename);
428 			error = TRUE;
429 		} else {
430 			if (!trusted) {
431 				/* set title only when importing as folder and not as OPML source */
432 				xmlNodePtr title = xpath_find (cur, "/opml/head/title");
433 				if (title) {
434 					xmlChar *titleStr = xmlNodeListGetString (title->doc, title->xmlChildrenNode, 1);
435 					if (titleStr) {
436 						node_set_title (parentNode, titleStr);
437 						xmlFree (titleStr);
438 					}
439 				}
440 			}
441 
442 			while (cur) {
443 				if (!xmlIsBlankNode (cur)) {
444 					if (!xmlStrcmp (cur->name, BAD_CAST"opml")) {
445 						import_parse_OPML (cur, parentNode, trusted);
446 					} else {
447 						if (showErrors)
448 							ui_show_error_box (_("\"%s\" is not a valid OPML document! Liferea cannot import this file!"), filename);
449 						else
450 							g_warning (_("\"%s\" is not a valid OPML document! Liferea cannot import this file!"), filename);
451 					}
452 				}
453 				cur = cur->next;
454 			}
455 		}
456 		xmlFreeDoc (doc);
457 	}
458 
459 	return !error;
460 }
461 
462 /* UI stuff */
463 
464 static void
on_import_activate_cb(const gchar * filename,gpointer user_data)465 on_import_activate_cb (const gchar *filename, gpointer user_data)
466 {
467 	if (filename) {
468 		nodePtr node = node_new (folder_get_node_type ());
469 		node_set_title (node, _("Imported feed list"));
470 		feedlist_node_added (node);
471 
472 		if (!import_OPML_feedlist (filename, node, TRUE /* show errors */, FALSE /* not trusted */)) {
473 			feedlist_remove_node (node);
474 		}
475 	}
476 }
477 
478 void
import_OPML_file(void)479 import_OPML_file (void)
480 {
481 	ui_choose_file(_("Import Feed List"), _("Import"), FALSE, on_import_activate_cb, NULL, NULL, "*.opml|*.xml", _("OPML Files"), NULL);
482 }
483 
484 static void
on_export_activate_cb(const gchar * filename,gpointer user_data)485 on_export_activate_cb (const gchar *filename, gpointer user_data)
486 {
487 	if (filename) {
488 		if (!export_OPML_feedlist (filename, feedlist_get_root (), FALSE))
489 			ui_show_error_box (_("Error while exporting feed list!"));
490 		else
491 			ui_show_info_box (_("Feed List exported!"));
492 	}
493 }
494 
495 void
export_OPML_file(void)496 export_OPML_file (void)
497 {
498 	ui_choose_file (_("Export Feed List"), _("Export"), TRUE, on_export_activate_cb,  NULL, "feedlist.opml", "*.opml", _("OPML Files"), NULL);
499 }
500