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