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, ¶ms);
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