1 /*
2  * @file node_source.c  generic node source provider implementation
3  *
4  * Copyright (C) 2005-2018 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/node_source.h"
22 
23 #include <gmodule.h>
24 #include <gtk/gtk.h>
25 #include <string.h>
26 
27 #include "common.h"
28 #include "db.h"
29 #include "debug.h"
30 #include "feed.h"
31 #include "feedlist.h"
32 #include "folder.h"
33 #include "item_state.h"
34 #include "node.h"
35 #include "node_type.h"
36 #include "plugins_engine.h"
37 #include "ui/icons.h"
38 #include "ui/liferea_dialog.h"
39 #include "ui/ui_common.h"
40 #include "ui/feed_list_node.h"
41 #include "fl_sources/default_source.h"
42 #include "fl_sources/dummy_source.h"
43 #include "fl_sources/google_source.h"
44 #include "fl_sources/opml_source.h"
45 #include "fl_sources/reedah_source.h"
46 #include "fl_sources/theoldreader_source.h"
47 #include "fl_sources/ttrss_source.h"
48 #include "fl_sources/node_source_activatable.h"
49 
50 static GSList		*nodeSourceTypes = NULL;
51 static PeasExtensionSet	*extensions = NULL;
52 
53 nodePtr
node_source_root_from_node(nodePtr node)54 node_source_root_from_node (nodePtr node)
55 {
56 	while (node->parent->source == node->source)
57 		node = node->parent;
58 
59 	return node;
60 }
61 
62 static nodeSourceTypePtr
node_source_type_find(const gchar * typeStr,guint capabilities)63 node_source_type_find (const gchar *typeStr, guint capabilities)
64 {
65 	GSList *iter = nodeSourceTypes;
66 
67 	while (iter) {
68 		nodeSourceTypePtr type = (nodeSourceTypePtr)iter->data;
69 		if (((NULL == typeStr) || !strcmp(type->id, typeStr)) &&
70 		    ((0 == capabilities) || (type->capabilities & capabilities)))
71 			return type;
72 		iter = g_slist_next (iter);
73 	}
74 
75 	g_print ("Could not find source type \"%s\"\n!", typeStr);
76 	return NULL;
77 }
78 
79 gboolean
node_source_type_register(nodeSourceTypePtr type)80 node_source_type_register (nodeSourceTypePtr type)
81 {
82 	debug1 (DEBUG_PARSING, "Registering node source type %s", type->name);
83 
84 	/* allow the plugin to initialize */
85 	type->source_type_init ();
86 
87 	/* Check if Google reader clones provide all API methods */
88 	if(type->capabilities & NODE_SOURCE_CAPABILITY_GOOGLE_READER_API) {
89 		g_assert (type->api.unread_count);
90 		g_assert (type->api.subscription_list);
91 		g_assert (type->api.add_subscription);
92 		g_assert (type->api.add_subscription_post);
93 		g_assert (type->api.remove_subscription);
94 		g_assert (type->api.remove_subscription_post);
95 		g_assert (type->api.edit_add_label);
96 		g_assert (type->api.edit_add_label_post);
97 		g_assert (type->api.edit_tag);
98 		g_assert (type->api.edit_tag_add_post);
99 		g_assert (type->api.edit_tag_remove_post);
100 		g_assert (type->api.edit_tag_ar_tag_post);
101 		g_assert (type->api.token);
102 	}
103 
104 	nodeSourceTypes = g_slist_append (nodeSourceTypes, type);
105 
106 	return TRUE;
107 }
108 
109 nodePtr
node_source_setup_root(void)110 node_source_setup_root (void)
111 {
112 	nodePtr	rootNode;
113 	nodeSourceTypePtr type;
114 
115 	debug_enter ("node_source_setup_root");
116 
117 	/* we need to register all source types once before doing anything... */
118 	node_source_type_register (default_source_get_type ());
119 	node_source_type_register (dummy_source_get_type ());
120 	node_source_type_register (opml_source_get_type ());
121 	node_source_type_register (google_source_get_type ());
122 	node_source_type_register (reedah_source_get_type ());
123 	node_source_type_register (ttrss_source_get_type ());
124 	node_source_type_register (theoldreader_source_get_type ());
125 
126 	extensions = peas_extension_set_new (PEAS_ENGINE (liferea_plugins_engine_get_default ()),
127 		                             LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE, NULL);
128 	liferea_plugins_engine_set_default_signals (extensions, NULL);
129 
130 	type = node_source_type_find (NULL, NODE_SOURCE_CAPABILITY_IS_ROOT);
131 	if (!type)
132 		g_error ("No root capable node source found!");
133 
134 	rootNode = node_new (root_get_node_type());
135 	rootNode->title = g_strdup ("root");
136 	rootNode->source = g_new0 (struct nodeSource, 1);
137 	rootNode->source->root = rootNode;
138 	rootNode->source->type = type;
139 	type->source_import (rootNode);
140 
141 	debug_exit ("node_source_setup_root");
142 
143 	return rootNode;
144 }
145 
146 static void
node_source_set_feed_subscription_type(nodePtr folder,subscriptionTypePtr type)147 node_source_set_feed_subscription_type (nodePtr folder, subscriptionTypePtr type)
148 {
149 	GSList *iter;
150 
151 	for (iter = folder->children; iter; iter = g_slist_next(iter)) {
152 		nodePtr node = (nodePtr) iter->data;
153 
154 		if (node->subscription)
155 			node->subscription->type = type;
156 
157 		/* Recurse for hierarchic nodes... */
158 		node_source_set_feed_subscription_type (node, type);
159 	}
160 }
161 
162 static void
node_source_import(nodePtr node,nodePtr parent,xmlNodePtr xml,gboolean trusted)163 node_source_import (nodePtr node, nodePtr parent, xmlNodePtr xml, gboolean trusted)
164 {
165 	nodeSourceTypePtr	type;
166 	xmlChar			*typeStr = NULL;
167 
168 	debug_enter ("node_source_import");
169 
170 	typeStr = xmlGetProp (xml, BAD_CAST"sourceType");
171 	if (!typeStr)
172 		typeStr = xmlGetProp (xml, BAD_CAST"pluginType"); /* for migration only */
173 
174 	if (typeStr) {
175 		debug2 (DEBUG_CACHE, "creating node source instance (type=%s,id=%s)", typeStr, node->id);
176 
177 		node->available = FALSE;
178 
179 		/* scan for matching node source and create new instance */
180 		type = node_source_type_find (typeStr, 0);
181 
182 		if (!type) {
183 			/* Source type is not available for some reason, but
184 			   we need a representation to keep the node source
185 			   in the feed list. So we load a dummy source type
186 			   instead and save the real source id in the
187 			   unused node's data field */
188 			type = node_source_type_find (NODE_SOURCE_TYPE_DUMMY_ID, 0);
189 			g_assert (NULL != type);
190 			node->data = g_strdup (typeStr);
191 		}
192 
193 		node->available = TRUE;
194 		node->source = NULL;
195 		node_source_new (node, type, NULL);
196 		node_set_subscription (node, subscription_import (xml, trusted));
197 
198 		type->source_import (node);
199 
200 		/* Set subscription type for all child nodes imported */
201 		node_source_set_feed_subscription_type (node, type->feedSubscriptionType);
202 
203 		if (!strcmp (typeStr, "fl_bloglines")) {
204 			g_print ("Removing obsolete Bloglines subscription.");
205 			feedlist_node_removed (node);
206 		}
207 	} else {
208 		g_print ("No source type given for node \"%s\". Ignoring it.", node_get_title (node));
209 	}
210 
211 	debug_exit ("node_source_import");
212 }
213 
214 static void
node_source_export(nodePtr node,xmlNodePtr xml,gboolean trusted)215 node_source_export (nodePtr node, xmlNodePtr xml, gboolean trusted)
216 {
217 	debug_enter ("node_source_export");
218 
219 	debug2 (DEBUG_CACHE, "node source export for node %s, id=%s", node->title, NODE_SOURCE_TYPE (node)->id);
220 
221 	/* If the node source type was loaded using the dummy node source
222 	   type we need to restore the original node source type id from
223 	   temporarily saved into node->data */
224 	if (!strcmp (NODE_SOURCE_TYPE (node)->id, NODE_SOURCE_TYPE_DUMMY_ID))
225 		xmlNewProp (xml, BAD_CAST"sourceType", BAD_CAST (node->data));
226 	else
227 		xmlNewProp (xml, BAD_CAST"sourceType", BAD_CAST (NODE_SOURCE_TYPE(node)->id));
228 
229 	subscription_export (node->subscription, xml, trusted);
230 
231 	debug_exit("node_source_export");
232 }
233 
234 void
node_source_new(nodePtr node,nodeSourceTypePtr type,const gchar * url)235 node_source_new (nodePtr node, nodeSourceTypePtr type, const gchar *url)
236 {
237 	subscriptionPtr	subscription;
238 
239 	g_assert (NULL == node->source);
240 
241 	node->source = g_new0 (struct nodeSource, 1);
242 	node->source->root = node;
243 	node->source->type = type;
244 	node->source->loginState = NODE_SOURCE_STATE_NONE;
245 	node->source->actionQueue = g_queue_new ();
246 
247 	node_set_title (node, type->name);
248 
249 	if (url) {
250 		subscription = subscription_new (url, NULL, NULL);
251 		node_set_subscription (node, subscription);
252 
253 		subscription->type = node->source->type->sourceSubscriptionType;
254 	}
255 }
256 
257 void
node_source_set_state(nodePtr node,gint newState)258 node_source_set_state (nodePtr node, gint newState)
259 {
260 	debug3 (DEBUG_UPDATE, "node source '%s' now in state %d (was %d)", node->id, newState, node->source->loginState);
261 
262 	/* State transition actions below... */
263 	if (newState == NODE_SOURCE_STATE_ACTIVE)
264 		node->source->authFailures = 0;
265 
266 	if (newState == NODE_SOURCE_STATE_NONE) {
267 		node->source->authFailures++;
268 		node->available = FALSE;
269 	}
270 
271 	if (node->source->authFailures >= NODE_SOURCE_MAX_AUTH_FAILURES)
272 		newState = NODE_SOURCE_STATE_NO_AUTH;
273 
274 	node->source->loginState = newState;
275 }
276 
277 void
node_source_set_auth_token(nodePtr node,gchar * token)278 node_source_set_auth_token (nodePtr node, gchar *token)
279 {
280 	g_assert (!node->source->authToken);
281 
282 	debug2 (DEBUG_UPDATE, "node source \"%s\" Auth token found: %s", node->id, token);
283 	node->source->authToken = token;
284 
285 	node_source_set_state (node, NODE_SOURCE_STATE_ACTIVE);
286 }
287 
288 /* source instance creation dialog */
289 
290 static void
on_node_source_type_selected(GtkTreeSelection * selection,gpointer user_data)291 on_node_source_type_selected (GtkTreeSelection *selection, gpointer user_data)
292 {
293 	GtkTreeIter	iter;
294 	GtkTreeModel	*model;
295 
296 	if (gtk_tree_selection_get_selected (selection, &model, &iter))
297 		gtk_widget_set_sensitive (GTK_WIDGET (user_data), TRUE);
298 	else
299 		gtk_widget_set_sensitive (GTK_WIDGET (user_data), FALSE);
300 }
301 
302 static void
on_node_source_type_response(GtkDialog * dialog,gint response_id,gpointer user_data)303 on_node_source_type_response (GtkDialog *dialog, gint response_id, gpointer user_data)
304 {
305 	GtkTreeSelection	*selection;
306 	GtkTreeModel		*model;
307 	GtkTreeIter		iter;
308 	nodeSourceTypePtr	type;
309 
310 	if (response_id == GTK_RESPONSE_OK) {
311 		selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (liferea_dialog_lookup (GTK_WIDGET (dialog), "type_list")));
312 		g_assert (NULL != selection);
313 		if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
314 			gtk_tree_model_get (model, &iter, 1, &type, -1);
315 			if (type)
316 				type->source_new ();
317 		}
318 	}
319 
320 	gtk_widget_destroy (GTK_WIDGET (dialog));
321 }
322 
323 static gboolean
feed_list_node_source_type_dialog(void)324 feed_list_node_source_type_dialog (void)
325 {
326 	GSList 			*iter = nodeSourceTypes;
327 	GtkWidget 		*dialog, *treeview;
328 	GtkTreeStore		*treestore;
329 	GtkCellRenderer		*renderer;
330 	GtkTreeIter		treeiter;
331 	nodeSourceTypePtr	type;
332 
333 	if (!nodeSourceTypes) {
334 		ui_show_error_box (_("No feed list source types found!"));
335 		return FALSE;
336 	}
337 
338 	/* set up the dialog */
339 	dialog = liferea_dialog_new ("node_source");
340 
341 	treestore = gtk_tree_store_new (2, G_TYPE_STRING, G_TYPE_POINTER);
342 
343 	/* add available feed list source to treestore */
344 	while (iter) {
345 		type = (nodeSourceTypePtr) iter->data;
346 		if (type->capabilities & NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION) {
347 
348 			gtk_tree_store_append (treestore, &treeiter, NULL);
349 			gtk_tree_store_set (treestore, &treeiter,
350 			                               0, type->name,
351 			                               1, type,
352 						       -1);
353 		}
354 		iter = g_slist_next (iter);
355 	}
356 
357 	treeview = liferea_dialog_lookup (dialog, "type_list");
358 	g_assert (NULL != treeview);
359 
360 	renderer = gtk_cell_renderer_text_new ();
361 	g_object_set (renderer, "wrap-width", 400, NULL);
362 	g_object_set (renderer, "wrap-mode", PANGO_WRAP_WORD, NULL);
363 	gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview), -1, _("Source Type"), renderer, "markup", 0, NULL);
364 	gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (treestore));
365 	g_object_unref (treestore);
366 
367 	gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview)),
368 	                             GTK_SELECTION_SINGLE);
369 
370 	g_signal_connect (G_OBJECT (dialog), "response",
371 			  G_CALLBACK (on_node_source_type_response),
372 			  NULL);
373 	g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview))), "changed",
374 	                  G_CALLBACK (on_node_source_type_selected),
375 	                  liferea_dialog_lookup (dialog, "ok_button"));
376 
377 
378 	return TRUE;
379 }
380 
381 void
node_source_update(nodePtr node)382 node_source_update (nodePtr node)
383 {
384 	if (node->subscription) {
385 		/* Reset NODE_SOURCE_STATE_NO_AUTH as this is a manual
386 		   user interaction and no auto-update so we can query
387 		   for credentials again. */
388 		if (node->source->loginState == NODE_SOURCE_STATE_NO_AUTH)
389 			node_source_set_state (node, NODE_SOURCE_STATE_NONE);
390 
391 		subscription_update (node->subscription, 0);
392 
393 		/* Note that node sources are required to auto-update child
394 		   nodes themselves once login and feed list update is fine. */
395 	} else {
396 		/* for default source */
397 		node_foreach_child_data (node, node_update_subscription, GUINT_TO_POINTER (0));
398 	}
399 }
400 
401 void
node_source_auto_update(nodePtr node)402 node_source_auto_update (nodePtr node)
403 {
404 	NODE_SOURCE_TYPE (node)->source_auto_update (node);
405 }
406 
407 static gboolean
node_source_is_logged_in(nodePtr node)408 node_source_is_logged_in (nodePtr node)
409 {
410 	if (FALSE == (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_CAN_LOGIN))
411 		return TRUE;
412 
413 	if (node->source->loginState != NODE_SOURCE_STATE_ACTIVE)
414 		ui_show_error_box (_("Login for '%s' has not yet completed! Please wait until login is done."), node->title);
415 
416 	return node->source->loginState == NODE_SOURCE_STATE_ACTIVE;
417 }
418 
419 nodePtr
node_source_add_subscription(nodePtr node,subscriptionPtr subscription)420 node_source_add_subscription (nodePtr node, subscriptionPtr subscription)
421 {
422 	if (!node_source_is_logged_in (node))
423 		return NULL;
424 
425 	if (NODE_SOURCE_TYPE (node)->add_subscription)
426 		return NODE_SOURCE_TYPE (node)->add_subscription (node, subscription);
427 	else
428 		g_print ("node_source_add_subscription(): called on node source type that doesn't implement me!");
429 
430 	return NULL;
431 }
432 
433 nodePtr
node_source_add_folder(nodePtr node,const gchar * title)434 node_source_add_folder (nodePtr node, const gchar *title)
435 {
436 	if (!node_source_is_logged_in (node))
437 		return NULL;
438 
439 	if (NODE_SOURCE_TYPE (node)->add_folder)
440 		return NODE_SOURCE_TYPE (node)->add_folder (node, title);
441 	else
442 		g_print ("node_source_add_folder(): called on node source type that doesn't implement me!");
443 
444 	return NULL;
445 }
446 
447 void
node_source_update_folder(nodePtr node,nodePtr folder)448 node_source_update_folder (nodePtr node, nodePtr folder)
449 {
450 	if (!node_source_is_logged_in (node))
451 		return;
452 
453 	if (!folder)
454 		folder = node->source->root;
455 
456 	if (node->parent != folder) {
457 		debug2 (DEBUG_UPDATE, "Moving node \"%s\" to folder \"%s\"", node->title, folder->title);
458 		node_reparent (node, folder);
459 	}
460 }
461 
462 nodePtr
node_source_find_or_create_folder(nodePtr parent,const gchar * id,const gchar * name)463 node_source_find_or_create_folder (nodePtr parent, const gchar *id, const gchar *name)
464 {
465 	nodePtr		folder = NULL;
466 	gchar		*folderNodeId;
467 
468 	if (!id)
469 		return parent->source->root;	/* No id means folder is root node */
470 
471 	folderNodeId = g_strdup_printf ("%s-folder-%s", NODE_SOURCE_TYPE (parent->source->root)->id, id);
472 	folder = node_from_id (folderNodeId);
473 	if (!folder) {
474 		folder = node_new (folder_get_node_type ());
475 		node_set_id (folder, folderNodeId);
476 		node_set_title (folder, name);
477 		node_set_parent (folder, parent, -1);
478 		feedlist_node_imported (folder);
479 		subscription_update (folder->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH);
480 	}
481 
482 	return folder;
483 }
484 
485 void
node_source_remove_node(nodePtr node,nodePtr child)486 node_source_remove_node (nodePtr node, nodePtr child)
487 {
488 	if (!node_source_is_logged_in (node))
489 		return;
490 
491 	g_assert (child != node);
492 	g_assert (child != child->source->root);
493 
494 	if (NODE_SOURCE_TYPE (node)->remove_node)
495 		NODE_SOURCE_TYPE (node)->remove_node (node, child);
496 	else
497 		g_print ("node_source_remove_node(): called on node source type that doesn't implement me!");
498 }
499 
500 void
node_source_item_mark_read(nodePtr node,itemPtr item,gboolean newState)501 node_source_item_mark_read (nodePtr node, itemPtr item, gboolean newState)
502 {
503 	/* Item read state changes are optional for node source
504 	   implementations. If they are supported the implementation
505 	   has to call item_read_state_changed(), otherwise we do
506 	   call it here. */
507 
508 	if (NODE_SOURCE_TYPE (node)->item_mark_read)
509 		NODE_SOURCE_TYPE (node)->item_mark_read (node, item, newState);
510 	else
511 		item_read_state_changed (item, newState);
512 }
513 
514 void
node_source_item_set_flag(nodePtr node,itemPtr item,gboolean newState)515 node_source_item_set_flag (nodePtr node, itemPtr item, gboolean newState)
516 {
517 	/* Item flag state changes are optional for node source
518 	   implementations. If they are supported the implementation
519 	   has to call item_flag_state_changed(), otherwise we do
520 	   call it here. */
521 
522 	if (NODE_SOURCE_TYPE (node)->item_set_flag)
523 		NODE_SOURCE_TYPE (node)->item_set_flag (node, item, newState);
524 	else
525 		item_flag_state_changed (item, newState);
526 }
527 
528 static void
node_source_convert_to_local_child_node(nodePtr node)529 node_source_convert_to_local_child_node (nodePtr node)
530 {
531 	/* Ensure to remove special subscription types and cancel updates
532 	   Note: we expect that all feeds already have the subscription URL
533 	   set. This might need to be done by the node type specific
534 	   convert_to_local() method! */
535 	if (node->subscription) {
536 		update_job_cancel_by_owner ((gpointer)node);
537 		update_job_cancel_by_owner ((gpointer)node->subscription);
538 
539 		debug2 (DEBUG_UPDATE, "Converting feed: %s = %s\n", node->title, node->subscription->source);
540 
541 		node->subscription->type = feed_get_subscription_type ();
542 	}
543 
544 	if (IS_FOLDER (node))
545 		node_foreach_child (node, node_source_convert_to_local_child_node);
546 
547 	node->source = ((nodePtr)feedlist_get_root ())->source;
548 }
549 
550 void
node_source_convert_to_local(nodePtr node)551 node_source_convert_to_local (nodePtr node)
552 {
553 	g_assert (node == node->source->root);
554 
555 	/* Preparation */
556 
557 	update_job_cancel_by_owner ((gpointer)node);
558 	update_job_cancel_by_owner ((gpointer)node->subscription);
559 	update_job_cancel_by_owner ((gpointer)node->source);
560 
561 	/* Give the node source type the chance to do things ... */
562 	if (NULL != NODE_SOURCE_TYPE (node)->convert_to_local)
563 		NODE_SOURCE_TYPE (node)->convert_to_local (node);
564 
565 	/* Perform conversion */
566 
567 	debug0 (DEBUG_UPDATE, "Converting root node to folder...");
568 	node->source = ((nodePtr)feedlist_get_root ())->source;
569 	node->type = folder_get_node_type ();
570 	node->subscription = NULL;	/* leaking subscription is ok */
571 	node->data = NULL;		/* leaking data is ok */
572 
573 	node_foreach_child (node, node_source_convert_to_local_child_node);
574 
575 	feedlist_schedule_save ();
576 
577 	/* FIXME: something is not perfect, because if you immediately
578 	   remove the subscription tree afterwards there is a double free */
579 
580 	ui_show_info_box (_("The '%s' subscription was successfully converted to local feeds!"), node->title);
581 }
582 
583 /* implementation of the node type interface */
584 
585 static void
node_source_remove(nodePtr node)586 node_source_remove (nodePtr node)
587 {
588 	if (!node_source_is_logged_in (node))
589 		return;
590 
591 	g_assert (node == node->source->root);
592 
593 	if (NULL != NODE_SOURCE_TYPE (node)->source_delete)
594 		NODE_SOURCE_TYPE (node)->source_delete (node);
595 
596 	feed_list_node_remove_node (node);
597 }
598 
599 static void
node_source_save(nodePtr node)600 node_source_save (nodePtr node)
601 {
602 	node_foreach_child (node, node_save);
603 }
604 
605 static void
node_source_free(nodePtr node)606 node_source_free (nodePtr node)
607 {
608 	if (NULL != NODE_SOURCE_TYPE (node)->free)
609 		NODE_SOURCE_TYPE (node)->free (node);
610 
611 	g_free (node->source->authToken);
612 	g_free (node->source);
613 	node->source = NULL;
614 }
615 
616 nodeTypePtr
node_source_get_node_type(void)617 node_source_get_node_type (void)
618 {
619 	static nodeTypePtr	nodeType;
620 
621 	if (!nodeType) {
622 		/* derive the node source node type from the folder node type */
623 		nodeType = (nodeTypePtr) g_new0 (struct nodeType, 1);
624 		nodeType->id			= "source";
625 		nodeType->icon			= icon_get (ICON_DEFAULT);
626 		nodeType->capabilities		= NODE_CAPABILITY_SHOW_UNREAD_COUNT |
627 						  NODE_CAPABILITY_SHOW_ITEM_FAVICONS |
628 						  NODE_CAPABILITY_UPDATE_CHILDS |
629 						  NODE_CAPABILITY_UPDATE |
630 						  NODE_CAPABILITY_UPDATE_FAVICON |
631 						  NODE_CAPABILITY_ADD_CHILDS |
632 						  NODE_CAPABILITY_REMOVE_CHILDS;
633 		nodeType->import		= node_source_import;
634 		nodeType->export		= node_source_export;
635 		nodeType->load			= folder_get_node_type()->load;
636 		nodeType->save			= node_source_save;
637 		nodeType->update_counters	= folder_get_node_type()->update_counters;
638 		nodeType->remove		= node_source_remove;
639 		nodeType->render		= node_default_render;
640 		nodeType->request_add		= feed_list_node_source_type_dialog;
641 		nodeType->request_properties	= feed_list_node_rename;
642 		nodeType->free			= node_source_free;
643 	}
644 
645 	return nodeType;
646 }
647