1 /*
2  * @file feedlist.c  subscriptions as an hierarchic tree
3  *
4  * Copyright (C) 2005-2013 Lars Windolf <lars.windolf@gmx.de>
5  * Copyright (C) 2005-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
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 <libxml/uri.h>
23 
24 #include "comments.h"
25 #include "common.h"
26 #include "conf.h"
27 #include "db.h"
28 #include "debug.h"
29 #include "feed.h"
30 #include "feedlist.h"
31 #include "folder.h"
32 #include "itemlist.h"
33 #include "net_monitor.h"
34 #include "node.h"
35 #include "update.h"
36 #include "vfolder.h"
37 #include "ui/feed_list_view.h"
38 #include "ui/itemview.h"
39 #include "ui/liferea_shell.h"
40 #include "ui/feed_list_node.h"
41 #include "fl_sources/node_source.h"
42 
43 static void feedlist_save	(void);
44 
45 #define FEEDLIST_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), FEEDLIST_TYPE, FeedListPrivate))
46 
47 struct FeedListPrivate {
48 	guint		newCount;	/*<< overall new item count */
49 
50 	nodePtr		rootNode;	/*<< the feed list root node */
51 	nodePtr		selectedNode;	/*<< matches the node selected in the feed list tree view, which
52 					     is not necessarily the displayed one (e.g. folders without recursive
53 					     display enabled) */
54 
55 	guint		saveTimer;	/*<< timer id for delayed feed list saving */
56 	guint		autoUpdateTimer; /*<< timer id for auto update */
57 
58 	gboolean	loading;	/*<< prevents the feed list being saved before it is completely loaded */
59 };
60 
61 enum {
62 	NEW_ITEMS,		/*<< node has new items after update */
63 	NODE_UPDATED,		/*<< node display info (title, unread count) has changed */
64 	LAST_SIGNAL
65 };
66 
67 #define ROOTNODE feedlist->priv->rootNode
68 #define SELECTED feedlist->priv->selectedNode
69 
70 static guint feedlist_signals[LAST_SIGNAL] = { 0 };
71 
72 static GObjectClass *parent_class = NULL;
73 FeedList *feedlist = NULL;
74 
75 G_DEFINE_TYPE (FeedList, feedlist, G_TYPE_OBJECT);
76 
77 static void
feedlist_free_node(nodePtr node)78 feedlist_free_node (nodePtr node)
79 {
80 	if (node->children)
81 		node_foreach_child (node, feedlist_free_node);
82 
83 	node->parent->children = g_slist_remove (node->parent->children, node);
84 	node_free (node);
85 }
86 
87 static void
feedlist_finalize(GObject * object)88 feedlist_finalize (GObject *object)
89 {
90 	/* Stop all timer based activity */
91 	if (feedlist->priv->autoUpdateTimer) {
92 		g_source_remove (feedlist->priv->autoUpdateTimer);
93 		feedlist->priv->autoUpdateTimer = 0;
94 	}
95 	if (feedlist->priv->saveTimer) {
96 		g_source_remove (feedlist->priv->saveTimer);
97 		feedlist->priv->saveTimer = 0;
98 	}
99 
100 	/* Enforce synchronous save upon exit */
101 	feedlist_save ();
102 
103 	/* Save last selection for next start */
104 	if (feedlist->priv->selectedNode)
105 		conf_set_str_value (LAST_NODE_SELECTED, feedlist->priv->selectedNode->id);
106 
107 
108 	/* And destroy everything */
109 	feedlist_foreach (feedlist_free_node);
110 	node_free (ROOTNODE);
111 	ROOTNODE = NULL;
112 
113 	/* This might also be a good place to get for some other cleanup */
114 	comments_deinit ();
115 
116 	G_OBJECT_CLASS (parent_class)->finalize (object);
117 }
118 
119 static void
feedlist_class_init(FeedListClass * klass)120 feedlist_class_init (FeedListClass *klass)
121 {
122 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
123 
124 	parent_class = g_type_class_peek_parent (klass);
125 
126 	object_class->finalize = feedlist_finalize;
127 
128 	feedlist_signals[NEW_ITEMS] =
129 		g_signal_new ("new-items",
130 		G_OBJECT_CLASS_TYPE (object_class),
131 		(GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION),
132 		0,
133 		NULL,
134 		NULL,
135 		g_cclosure_marshal_VOID__POINTER,
136 		G_TYPE_NONE,
137 		1,
138 		G_TYPE_POINTER);
139 
140 	feedlist_signals[NODE_UPDATED] =
141 		g_signal_new ("node-updated",
142 		G_OBJECT_CLASS_TYPE (object_class),
143 		(GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION),
144 		0,
145 		NULL,
146 		NULL,
147 		g_cclosure_marshal_VOID__STRING,
148 		G_TYPE_NONE,
149 		1,
150 		G_TYPE_STRING);
151 
152 	g_type_class_add_private (object_class, sizeof(FeedListPrivate));
153 }
154 
155 static gboolean
feedlist_auto_update(void * data)156 feedlist_auto_update (void *data)
157 {
158 	debug_enter ("feedlist_auto_update");
159 
160 	if (network_monitor_is_online ())
161 		node_auto_update_subscription (ROOTNODE);
162 	else
163 		debug0 (DEBUG_UPDATE, "no update processing because we are offline!");
164 
165 	debug_exit ("feedlist_auto_update");
166 
167 	return TRUE;
168 }
169 
170 static void
on_network_status_changed(gpointer instance,gboolean online,gpointer data)171 on_network_status_changed (gpointer instance, gboolean online, gpointer data)
172 {
173 	if (online) feedlist_auto_update (NULL);
174 }
175 
176 /* This method is used to initialize the node states in the feed list */
177 static void
feedlist_init_node(nodePtr node)178 feedlist_init_node (nodePtr node)
179 {
180 	if (node->expanded)
181 		feed_list_node_set_expansion (node, TRUE);
182 
183 	if (node->subscription)
184 		db_subscription_load (node->subscription);
185 
186 	node_update_counters (node);
187 	feed_list_node_update (node->id);	/* Necessary to initially set folder unread counters */
188 
189 	node_foreach_child (node, feedlist_init_node);
190 }
191 
192 static void
feedlist_init(FeedList * fl)193 feedlist_init (FeedList *fl)
194 {
195 	gint	startup_feed_action;
196 
197 	debug_enter ("feedlist_init");
198 
199 	/* 1. Prepare globally accessible singleton */
200 	g_assert (NULL == feedlist);
201 	feedlist = fl;
202 
203 	feedlist->priv = FEEDLIST_GET_PRIVATE (fl);
204 	feedlist->priv->loading = TRUE;
205 
206 	/* 2. Set up a root node and import the feed list source structure. */
207 	debug0 (DEBUG_CACHE, "Setting up root node");
208 	ROOTNODE = node_source_setup_root ();
209 
210 	/* 3. Ensure folder expansion and unread count*/
211 	debug0 (DEBUG_CACHE, "Initializing node state");
212 	feedlist_foreach (feedlist_init_node);
213 
214 	/* 4. Check if feeds do need updating. */
215 	debug0 (DEBUG_UPDATE, "Performing initial feed update");
216 	conf_get_int_value (STARTUP_FEED_ACTION, &startup_feed_action);
217 	if (0 == startup_feed_action) {
218 		/* Update all feeds */
219 		if (network_monitor_is_online ()) {
220 			debug0 (DEBUG_UPDATE, "initial update: updating all feeds");
221 			node_update_subscription (feedlist_get_root (), GUINT_TO_POINTER (0));
222 		} else {
223 			debug0 (DEBUG_UPDATE, "initial update: prevented because we are offline");
224 		}
225 	} else {
226 		debug0 (DEBUG_UPDATE, "initial update: resetting feed counter");
227 		feedlist_reset_update_counters (NULL);
228 	}
229 
230 	/* 5. Purge old nodes from the database */
231 	db_node_cleanup (feedlist_get_root ());
232 
233 	/* 6. Start automatic updating */
234 	feedlist->priv->autoUpdateTimer = g_timeout_add_seconds (10, feedlist_auto_update, NULL);
235 	g_signal_connect (network_monitor_get (), "online-status-changed", G_CALLBACK (on_network_status_changed), NULL);
236 
237 	/* 7. Finally save the new feed list state */
238 	feedlist->priv->loading = FALSE;
239 	feedlist_schedule_save ();
240 
241 	debug_exit ("feedlist_init");
242 }
243 
244 static void feedlist_unselect(void);
245 
246 nodePtr
feedlist_get_root(void)247 feedlist_get_root (void)
248 {
249 	return ROOTNODE;
250 }
251 
252 nodePtr
feedlist_get_selected(void)253 feedlist_get_selected (void)
254 {
255 	return SELECTED;
256 }
257 
258 static nodePtr
feedlist_get_parent_node(void)259 feedlist_get_parent_node (void)
260 {
261 
262 	g_assert (NULL != ROOTNODE);
263 
264 	if (!SELECTED)
265 		return ROOTNODE;
266 
267 	if (IS_FOLDER (SELECTED))
268 		return SELECTED;
269 
270 	if (SELECTED->parent)
271 		return SELECTED->parent;
272 
273 	return ROOTNODE;
274 }
275 
276 nodePtr
feedlist_find_node(nodePtr parent,feedListFindType type,const gchar * str)277 feedlist_find_node (nodePtr parent, feedListFindType type, const gchar *str)
278 {
279 	GSList	*iter;
280 
281 	g_assert (str);
282 
283 	iter = parent->children;
284 	while (iter) {
285 		gboolean found = FALSE;
286 		nodePtr result, node = (nodePtr)iter->data;
287 
288 		/* Check child node */
289 		switch (type) {
290 			case NODE_BY_URL:
291 				if (node->subscription)
292 					found = g_str_equal (str, subscription_get_source (node->subscription));
293 				break;
294 			case NODE_BY_ID:
295 				found = g_str_equal (str, node->id);
296 				break;
297 			case FOLDER_BY_TITLE:
298 				if (IS_FOLDER (node))
299 					found = g_str_equal (str, node->title);
300 				break;
301 
302 			default:
303 				break;
304 		}
305 		if (found)
306 			return node;
307 
308 		/* And recurse */
309 		result = feedlist_find_node (node, type, str);
310 		if (result)
311 			return result;
312 
313 		iter = g_slist_next (iter);
314 	}
315 
316 	return NULL;
317 }
318 
319 gboolean
feedlist_is_writable(void)320 feedlist_is_writable (void)
321 {
322 	nodePtr node;
323 
324 	node = feedlist_get_parent_node ();
325 
326 	return (0 != (NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS));
327 }
328 
329 static void
feedlist_update_node_counters(nodePtr node)330 feedlist_update_node_counters (nodePtr node)
331 {
332 	node_update_counters (node);	/* update with parent propagation */
333 
334 	if (node->needsUpdate)
335 		feed_list_node_update (node->id);
336 	if (node->children)
337 		node_foreach_child (node, feedlist_update_node_counters);
338 }
339 
340 void
feedlist_mark_all_read(nodePtr node)341 feedlist_mark_all_read (nodePtr node)
342 {
343 	if (!node)
344 		return;
345 
346 	feedlist_reset_new_item_count ();
347 
348 	if (node != ROOTNODE)
349 		node_mark_all_read (node);
350 	else
351 		node_foreach_child (ROOTNODE, node_mark_all_read);
352 
353 	feedlist_foreach (feedlist_update_node_counters);
354 	itemview_select_item (NULL);
355 	itemview_update_all_items ();
356 	itemview_update ();
357 }
358 
359 /* statistic handling methods */
360 
361 guint
feedlist_get_unread_item_count(void)362 feedlist_get_unread_item_count (void)
363 {
364 	if (!feedlist)
365 		return 0;
366 
367 	return (ROOTNODE->unreadCount > 0)?ROOTNODE->unreadCount:0;
368 }
369 
370 guint
feedlist_get_new_item_count(void)371 feedlist_get_new_item_count (void)
372 {
373 	if (!feedlist)
374 		return 0;
375 
376 	return (feedlist->priv->newCount > 0)?feedlist->priv->newCount:0;
377 }
378 
379 void
feedlist_reset_new_item_count(void)380 feedlist_reset_new_item_count (void)
381 {
382 	if (feedlist->priv->newCount)
383 		feedlist->priv->newCount = 0;
384 
385 	feedlist_new_items (0);
386 }
387 
388 void
feedlist_add_folder(const gchar * title)389 feedlist_add_folder (const gchar *title)
390 {
391 	nodePtr		parent;
392 
393 	g_assert (NULL != title);
394 
395 	parent = feedlist_get_parent_node ();
396 
397 	if(0 == (NODE_TYPE (parent->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS))
398 		return;
399 
400 	node_source_add_folder (parent->source->root, title);
401 }
402 
403 void
feedlist_add_subscription(const gchar * source,const gchar * filter,updateOptionsPtr options,gint flags)404 feedlist_add_subscription (const gchar *source, const gchar *filter, updateOptionsPtr options, gint flags)
405 {
406 	nodePtr		parent;
407 
408 	g_assert (NULL != source);
409 
410 	parent = feedlist_get_parent_node ();
411 
412 	if (0 == (NODE_TYPE (parent->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)) {
413 		g_warning ("feedlist_add_subscription: this should never happen!");
414 		return;
415 	}
416 
417 	node_source_add_subscription (parent->source->root, subscription_new (source, filter, options));
418 }
419 
420 void
feedlist_node_imported(nodePtr node)421 feedlist_node_imported (nodePtr node)
422 {
423 	feed_list_node_add (node);
424 
425 	feedlist_schedule_save ();
426 }
427 
428 void
feedlist_node_added(nodePtr node)429 feedlist_node_added (nodePtr node)
430 {
431 	gint	position = -1;
432 
433 	g_assert (NULL == node->parent);
434 
435 	if (SELECTED && !IS_FOLDER (SELECTED)) {
436 		position = g_slist_index (SELECTED->parent->children, SELECTED);
437 		if (position > -1)
438 			position++;	/* insert after selected child index */
439 	}
440 
441 	node_set_parent (node, feedlist_get_parent_node (), position);
442 
443 	if (node->subscription)
444 		db_subscription_update (node->subscription);
445 
446 	db_node_update (node);
447 
448 	feedlist_node_imported (node);
449 
450 	feed_list_view_select (node);
451 }
452 
453 void
feedlist_remove_node(nodePtr node)454 feedlist_remove_node (nodePtr node)
455 {
456 	if (node->source->root != node)
457 		node_source_remove_node (node->source->root, node);
458 	else
459 		feedlist_node_removed (node);
460 }
461 
462 void
feedlist_node_removed(nodePtr node)463 feedlist_node_removed (nodePtr node)
464 {
465 	if (node == SELECTED)
466 		feedlist_unselect ();
467 
468 	/* First remove all children */
469 	node_foreach_child (node, feedlist_node_removed);
470 
471 	node_remove (node);
472 
473 	feed_list_node_remove_node (node);
474 
475 	node->parent->children = g_slist_remove (node->parent->children, node);
476 
477 	node_free (node);
478 
479 	feedlist_schedule_save ();
480 }
481 
482 /* Checks if the given node is a subscription node and
483    has at least one unread item or is selected, if yes it
484    is added to the list ref passed as user_data */
485 static void
feedlist_collect_unread(nodePtr node,gpointer user_data)486 feedlist_collect_unread (nodePtr node, gpointer user_data)
487 {
488 	GSList	**list = (GSList **)user_data;
489 
490 	if (node->children) {
491 		node_foreach_child_data (node, feedlist_collect_unread, user_data);
492 		return;
493 	}
494 	if (!node->subscription)
495 		return;
496 	if (!node->unreadCount && !g_str_equal (node->id, SELECTED->id))
497 		return;
498 
499 	*list = g_slist_append (*list, g_strdup (node->id));
500 }
501 
502 nodePtr
feedlist_find_unread_feed(nodePtr folder)503 feedlist_find_unread_feed (nodePtr folder)
504 {
505 	GSList	*list = NULL;
506 	nodePtr	result = NULL;
507 
508 	feedlist_foreach_data (feedlist_collect_unread, &list);
509 
510 	if (list) {
511 		// Pass 1: try after selected node in list
512 		if (SELECTED) {
513 			GSList *s = g_slist_find_custom (list, SELECTED->id, g_strcmp0);
514 			if (s)
515 				s = g_slist_next (s);
516 			if (s)
517 				result = node_from_id (s->data);
518 		}
519 
520 		// Pass 2: just return first node in list
521 		if (!result)
522 			result = node_from_id (list->data);
523 
524 		g_slist_free_full (list, g_free);
525 	}
526 	return result;
527 }
528 
529 /* selection handling */
530 
531 static void
feedlist_unselect(void)532 feedlist_unselect (void)
533 {
534 	SELECTED = NULL;
535 
536 	itemview_set_displayed_node (NULL);
537 	itemview_update ();
538 
539 	itemlist_unload (FALSE /* mark all read */);
540 	feed_list_view_select (NULL);
541 	liferea_shell_update_feed_menu (TRUE, FALSE, FALSE);
542 	liferea_shell_update_allitems_actions (FALSE, FALSE);
543 }
544 
545 void
feedlist_selection_changed(nodePtr node)546 feedlist_selection_changed (nodePtr node)
547 {
548 	debug_enter ("feedlist_selection_changed");
549 
550 	debug1 (DEBUG_GUI, "new selected node: %s", node?node_get_title (node):"none");
551 	if (node != SELECTED) {
552 
553 		/* When the user selects a feed in the feed list we
554 		   assume that he got notified of the new items or
555 		   isn't interested in the event anymore... */
556 		if (0 != feedlist->priv->newCount)
557 			feedlist_reset_new_item_count ();
558 
559 		/* Unload visible items. */
560 		itemlist_unload (TRUE);
561 
562 		/* Load items of new selected node. */
563 		SELECTED = node;
564 		if (SELECTED) {
565 			itemlist_set_view_mode (node_get_view_mode (SELECTED));
566 			itemlist_load (SELECTED);
567 		} else {
568 			itemview_clear ();
569 		}
570 	}
571 
572 	debug_exit ("feedlist_selection_changed");
573 }
574 
575 static gboolean
feedlist_schedule_save_cb(gpointer user_data)576 feedlist_schedule_save_cb (gpointer user_data)
577 {
578 	/* step 1: request each node to save its state, that is
579 	   mostly needed for nodes that are node sources */
580 	feedlist_foreach (node_save);
581 
582 	/* step 2: request saving for the root node and thereby
583 	   forcing the default source to write an OPML file */
584 	NODE_SOURCE_TYPE (ROOTNODE)->source_export (ROOTNODE);
585 
586 	feedlist->priv->saveTimer = 0;
587 
588 	return FALSE;
589 }
590 
591 void
feedlist_schedule_save(void)592 feedlist_schedule_save (void)
593 {
594 	if (feedlist->priv->loading || feedlist->priv->saveTimer)
595 		return;
596 
597 	debug0 (DEBUG_CONF, "Scheduling feedlist save");
598 
599 	/* By waiting here 5s and checking feedlist_save_time
600 	   we hope to catch bulks of feed list changes and save
601 	   less often */
602 	feedlist->priv->saveTimer = g_timeout_add_seconds (5, feedlist_schedule_save_cb, NULL);
603 }
604 
605 /* Handling updates */
606 
607 void
feedlist_new_items(guint newCount)608 feedlist_new_items (guint newCount)
609 {
610 	feedlist->priv->newCount += newCount;
611 
612 	/* On subsequent feed updates with cache drops
613 	   more new items can be reported than effectively
614 	   were merged. The simplest way to catch this case
615 	   is by checking for new count > unread count here. */
616 	if (feedlist->priv->newCount > ROOTNODE->unreadCount)
617 		feedlist->priv->newCount = ROOTNODE->unreadCount;
618 
619 	g_signal_emit_by_name (feedlist, "new-items", feedlist->priv->newCount);
620 }
621 
622 void
feedlist_node_was_updated(nodePtr node)623 feedlist_node_was_updated (nodePtr node)
624 {
625 	node_update_counters (node);
626 	feedlist_schedule_save ();
627 
628 	g_signal_emit_by_name (feedlist, "node-updated", node->title);
629 }
630 
631 /* This method is only to be used when exiting the program! */
632 static void
feedlist_save(void)633 feedlist_save (void)
634 {
635 	debug0 (DEBUG_CONF, "Forced feed list save");
636 	feedlist_schedule_save_cb (NULL);
637 }
638 
639 void
feedlist_reset_update_counters(nodePtr node)640 feedlist_reset_update_counters (nodePtr node)
641 {
642 	GTimeVal now;
643 
644 	if (!node)
645 		node = feedlist_get_root ();
646 
647 	g_get_current_time (&now);
648 	node_foreach_child_data (node, node_reset_update_counter, &now);
649 }
650 
651 FeedList *
feedlist_create(void)652 feedlist_create (void)
653 {
654 	return FEEDLIST (g_object_new (FEEDLIST_TYPE, NULL));
655 }
656