1 /*
2 * @file node.c hierarchic feed list node handling
3 *
4 * Copyright (C) 2003-2016 Lars Windolf <lars.windolf@gmx.de>
5 * Copyright (C) 2004-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 <string.h>
23
24 #include "common.h"
25 #include "conf.h"
26 #include "db.h"
27 #include "debug.h"
28 #include "favicon.h"
29 #include "feedlist.h"
30 #include "itemlist.h"
31 #include "itemset.h"
32 #include "item_state.h"
33 #include "node.h"
34 #include "node_view.h"
35 #include "render.h"
36 #include "update.h"
37 #include "vfolder.h"
38 #include "fl_sources/node_source.h"
39 #include "ui/liferea_shell.h"
40 #include "ui/feed_list_node.h"
41
42 static GHashTable *nodes = NULL; /*<< node id -> node lookup table */
43
44 #define NODE_ID_LEN 7
45
46 nodePtr
node_is_used_id(const gchar * id)47 node_is_used_id (const gchar *id)
48 {
49 if (!id || !nodes)
50 return NULL;
51
52 return (nodePtr)g_hash_table_lookup (nodes, id);
53 }
54
55 gchar *
node_new_id(void)56 node_new_id (void)
57 {
58 gchar *id;
59
60 id = g_new0 (gchar, NODE_ID_LEN + 1);
61 do {
62 int i;
63 for (i = 0; i < NODE_ID_LEN; i++)
64 id[i] = (gchar)g_random_int_range ('a', 'z');
65 } while (NULL != node_is_used_id (id));
66
67 return id;
68 }
69
70 nodePtr
node_from_id(const gchar * id)71 node_from_id (const gchar *id)
72 {
73 nodePtr node;
74
75 node = node_is_used_id (id);
76 if (!node)
77 debug1 (DEBUG_GUI, "Fatal: no node with id \"%s\" found!", id);
78
79 return node;
80 }
81
82 nodePtr
node_new(nodeTypePtr type)83 node_new (nodeTypePtr type)
84 {
85 nodePtr node;
86 gchar *id;
87
88 g_assert (NULL != type);
89
90 node = g_new0 (struct node, 1);
91 node->type = type;
92 node->viewMode = NODE_VIEW_MODE_DEFAULT;
93 node->sortColumn = NODE_VIEW_SORT_BY_TIME;
94 node->sortReversed = TRUE; /* default sorting is newest date at top */
95 node->available = TRUE;
96
97 id = node_new_id ();
98 node_set_id (node, id);
99 g_free (id);
100
101 return node;
102 }
103
104 void
node_set_data(nodePtr node,gpointer data)105 node_set_data (nodePtr node, gpointer data)
106 {
107 g_assert (NULL == node->data);
108 g_assert (NULL != node->type);
109
110 node->data = data;
111 }
112
113 void
node_set_subscription(nodePtr node,subscriptionPtr subscription)114 node_set_subscription (nodePtr node, subscriptionPtr subscription)
115 {
116 g_assert (NULL == node->subscription);
117 g_assert (NULL != node->type);
118
119 node->subscription = subscription;
120 subscription->node = node;
121
122 /* Besides the favicon age we have no persistent
123 update state field, so everything else goes NULL */
124 if (node->iconFile && !strstr(node->iconFile, "default.png")) {
125 subscription->updateState->lastFaviconPoll.tv_sec = common_get_mod_time (node->iconFile);
126 debug2 (DEBUG_UPDATE, "Setting last favicon poll time for %s to %lu", node->id, subscription->updateState->lastFaviconPoll.tv_sec);
127 }
128 }
129
130 void
node_update_subscription(nodePtr node,gpointer user_data)131 node_update_subscription (nodePtr node, gpointer user_data)
132 {
133 if (node->source->root == node) {
134 node_source_update (node);
135 return;
136 }
137
138 if (node->subscription)
139 subscription_update (node->subscription, GPOINTER_TO_UINT (user_data));
140
141 node_foreach_child_data (node, node_update_subscription, user_data);
142 }
143
144 void
node_auto_update_subscription(nodePtr node)145 node_auto_update_subscription (nodePtr node)
146 {
147 if (node->source->root == node) {
148 node_source_auto_update (node);
149 return;
150 }
151
152 if (node->subscription)
153 subscription_auto_update (node->subscription);
154
155 node_foreach_child (node, node_auto_update_subscription);
156 }
157
158 void
node_reset_update_counter(nodePtr node,GTimeVal * now)159 node_reset_update_counter (nodePtr node, GTimeVal *now)
160 {
161 subscription_reset_update_counter (node->subscription, now);
162
163 node_foreach_child_data (node, node_reset_update_counter, now);
164 }
165
166 gboolean
node_is_ancestor(nodePtr node1,nodePtr node2)167 node_is_ancestor (nodePtr node1, nodePtr node2)
168 {
169 nodePtr tmp;
170
171 tmp = node2->parent;
172 while (tmp) {
173 if (node1 == tmp)
174 return TRUE;
175 tmp = tmp->parent;
176 }
177 return FALSE;
178 }
179
180 void
node_free(nodePtr node)181 node_free (nodePtr node)
182 {
183 if (node->data && NODE_TYPE (node)->free)
184 NODE_TYPE (node)->free (node);
185
186 g_assert (NULL == node->children);
187
188 g_hash_table_remove (nodes, node->id);
189
190 update_job_cancel_by_owner (node);
191
192 if (node->subscription)
193 subscription_free (node->subscription);
194
195 if (node->icon)
196 g_object_unref (node->icon);
197 g_free (node->iconFile);
198 g_free (node->title);
199 g_free (node->id);
200 g_free (node);
201 }
202
203 static void
node_calc_counters(nodePtr node)204 node_calc_counters (nodePtr node)
205 {
206 /* Order is important! First update all children
207 so that hierarchical nodes (folders and feed
208 list sources) can determine their own unread
209 count as the sum of all childs afterwards */
210 node_foreach_child (node, node_calc_counters);
211
212 NODE_TYPE (node)->update_counters (node);
213 }
214
215 static void
node_update_parent_counters(nodePtr node)216 node_update_parent_counters (nodePtr node)
217 {
218 guint old;
219
220 if (!node)
221 return;
222
223 old = node->unreadCount;
224
225 NODE_TYPE (node)->update_counters (node);
226
227 if (old != node->unreadCount) {
228 feed_list_node_update (node->id);
229 feedlist_new_items (0); /* add 0 new items, as 'new-items' signal updates unread items also */
230 }
231
232 if (node->parent)
233 node_update_parent_counters (node->parent);
234 }
235
236 void
node_update_counters(nodePtr node)237 node_update_counters (nodePtr node)
238 {
239 guint oldUnreadCount = node->unreadCount;
240 guint oldItemCount = node->itemCount;
241
242 /* Update the node itself and its children */
243 node_calc_counters (node);
244
245 if ((oldUnreadCount != node->unreadCount) ||
246 (oldItemCount != node->itemCount))
247 feed_list_node_update (node->id);
248
249 /* Update the unread count of the parent nodes,
250 usually they just add all child unread counters */
251 if (!IS_VFOLDER (node))
252 node_update_parent_counters (node->parent);
253 }
254
255 void
node_update_favicon(nodePtr node)256 node_update_favicon (nodePtr node)
257 {
258 if (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE_FAVICON) {
259 debug1 (DEBUG_UPDATE, "favicon of node %s needs to be updated...", node->title);
260 subscription_update_favicon (node->subscription);
261 }
262
263 /* Recursion */
264 if (node->children)
265 node_foreach_child (node, node_update_favicon);
266 }
267
268 itemSetPtr
node_get_itemset(nodePtr node)269 node_get_itemset (nodePtr node)
270 {
271 return NODE_TYPE (node)->load (node);
272 }
273
274 void
node_mark_all_read(nodePtr node)275 node_mark_all_read (nodePtr node)
276 {
277 if (!node)
278 return;
279
280 if ((node->unreadCount > 0) || (IS_VFOLDER (node))) {
281 itemset_mark_read (node);
282 node->unreadCount = 0;
283 node->needsUpdate = TRUE;
284 }
285
286 if (node->children)
287 node_foreach_child (node, node_mark_all_read);
288 }
289
290 gchar *
node_render(nodePtr node)291 node_render(nodePtr node)
292 {
293 return NODE_TYPE (node)->render (node);
294 }
295
296 /* import callbacks and helper functions */
297
298 void
node_set_parent(nodePtr node,nodePtr parent,gint position)299 node_set_parent (nodePtr node, nodePtr parent, gint position)
300 {
301 g_assert (NULL != parent);
302
303 parent->children = g_slist_insert (parent->children, node, position);
304 node->parent = parent;
305
306 /* new nodes may be provided by another node source, if
307 not they are handled by the parents node source */
308 if (!node->source)
309 node->source = parent->source;
310 }
311
312 void
node_reparent(nodePtr node,nodePtr new_parent)313 node_reparent (nodePtr node, nodePtr new_parent)
314 {
315 nodePtr old_parent;
316
317 g_assert (NULL != new_parent);
318 g_assert (NULL != node);
319
320 debug2 (DEBUG_GUI, "Reparenting node '%s' to a parent '%s'", node_get_title(node), node_get_title(new_parent));
321
322 old_parent = node->parent;
323 if (NULL != old_parent)
324 old_parent->children = g_slist_remove (old_parent->children, node);
325
326 new_parent->children = g_slist_insert (new_parent->children, node, -1);
327 node->parent = new_parent;
328
329 feed_list_node_remove_node (node);
330 feed_list_node_add (node);
331 }
332
333 void
node_remove(nodePtr node)334 node_remove (nodePtr node)
335 {
336 /* using itemlist_remove_all_items() ensures correct unread
337 and item counters for all parent folders and matching
338 search folders */
339 itemlist_remove_all_items (node);
340
341 NODE_TYPE (node)->remove (node);
342 }
343
344 static xmlDocPtr
node_to_xml(nodePtr node)345 node_to_xml (nodePtr node)
346 {
347 xmlDocPtr doc;
348 xmlNodePtr rootNode;
349 gchar *tmp;
350
351 doc = xmlNewDoc("1.0");
352 rootNode = xmlNewDocNode(doc, NULL, "node", NULL);
353 xmlDocSetRootElement(doc, rootNode);
354
355 xmlNewTextChild(rootNode, NULL, "title", node_get_title(node));
356
357 tmp = g_strdup_printf("%u", node->unreadCount);
358 xmlNewTextChild(rootNode, NULL, "unreadCount", tmp);
359 g_free(tmp);
360
361 tmp = g_strdup_printf("%u", g_slist_length(node->children));
362 xmlNewTextChild(rootNode, NULL, "children", tmp);
363 g_free(tmp);
364
365 return doc;
366 }
367
368 gchar *
node_default_render(nodePtr node)369 node_default_render (nodePtr node)
370 {
371 gchar *result;
372 xmlDocPtr doc;
373
374 doc = node_to_xml (node);
375 result = render_xml (doc, NODE_TYPE(node)->id, NULL);
376 xmlFreeDoc (doc);
377
378 return result;
379 }
380
381 /* helper functions to be used with node_foreach* */
382
383 void
node_save(nodePtr node)384 node_save(nodePtr node)
385 {
386 NODE_TYPE(node)->save(node);
387 }
388
389 /* node attributes encapsulation */
390
391 void
node_set_title(nodePtr node,const gchar * title)392 node_set_title (nodePtr node, const gchar *title)
393 {
394 g_free (node->title);
395 node->title = g_strstrip (g_strdelimit (g_strdup (title), "\r\n", ' '));
396 }
397
398 const gchar *
node_get_title(nodePtr node)399 node_get_title (nodePtr node)
400 {
401 return node->title;
402 }
403
404 void
node_load_icon(nodePtr node)405 node_load_icon (nodePtr node)
406 {
407 /* Load pixbuf for all widget based rendering */
408 if (node->icon)
409 g_object_unref (node->icon);
410
411 node->icon = favicon_load_from_cache (node->id, 32);
412
413 /* Create filename for HTML rendering */
414 g_free (node->iconFile);
415
416 if (node->icon)
417 node->iconFile = common_create_cache_filename ("favicons", node->id, "png");
418 else
419 node->iconFile = g_build_filename (PACKAGE_DATA_DIR, PACKAGE, "pixmaps", "default.png", NULL);
420 }
421
422 /* determines the nodes favicon or default icon */
423 gpointer
node_get_icon(nodePtr node)424 node_get_icon (nodePtr node)
425 {
426 if (!node->icon)
427 return (gpointer) NODE_TYPE(node)->icon;
428
429 return node->icon;
430 }
431
432 const gchar *
node_get_favicon_file(nodePtr node)433 node_get_favicon_file (nodePtr node)
434 {
435 return node->iconFile;
436 }
437
438 void
node_set_id(nodePtr node,const gchar * id)439 node_set_id (nodePtr node, const gchar *id)
440 {
441 if (!nodes)
442 nodes = g_hash_table_new(g_str_hash, g_str_equal);
443
444 if (node->id) {
445 g_hash_table_remove (nodes, node->id);
446 g_free (node->id);
447 }
448 node->id = g_strdup (id);
449
450 g_hash_table_insert (nodes, node->id, node);
451 }
452
453 const gchar *
node_get_id(nodePtr node)454 node_get_id (nodePtr node)
455 {
456 return node->id;
457 }
458
459 gboolean
node_set_sort_column(nodePtr node,nodeViewSortType sortColumn,gboolean reversed)460 node_set_sort_column (nodePtr node, nodeViewSortType sortColumn, gboolean reversed)
461 {
462 if (node->sortColumn == sortColumn &&
463 node->sortReversed == reversed)
464 return FALSE;
465
466 node->sortColumn = sortColumn;
467 node->sortReversed = reversed;
468
469 return TRUE;
470 }
471
472 void
node_set_view_mode(nodePtr node,nodeViewType viewMode)473 node_set_view_mode (nodePtr node, nodeViewType viewMode)
474 {
475 gint defaultViewMode;
476
477 /* To allow users to select a default viewing mode for the layout
478 we need to store only exceptions from this mode, which is why
479 we compare the mode to be set with the default and if it's equal
480 we just set NODE_VIEW_MODE_DEFAULT.
481
482 This allows to not OPML export the viewMode attribute for nodes
483 the are in default viewing mode, which then allows to follow
484 a switch in the preference to a new default viewing mode.
485
486 This of course also means that the we use some state on each
487 changing of the view mode preference.
488 */
489
490 conf_get_int_value (DEFAULT_VIEW_MODE, &defaultViewMode);
491
492 if (viewMode != (nodeViewType)defaultViewMode)
493 node->viewMode = viewMode;
494 else
495 node->viewMode = NODE_VIEW_MODE_DEFAULT;
496 }
497
498 nodeViewType
node_get_view_mode(nodePtr node)499 node_get_view_mode (nodePtr node)
500 {
501 gint defaultViewMode;
502
503 conf_get_int_value (DEFAULT_VIEW_MODE, &defaultViewMode);
504
505 if (NODE_VIEW_MODE_DEFAULT == node->viewMode)
506 return defaultViewMode;
507 else
508 return node->viewMode;
509 }
510
511 const gchar *
node_get_base_url(nodePtr node)512 node_get_base_url(nodePtr node)
513 {
514 const gchar *baseUrl = NULL;
515
516 if (node->subscription) {
517 baseUrl = subscription_get_homepage (node->subscription);
518 if (!baseUrl)
519 baseUrl = subscription_get_source (node->subscription);
520 }
521
522
523 /* prevent feed scraping commands to end up as base URI */
524 if (!((baseUrl != NULL) &&
525 (baseUrl[0] != '|') &&
526 (strstr(baseUrl, "://") != NULL)))
527 baseUrl = NULL;
528
529 return baseUrl;
530 }
531
532 gboolean
node_can_add_child_feed(nodePtr node)533 node_can_add_child_feed (nodePtr node)
534 {
535 g_assert (node->source->root);
536
537 if (!(NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS))
538 return FALSE;
539
540 return (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_ADD_FEED);
541 }
542
543 gboolean
node_can_add_child_folder(nodePtr node)544 node_can_add_child_folder (nodePtr node)
545 {
546 g_assert (node->source->root);
547
548 if (!(NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS))
549 return FALSE;
550
551 return (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_ADD_FOLDER);
552 }
553
554 /* node children iterating interface */
555
556 void
node_foreach_child_full(nodePtr node,gpointer func,gint params,gpointer user_data)557 node_foreach_child_full (nodePtr node, gpointer func, gint params, gpointer user_data)
558 {
559 GSList *children, *iter;
560
561 g_assert (NULL != node);
562
563 /* We need to copy because func might modify the list */
564 iter = children = g_slist_copy (node->children);
565 while (iter) {
566 nodePtr childNode = (nodePtr)iter->data;
567
568 /* Apply the method to the child */
569 if (0 == params)
570 ((nodeActionFunc)func) (childNode);
571 else
572 ((nodeActionDataFunc)func) (childNode, user_data);
573
574 /* Never descend! */
575
576 iter = g_slist_next (iter);
577 }
578
579 g_slist_free (children);
580 }
581
582