diff --git a/data/icons/CMakeLists.txt b/data/icons/CMakeLists.txt index ba48344de1..9aaaf4d37d 100644 --- a/data/icons/CMakeLists.txt +++ b/data/icons/CMakeLists.txt @@ -180,10 +180,12 @@ set(private_icons hicolor_places_24x24_mail-outbox.png hicolor_places_24x24_mail-sent.png hicolor_status_16x16_wrapped.png - hicolor_status_32x32_offline.png - hicolor_status_32x32_online.png hicolor_status_32x32_aspect-ratio-lock.png hicolor_status_32x32_aspect-ratio-unlock.png + hicolor_status_32x32_offline.png + hicolor_status_32x32_online.png + hicolor_status_scalable_rss.svg + hicolor_status_scalable_rss-symbolic.svg ) # These icons were in gnome-icon-theme prior to GNOME 2.30. diff --git a/data/icons/hicolor_status_scalable_rss-symbolic.svg b/data/icons/hicolor_status_scalable_rss-symbolic.svg new file mode 100644 index 0000000000..965167b618 --- /dev/null +++ b/data/icons/hicolor_status_scalable_rss-symbolic.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/hicolor_status_scalable_rss.svg b/data/icons/hicolor_status_scalable_rss.svg new file mode 100644 index 0000000000..74d174fba6 --- /dev/null +++ b/data/icons/hicolor_status_scalable_rss.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index eed67030f2..89f510ae74 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -482,6 +482,14 @@ src/modules/prefer-plain/e-mail-display-popup-prefer-plain.c src/modules/prefer-plain/e-mail-parser-prefer-plain.c src/modules/prefer-plain/plugin/config-ui.c src/modules/prefer-plain/plugin/org-gnome-prefer-plain.eplug.xml +src/modules/rss/camel/camel-rss-folder.c +src/modules/rss/camel/camel-rss-folder-summary.c +src/modules/rss/camel/camel-rss-provider.c +src/modules/rss/camel/camel-rss-store.c +src/modules/rss/e-rss-parser.c +src/modules/rss/evolution/e-rss-preferences.c +src/modules/rss/evolution/e-rss-shell-extension.c +src/modules/rss/evolution/e-rss-shell-view-extension.c src/modules/spamassassin/evolution-spamassassin.c src/modules/spamassassin/org.gnome.Evolution-spamassassin.metainfo.xml.in src/modules/startup-wizard/e-mail-config-import-page.c diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 66e835c2cf..d995f4dc60 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -85,6 +85,7 @@ add_subdirectory(offline-alert) add_subdirectory(plugin-lib) add_subdirectory(plugin-manager) add_subdirectory(prefer-plain) +add_subdirectory(rss) add_subdirectory(settings) add_subdirectory(startup-wizard) add_subdirectory(vcard-inline) diff --git a/src/modules/rss/CMakeLists.txt b/src/modules/rss/CMakeLists.txt new file mode 100644 index 0000000000..b7442fd613 --- /dev/null +++ b/src/modules/rss/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(camel) +add_subdirectory(evolution) diff --git a/src/modules/rss/camel-rss-store-summary.c b/src/modules/rss/camel-rss-store-summary.c new file mode 100644 index 0000000000..ec9a6bf431 --- /dev/null +++ b/src/modules/rss/camel-rss-store-summary.c @@ -0,0 +1,852 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include +#include +#include + +#include "camel-rss-store-summary.h" + +struct _CamelRssStoreSummaryPrivate { + GRecMutex mutex; + gboolean dirty; + gchar *filename; + GHashTable *feeds; /* gchar *uid ~> RssFeed * */ +}; + +enum { + FEED_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE_WITH_PRIVATE (CamelRssStoreSummary, camel_rss_store_summary, G_TYPE_OBJECT) + +typedef struct _RssFeed { + guint index; /* to preserve order of adding */ + gchar *href; + gchar *display_name; + gchar *icon_filename; + CamelRssContentType content_type; + guint32 total_count; + guint32 unread_count; + gint64 last_updated; +} RssFeed; + +static void +rss_feed_free (gpointer ptr) +{ + RssFeed *feed = ptr; + + if (feed) { + g_free (feed->href); + g_free (feed->display_name); + g_free (feed->icon_filename); + g_free (feed); + } +} + +typedef struct _EmitIdleData { + GWeakRef *weak_ref; + gchar *id; +} EmitIdleData; + +static void +emit_idle_data_free (gpointer ptr) +{ + EmitIdleData *eid = ptr; + + if (eid) { + e_weak_ref_free (eid->weak_ref); + g_free (eid->id); + g_slice_free (EmitIdleData, eid); + } +} + +static gboolean +camel_rss_store_summary_emit_feed_changed_cb (gpointer user_data) +{ + EmitIdleData *eid = user_data; + CamelRssStoreSummary *self; + + self = g_weak_ref_get (eid->weak_ref); + if (self) { + g_signal_emit (self, signals[FEED_CHANGED], 0, eid->id, NULL); + g_object_unref (self); + } + + return G_SOURCE_REMOVE; +} + +static void +camel_rss_store_summary_schedule_feed_changed (CamelRssStoreSummary *self, + const gchar *id) +{ + EmitIdleData *eid; + + eid = g_slice_new (EmitIdleData); + eid->weak_ref = e_weak_ref_new (self); + eid->id = g_strdup (id); + + g_idle_add_full (G_PRIORITY_HIGH, + camel_rss_store_summary_emit_feed_changed_cb, + eid, emit_idle_data_free); +} + +static void +rss_store_summary_finalize (GObject *object) +{ + CamelRssStoreSummary *self = CAMEL_RSS_STORE_SUMMARY (object); + + g_hash_table_destroy (self->priv->feeds); + g_free (self->priv->filename); + + g_rec_mutex_clear (&self->priv->mutex); + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (camel_rss_store_summary_parent_class)->finalize (object); +} + +static void +camel_rss_store_summary_class_init (CamelRssStoreSummaryClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = rss_store_summary_finalize; + + signals[FEED_CHANGED] = g_signal_new ( + "feed-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | + G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + G_TYPE_STRING); +} + +static void +camel_rss_store_summary_init (CamelRssStoreSummary *self) +{ + self->priv = camel_rss_store_summary_get_instance_private (self); + + self->priv->dirty = FALSE; + self->priv->feeds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, rss_feed_free); + + g_rec_mutex_init (&self->priv->mutex); +} + +CamelRssStoreSummary * +camel_rss_store_summary_new (const gchar *filename) +{ + CamelRssStoreSummary *self = g_object_new (CAMEL_TYPE_RSS_STORE_SUMMARY, NULL); + + self->priv->filename = g_strdup (filename); + + return self; +} + +void +camel_rss_store_summary_lock (CamelRssStoreSummary *self) +{ + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + + g_rec_mutex_lock (&self->priv->mutex); +} + +void +camel_rss_store_summary_unlock (CamelRssStoreSummary *self) +{ + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + + g_rec_mutex_unlock (&self->priv->mutex); +} + +static gint +compare_feeds_by_index (gconstpointer fd1, + gconstpointer fd2) +{ + const RssFeed *feed1 = fd1, *feed2 = fd2; + + if (!feed1 || !feed2) + return 0; + + return feed1->index - feed2->index; +} + +gboolean +camel_rss_store_summary_load (CamelRssStoreSummary *self, + GError **error) +{ + GKeyFile *key_file; + GError *local_error = NULL; + gboolean success; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE); + + camel_rss_store_summary_lock (self); + + g_hash_table_remove_all (self->priv->feeds); + + key_file = g_key_file_new (); + success = g_key_file_load_from_file (key_file, self->priv->filename, G_KEY_FILE_NONE, &local_error); + + if (success) { + GSList *feeds = NULL, *link; + gchar **groups; + guint ii; + + groups = g_key_file_get_groups (key_file, NULL); + + for (ii = 0; groups && groups[ii]; ii++) { + const gchar *group = groups[ii]; + + if (g_str_has_prefix (group, "feed:")) { + RssFeed *feed; + + feed = g_new0 (RssFeed, 1); + feed->href = g_key_file_get_string (key_file, group, "href", NULL); + feed->display_name = g_key_file_get_string (key_file, group, "display-name", NULL); + feed->icon_filename = g_key_file_get_string (key_file, group, "icon-filename", NULL); + feed->content_type = g_key_file_get_integer (key_file, group, "content-type", NULL); + feed->total_count = (guint32) g_key_file_get_uint64 (key_file, group, "total-count", NULL); + feed->unread_count = (guint32) g_key_file_get_uint64 (key_file, group, "unread-count", NULL); + feed->last_updated = g_key_file_get_int64 (key_file, group, "last-updated", NULL); + feed->index = (gint) g_key_file_get_int64 (key_file, group, "index", NULL); + + if (feed->href && *feed->href && feed->display_name && *feed->display_name) { + if (feed->icon_filename && !*feed->icon_filename) + g_clear_pointer (&feed->icon_filename, g_free); + + g_hash_table_insert (self->priv->feeds, g_strdup (group + 5 /* strlen ("feed:") */), feed); + + feeds = g_slist_prepend (feeds, feed); + } else { + rss_feed_free (feed); + } + } + } + + /* renumber indexes on load */ + feeds = g_slist_sort (feeds, compare_feeds_by_index); + + for (ii = 1, link = feeds; link; ii++, link = g_slist_next (link)) { + RssFeed *feed = link->data; + + feed->index = ii; + } + + g_slist_free (feeds); + g_strfreev (groups); + } else { + if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { + success = TRUE; + g_clear_error (&local_error); + } else { + g_propagate_error (error, local_error); + } + } + + g_key_file_free (key_file); + + self->priv->dirty = FALSE; + + camel_rss_store_summary_unlock (self); + + return success; +} + +gboolean +camel_rss_store_summary_save (CamelRssStoreSummary *self, + GError **error) +{ + gboolean success = TRUE; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE); + + camel_rss_store_summary_lock (self); + + if (self->priv->dirty) { + GKeyFile *key_file; + GHashTableIter iter; + gpointer key, value; + + key_file = g_key_file_new (); + + g_hash_table_iter_init (&iter, self->priv->feeds); + + while (g_hash_table_iter_next (&iter, &key, &value)) { + const gchar *id = key; + const RssFeed *feed = value; + gchar *group = g_strconcat ("feed:", id, NULL); + + g_key_file_set_string (key_file, group, "href", feed->href); + g_key_file_set_string (key_file, group, "display-name", feed->display_name); + g_key_file_set_string (key_file, group, "icon-filename", feed->icon_filename ? feed->icon_filename : ""); + g_key_file_set_integer (key_file, group, "content-type", feed->content_type); + g_key_file_set_uint64 (key_file, group, "total-count", feed->total_count); + g_key_file_set_uint64 (key_file, group, "unread-count", feed->unread_count); + g_key_file_set_int64 (key_file, group, "last-updated", feed->last_updated); + g_key_file_set_int64 (key_file, group, "index", feed->index); + + g_free (group); + } + + success = g_key_file_save_to_file (key_file, self->priv->filename, error); + + g_key_file_free (key_file); + + self->priv->dirty = !success; + } + + camel_rss_store_summary_unlock (self); + + return success; +} + +const gchar * +camel_rss_store_summary_add (CamelRssStoreSummary *self, + const gchar *href, + const gchar *display_name, + const gchar *icon_filename, + CamelRssContentType content_type) +{ + RssFeed *feed; + gchar *id; + guint index = 1; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + g_return_val_if_fail (href != NULL, NULL); + g_return_val_if_fail (display_name != NULL, NULL); + + camel_rss_store_summary_lock (self); + + self->priv->dirty = TRUE; + + id = g_compute_checksum_for_string (G_CHECKSUM_SHA1, href, -1); + + while (g_hash_table_contains (self->priv->feeds, id) && index != 0) { + gchar *tmp; + + tmp = g_strdup_printf ("%s::%u", href, index); + g_free (id); + id = g_compute_checksum_for_string (G_CHECKSUM_SHA1, tmp, -1); + g_free (tmp); + index++; + } + + feed = g_new0 (RssFeed, 1); + feed->href = g_strdup (href); + feed->display_name = g_strdup (display_name); + feed->icon_filename = g_strdup (icon_filename); + feed->content_type = content_type; + feed->index = g_hash_table_size (self->priv->feeds) + 1; + + g_hash_table_insert (self->priv->feeds, id, feed); + + camel_rss_store_summary_unlock (self); + camel_rss_store_summary_schedule_feed_changed (self, id); + + return id; +} + +static void +camel_rss_store_summary_maybe_remove_filename (CamelRssStoreSummary *self, + const gchar *filename) +{ + if (filename && *filename) { + gchar *prefix, *dirsep; + + prefix = g_strdup (self->priv->filename); + dirsep = strrchr (prefix, G_DIR_SEPARATOR); + + if (dirsep) { + dirsep[1] = '\0'; + + if (g_str_has_prefix (filename, prefix) && + g_unlink (filename) == -1) { + gint errn = errno; + + if (errn != ENOENT && camel_debug ("rss")) + g_printerr ("%s: Failed to delete '%s': %s", G_STRFUNC, filename, g_strerror (errn)); + } + } + + g_free (prefix); + } +} + +gboolean +camel_rss_store_summary_remove (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + gboolean result = FALSE; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE); + g_return_val_if_fail (id != NULL, FALSE); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + + if (feed) { + guint removed_index = feed->index; + + camel_rss_store_summary_maybe_remove_filename (self, feed->icon_filename); + + result = g_hash_table_remove (self->priv->feeds, id); + + /* Correct indexes of the left feeds */ + if (result) { + GHashTableIter iter; + gpointer value; + + g_hash_table_iter_init (&iter, self->priv->feeds); + while (g_hash_table_iter_next (&iter, NULL, &value)) { + RssFeed *feed2 = value; + + if (feed2 && feed2->index > removed_index) + feed2->index--; + } + } + } + + if (result) + self->priv->dirty = TRUE; + + camel_rss_store_summary_unlock (self); + + if (result) + camel_rss_store_summary_schedule_feed_changed (self, id); + + return result; +} + +gboolean +camel_rss_store_summary_contains (CamelRssStoreSummary *self, + const gchar *id) +{ + gboolean result = FALSE; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE); + g_return_val_if_fail (id != NULL, FALSE); + + camel_rss_store_summary_lock (self); + + result = g_hash_table_contains (self->priv->feeds, id); + + camel_rss_store_summary_unlock (self); + + return result; +} + +static gint +compare_ids_by_index (gconstpointer id1, + gconstpointer id2, + gpointer user_data) +{ + GHashTable *feeds = user_data; + RssFeed *feed1, *feed2; + + feed1 = g_hash_table_lookup (feeds, id1); + feed2 = g_hash_table_lookup (feeds, id2); + + if (!feed1 || !feed2) + return 0; + + return feed1->index - feed2->index; +} + +GSList * /* gchar *id */ +camel_rss_store_summary_dup_feeds (CamelRssStoreSummary *self) +{ + GSList *ids = NULL; + GHashTableIter iter; + gpointer key; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + + camel_rss_store_summary_lock (self); + + g_hash_table_iter_init (&iter, self->priv->feeds); + + while (g_hash_table_iter_next (&iter, &key, NULL)) { + ids = g_slist_prepend (ids, g_strdup (key)); + } + + ids = g_slist_sort_with_data (ids, compare_ids_by_index, self->priv->feeds); + + camel_rss_store_summary_unlock (self); + + return ids; +} + +CamelFolderInfo * +camel_rss_store_summary_dup_folder_info (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + CamelFolderInfo *fi = NULL; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + g_return_val_if_fail (id != NULL, NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + fi = camel_folder_info_new (); + fi->full_name = g_strdup (id); + fi->display_name = g_strdup (feed->display_name); + fi->flags = CAMEL_FOLDER_NOCHILDREN; + fi->unread = feed->unread_count; + fi->total = feed->total_count; + } + + camel_rss_store_summary_unlock (self); + + return fi; +} + +CamelFolderInfo * +camel_rss_store_summary_dup_folder_info_for_display_name (CamelRssStoreSummary *self, + const gchar *display_name) +{ + CamelFolderInfo *fi = NULL; + GHashTableIter iter; + gpointer key, value; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + g_return_val_if_fail (display_name != NULL, NULL); + + camel_rss_store_summary_lock (self); + + g_hash_table_iter_init (&iter, self->priv->feeds); + + while (g_hash_table_iter_next (&iter, &key, &value)) { + const gchar *id = key; + RssFeed *feed = value; + + if (g_strcmp0 (display_name, feed->display_name) == 0) { + fi = camel_rss_store_summary_dup_folder_info (self, id); + break; + } + } + + camel_rss_store_summary_unlock (self); + + return fi; +} + +const gchar * +camel_rss_store_summary_get_href (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + const gchar *result = NULL; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + g_return_val_if_fail (id != NULL, NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->href; + + camel_rss_store_summary_unlock (self); + + return result; +} + +const gchar * +camel_rss_store_summary_get_display_name (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + const gchar *result = NULL; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + g_return_val_if_fail (id != NULL, NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->display_name; + + camel_rss_store_summary_unlock (self); + + return result; +} + +void +camel_rss_store_summary_set_display_name (CamelRssStoreSummary *self, + const gchar *id, + const gchar *display_name) +{ + RssFeed *feed; + gboolean changed = FALSE; + + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + g_return_if_fail (id != NULL); + g_return_if_fail (display_name && *display_name); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + if (g_strcmp0 (feed->display_name, display_name) != 0) { + g_free (feed->display_name); + feed->display_name = g_strdup (display_name); + self->priv->dirty = TRUE; + changed = TRUE; + } + } + + camel_rss_store_summary_unlock (self); + + if (changed) + camel_rss_store_summary_schedule_feed_changed (self, id); +} + +const gchar * +camel_rss_store_summary_get_icon_filename (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + const gchar *result = NULL; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL); + g_return_val_if_fail (id != NULL, NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->icon_filename; + + camel_rss_store_summary_unlock (self); + + return result; +} + +void +camel_rss_store_summary_set_icon_filename (CamelRssStoreSummary *self, + const gchar *id, + const gchar *icon_filename) +{ + RssFeed *feed; + gboolean changed = FALSE; + + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + g_return_if_fail (id != NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + if (g_strcmp0 (feed->icon_filename, icon_filename) != 0) { + camel_rss_store_summary_maybe_remove_filename (self, feed->icon_filename); + g_free (feed->icon_filename); + feed->icon_filename = g_strdup (icon_filename); + self->priv->dirty = TRUE; + changed = TRUE; + } + } + + camel_rss_store_summary_unlock (self); + + if (changed) + camel_rss_store_summary_schedule_feed_changed (self, id); +} + +CamelRssContentType +camel_rss_store_summary_get_content_type (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + CamelRssContentType result = CAMEL_RSS_CONTENT_TYPE_HTML; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), result); + g_return_val_if_fail (id != NULL, result); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->content_type; + + camel_rss_store_summary_unlock (self); + + return result; +} + +void +camel_rss_store_summary_set_content_type (CamelRssStoreSummary *self, + const gchar *id, + CamelRssContentType content_type) +{ + RssFeed *feed; + gboolean changed = FALSE; + + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + g_return_if_fail (id != NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + if (feed->content_type != content_type) { + feed->content_type = content_type; + self->priv->dirty = TRUE; + changed = TRUE; + } + } + + camel_rss_store_summary_unlock (self); + + if (changed) + camel_rss_store_summary_schedule_feed_changed (self, id); +} + +guint32 +camel_rss_store_summary_get_total_count (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + guint32 result = 0; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), 0); + g_return_val_if_fail (id != NULL, 0); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->total_count; + + camel_rss_store_summary_unlock (self); + + return result; +} + +void +camel_rss_store_summary_set_total_count (CamelRssStoreSummary *self, + const gchar *id, + guint32 total_count) +{ + RssFeed *feed; + + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + g_return_if_fail (id != NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + if (feed->total_count != total_count) { + feed->total_count = total_count; + self->priv->dirty = TRUE; + } + } + + camel_rss_store_summary_unlock (self); +} + +guint32 +camel_rss_store_summary_get_unread_count (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + guint32 result = 0; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), 0); + g_return_val_if_fail (id != NULL, 0); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->unread_count; + + camel_rss_store_summary_unlock (self); + + return result; +} + +void +camel_rss_store_summary_set_unread_count (CamelRssStoreSummary *self, + const gchar *id, + guint32 unread_count) +{ + RssFeed *feed; + + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + g_return_if_fail (id != NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + if (feed->unread_count != unread_count) { + feed->unread_count = unread_count; + self->priv->dirty = TRUE; + } + } + + camel_rss_store_summary_unlock (self); +} + +gint64 +camel_rss_store_summary_get_last_updated (CamelRssStoreSummary *self, + const gchar *id) +{ + RssFeed *feed; + gint64 result = 0; + + g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), 0); + g_return_val_if_fail (id != NULL, 0); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) + result = feed->last_updated; + + camel_rss_store_summary_unlock (self); + + return result; +} + +void +camel_rss_store_summary_set_last_updated (CamelRssStoreSummary *self, + const gchar *id, + gint64 last_updated) +{ + RssFeed *feed; + + g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self)); + g_return_if_fail (id != NULL); + + camel_rss_store_summary_lock (self); + + feed = g_hash_table_lookup (self->priv->feeds, id); + if (feed) { + if (feed->last_updated != last_updated) { + feed->last_updated = last_updated; + self->priv->dirty = TRUE; + } + } + + camel_rss_store_summary_unlock (self); +} diff --git a/src/modules/rss/camel-rss-store-summary.h b/src/modules/rss/camel-rss-store-summary.h new file mode 100644 index 0000000000..9c1b2b36e0 --- /dev/null +++ b/src/modules/rss/camel-rss-store-summary.h @@ -0,0 +1,116 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef CAMEL_RSS_STORE_SUMMARY_H +#define CAMEL_RSS_STORE_SUMMARY_H + +#include + +/* Standard GObject macros */ +#define CAMEL_TYPE_RSS_STORE_SUMMARY \ + (camel_rss_store_summary_get_type ()) +#define CAMEL_RSS_STORE_SUMMARY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), CAMEL_TYPE_RSS_STORE_SUMMARY, CamelRssStoreSummary)) +#define CAMEL_RSS_STORE_SUMMARY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), CAMEL_TYPE_RSS_STORE_SUMMARY, CamelRssStoreSummaryClass)) +#define CAMEL_IS_RSS_STORE_SUMMARY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), CAMEL_TYPE_RSS_STORE_SUMMARY)) +#define CAMEL_IS_RSS_STORE_SUMMARY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), CAMEL_TYPE_RSS_STORE_SUMMARY)) +#define CAMEL_RSS_STORE_SUMMARY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), CAMEL_TYPE_RSS_STORE_SUMMARY, CamelRssStoreSummaryClass)) + +G_BEGIN_DECLS + +typedef enum { + CAMEL_RSS_CONTENT_TYPE_HTML, + CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT, + CAMEL_RSS_CONTENT_TYPE_MARKDOWN +} CamelRssContentType; + +typedef struct _CamelRssStoreSummary CamelRssStoreSummary; +typedef struct _CamelRssStoreSummaryClass CamelRssStoreSummaryClass; +typedef struct _CamelRssStoreSummaryPrivate CamelRssStoreSummaryPrivate; + +struct _CamelRssStoreSummary { + GObject object; + CamelRssStoreSummaryPrivate *priv; +}; + +struct _CamelRssStoreSummaryClass { + GObjectClass object_class; +}; + +GType camel_rss_store_summary_get_type (void); +CamelRssStoreSummary * + camel_rss_store_summary_new (const gchar *filename); +void camel_rss_store_summary_lock (CamelRssStoreSummary *self); +void camel_rss_store_summary_unlock (CamelRssStoreSummary *self); +gboolean camel_rss_store_summary_load (CamelRssStoreSummary *self, + GError **error); +gboolean camel_rss_store_summary_save (CamelRssStoreSummary *self, + GError **error); +const gchar * camel_rss_store_summary_add (CamelRssStoreSummary *self, + const gchar *href, + const gchar *display_name, + const gchar *icon_filename, + CamelRssContentType content_type); +gboolean camel_rss_store_summary_remove (CamelRssStoreSummary *self, + const gchar *id); +gboolean camel_rss_store_summary_contains (CamelRssStoreSummary *self, + const gchar *id); +GSList * camel_rss_store_summary_dup_feeds (CamelRssStoreSummary *self); /* gchar *id */ +CamelFolderInfo * + camel_rss_store_summary_dup_folder_info (CamelRssStoreSummary *self, + const gchar *id); +CamelFolderInfo * + camel_rss_store_summary_dup_folder_info_for_display_name + (CamelRssStoreSummary *self, + const gchar *display_name); +const gchar * camel_rss_store_summary_get_href (CamelRssStoreSummary *self, + const gchar *id); +const gchar * camel_rss_store_summary_get_display_name(CamelRssStoreSummary *self, + const gchar *id); +void camel_rss_store_summary_set_display_name(CamelRssStoreSummary *self, + const gchar *id, + const gchar *display_name); +const gchar * camel_rss_store_summary_get_icon_filename + (CamelRssStoreSummary *self, + const gchar *id); +void camel_rss_store_summary_set_icon_filename + (CamelRssStoreSummary *self, + const gchar *id, + const gchar *filename); +CamelRssContentType + camel_rss_store_summary_get_content_type(CamelRssStoreSummary *self, + const gchar *id); +void camel_rss_store_summary_set_content_type(CamelRssStoreSummary *self, + const gchar *id, + CamelRssContentType content_type); +guint32 camel_rss_store_summary_get_total_count (CamelRssStoreSummary *self, + const gchar *id); +void camel_rss_store_summary_set_total_count (CamelRssStoreSummary *self, + const gchar *id, + guint32 total_count); +guint32 camel_rss_store_summary_get_unread_count(CamelRssStoreSummary *self, + const gchar *id); +void camel_rss_store_summary_set_unread_count(CamelRssStoreSummary *self, + const gchar *id, + guint32 unread_count); +gint64 camel_rss_store_summary_get_last_updated(CamelRssStoreSummary *self, + const gchar *id); +void camel_rss_store_summary_set_last_updated(CamelRssStoreSummary *self, + const gchar *id, + gint64 last_updated); + +G_END_DECLS + +#endif /* CAMEL_RSS_STORE_SUMMARY_H */ diff --git a/src/modules/rss/camel/CMakeLists.txt b/src/modules/rss/camel/CMakeLists.txt new file mode 100644 index 0000000000..1e8ceaff08 --- /dev/null +++ b/src/modules/rss/camel/CMakeLists.txt @@ -0,0 +1,50 @@ +pkg_check_modules(LIBEDATASERVER libedataserver-1.2) +pkg_check_modules(CAMEL camel-1.2) +pkg_check_variable(camel_providerdir camel-1.2 camel_providerdir) + +set(sources + camel-rss-folder.c + camel-rss-folder.h + camel-rss-folder-summary.c + camel-rss-folder-summary.h + camel-rss-provider.c + camel-rss-settings.c + camel-rss-settings.h + camel-rss-store.c + camel-rss-store.h + ../camel-rss-store-summary.c + ../camel-rss-store-summary.h + ../e-rss-parser.h + ../e-rss-parser.c +) + +add_library(camelrss MODULE ${sources}) + +target_compile_definitions(camelrss PRIVATE + -DG_LOG_DOMAIN=\"camel-rss-provider\" +) + +target_compile_options(camelrss PUBLIC + ${CAMEL_CFLAGS} + ${LIBEDATASERVER_CFLAGS} +) + +target_include_directories(camelrss PUBLIC + ${CMAKE_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CAMEL_INCLUDE_DIRS} + ${LIBEDATASERVER_INCLUDE_DIRS} +) + +target_link_libraries(camelrss + ${CAMEL_LDFLAGS} + ${LIBEDATASERVER_LDFLAGS} +) + +install(TARGETS camelrss + DESTINATION ${camel_providerdir} +) + +install(FILES libcamelrss.urls + DESTINATION ${camel_providerdir} +) diff --git a/src/modules/rss/camel/camel-rss-folder-summary.c b/src/modules/rss/camel/camel-rss-folder-summary.c new file mode 100644 index 0000000000..940765ed81 --- /dev/null +++ b/src/modules/rss/camel/camel-rss-folder-summary.c @@ -0,0 +1,412 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include + +#include +#include + +#include "camel-rss-folder.h" +#include "camel-rss-store.h" +#include "camel-rss-folder-summary.h" + +struct _CamelRssFolderSummaryPrivate { + gulong saved_count_id; + gulong unread_count_id; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (CamelRssFolderSummary, camel_rss_folder_summary, CAMEL_TYPE_FOLDER_SUMMARY) + +static void +rss_folder_summary_sync_counts_cb (GObject *object, + GParamSpec *param, + gpointer user_data) +{ + CamelRssFolderSummary *self = CAMEL_RSS_FOLDER_SUMMARY (object); + CamelFolder *folder; + CamelStore *parent_store; + CamelRssStoreSummary *rss_store_summary; + const gchar *id; + + folder = camel_folder_summary_get_folder (CAMEL_FOLDER_SUMMARY (self)); + parent_store = camel_folder_get_parent_store (folder); + + if (!parent_store) + return; + + rss_store_summary = camel_rss_store_get_summary (CAMEL_RSS_STORE (parent_store)); + + if (!rss_store_summary) + return; + + id = camel_rss_folder_get_id (CAMEL_RSS_FOLDER (folder)); + + if (g_strcmp0 (g_param_spec_get_name (param), "saved-count") == 0) + camel_rss_store_summary_set_total_count (rss_store_summary, id, camel_folder_summary_get_saved_count (CAMEL_FOLDER_SUMMARY (self))); + else if (g_strcmp0 (g_param_spec_get_name (param), "unread-count") == 0) + camel_rss_store_summary_set_unread_count (rss_store_summary, id, camel_folder_summary_get_unread_count (CAMEL_FOLDER_SUMMARY (self))); +} + +static void +rss_folder_summary_constructed (GObject *object) +{ + CamelRssFolderSummary *self = CAMEL_RSS_FOLDER_SUMMARY (object); + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (camel_rss_folder_summary_parent_class)->constructed (object); + + self->priv->saved_count_id = g_signal_connect (self, "notify::saved-count", + G_CALLBACK (rss_folder_summary_sync_counts_cb), NULL); + + self->priv->unread_count_id = g_signal_connect (self, "notify::unread-count", + G_CALLBACK (rss_folder_summary_sync_counts_cb), NULL); +} + +static void +rss_folder_summary_dispose (GObject *object) +{ + CamelRssFolderSummary *self = CAMEL_RSS_FOLDER_SUMMARY (object); + + if (self->priv->saved_count_id) { + g_signal_handler_disconnect (self, self->priv->saved_count_id); + self->priv->saved_count_id = 0; + } + + if (self->priv->unread_count_id) { + g_signal_handler_disconnect (self, self->priv->unread_count_id); + self->priv->unread_count_id = 0; + } + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (camel_rss_folder_summary_parent_class)->dispose (object); +} + +static void +camel_rss_folder_summary_class_init (CamelRssFolderSummaryClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = rss_folder_summary_constructed; + object_class->dispose = rss_folder_summary_dispose; +} + +static void +camel_rss_folder_summary_init (CamelRssFolderSummary *rss_folder_summary) +{ + rss_folder_summary->priv = camel_rss_folder_summary_get_instance_private (rss_folder_summary); +} + +CamelFolderSummary * +camel_rss_folder_summary_new (CamelFolder *folder) +{ + return g_object_new (CAMEL_TYPE_RSS_FOLDER_SUMMARY, "folder", folder, NULL); +} + +CamelMimeMessage * +camel_rss_folder_summary_dup_message (CamelRssFolderSummary *self, + const gchar *uid, + CamelDataCache **out_rss_cache, + CamelRssContentType *out_content_type, + GCancellable *cancellable, + GError **error) +{ + CamelFolder *folder; + CamelDataCache *rss_cache; + CamelMimeMessage *message = NULL; + CamelRssStore *rss_store; + GIOStream *base_stream; + + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER_SUMMARY (self), NULL); + g_return_val_if_fail (uid != NULL, NULL); + + folder = camel_folder_summary_get_folder (CAMEL_FOLDER_SUMMARY (self)); + rss_store = CAMEL_RSS_STORE (camel_folder_get_parent_store (folder)); + + if (out_content_type) { + *out_content_type = camel_rss_store_summary_get_content_type ( + camel_rss_store_get_summary (rss_store), + camel_rss_folder_get_id (CAMEL_RSS_FOLDER (folder))); + } + + rss_cache = camel_rss_store_get_cache (rss_store); + base_stream = camel_data_cache_get (rss_cache, camel_rss_folder_get_id (CAMEL_RSS_FOLDER (folder)), uid, error); + + if (base_stream) { + CamelStream *stream; + + stream = camel_stream_new (base_stream); + g_object_unref (base_stream); + + message = camel_mime_message_new (); + if (!camel_data_wrapper_construct_from_stream_sync (CAMEL_DATA_WRAPPER (message), stream, cancellable, error)) { + g_object_unref (message); + message = NULL; + } + + g_object_unref (stream); + } + + if (out_rss_cache) + *out_rss_cache = g_object_ref (rss_cache); + + return message; +} + +gboolean +camel_rss_folder_summary_add_or_update_feed_sync (CamelRssFolderSummary *self, + const gchar *href, + ERssFeed *feed, + GBytes *complete_article, + CamelFolderChangeInfo **inout_changes, + GCancellable *cancellable, + GError **error) +{ + CamelDataCache *rss_cache = NULL; + CamelDataWrapper *body_wrapper; + CamelMimeMessage *message; + CamelRssContentType content_type = CAMEL_RSS_CONTENT_TYPE_HTML; + gchar *uid, *received, *received_tm; + GSList *link; + gboolean has_downloaded_eclosure = FALSE; + gboolean existing_message; + gboolean success = TRUE; + + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER_SUMMARY (self), FALSE); + g_return_val_if_fail (href != NULL, FALSE); + g_return_val_if_fail (feed != NULL, FALSE); + g_return_val_if_fail (feed->link != NULL, FALSE); + g_return_val_if_fail (inout_changes != NULL, FALSE); + + uid = g_compute_checksum_for_string (G_CHECKSUM_SHA1, feed->id ? feed->id : feed->link, -1); + g_return_val_if_fail (uid != NULL, FALSE); + + message = camel_rss_folder_summary_dup_message (self, uid, &rss_cache, &content_type, cancellable, NULL); + existing_message = camel_folder_summary_get_info_flags (CAMEL_FOLDER_SUMMARY (self), uid) != (~0); + if (!existing_message) + g_clear_object (&message); + if (!message) { + gchar *msg_id; + + msg_id = g_strconcat (uid, "@localhost", NULL); + + message = camel_mime_message_new (); + + camel_mime_message_set_message_id (message, msg_id); + camel_mime_message_set_date (message, feed->last_modified, 0); + camel_medium_set_header (CAMEL_MEDIUM (message), "From", feed->author); + camel_medium_set_header (CAMEL_MEDIUM (message), "X-RSS-Feed", href); + + g_free (msg_id); + } + + camel_mime_message_set_subject (message, feed->title); + + received_tm = camel_header_format_date (time (NULL), 0); + received = g_strconcat ("from ", href, " by localhost; ", received_tm, NULL); + + camel_medium_add_header (CAMEL_MEDIUM (message), "Received", received); + + for (link = feed->enclosures; link && !has_downloaded_eclosure; link = g_slist_next (link)) { + ERssEnclosure *enclosure = link->data; + + if (enclosure->data && g_bytes_get_size (enclosure->data) > 0) + has_downloaded_eclosure = TRUE; + } + + body_wrapper = camel_data_wrapper_new (); + + if (complete_article && g_bytes_get_size (complete_article) > 0) { + camel_data_wrapper_set_encoding (body_wrapper, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE); + camel_data_wrapper_set_mime_type (body_wrapper, "text/html; charset=utf-8"); + success = camel_data_wrapper_construct_from_data_sync (body_wrapper, g_bytes_get_data (complete_article, NULL), g_bytes_get_size (complete_article), cancellable, error); + } else { + GString *body; + const gchar *ct; + gboolean first_enclosure = TRUE; + + body = g_string_new (NULL); + + if (content_type == CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT) { + ct = "text/plain; charset=utf-8"; + + g_string_append (body, feed->link); + g_string_append_c (body, '\n'); + g_string_append_c (body, '\n'); + } else if (content_type == CAMEL_RSS_CONTENT_TYPE_MARKDOWN) { + ct = "text/markdown; charset=utf-8"; + } else { + ct = "text/html; charset=utf-8"; + } + + if (content_type != CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT) { + gchar *tmp; + + tmp = g_markup_printf_escaped ("

%s


", feed->link, feed->title); + g_string_append (body, tmp); + g_free (tmp); + } + + if (feed->body) + g_string_append (body, feed->body); + + for (link = feed->enclosures; link; link = g_slist_next (link)) { + ERssEnclosure *enclosure = link->data; + gchar *tmp; + + if (enclosure->data && g_bytes_get_size (enclosure->data) > 0) + continue; + + if (first_enclosure) { + first_enclosure = FALSE; + + g_string_append (body, "


\n"); + g_string_append (body, _("Enclosures:")); + g_string_append (body, "
\n"); + } + + tmp = g_markup_printf_escaped ("\n", + enclosure->href, enclosure->title ? enclosure->title : enclosure->href); + + g_string_append (body, tmp); + + g_free (tmp); + } + + camel_data_wrapper_set_encoding (body_wrapper, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE); + camel_data_wrapper_set_mime_type (body_wrapper, ct); + success = camel_data_wrapper_construct_from_data_sync (body_wrapper, body->str, body->len, cancellable, error); + + g_string_free (body, TRUE); + } + + if (success && has_downloaded_eclosure) { + CamelMultipart *mixed; + CamelMimePart *subpart; + + mixed = camel_multipart_new (); + camel_data_wrapper_set_mime_type (CAMEL_DATA_WRAPPER (mixed), "multipart/mixed"); + camel_multipart_set_boundary (mixed, NULL); + + subpart = camel_mime_part_new (); + camel_medium_set_content (CAMEL_MEDIUM (subpart), body_wrapper); + camel_multipart_add_part (mixed, subpart); + g_object_unref (subpart); + + for (link = feed->enclosures; link; link = g_slist_next (link)) { + ERssEnclosure *enclosure = link->data; + GUri *link_uri; + + if (!enclosure->data || !g_bytes_get_size (enclosure->data)) + continue; + + subpart = camel_mime_part_new (); + camel_mime_part_set_content (subpart, (const gchar *) g_bytes_get_data (enclosure->data, NULL), g_bytes_get_size (enclosure->data), + enclosure->content_type ? enclosure->content_type : "application/octet-stream"); + + camel_mime_part_set_disposition (subpart, "inline"); + + link_uri = g_uri_parse (enclosure->href, G_URI_FLAGS_PARSE_RELAXED, NULL); + if (link_uri) { + const gchar *path = g_uri_get_path (link_uri); + const gchar *slash = path ? strrchr (path, '/') : NULL; + + if (slash && *slash && slash[1]) + camel_mime_part_set_filename (subpart, slash + 1); + + g_uri_unref (link_uri); + } + + camel_mime_part_set_encoding (subpart, CAMEL_TRANSFER_ENCODING_BASE64); + + camel_multipart_add_part (mixed, subpart); + + g_object_unref (subpart); + } + + g_object_unref (body_wrapper); + body_wrapper = CAMEL_DATA_WRAPPER (mixed); + } + + if (CAMEL_IS_MIME_PART (body_wrapper)) { + CamelDataWrapper *content; + CamelMedium *imedium, *omedium; + const CamelNameValueArray *headers; + + imedium = CAMEL_MEDIUM (body_wrapper); + omedium = CAMEL_MEDIUM (message); + + content = camel_medium_get_content (imedium); + camel_medium_set_content (omedium, content); + camel_data_wrapper_set_encoding (CAMEL_DATA_WRAPPER (omedium), camel_data_wrapper_get_encoding (CAMEL_DATA_WRAPPER (imedium))); + + headers = camel_medium_get_headers (imedium); + if (headers) { + gint ii, length; + length = camel_name_value_array_get_length (headers); + + for (ii = 0; ii < length; ii++) { + const gchar *header_name = NULL; + const gchar *header_value = NULL; + + if (camel_name_value_array_get (headers, ii, &header_name, &header_value)) + camel_medium_set_header (omedium, header_name, header_value); + } + } + } else { + camel_medium_set_content (CAMEL_MEDIUM (message), body_wrapper); + } + + if (success) { + CamelRssFolder *rss_folder; + GIOStream *io_stream; + + rss_folder = CAMEL_RSS_FOLDER (camel_folder_summary_get_folder (CAMEL_FOLDER_SUMMARY (self))); + io_stream = camel_data_cache_add (rss_cache, camel_rss_folder_get_id (rss_folder), uid, error); + success = io_stream != NULL; + + if (io_stream) { + success = camel_data_wrapper_write_to_output_stream_sync (CAMEL_DATA_WRAPPER (message), + g_io_stream_get_output_stream (io_stream), cancellable, error); + } + + g_clear_object (&io_stream); + } + + if (success) { + if (!*inout_changes) + *inout_changes = camel_folder_change_info_new (); + + if (existing_message) { + camel_folder_change_info_change_uid (*inout_changes, uid); + } else { + CamelFolderSummary *folder_summary = CAMEL_FOLDER_SUMMARY (self); + CamelMessageInfo *info; + + info = camel_folder_summary_info_new_from_message (folder_summary, message); + g_warn_if_fail (info != NULL); + + camel_message_info_set_uid (info, uid); + camel_folder_summary_add (folder_summary, info, TRUE); + + g_clear_object (&info); + + camel_folder_change_info_add_uid (*inout_changes, uid); + camel_folder_change_info_recent_uid (*inout_changes, uid); + } + } + + g_clear_object (&rss_cache); + g_clear_object (&body_wrapper); + g_clear_object (&message); + g_free (received_tm); + g_free (received); + g_free (uid); + + return success; +} diff --git a/src/modules/rss/camel/camel-rss-folder-summary.h b/src/modules/rss/camel/camel-rss-folder-summary.h new file mode 100644 index 0000000000..df9ceab91d --- /dev/null +++ b/src/modules/rss/camel/camel-rss-folder-summary.h @@ -0,0 +1,69 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef CAMEL_RSS_FOLDER_SUMMARY_H +#define CAMEL_RSS_FOLDER_SUMMARY_H + +#include + +#include "camel-rss-store-summary.h" +#include "e-rss-parser.h" + +/* Standard GObject macros */ +#define CAMEL_TYPE_RSS_FOLDER_SUMMARY \ + (camel_rss_folder_summary_get_type ()) +#define CAMEL_RSS_FOLDER_SUMMARY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), CAMEL_TYPE_RSS_FOLDER_SUMMARY, CamelRssFolderSummary)) +#define CAMEL_RSS_FOLDER_SUMMARY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), CAMEL_TYPE_RSS_FOLDER_SUMMARY, CamelRssFolderSummaryClass)) +#define CAMEL_IS_RSS_FOLDER_SUMMARY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), CAMEL_TYPE_RSS_FOLDER_SUMMARY)) +#define CAMEL_IS_RSS_FOLDER_SUMMARY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), CAMEL_TYPE_RSS_FOLDER_SUMMARY)) +#define CAMEL_RSS_FOLDER_SUMMARY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), CAMEL_TYPE_RSS_FOLDER_SUMMARY, CamelRssFolderSummaryClass)) + +G_BEGIN_DECLS + +typedef struct _CamelRssFolderSummary CamelRssFolderSummary; +typedef struct _CamelRssFolderSummaryClass CamelRssFolderSummaryClass; +typedef struct _CamelRssFolderSummaryPrivate CamelRssFolderSummaryPrivate; + +struct _CamelRssFolderSummary { + CamelFolderSummary parent; + CamelRssFolderSummaryPrivate *priv; +}; + +struct _CamelRssFolderSummaryClass { + CamelFolderSummaryClass parent_class; +}; + +GType camel_rss_folder_summary_get_type (void); +CamelFolderSummary * + camel_rss_folder_summary_new (CamelFolder *folder); +CamelMimeMessage * + camel_rss_folder_summary_dup_message (CamelRssFolderSummary *self, + const gchar *uid, + CamelDataCache **out_rss_cache, + CamelRssContentType *out_content_type, + GCancellable *cancellable, + GError **error); +gboolean camel_rss_folder_summary_add_or_update_feed_sync(CamelRssFolderSummary *self, + const gchar *href, + ERssFeed *feed, + GBytes *complete_article, + CamelFolderChangeInfo **inout_changes, + GCancellable *cancellable, + GError **error); + +G_END_DECLS + +#endif /* CAMEL_RSS_FOLDER_SUMMARY_H */ diff --git a/src/modules/rss/camel/camel-rss-folder.c b/src/modules/rss/camel/camel-rss-folder.c new file mode 100644 index 0000000000..13a783efd7 --- /dev/null +++ b/src/modules/rss/camel/camel-rss-folder.c @@ -0,0 +1,778 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include +#include + +#include "camel-rss-folder-summary.h" +#include "camel-rss-settings.h" +#include "camel-rss-store.h" +#include "camel-rss-store-summary.h" + +#include "camel-rss-folder.h" + +struct _CamelRssFolderPrivate { + gboolean apply_filters; + CamelThreeState complete_articles; + CamelThreeState feed_enclosures; + gchar *id; +}; + +/* The custom property ID is a CamelArg artifact. + * It still identifies the property in state files. */ +enum { + PROP_0, + PROP_APPLY_FILTERS = 0x2501, + PROP_COMPLETE_ARTICLES = 0x2502, + PROP_FEED_ENCLOSURES = 0x2503 +}; + +G_DEFINE_TYPE_WITH_PRIVATE (CamelRssFolder, camel_rss_folder, CAMEL_TYPE_FOLDER) + +static GPtrArray * +rss_folder_search_by_expression (CamelFolder *folder, + const gchar *expression, + GCancellable *cancellable, + GError **error) +{ + CamelFolderSearch *search; + GPtrArray *matches; + + search = camel_folder_search_new (); + camel_folder_search_set_folder (search, folder); + + matches = camel_folder_search_search (search, expression, NULL, cancellable, error); + + g_clear_object (&search); + + return matches; +} + +static guint32 +rss_folder_count_by_expression (CamelFolder *folder, + const gchar *expression, + GCancellable *cancellable, + GError **error) +{ + CamelFolderSearch *search; + guint32 count; + + search = camel_folder_search_new (); + camel_folder_search_set_folder (search, folder); + + count = camel_folder_search_count (search, expression, cancellable, error); + + g_clear_object (&search); + + return count; +} + +static GPtrArray * +rss_folder_search_by_uids (CamelFolder *folder, + const gchar *expression, + GPtrArray *uids, + GCancellable *cancellable, + GError **error) +{ + CamelFolderSearch *search; + GPtrArray *matches; + + if (uids->len == 0) + return g_ptr_array_new (); + + search = camel_folder_search_new (); + camel_folder_search_set_folder (search, folder); + + matches = camel_folder_search_search (search, expression, uids, cancellable, error); + + g_clear_object (&search); + + return matches; +} + +static void +rss_folder_search_free (CamelFolder *folder, + GPtrArray *result) +{ + camel_folder_search_free_result (NULL, result); +} + +static gchar * +rss_get_filename (CamelFolder *folder, + const gchar *uid, + GError **error) +{ + CamelStore *parent_store; + CamelDataCache *rss_cache; + CamelRssFolder *rss_folder; + CamelRssStore *rss_store; + + parent_store = camel_folder_get_parent_store (folder); + rss_folder = CAMEL_RSS_FOLDER (folder); + rss_store = CAMEL_RSS_STORE (parent_store); + rss_cache = camel_rss_store_get_cache (rss_store); + + return camel_data_cache_get_filename (rss_cache, rss_folder->priv->id, uid); +} + +static gboolean +rss_folder_append_message_sync (CamelFolder *folder, + CamelMimeMessage *message, + CamelMessageInfo *info, + gchar **appended_uid, + GCancellable *cancellable, + GError **error) +{ + g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID, + _("Cannot add message into News and Blogs folder")); + + return FALSE; +} + +static gboolean +rss_folder_expunge_sync (CamelFolder *folder, + GCancellable *cancellable, + GError **error) +{ + CamelDataCache *cache; + CamelFolderSummary *summary; + CamelFolderChangeInfo *changes; + CamelRssFolder *rss_folder; + CamelStore *store; + GPtrArray *known_uids; + GList *to_remove = NULL; + guint ii; + + summary = camel_folder_get_folder_summary (folder); + store = camel_folder_get_parent_store (folder); + + if (!store) + return TRUE; + + camel_folder_summary_prepare_fetch_all (summary, NULL); + known_uids = camel_folder_summary_get_array (summary); + + if (known_uids == NULL) + return TRUE; + + rss_folder = CAMEL_RSS_FOLDER (folder); + cache = camel_rss_store_get_cache (CAMEL_RSS_STORE (store)); + changes = camel_folder_change_info_new (); + + for (ii = 0; ii < known_uids->len; ii++) { + guint32 flags; + const gchar *uid; + + uid = g_ptr_array_index (known_uids, ii); + flags = camel_folder_summary_get_info_flags (summary, uid); + + if ((flags & CAMEL_MESSAGE_DELETED) != 0) { + /* ignore cache removal error */ + camel_data_cache_remove (cache, rss_folder->priv->id, uid, NULL); + + camel_folder_change_info_remove_uid (changes, uid); + to_remove = g_list_prepend (to_remove, (gpointer) camel_pstring_strdup (uid)); + } + } + + if (to_remove) { + camel_folder_summary_remove_uids (summary, to_remove); + camel_folder_summary_save (summary, NULL); + camel_folder_changed (folder, changes); + + g_list_free_full (to_remove, (GDestroyNotify) camel_pstring_free); + } + + camel_folder_change_info_free (changes); + camel_folder_summary_free_array (known_uids); + + return TRUE; +} + +static CamelMimeMessage * +rss_folder_get_message_cached (CamelFolder *folder, + const gchar *uid, + GCancellable *cancellable) +{ + CamelRssFolderSummary *rss_summary; + + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), NULL); + g_return_val_if_fail (uid != NULL, NULL); + + rss_summary = CAMEL_RSS_FOLDER_SUMMARY (camel_folder_get_folder_summary (folder)); + + return camel_rss_folder_summary_dup_message (rss_summary, uid, NULL, NULL, cancellable, NULL); +} + +static CamelMimeMessage * +rss_folder_get_message_sync (CamelFolder *folder, + const gchar *uid, + GCancellable *cancellable, + GError **error) +{ + CamelMimeMessage *message; + + message = rss_folder_get_message_cached (folder, uid, cancellable); + + if (!message) { + g_set_error_literal (error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_UNAVAILABLE, + _("Message is not available")); + } + + return message; +} + +static gboolean +rss_folder_refresh_info_sync (CamelFolder *folder, + GCancellable *cancellable, + GError **error) +{ + CamelRssFolder *self; + CamelRssFolderSummary *rss_folder_summary; + CamelRssStore *rss_store; + CamelRssStoreSummary *rss_store_summary; + CamelFolderChangeInfo *changes = NULL; + CamelSession *session; + gchar *href; + gint64 last_updated; + gboolean success = TRUE; + + self = CAMEL_RSS_FOLDER (folder); + rss_store = CAMEL_RSS_STORE (camel_folder_get_parent_store (folder)); + session = camel_service_ref_session (CAMEL_SERVICE (rss_store)); + + if (!session || !camel_session_get_online (session)) { + g_clear_object (&session); + return TRUE; + } + + g_clear_object (&session); + + rss_store_summary = camel_rss_store_get_summary (rss_store); + rss_folder_summary = CAMEL_RSS_FOLDER_SUMMARY (camel_folder_get_folder_summary (folder)); + + camel_rss_store_summary_lock (rss_store_summary); + + href = g_strdup (camel_rss_store_summary_get_href (rss_store_summary, self->priv->id)); + last_updated = camel_rss_store_summary_get_last_updated (rss_store_summary, self->priv->id); + + camel_rss_store_summary_unlock (rss_store_summary); + + if (href && *href) { + SoupSession *soup_session; + SoupMessage *message; + GBytes *bytes; + + message = soup_message_new (SOUP_METHOD_GET, href); + if (!message) { + g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID, _("Invalid Feed URL “%s”."), href); + g_free (href); + + return FALSE; + } + + soup_session = soup_session_new_with_options ( + "timeout", 30, + "user-agent", "Evolution/" VERSION, + NULL); + + if (camel_debug ("rss")) { + SoupLogger *logger; + + logger = soup_logger_new (SOUP_LOGGER_LOG_BODY); + soup_session_add_feature (soup_session, SOUP_SESSION_FEATURE (logger)); + g_object_unref (logger); + } + + bytes = soup_session_send_and_read (soup_session, message, cancellable, error); + + if (bytes) { + GSList *feeds = NULL; + + success = SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message)); + + if (success && e_rss_parser_parse ((const gchar *) g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), NULL, NULL, NULL, NULL, &feeds)) { + CamelSettings *settings; + CamelRssSettings *rss_settings; + gboolean download_complete_article; + gboolean feed_enclosures, limit_feed_enclosure_size; + guint32 max_feed_enclosure_size; + gint64 max_last_modified = last_updated; + GSList *link; + + settings = camel_service_ref_settings (CAMEL_SERVICE (rss_store)); + rss_settings = CAMEL_RSS_SETTINGS (settings); + limit_feed_enclosure_size = camel_rss_settings_get_limit_feed_enclosure_size (rss_settings); + max_feed_enclosure_size = camel_rss_settings_get_max_feed_enclosure_size (rss_settings); + + switch (self->priv->complete_articles) { + case CAMEL_THREE_STATE_ON: + download_complete_article = TRUE; + break; + case CAMEL_THREE_STATE_OFF: + download_complete_article = FALSE; + break; + default: + download_complete_article = camel_rss_settings_get_complete_articles (rss_settings); + break; + } + + switch (self->priv->feed_enclosures) { + case CAMEL_THREE_STATE_ON: + feed_enclosures = TRUE; + break; + case CAMEL_THREE_STATE_OFF: + feed_enclosures = FALSE; + break; + default: + feed_enclosures = camel_rss_settings_get_feed_enclosures (rss_settings); + break; + } + + g_clear_object (&settings); + + for (link = feeds; link && success; link = g_slist_next (link)) { + ERssFeed *feed = link->data; + + if (feed->last_modified > last_updated) { + GBytes *complete_article = NULL; + + if (max_last_modified < feed->last_modified) + max_last_modified = feed->last_modified; + + if (download_complete_article) { + g_clear_object (&message); + g_clear_pointer (&bytes, g_bytes_unref); + + message = soup_message_new (SOUP_METHOD_GET, feed->link); + if (message) { + complete_article = soup_session_send_and_read (soup_session, message, cancellable, NULL); + + if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) + g_clear_pointer (&complete_article, g_bytes_unref); + } + } + + if (success && feed_enclosures && feed->enclosures) { + GSList *elink; + + for (elink = feed->enclosures; elink && success; elink = g_slist_next (elink)) { + ERssEnclosure *enclosure = elink->data; + + if (limit_feed_enclosure_size && enclosure->size > max_feed_enclosure_size) + continue; + + g_clear_object (&message); + g_clear_pointer (&bytes, g_bytes_unref); + + message = soup_message_new (SOUP_METHOD_GET, enclosure->href); + if (message) { + enclosure->data = soup_session_send_and_read (soup_session, message, cancellable, NULL); + + if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) + g_clear_pointer (&enclosure->data, g_bytes_unref); + } + } + } + + success = success && camel_rss_folder_summary_add_or_update_feed_sync (rss_folder_summary, href, feed, complete_article, &changes, cancellable, error); + + g_clear_pointer (&complete_article, g_bytes_unref); + } + } + + if (success && max_last_modified != last_updated) { + camel_rss_store_summary_lock (rss_store_summary); + camel_rss_store_summary_set_last_updated (rss_store_summary, self->priv->id, max_last_modified); + camel_rss_store_summary_unlock (rss_store_summary); + + success = camel_rss_store_summary_save (rss_store_summary, error); + } + } + + g_slist_free_full (feeds, e_rss_feed_free); + } else { + success = FALSE; + } + + g_clear_pointer (&bytes, g_bytes_unref); + g_clear_object (&soup_session); + g_clear_object (&message); + } else { + g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID, _("Invalid Feed URL.")); + success = FALSE; + } + + g_free (href); + + if (changes) { + GError *local_error = NULL; + + if (!camel_folder_summary_save (CAMEL_FOLDER_SUMMARY (rss_folder_summary), (error && !*error) ? error : &local_error)) { + if (local_error) + g_warning ("Failed to save RSS folder summary: %s", local_error->message); + } + + g_clear_error (&local_error); + + camel_folder_changed (folder, changes); + camel_folder_change_info_free (changes); + } + + return success; +} + +static void +rss_unset_flagged_flag (const gchar *uid, + CamelFolderSummary *summary) +{ + CamelMessageInfo *info; + + info = camel_folder_summary_get (summary, uid); + if (info) { + camel_message_info_set_folder_flagged (info, FALSE); + g_clear_object (&info); + } +} + +static gboolean +rss_folder_synchronize_sync (CamelFolder *folder, + gboolean expunge, + GCancellable *cancellable, + GError **error) +{ + CamelFolderSummary *summary; + GPtrArray *changed; + + if (expunge) { + if (!camel_folder_expunge_sync (folder, cancellable, error)) + return FALSE; + } + + summary = camel_folder_get_folder_summary (folder); + changed = camel_folder_summary_get_changed (summary); + + if (changed) { + g_ptr_array_foreach (changed, (GFunc) rss_unset_flagged_flag, summary); + g_ptr_array_foreach (changed, (GFunc) camel_pstring_free, NULL); + camel_folder_summary_touch (summary); + g_ptr_array_free (changed, TRUE); + } + + return camel_folder_summary_save (summary, error); +} + +static void +rss_folder_changed (CamelFolder *folder, + CamelFolderChangeInfo *info) +{ + g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder)); + + if (info && info->uid_removed && info->uid_removed->len) { + CamelDataCache *rss_cache; + + rss_cache = camel_rss_store_get_cache (CAMEL_RSS_STORE (camel_folder_get_parent_store (folder))); + + if (rss_cache) { + CamelRssFolder *self = CAMEL_RSS_FOLDER (folder); + guint ii; + + for (ii = 0; ii < info->uid_removed->len; ii++) { + const gchar *message_uid = info->uid_removed->pdata[ii], *real_uid; + + if (!message_uid) + continue; + + real_uid = strchr (message_uid, ','); + if (real_uid) + camel_data_cache_remove (rss_cache, self->priv->id, real_uid + 1, NULL); + } + } + } + + /* Chain up to parent's method. */ + CAMEL_FOLDER_CLASS (camel_rss_folder_parent_class)->changed (folder, info); +} + +static gboolean +rss_folder_get_apply_filters (CamelRssFolder *folder) +{ + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), FALSE); + + return folder->priv->apply_filters; +} + +static void +rss_folder_set_apply_filters (CamelRssFolder *folder, + gboolean apply_filters) +{ + g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder)); + + if ((folder->priv->apply_filters ? 1 : 0) == (apply_filters ? 1 : 0)) + return; + + folder->priv->apply_filters = apply_filters; + + g_object_notify (G_OBJECT (folder), "apply-filters"); +} + +static CamelThreeState +rss_folder_get_complete_articles (CamelRssFolder *folder) +{ + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), CAMEL_THREE_STATE_INCONSISTENT); + + return folder->priv->complete_articles; +} + +static void +rss_folder_set_complete_articles (CamelRssFolder *folder, + CamelThreeState value) +{ + g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder)); + + if (folder->priv->complete_articles == value) + return; + + folder->priv->complete_articles = value; + + g_object_notify (G_OBJECT (folder), "complete-articles"); +} + +static CamelThreeState +rss_folder_get_feed_enclosures (CamelRssFolder *folder) +{ + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), CAMEL_THREE_STATE_INCONSISTENT); + + return folder->priv->feed_enclosures; +} + +static void +rss_folder_set_feed_enclosures (CamelRssFolder *folder, + CamelThreeState value) +{ + g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder)); + + if (folder->priv->feed_enclosures == value) + return; + + folder->priv->feed_enclosures = value; + + g_object_notify (G_OBJECT (folder), "feed-enclosures"); +} + +static void +rss_folder_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_APPLY_FILTERS: + rss_folder_set_apply_filters (CAMEL_RSS_FOLDER (object), g_value_get_boolean (value)); + return; + case PROP_COMPLETE_ARTICLES: + rss_folder_set_complete_articles (CAMEL_RSS_FOLDER (object), g_value_get_enum (value)); + return; + case PROP_FEED_ENCLOSURES: + rss_folder_set_feed_enclosures (CAMEL_RSS_FOLDER (object), g_value_get_enum (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +rss_folder_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_APPLY_FILTERS: + g_value_set_boolean (value, rss_folder_get_apply_filters (CAMEL_RSS_FOLDER (object))); + return; + case PROP_COMPLETE_ARTICLES: + g_value_set_enum (value, rss_folder_get_complete_articles (CAMEL_RSS_FOLDER (object))); + return; + case PROP_FEED_ENCLOSURES: + g_value_set_enum (value, rss_folder_get_feed_enclosures (CAMEL_RSS_FOLDER (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +rss_folder_dispose (GObject *object) +{ + camel_folder_summary_save (camel_folder_get_folder_summary (CAMEL_FOLDER (object)), NULL); + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (camel_rss_folder_parent_class)->dispose (object); +} + +static void +rss_folder_finalize (GObject *object) +{ + CamelRssFolder *self = CAMEL_RSS_FOLDER (object); + + g_free (self->priv->id); + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (camel_rss_folder_parent_class)->finalize (object); +} + +static void +camel_rss_folder_class_init (CamelRssFolderClass *class) +{ + GObjectClass *object_class; + CamelFolderClass *folder_class; + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = rss_folder_set_property; + object_class->get_property = rss_folder_get_property; + object_class->dispose = rss_folder_dispose; + object_class->finalize = rss_folder_finalize; + + folder_class = CAMEL_FOLDER_CLASS (class); + folder_class->search_by_expression = rss_folder_search_by_expression; + folder_class->count_by_expression = rss_folder_count_by_expression; + folder_class->search_by_uids = rss_folder_search_by_uids; + folder_class->search_free = rss_folder_search_free; + folder_class->get_filename = rss_get_filename; + folder_class->append_message_sync = rss_folder_append_message_sync; + folder_class->expunge_sync = rss_folder_expunge_sync; + folder_class->get_message_cached = rss_folder_get_message_cached; + folder_class->get_message_sync = rss_folder_get_message_sync; + folder_class->refresh_info_sync = rss_folder_refresh_info_sync; + folder_class->synchronize_sync = rss_folder_synchronize_sync; + folder_class->changed = rss_folder_changed; + + g_object_class_install_property ( + object_class, + PROP_APPLY_FILTERS, + g_param_spec_boolean ( + "apply-filters", + "Apply Filters", + _("Apply message _filters to this folder"), + FALSE, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS | + CAMEL_PARAM_PERSISTENT)); + + g_object_class_install_property ( + object_class, + PROP_COMPLETE_ARTICLES, + g_param_spec_enum ( + "complete-articles", + "Complete Articles", + _("_Download complete articles"), + CAMEL_TYPE_THREE_STATE, + CAMEL_THREE_STATE_INCONSISTENT, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + CAMEL_PARAM_PERSISTENT)); + + g_object_class_install_property ( + object_class, + PROP_FEED_ENCLOSURES, + g_param_spec_enum ( + "feed-enclosures", + "Feed Enclosures", + _("Download feed _enclosures"), + CAMEL_TYPE_THREE_STATE, + CAMEL_THREE_STATE_INCONSISTENT, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + CAMEL_PARAM_PERSISTENT)); +} + +static void +camel_rss_folder_init (CamelRssFolder *self) +{ + self->priv = camel_rss_folder_get_instance_private (self); + self->priv->complete_articles = CAMEL_THREE_STATE_INCONSISTENT; + self->priv->feed_enclosures = CAMEL_THREE_STATE_INCONSISTENT; +} + +CamelFolder * +camel_rss_folder_new (CamelStore *parent, + const gchar *id, + GCancellable *cancellable, + GError **error) +{ + CamelFolder *folder; + CamelRssFolder *self; + CamelFolderSummary *folder_summary; + CamelRssStoreSummary *store_summary; + gchar *storage_path, *root; + CamelService *service; + CamelSettings *settings; + const gchar *user_data_dir; + gboolean filter_all = FALSE; + + g_return_val_if_fail (id != NULL, NULL); + + store_summary = camel_rss_store_get_summary (CAMEL_RSS_STORE (parent)); + g_return_val_if_fail (store_summary != NULL, NULL); + + service = CAMEL_SERVICE (parent); + user_data_dir = camel_service_get_user_data_dir (service); + + settings = camel_service_ref_settings (service); + + g_object_get ( + settings, + "filter-all", &filter_all, + NULL); + + g_object_unref (settings); + + camel_rss_store_summary_lock (store_summary); + + folder = g_object_new ( + CAMEL_TYPE_RSS_FOLDER, + "display-name", camel_rss_store_summary_get_display_name (store_summary, id), + "full-name", id, + "parent-store", parent, NULL); + + camel_rss_store_summary_unlock (store_summary); + + self = CAMEL_RSS_FOLDER (folder); + self->priv->id = g_strdup (id); + + camel_folder_set_flags (folder, camel_folder_get_flags (folder) | CAMEL_FOLDER_HAS_SUMMARY_CAPABILITY); + + storage_path = g_build_filename (user_data_dir, id, NULL); + root = g_strdup_printf ("%s.cmeta", storage_path); + camel_object_set_state_filename (CAMEL_OBJECT (self), root); + camel_object_state_read (CAMEL_OBJECT (self)); + g_free (root); + g_free (storage_path); + + folder_summary = camel_rss_folder_summary_new (folder); + + camel_folder_take_folder_summary (folder, folder_summary); + + if (filter_all || rss_folder_get_apply_filters (self)) + camel_folder_set_flags (folder, camel_folder_get_flags (folder) | CAMEL_FOLDER_FILTER_RECENT); + + camel_folder_summary_load (folder_summary, NULL); + + return folder; +} + +const gchar * +camel_rss_folder_get_id (CamelRssFolder *self) +{ + g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (self), NULL); + + return self->priv->id; +} diff --git a/src/modules/rss/camel/camel-rss-folder.h b/src/modules/rss/camel/camel-rss-folder.h new file mode 100644 index 0000000000..633548a5ec --- /dev/null +++ b/src/modules/rss/camel/camel-rss-folder.h @@ -0,0 +1,55 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef CAMEL_RSS_FOLDER_H +#define CAMEL_RSS_FOLDER_H + +#include + +/* Standard GObject macros */ +#define CAMEL_TYPE_RSS_FOLDER \ + (camel_rss_folder_get_type ()) +#define CAMEL_RSS_FOLDER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), CAMEL_TYPE_RSS_FOLDER, CamelRssFolder)) +#define CAMEL_RSS_FOLDER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), CAMEL_TYPE_RSS_FOLDER, CamelRssFolderClass)) +#define CAMEL_IS_RSS_FOLDER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), CAMEL_TYPE_RSS_FOLDER)) +#define CAMEL_IS_RSS_FOLDER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), CAMEL_TYPE_RSS_FOLDER)) +#define CAMEL_RSS_FOLDER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), CAMEL_TYPE_RSS_FOLDER, CamelRssFolderClass)) + +G_BEGIN_DECLS + +typedef struct _CamelRssFolder CamelRssFolder; +typedef struct _CamelRssFolderClass CamelRssFolderClass; +typedef struct _CamelRssFolderPrivate CamelRssFolderPrivate; + +struct _CamelRssFolder { + CamelFolder parent; + CamelRssFolderPrivate *priv; +}; + +struct _CamelRssFolderClass { + CamelFolderClass parent; +}; + +GType camel_rss_folder_get_type (void); +CamelFolder * camel_rss_folder_new (CamelStore *parent, + const gchar *id, + GCancellable *cancellable, + GError **error); +const gchar * camel_rss_folder_get_id (CamelRssFolder *self); + +G_END_DECLS + +#endif /* CAMEL_RSS_FOLDER_H */ diff --git a/src/modules/rss/camel/camel-rss-provider.c b/src/modules/rss/camel/camel-rss-provider.c new file mode 100644 index 0000000000..e6b05a33f3 --- /dev/null +++ b/src/modules/rss/camel/camel-rss-provider.c @@ -0,0 +1,94 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include +#include + +#include "camel-rss-store.h" + +static CamelProvider rss_provider = { + "rss", + N_("News and Blogs"), + + N_("This is a provider for reading RSS news and blogs."), + + "rss", + + CAMEL_PROVIDER_IS_LOCAL | CAMEL_PROVIDER_IS_SOURCE | CAMEL_PROVIDER_IS_STORAGE, + + CAMEL_URL_NEED_HOST, + + NULL, /* conf_entries */ + + NULL, /* port_entries */ + + /* ... */ +}; + +static void +add_hash (guint *hash, + gchar *s) +{ + if (s) + *hash ^= g_str_hash(s); +} + +static guint +rss_url_hash (gconstpointer key) +{ + const CamelURL *u = (CamelURL *) key; + guint hash = 0; + + add_hash (&hash, u->user); + add_hash (&hash, u->host); + hash ^= u->port; + + return hash; +} + +static gint +check_equal (gchar *s1, + gchar *s2) +{ + if (s1 == NULL) { + if (s2 == NULL) + return TRUE; + else + return FALSE; + } + + if (s2 == NULL) + return FALSE; + + return strcmp (s1, s2) == 0; +} + +static gint +rss_url_equal (gconstpointer a, + gconstpointer b) +{ + const CamelURL *u1 = a, *u2 = b; + + return check_equal (u1->protocol, u2->protocol) + && check_equal (u1->user, u2->user) + && check_equal (u1->host, u2->host) + && u1->port == u2->port; +} + +void +camel_provider_module_init (void) +{ + rss_provider.object_types[CAMEL_PROVIDER_STORE] = camel_rss_store_get_type (); + rss_provider.url_hash = rss_url_hash; + rss_provider.url_equal = rss_url_equal; + rss_provider.authtypes = NULL; + rss_provider.translation_domain = GETTEXT_PACKAGE; + + camel_provider_register (&rss_provider); +} diff --git a/src/modules/rss/camel/camel-rss-settings.c b/src/modules/rss/camel/camel-rss-settings.c new file mode 100644 index 0000000000..cbcfef1876 --- /dev/null +++ b/src/modules/rss/camel/camel-rss-settings.c @@ -0,0 +1,301 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include + +#include "camel-rss-settings.h" + +struct _CamelRssSettingsPrivate { + gboolean filter_all; + gboolean complete_articles; + gboolean feed_enclosures; + gboolean limit_feed_enclosure_size; + guint32 max_feed_enclosure_size; +}; + +enum { + PROP_0, + PROP_FILTER_ALL, + PROP_COMPLETE_ARTICLES, + PROP_FEED_ENCLOSURES, + PROP_LIMIT_FEED_ENCLOSURE_SIZE, + PROP_MAX_FEED_ENCLOSURE_SIZE +}; + +G_DEFINE_TYPE_WITH_CODE (CamelRssSettings, camel_rss_settings, CAMEL_TYPE_OFFLINE_SETTINGS, + G_ADD_PRIVATE (CamelRssSettings)) + +static void +rss_settings_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_FILTER_ALL: + camel_rss_settings_set_filter_all ( + CAMEL_RSS_SETTINGS (object), + g_value_get_boolean (value)); + return; + case PROP_COMPLETE_ARTICLES: + camel_rss_settings_set_complete_articles ( + CAMEL_RSS_SETTINGS (object), + g_value_get_boolean (value)); + return; + case PROP_FEED_ENCLOSURES: + camel_rss_settings_set_feed_enclosures ( + CAMEL_RSS_SETTINGS (object), + g_value_get_boolean (value)); + return; + case PROP_LIMIT_FEED_ENCLOSURE_SIZE: + camel_rss_settings_set_limit_feed_enclosure_size ( + CAMEL_RSS_SETTINGS (object), + g_value_get_boolean (value)); + return; + case PROP_MAX_FEED_ENCLOSURE_SIZE: + camel_rss_settings_set_max_feed_enclosure_size ( + CAMEL_RSS_SETTINGS (object), + g_value_get_uint (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +rss_settings_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_FILTER_ALL: + g_value_set_boolean ( + value, + camel_rss_settings_get_filter_all ( + CAMEL_RSS_SETTINGS (object))); + return; + case PROP_COMPLETE_ARTICLES: + g_value_set_boolean ( + value, + camel_rss_settings_get_complete_articles ( + CAMEL_RSS_SETTINGS (object))); + return; + case PROP_FEED_ENCLOSURES: + g_value_set_boolean ( + value, + camel_rss_settings_get_feed_enclosures ( + CAMEL_RSS_SETTINGS (object))); + return; + case PROP_LIMIT_FEED_ENCLOSURE_SIZE: + g_value_set_boolean ( + value, + camel_rss_settings_get_limit_feed_enclosure_size ( + CAMEL_RSS_SETTINGS (object))); + return; + case PROP_MAX_FEED_ENCLOSURE_SIZE: + g_value_set_uint ( + value, + camel_rss_settings_get_max_feed_enclosure_size ( + CAMEL_RSS_SETTINGS (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +camel_rss_settings_class_init (CamelRssSettingsClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = rss_settings_set_property; + object_class->get_property = rss_settings_get_property; + + g_object_class_install_property ( + object_class, + PROP_FILTER_ALL, + g_param_spec_boolean ( + "filter-all", + "Filter All", + "Whether to apply filters in all folders", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_COMPLETE_ARTICLES, + g_param_spec_boolean ( + "complete-articles", + "Complete Articles", + "Whether to download complete articles", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_FEED_ENCLOSURES, + g_param_spec_boolean ( + "feed-enclosures", + "Feed Enclosures", + "Whether to download feed enclosures", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_LIMIT_FEED_ENCLOSURE_SIZE, + g_param_spec_boolean ( + "limit-feed-enclosure-size", + "Limit Feed Enclosure Size", + "Whether to limit feed enclosure size", + FALSE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_MAX_FEED_ENCLOSURE_SIZE, + g_param_spec_uint ( + "max-feed-enclosure-size", + "Max Feed Enclosure Size", + "Max size, in kB, of feed enclosure to download", + 0, G_MAXUINT32, 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS)); +} + +static void +camel_rss_settings_init (CamelRssSettings *settings) +{ + settings->priv = camel_rss_settings_get_instance_private (settings); +} + +gboolean +camel_rss_settings_get_filter_all (CamelRssSettings *settings) +{ + g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE); + + return settings->priv->filter_all; +} + +void +camel_rss_settings_set_filter_all (CamelRssSettings *settings, + gboolean filter_all) +{ + g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings)); + + if ((!settings->priv->filter_all) == (!filter_all)) + return; + + settings->priv->filter_all = filter_all; + + g_object_notify (G_OBJECT (settings), "filter-all"); +} + +gboolean +camel_rss_settings_get_complete_articles (CamelRssSettings *settings) +{ + g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE); + + return settings->priv->complete_articles; +} + +void +camel_rss_settings_set_complete_articles (CamelRssSettings *settings, + gboolean value) +{ + g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings)); + + if ((!settings->priv->complete_articles) == (!value)) + return; + + settings->priv->complete_articles = value; + + g_object_notify (G_OBJECT (settings), "complete-articles"); +} + +gboolean +camel_rss_settings_get_feed_enclosures (CamelRssSettings *settings) +{ + g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE); + + return settings->priv->feed_enclosures; +} + +void +camel_rss_settings_set_feed_enclosures (CamelRssSettings *settings, + gboolean value) +{ + g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings)); + + if ((!settings->priv->feed_enclosures) == (!value)) + return; + + settings->priv->feed_enclosures = value; + + g_object_notify (G_OBJECT (settings), "feed-enclosures"); +} + +gboolean +camel_rss_settings_get_limit_feed_enclosure_size (CamelRssSettings *settings) +{ + g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE); + + return settings->priv->limit_feed_enclosure_size; +} + +void +camel_rss_settings_set_limit_feed_enclosure_size (CamelRssSettings *settings, + gboolean value) +{ + g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings)); + + if ((!settings->priv->limit_feed_enclosure_size) == (!value)) + return; + + settings->priv->limit_feed_enclosure_size = value; + + g_object_notify (G_OBJECT (settings), "limit-feed-enclosure-size"); +} + +guint32 +camel_rss_settings_get_max_feed_enclosure_size (CamelRssSettings *settings) +{ + g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), 0); + + return settings->priv->max_feed_enclosure_size; +} + +void +camel_rss_settings_set_max_feed_enclosure_size (CamelRssSettings *settings, + guint32 value) +{ + g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings)); + + if (settings->priv->max_feed_enclosure_size == value) + return; + + settings->priv->max_feed_enclosure_size = value; + + g_object_notify (G_OBJECT (settings), "max-feed-enclosure-size"); +} diff --git a/src/modules/rss/camel/camel-rss-settings.h b/src/modules/rss/camel/camel-rss-settings.h new file mode 100644 index 0000000000..6874c42e0e --- /dev/null +++ b/src/modules/rss/camel/camel-rss-settings.h @@ -0,0 +1,76 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef CAMEL_RSS_SETTINGS_H +#define CAMEL_RSS_SETTINGS_H + +#include + +/* Standard GObject macros */ +#define CAMEL_TYPE_RSS_SETTINGS \ + (camel_rss_settings_get_type ()) +#define CAMEL_RSS_SETTINGS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), CAMEL_TYPE_RSS_SETTINGS, CamelRssSettings)) +#define CAMEL_RSS_SETTINGS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), CAMEL_TYPE_RSS_SETTINGS, CamelRssSettingsClass)) +#define CAMEL_IS_RSS_SETTINGS(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), CAMEL_TYPE_RSS_SETTINGS)) +#define CAMEL_IS_RSS_SETTINGS_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), CAMEL_TYPE_RSS_SETTINGS)) +#define CAMEL_RSS_SETTINGS_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), CAMEL_TYPE_RSS_SETTINGS, CamelRssSettingsClass)) + +G_BEGIN_DECLS + +typedef struct _CamelRssSettings CamelRssSettings; +typedef struct _CamelRssSettingsClass CamelRssSettingsClass; +typedef struct _CamelRssSettingsPrivate CamelRssSettingsPrivate; + +struct _CamelRssSettings { + CamelOfflineSettings parent; + CamelRssSettingsPrivate *priv; +}; + +struct _CamelRssSettingsClass { + CamelOfflineSettingsClass parent_class; +}; + +GType camel_rss_settings_get_type + (void) G_GNUC_CONST; +gboolean camel_rss_settings_get_filter_all + (CamelRssSettings *settings); +void camel_rss_settings_set_filter_all + (CamelRssSettings *settings, + gboolean filter_all); +gboolean camel_rss_settings_get_complete_articles + (CamelRssSettings *settings); +void camel_rss_settings_set_complete_articles + (CamelRssSettings *settings, + gboolean value); +gboolean camel_rss_settings_get_feed_enclosures + (CamelRssSettings *settings); +void camel_rss_settings_set_feed_enclosures + (CamelRssSettings *settings, + gboolean value); +gboolean camel_rss_settings_get_limit_feed_enclosure_size + (CamelRssSettings *settings); +void camel_rss_settings_set_limit_feed_enclosure_size + (CamelRssSettings *settings, + gboolean value); +guint32 camel_rss_settings_get_max_feed_enclosure_size + (CamelRssSettings *settings); +void camel_rss_settings_set_max_feed_enclosure_size + (CamelRssSettings *settings, + guint32 value); + +G_END_DECLS + +#endif /* CAMEL_RSS_SETTINGS_H */ diff --git a/src/modules/rss/camel/camel-rss-store.c b/src/modules/rss/camel/camel-rss-store.c new file mode 100644 index 0000000000..c430bb4ce4 --- /dev/null +++ b/src/modules/rss/camel/camel-rss-store.c @@ -0,0 +1,397 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "camel-rss-folder.h" +#include "camel-rss-settings.h" +#include "camel-rss-store-summary.h" +#include "camel-rss-store.h" + +struct _CamelRssStorePrivate { + CamelDataCache *cache; + CamelRssStoreSummary *summary; +}; + +enum { + PROP_0, + PROP_SUMMARY +}; + +static GInitableIface *parent_initable_interface; + +/* Forward Declarations */ +static void camel_rss_store_initable_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (CamelRssStore, camel_rss_store, CAMEL_TYPE_STORE, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, camel_rss_store_initable_init) + G_ADD_PRIVATE (CamelRssStore)) + +static gchar * +rss_store_get_name (CamelService *service, + gboolean brief) +{ + return g_strdup (_("News and Blogs")); +} + +static gboolean +rss_store_can_refresh_folder (CamelStore *store, + CamelFolderInfo *info, + GError **error) +{ + /* Any RSS folder can be refreshed */ + return TRUE; +} + +static CamelFolder * +rss_store_get_folder_sync (CamelStore *store, + const gchar *folder_name, + CamelStoreGetFolderFlags flags, + GCancellable *cancellable, + GError **error) +{ + CamelRssStore *self = CAMEL_RSS_STORE (store); + CamelFolder *folder = NULL; + + camel_rss_store_summary_lock (self->priv->summary); + + /* The 'folder_name' is the folder ID */ + if (camel_rss_store_summary_contains (self->priv->summary, folder_name)) { + folder = camel_rss_folder_new (store, folder_name, cancellable, error); + } else { + g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER, + _("Folder '%s' not found"), folder_name); + } + + camel_rss_store_summary_unlock (self->priv->summary); + + return folder; +} + +static CamelFolderInfo * +rss_store_get_folder_info_sync (CamelStore *store, + const gchar *top, + CamelStoreGetFolderInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + CamelRssStore *self = CAMEL_RSS_STORE (store); + CamelFolderInfo *fi = NULL, *first = NULL, *last = NULL; + + if (!top || !*top) { + GSList *ids, *link; + + ids = camel_rss_store_summary_dup_feeds (self->priv->summary); + + for (link = ids; link; link = g_slist_next (link)) { + const gchar *id = link->data; + + fi = camel_rss_store_summary_dup_folder_info (self->priv->summary, id); + if (fi) { + if (last) { + last->next = fi; + last = fi; + } else { + first = fi; + last = first; + } + } + } + + g_slist_free_full (ids, g_free); + } else { + first = camel_rss_store_summary_dup_folder_info (self->priv->summary, top); + if (!first) + first = camel_rss_store_summary_dup_folder_info_for_display_name (self->priv->summary, top); + + if (!first) { + g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER, + _("Folder '%s' not found"), top); + } + } + + return first; +} + +static CamelFolderInfo * +rss_store_create_folder_sync (CamelStore *store, + const gchar *parent_name, + const gchar *folder_name, + GCancellable *cancellable, + GError **error) +{ + g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_INVALID, + _("Cannot create a folder in a News and Blogs store.")); + + return NULL; +} + +static gboolean +rss_store_rename_folder_sync (CamelStore *store, + const gchar *old_name, + const gchar *new_name_in, + GCancellable *cancellable, + GError **error) +{ + CamelRssStore *self = CAMEL_RSS_STORE (store); + gboolean success = FALSE; + + camel_rss_store_summary_lock (self->priv->summary); + + if (camel_rss_store_summary_contains (self->priv->summary, old_name)) { + const gchar *display_name; + + success = TRUE; + + display_name = camel_rss_store_summary_get_display_name (self->priv->summary, old_name); + if (g_strcmp0 (display_name, new_name_in) != 0) { + camel_rss_store_summary_set_display_name (self->priv->summary, old_name, new_name_in); + + success = camel_rss_store_summary_save (self->priv->summary, error); + + if (success) { + CamelFolderInfo *fi; + + fi = camel_rss_store_summary_dup_folder_info (self->priv->summary, old_name); + camel_store_folder_renamed (store, old_name, fi); + camel_folder_info_free (fi); + } + } + } else { + g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER, + _("Folder '%s' not found"), old_name); + } + + camel_rss_store_summary_unlock (self->priv->summary); + + return success; +} + +static gboolean +rss_store_delete_folder_sync (CamelStore *store, + const gchar *folder_name, + GCancellable *cancellable, + GError **error) +{ + CamelRssStore *self = CAMEL_RSS_STORE (store); + CamelFolderInfo *fi; + gboolean success = FALSE; + + camel_rss_store_summary_lock (self->priv->summary); + + /* The 'folder_name' is the folder ID */ + fi = camel_rss_store_summary_dup_folder_info (self->priv->summary, folder_name); + + if (camel_rss_store_summary_remove (self->priv->summary, folder_name)) { + GFile *file; + gchar *cmeta_filename; + GError *local_error = NULL; + + file = g_file_new_build_filename (camel_data_cache_get_path (self->priv->cache), folder_name, NULL); + + /* Ignore errors */ + if (!e_file_recursive_delete_sync (file, cancellable, &local_error)) { + if (camel_debug ("rss") && + !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && + !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_printerr ("%s: Failed to delete cache directory '%s': %s", G_STRFUNC, g_file_peek_path (file), local_error ? local_error->message : "Unknown error"); + + g_clear_error (&local_error); + } + + g_clear_object (&file); + + cmeta_filename = g_strdup_printf ("%s%c%s.cmeta", camel_data_cache_get_path (self->priv->cache), G_DIR_SEPARATOR, folder_name); + if (g_unlink (cmeta_filename)) { + gint errn = errno; + + if (errn != ENOENT && camel_debug ("rss")) + g_printerr ("%s: Failed to delete '%s': %s", G_STRFUNC, cmeta_filename, g_strerror (errn)); + } + + g_free (cmeta_filename); + + camel_store_folder_deleted (store, fi); + success = camel_rss_store_summary_save (self->priv->summary, error); + } else { + g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER, + _("Folder '%s' not found"), folder_name); + } + + camel_rss_store_summary_unlock (self->priv->summary); + + if (fi) + camel_folder_info_free (fi); + + return success; +} + +static void +rss_store_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SUMMARY: + g_value_set_object (value, + camel_rss_store_get_summary ( + CAMEL_RSS_STORE (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +rss_store_dispose (GObject *object) +{ + CamelRssStore *self = CAMEL_RSS_STORE (object); + + if (self->priv->summary) { + GError *local_error = NULL; + + if (!camel_rss_store_summary_save (self->priv->summary, &local_error)) { + g_warning ("%s: Failed to save RSS store summary: %s", G_STRFUNC, local_error ? local_error->message : "Unknown error"); + g_clear_error (&local_error); + } + } + + g_clear_object (&self->priv->cache); + g_clear_object (&self->priv->summary); + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (camel_rss_store_parent_class)->dispose (object); +} + +static void +camel_rss_store_class_init (CamelRssStoreClass *klass) +{ + GObjectClass *object_class; + CamelServiceClass *service_class; + CamelStoreClass *store_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->get_property = rss_store_get_property; + object_class->dispose = rss_store_dispose; + + service_class = CAMEL_SERVICE_CLASS (klass); + service_class->settings_type = CAMEL_TYPE_RSS_SETTINGS; + service_class->get_name = rss_store_get_name; + + store_class = CAMEL_STORE_CLASS (klass); + store_class->can_refresh_folder = rss_store_can_refresh_folder; + store_class->get_folder_sync = rss_store_get_folder_sync; + store_class->get_folder_info_sync = rss_store_get_folder_info_sync; + store_class->create_folder_sync = rss_store_create_folder_sync; + store_class->delete_folder_sync = rss_store_delete_folder_sync; + store_class->rename_folder_sync = rss_store_rename_folder_sync; + + g_object_class_install_property ( + object_class, + PROP_SUMMARY, + g_param_spec_object ( + "summary", NULL, NULL, + CAMEL_TYPE_RSS_STORE_SUMMARY, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); +} + +static gboolean +rss_store_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + CamelDataCache *rss_cache; + CamelRssStore *self; + CamelStore *store; + CamelService *service; + const gchar *user_data_dir; + gchar *filename; + + self = CAMEL_RSS_STORE (initable); + store = CAMEL_STORE (initable); + service = CAMEL_SERVICE (initable); + + camel_store_set_flags (store, camel_store_get_flags (store) | CAMEL_STORE_VTRASH | CAMEL_STORE_VJUNK | CAMEL_STORE_IS_BUILTIN); + + /* Chain up to parent method. */ + if (!parent_initable_interface->init (initable, cancellable, error)) + return FALSE; + + service = CAMEL_SERVICE (initable); + user_data_dir = camel_service_get_user_data_dir (service); + + if (g_mkdir_with_parents (user_data_dir, S_IRWXU) == -1) { + g_set_error_literal ( + error, G_FILE_ERROR, + g_file_error_from_errno (errno), + g_strerror (errno)); + return FALSE; + } + + filename = g_build_filename (user_data_dir, "rss.ini", NULL); + self->priv->summary = camel_rss_store_summary_new (filename); + g_free (filename); + + if (!camel_rss_store_summary_load (self->priv->summary, error)) + return FALSE; + + /* setup store-wide cache */ + rss_cache = camel_data_cache_new (user_data_dir, error); + if (rss_cache == NULL) + return FALSE; + + /* Do not expire the cache */ + camel_data_cache_set_expire_enabled (rss_cache, FALSE); + + self->priv->cache = rss_cache; /* takes ownership */ + + return TRUE; +} + +static void +camel_rss_store_initable_init (GInitableIface *iface) +{ + parent_initable_interface = g_type_interface_peek_parent (iface); + + iface->init = rss_store_initable_init; +} + +static void +camel_rss_store_init (CamelRssStore *self) +{ + self->priv = camel_rss_store_get_instance_private (self); + + camel_store_set_flags (CAMEL_STORE (self), 0); +} + +CamelDataCache * +camel_rss_store_get_cache (CamelRssStore *self) +{ + g_return_val_if_fail (CAMEL_IS_RSS_STORE (self), NULL); + + return self->priv->cache; +} + +CamelRssStoreSummary * +camel_rss_store_get_summary (CamelRssStore *self) +{ + g_return_val_if_fail (CAMEL_IS_RSS_STORE (self), NULL); + + return self->priv->summary; +} diff --git a/src/modules/rss/camel/camel-rss-store.h b/src/modules/rss/camel/camel-rss-store.h new file mode 100644 index 0000000000..1ad34b2299 --- /dev/null +++ b/src/modules/rss/camel/camel-rss-store.h @@ -0,0 +1,55 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef CAMEL_RSS_STORE_H +#define CAMEL_RSS_STORE_H + +#include + +#include "camel-rss-store-summary.h" + +/* Standard GObject macros */ +#define CAMEL_TYPE_RSS_STORE \ + (camel_rss_store_get_type ()) +#define CAMEL_RSS_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), CAMEL_TYPE_RSS_STORE, CamelRssStore)) +#define CAMEL_RSS_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), CAMEL_TYPE_RSS_STORE, CamelRssStoreClass)) +#define CAMEL_IS_RSS_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), CAMEL_TYPE_RSS_STORE)) +#define CAMEL_IS_RSS_STORE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), CAMEL_TYPE_RSS_STORE)) +#define CAMEL_RSS_STORE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), CAMEL_TYPE_RSS_STORE, CamelRssStoreClass)) + +G_BEGIN_DECLS + +typedef struct _CamelRssStore CamelRssStore; +typedef struct _CamelRssStoreClass CamelRssStoreClass; +typedef struct _CamelRssStorePrivate CamelRssStorePrivate; + +struct _CamelRssStore { + CamelStore parent; + CamelRssStorePrivate *priv; +}; + +struct _CamelRssStoreClass { + CamelStoreClass parent_class; +}; + +GType camel_rss_store_get_type (void); +CamelDataCache *camel_rss_store_get_cache (CamelRssStore *self); +CamelRssStoreSummary * + camel_rss_store_get_summary (CamelRssStore *self); + +G_END_DECLS + +#endif /* CAMEL_RSS_STORE_H */ diff --git a/src/modules/rss/camel/libcamelrss.urls b/src/modules/rss/camel/libcamelrss.urls new file mode 100644 index 0000000000..b60d988a7d --- /dev/null +++ b/src/modules/rss/camel/libcamelrss.urls @@ -0,0 +1 @@ +rss diff --git a/src/modules/rss/e-rss-parser.c b/src/modules/rss/e-rss-parser.c new file mode 100644 index 0000000000..12736de9af --- /dev/null +++ b/src/modules/rss/e-rss-parser.c @@ -0,0 +1,656 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include + +#include +#include + +#include "e-rss-parser.h" + +ERssEnclosure * +e_rss_enclosure_new (void) +{ + return g_slice_new0 (ERssEnclosure); +} + +void +e_rss_enclosure_free (gpointer ptr) +{ + ERssEnclosure *enclosure = ptr; + + if (enclosure) { + g_clear_pointer (&enclosure->data, g_bytes_unref); + g_free (enclosure->title); + g_free (enclosure->href); + g_free (enclosure->content_type); + g_slice_free (ERssEnclosure, enclosure); + } +} + +ERssFeed * +e_rss_feed_new (void) +{ + return g_slice_new0 (ERssFeed); +} + +void +e_rss_feed_free (gpointer ptr) +{ + ERssFeed *feed = ptr; + + if (feed) { + g_free (feed->id); + g_free (feed->link); + g_free (feed->author); + g_free (feed->title); + g_free (feed->body); + g_slist_free_full (feed->enclosures, e_rss_enclosure_free); + g_slice_free (ERssFeed, feed); + } +} + +static void +e_rss_read_feed_person (xmlNodePtr author, + xmlChar **out_name, + xmlChar **out_email) +{ + xmlNodePtr node; + + for (node = author->children; node && (!*out_name || !*out_email); node = node->next) { + if (g_strcmp0 ((const gchar *) node->name, "name") == 0) { + g_clear_pointer (out_name, xmlFree); + *out_name = xmlNodeGetContent (node); + } else if (g_strcmp0 ((const gchar *) node->name, "email") == 0) { + g_clear_pointer (out_email, xmlFree); + *out_email = xmlNodeGetContent (node); + } else if (g_strcmp0 ((const gchar *) node->name, "uri") == 0 && + (!*out_email || !**out_email)) { + g_clear_pointer (out_email, xmlFree); + *out_email = xmlNodeGetContent (node); + } + } + + if (!*out_name && !*out_email) { + *out_name = xmlNodeGetContent (author); + if (*out_name && !**out_name) + g_clear_pointer (&out_name, xmlFree); + } +} + +static gchar * +e_rss_parser_encode_address (xmlChar *name, + xmlChar *email) +{ + gchar *address; + + if (!name && !email) + return NULL; + + address = camel_internet_address_format_address ((const gchar *) name, + email ? (const gchar *) email : ""); + + if (address && (!email || !*email) && g_str_has_suffix (address, " <>")) { + /* avoid empty email in the string */ + address[strlen (address) - 3] = '\0'; + } + + return address; +} + +static ERssEnclosure * +e_rss_read_enclosure (xmlNodePtr node) +{ + #define dup_attr(des, x) { \ + xmlChar *attr = xmlGetProp (node, (const xmlChar *) x); \ + if (attr && *attr) \ + des = g_strdup ((const gchar *) attr); \ + else \ + des = NULL; \ + g_clear_pointer (&attr, xmlFree); \ + } + + ERssEnclosure *enclosure; + xmlChar *length; + gchar *href; + + dup_attr (href, "href"); + if (!href) + dup_attr (href, "url"); + + if (!href || !*href) { + g_free (href); + return NULL; + } + + enclosure = e_rss_enclosure_new (); + + enclosure->href = href; + + dup_attr (enclosure->title, "title"); + dup_attr (enclosure->content_type, "type"); + + #undef dup_attr + + length = xmlGetProp (node, (const xmlChar *) "length"); + if (length && *length) + enclosure->size = g_ascii_strtoull ((const gchar *) length, NULL, 10); + g_clear_pointer (&length, xmlFree); + + return enclosure; +} + +typedef struct _FeedDefaults { + GUri *base_uri; /* 'base' as a GUri */ + xmlChar *base; + xmlChar *author_name; + xmlChar *author_email; + gint64 publish_date; + xmlChar *link; + xmlChar *alt_link; + xmlChar *title; + xmlChar *icon; +} FeedDefaults; + +static void +e_rss_ensure_uri_absolute (GUri *base_uri, + gchar **inout_uri) +{ + GUri *abs_uri; + const gchar *uri; + + if (!base_uri || !inout_uri) + return; + + uri = *inout_uri; + + if (!uri || *uri != '/') + return; + + abs_uri = g_uri_parse_relative (base_uri, uri, + G_URI_FLAGS_PARSE_RELAXED | + G_URI_FLAGS_HAS_PASSWORD | + G_URI_FLAGS_ENCODED_PATH | + G_URI_FLAGS_ENCODED_QUERY | + G_URI_FLAGS_ENCODED_FRAGMENT | + G_URI_FLAGS_SCHEME_NORMALIZE, NULL); + + if (abs_uri) { + g_free (*inout_uri); + *inout_uri = g_uri_to_string_partial (abs_uri, G_URI_HIDE_PASSWORD); + + g_uri_unref (abs_uri); + } +} + +static void +e_rss_read_item (xmlNodePtr item, + const FeedDefaults *defaults, + GSList **out_feeds) +{ + ERssFeed *feed = e_rss_feed_new (); + xmlNodePtr node; + + for (node = item->children; node; node = node->next) { + xmlChar *value = NULL; + + if (g_strcmp0 ((const gchar *) node->name, "title") == 0) { + value = xmlNodeGetContent (node); + g_clear_pointer (&feed->title, g_free); + feed->title = g_strdup ((const gchar *) value); + } else if (g_strcmp0 ((const gchar *) node->name, "link") == 0) { + xmlChar *rel = xmlGetProp (node, (const xmlChar *) "rel"); + if (!rel || + g_strcmp0 ((const gchar *) rel, "self") == 0 || + g_strcmp0 ((const gchar *) rel, "alternate") == 0) { + value = xmlGetProp (node, (const xmlChar *) "href"); + if (!value) + value = xmlNodeGetContent (node); + g_clear_pointer (&feed->link, g_free); + feed->link = g_strdup ((const gchar *) value); + + /* Use full URI-s, not relative */ + if (feed->link && *feed->link == '/' && defaults->base_uri) + e_rss_ensure_uri_absolute (defaults->base_uri, &feed->link); + } else if (g_strcmp0 ((const gchar *) rel, "enclosure") == 0) { + ERssEnclosure *enclosure = e_rss_read_enclosure (node); + + if (enclosure) + feed->enclosures = g_slist_prepend (feed->enclosures, enclosure); + } + g_clear_pointer (&rel, xmlFree); + } else if (g_strcmp0 ((const gchar *) node->name, "id") == 0 || + g_strcmp0 ((const gchar *) node->name, "guid") == 0) { + value = xmlNodeGetContent (node); + g_clear_pointer (&feed->id, g_free); + feed->id = g_strdup ((const gchar *) value); + } else if (g_strcmp0 ((const gchar *) node->name, "content") == 0) { + value = xmlNodeGetContent (node); + g_clear_pointer (&feed->body, g_free); + feed->body = g_strdup ((const gchar *) value); + } else if (g_strcmp0 ((const gchar *) node->name, "description") == 0 || + g_strcmp0 ((const gchar *) node->name, "summary") == 0) { + if (!feed->body || !*feed->body) { + value = xmlNodeGetContent (node); + g_clear_pointer (&feed->body, g_free); + feed->body = g_strdup ((const gchar *) value); + } + } else if (g_strcmp0 ((const gchar *) node->name, "enclosure") == 0) { + ERssEnclosure *enclosure = e_rss_read_enclosure (node); + + if (enclosure) + feed->enclosures = g_slist_prepend (feed->enclosures, enclosure); + } else if (g_strcmp0 ((const gchar *) node->name, "author") == 0) { + xmlChar *name = NULL, *email = NULL; + + e_rss_read_feed_person (node, &name, &email); + + if (name || email) { + g_clear_pointer (&feed->author, g_free); + feed->author = e_rss_parser_encode_address (name, email); + + g_clear_pointer (&name, xmlFree); + g_clear_pointer (&email, xmlFree); + } + } else if (g_strcmp0 ((const gchar *) node->name, "pubDate") == 0) { + value = xmlNodeGetContent (node); + + if (value && *value) + feed->last_modified = camel_header_decode_date ((const gchar *) value, NULL); + } else if (g_strcmp0 ((const gchar *) node->name, "updated") == 0) { + value = xmlNodeGetContent (node); + + if (value && *value) { + GDateTime *dt; + + dt = g_date_time_new_from_iso8601 ((const gchar *) value, NULL); + + if (dt) + feed->last_modified = g_date_time_to_unix (dt); + + g_clear_pointer (&dt, g_date_time_unref); + } + } + + g_clear_pointer (&value, xmlFree); + } + + if (feed->link && feed->title) { + if (!feed->author) { + if (defaults->author_name || defaults->author_email) { + feed->author = e_rss_parser_encode_address (defaults->author_name, defaults->author_email); + } else { + feed->author = g_strdup (_("Unknown author")); + } + } + + if (!feed->last_modified) + feed->last_modified = defaults->publish_date; + + feed->enclosures = g_slist_reverse (feed->enclosures); + + *out_feeds = g_slist_prepend (*out_feeds, feed); + } else { + e_rss_feed_free (feed); + } +} + +static void +e_rss_read_defaults_rdf (xmlNodePtr root, + FeedDefaults *defaults) +{ + xmlNodePtr node; + + defaults->base = xmlGetProp (root, (const xmlChar *) "base"); + + for (node = root->children; node; node = node->next) { + if (g_strcmp0 ((const gchar *) node->name, "channel") == 0) { + xmlNodePtr subnode; + gboolean has_author = FALSE, has_link = FALSE, has_title = FALSE, has_image = FALSE; + + for (subnode = node->children; subnode && (!has_author || !has_link || !has_title || !has_image); subnode = subnode->next) { + if (!has_author && g_strcmp0 ((const gchar *) subnode->name, "creator") == 0) { + g_clear_pointer (&defaults->author_name, xmlFree); + defaults->author_name = xmlNodeGetContent (subnode); + has_author = TRUE; + } else if (!has_author && g_strcmp0 ((const gchar *) subnode->name, "publisher") == 0) { + g_clear_pointer (&defaults->author_name, xmlFree); + defaults->author_name = xmlNodeGetContent (subnode); + /* do not set has_author here, creator is more suitable */ + } + + if (!has_link && g_strcmp0 ((const gchar *) subnode->name, "link") == 0) { + defaults->link = xmlNodeGetContent (subnode); + has_link = TRUE; + } + + if (!has_title && g_strcmp0 ((const gchar *) subnode->name, "title") == 0) { + defaults->title = xmlNodeGetContent (subnode); + has_title = TRUE; + } + + if (!has_image && g_strcmp0 ((const gchar *) subnode->name, "image") == 0) { + defaults->icon = xmlGetProp (subnode, (const xmlChar *) "resource"); + has_image = TRUE; + } + } + + break; + } + } +} + +static void +e_rss_read_rdf (xmlNodePtr node, + const FeedDefaults *defaults, + GSList **out_feeds) +{ + if (g_strcmp0 ((const gchar *) node->name, "item") == 0) { + e_rss_read_item (node, defaults, out_feeds); + } +} + +static void +e_rss_read_defaults_rss (xmlNodePtr root, + FeedDefaults *defaults) +{ + xmlNodePtr channel_node; + + defaults->base = xmlGetProp (root, (const xmlChar *) "base"); + + for (channel_node = root->children; channel_node; channel_node = channel_node->next) { + if (g_strcmp0 ((const gchar *) channel_node->name, "channel") == 0) { + xmlNodePtr node; + gboolean has_pubdate = FALSE, has_link = FALSE, has_title = FALSE, has_image = FALSE; + + for (node = channel_node->children; node && (!has_pubdate || !has_link || !has_title || !has_image); node = node->next) { + if (!has_pubdate && g_strcmp0 ((const gchar *) node->name, "pubDate") == 0) { + xmlChar *value = xmlNodeGetContent (node); + + if (value && *value) + defaults->publish_date = camel_header_decode_date ((const gchar *) value, NULL); + + g_clear_pointer (&value, xmlFree); + + has_pubdate = TRUE; + } + + if (!has_link && g_strcmp0 ((const gchar *) node->name, "link") == 0) { + xmlChar *value = xmlNodeGetContent (node); + + if (value && *value) { + defaults->link = value; + has_link = TRUE; + } else { + g_clear_pointer (&value, xmlFree); + } + } + + if (!has_title && g_strcmp0 ((const gchar *) node->name, "title") == 0) { + xmlChar *value = xmlNodeGetContent (node); + + if (value && *value) + defaults->title = value; + else + g_clear_pointer (&value, xmlFree); + + has_title = TRUE; + } + + if (!has_image && g_strcmp0 ((const gchar *) node->name, "image") == 0) { + xmlNodePtr image_node; + + for (image_node = node->children; image_node; image_node = image_node->next) { + if (g_strcmp0 ((const gchar *) image_node->name, "url") == 0) { + xmlChar *value = xmlNodeGetContent (image_node); + + if (value && *value) + defaults->icon = value; + else + g_clear_pointer (&value, xmlFree); + break; + } + } + + has_image = TRUE; + } + } + /* read only the first channel */ + break; + } + } +} + +static void +e_rss_read_rss (xmlNodePtr node, + const FeedDefaults *defaults, + GSList **out_feeds) +{ + if (g_strcmp0 ((const gchar *) node->name, "channel") == 0) { + xmlNodePtr subnode; + + for (subnode = node->children; subnode; subnode = subnode->next) { + if (g_strcmp0 ((const gchar *) subnode->name, "item") == 0) { + e_rss_read_item (subnode, defaults, out_feeds); + } + } + } +} + +static void +e_rss_read_defaults_feed (xmlNodePtr root, + FeedDefaults *defaults) +{ + xmlNodePtr node; + gboolean has_author = FALSE, has_published = FALSE, has_link = FALSE, has_alt_link = FALSE, has_title = FALSE, has_icon = FALSE; + + defaults->base = xmlGetProp (root, (const xmlChar *) "base"); + if (!defaults->base) { + for (node = root->children; node && !defaults->base; node = node->next) { + if (g_strcmp0 ((const gchar *) node->name, "link") == 0) + defaults->base = xmlGetProp (root, (const xmlChar *) "rel"); + else if (g_strcmp0 ((const gchar *) node->name, "alternate") == 0) + defaults->base = xmlGetProp (root, (const xmlChar *) "href"); + } + } + + for (node = root->children; node && (!has_author || !has_published || !has_link || !has_alt_link || !has_title || !has_icon); node = node->next) { + if (!has_author && g_strcmp0 ((const gchar *) node->name, "author") == 0) { + e_rss_read_feed_person (node, &defaults->author_name, &defaults->author_email); + has_author = TRUE; + } + + if (!has_published && ( + g_strcmp0 ((const gchar *) node->name, "published") == 0 || + g_strcmp0 ((const gchar *) node->name, "updated") == 0)) { + xmlChar *value = xmlNodeGetContent (node); + + if (value && *value) { + GDateTime *dt; + + dt = g_date_time_new_from_iso8601 ((const gchar *) value, NULL); + + if (dt) { + defaults->publish_date = g_date_time_to_unix (dt); + has_published = TRUE; + } + + g_clear_pointer (&dt, g_date_time_unref); + } + + g_clear_pointer (&value, xmlFree); + } + + if ((!has_link || !has_alt_link) && g_strcmp0 ((const gchar *) node->name, "link") == 0) { + xmlChar *rel, *href; + + rel = xmlGetProp (node, (const xmlChar *) "rel"); + href = xmlGetProp (node, (const xmlChar *) "href"); + + if (!has_link && href && *href && g_strcmp0 ((const gchar *) rel, "self") == 0) { + defaults->link = href; + href = NULL; + has_link = TRUE; + } + + if (!has_alt_link && href && *href && g_strcmp0 ((const gchar *) rel, "alternate") == 0) { + defaults->alt_link = href; + href = NULL; + has_alt_link = TRUE; + } + + g_clear_pointer (&rel, xmlFree); + g_clear_pointer (&href, xmlFree); + } + + if (!has_title && g_strcmp0 ((const gchar *) node->name, "title") == 0) { + xmlChar *value = xmlNodeGetContent (node); + + if (value && *value) + defaults->title = value; + else + g_clear_pointer (&value, xmlFree); + + has_title = TRUE; + } + + if (!has_icon && ( + g_strcmp0 ((const gchar *) node->name, "icon") == 0 || + g_strcmp0 ((const gchar *) node->name, "logo") == 0)) { + xmlChar *value = xmlNodeGetContent (node); + + if (value && *value) { + g_clear_pointer (&defaults->icon, xmlFree); + defaults->icon = value; + } else { + g_clear_pointer (&value, xmlFree); + } + + /* Prefer "icon", but if not available, then use "logo" */ + has_icon = g_strcmp0 ((const gchar *) node->name, "icon") == 0; + } + } +} + +static void +e_rss_read_feed (xmlNodePtr node, + const FeedDefaults *defaults, + GSList **out_feeds) +{ + if (g_strcmp0 ((const gchar *) node->name, "entry") == 0) { + e_rss_read_item (node, defaults, out_feeds); + } +} + +gboolean +e_rss_parser_parse (const gchar *xml, + gsize xml_len, + gchar **out_link, + gchar **out_alt_link, + gchar **out_title, + gchar **out_icon, + GSList **out_feeds) /* ERssFeed * */ +{ + xmlDoc *doc; + xmlNodePtr root; + + g_return_val_if_fail (xml != NULL, FALSE); + + if (out_feeds) + *out_feeds = NULL; + + doc = e_xml_parse_data (xml, xml_len); + + if (!doc) + return FALSE; + + root = xmlDocGetRootElement (doc); + if (root) { + FeedDefaults defaults = { 0, }; + void (*read_func) (xmlNodePtr node, + const FeedDefaults *defaults, + GSList **out_feeds) = NULL; + + if (g_strcmp0 ((const gchar *) root->name, "rdf") == 0) { + /* RSS 1.0 - https://web.resource.org/rss/1.0/ */ + e_rss_read_defaults_rdf (root, &defaults); + read_func = e_rss_read_rdf; + } else if (g_strcmp0 ((const gchar *) root->name, "rss") == 0) { + /* RSS 2.0 - https://www.rssboard.org/rss-specification */ + e_rss_read_defaults_rss (root, &defaults); + read_func = e_rss_read_rss; + } else if (g_strcmp0 ((const gchar *) root->name, "feed") == 0) { + /* Atom - https://validator.w3.org/feed/docs/atom.html */ + e_rss_read_defaults_feed (root, &defaults); + read_func = e_rss_read_feed; + } + + if (defaults.base || defaults.link || defaults.alt_link) { + const gchar *base; + + base = (const gchar *) defaults.base; + if (!base || *base == '/') + base = (const gchar *) defaults.link; + if (!base || *base == '/') + base = (const gchar *) defaults.alt_link; + + if (base && *base != '/') { + defaults.base_uri = g_uri_parse (base, + G_URI_FLAGS_PARSE_RELAXED | + G_URI_FLAGS_HAS_PASSWORD | + G_URI_FLAGS_ENCODED_PATH | + G_URI_FLAGS_ENCODED_QUERY | + G_URI_FLAGS_ENCODED_FRAGMENT | + G_URI_FLAGS_SCHEME_NORMALIZE, NULL); + } + } + + if (read_func && out_feeds) { + xmlNodePtr node; + + for (node = root->children; node; node = node->next) { + read_func (node, &defaults, out_feeds); + } + } + + if (out_link) { + *out_link = g_strdup ((const gchar *) defaults.link); + e_rss_ensure_uri_absolute (defaults.base_uri, out_link); + } + + if (out_alt_link) { + *out_alt_link = g_strdup ((const gchar *) defaults.alt_link); + e_rss_ensure_uri_absolute (defaults.base_uri, out_alt_link); + } + + if (out_title) + *out_title = g_strdup ((const gchar *) defaults.title); + + if (out_icon) { + *out_icon = g_strdup ((const gchar *) defaults.icon); + e_rss_ensure_uri_absolute (defaults.base_uri, out_icon); + } + + g_clear_pointer (&defaults.base_uri, g_uri_unref); + g_clear_pointer (&defaults.base, xmlFree); + g_clear_pointer (&defaults.author_name, xmlFree); + g_clear_pointer (&defaults.author_email, xmlFree); + g_clear_pointer (&defaults.link, xmlFree); + g_clear_pointer (&defaults.alt_link, xmlFree); + g_clear_pointer (&defaults.title, xmlFree); + g_clear_pointer (&defaults.icon, xmlFree); + + if (out_feeds) + *out_feeds = g_slist_reverse (*out_feeds); + } + + xmlFreeDoc (doc); + + return TRUE; +} diff --git a/src/modules/rss/e-rss-parser.h b/src/modules/rss/e-rss-parser.h new file mode 100644 index 0000000000..cf5f8fefa2 --- /dev/null +++ b/src/modules/rss/e-rss-parser.h @@ -0,0 +1,48 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef E_RSS_PARSER_H +#define E_RSS_PARSER_H + +#include + +G_BEGIN_DECLS + +typedef struct _ERssEnclosure { + gchar *title; + gchar *href; + gchar *content_type; + guint64 size; + GBytes *data; +} ERssEnclosure; + +typedef struct _ERssFeed { + gchar *id; + gchar *link; + gchar *author; + gchar *title; + gchar *body; + gint64 last_modified; + GSList *enclosures; /* ERssEnclosure * */ +} ERssFeed; + +ERssEnclosure * e_rss_enclosure_new (void); +void e_rss_enclosure_free (gpointer ptr); + +ERssFeed * e_rss_feed_new (void); +void e_rss_feed_free (gpointer ptr); + +gboolean e_rss_parser_parse (const gchar *xml, + gsize xml_len, + gchar **out_link, + gchar **out_alt_link, + gchar **out_title, + gchar **out_icon, + GSList **out_feeds); /* ERssFeed * */ + +G_END_DECLS + +#endif /* E_RSS_PARSER_H */ diff --git a/src/modules/rss/evolution/CMakeLists.txt b/src/modules/rss/evolution/CMakeLists.txt new file mode 100644 index 0000000000..b9cea767e2 --- /dev/null +++ b/src/modules/rss/evolution/CMakeLists.txt @@ -0,0 +1,32 @@ +set(extra_deps + evolution-mail + evolution-shell +) +set(sources + e-rss-preferences.c + e-rss-preferences.h + e-rss-folder-tree-model-extension.c + e-rss-shell-extension.c + e-rss-shell-view-extension.c + module-rss.c + module-rss.h + ../camel-rss-store-summary.c + ../camel-rss-store-summary.h + ../e-rss-parser.c + ../e-rss-parser.h +) +set(extra_defines) +set(extra_cflags) +set(extra_incdirs + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) +set(extra_ldflags) + +add_evolution_module(module-rss + sources + extra_deps + extra_defines + extra_cflags + extra_incdirs + extra_ldflags +) diff --git a/src/modules/rss/evolution/e-rss-folder-tree-model-extension.c b/src/modules/rss/evolution/e-rss-folder-tree-model-extension.c new file mode 100644 index 0000000000..1b6ad5437f --- /dev/null +++ b/src/modules/rss/evolution/e-rss-folder-tree-model-extension.c @@ -0,0 +1,217 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include + +#include "mail/em-folder-tree-model.h" +#include "../camel-rss-store-summary.h" + +#include "module-rss.h" + +#define E_TYPE_RSS_FOLDER_TREE_MODEL_EXTENSION (e_rss_folder_tree_model_extension_get_type ()) + +GType e_rss_folder_tree_model_extension_get_type (void); + +typedef struct _ERssFolderTreeModelExtension { + EExtension parent; + gboolean listens_feed_changed; +} ERssFolderTreeModelExtension; + +typedef struct _ERssFolderTreeModelExtensionClass { + EExtensionClass parent_class; +} ERssFolderTreeModelExtensionClass; + +G_DEFINE_DYNAMIC_TYPE (ERssFolderTreeModelExtension, e_rss_folder_tree_model_extension, E_TYPE_EXTENSION) + +static void +e_rss_update_custom_icon (CamelRssStoreSummary *store_summary, + const gchar *full_name, + EMFolderTreeModel *model, + GtkTreeIter *iter) +{ + const gchar *icon_filename; + + icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, full_name); + + if (icon_filename && g_file_test (icon_filename, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS)) + icon_filename = full_name; + else + icon_filename = "rss"; + + gtk_tree_store_set (GTK_TREE_STORE (model), iter, + COL_STRING_ICON_NAME, icon_filename, + -1); +} + +static void +e_rss_folder_custom_icon_feed_changed_cb (CamelRssStoreSummary *store_summary, + const gchar *feed_id, + EMFolderTreeModel *model) +{ + EMailSession *session; + CamelService *service; + + if (!feed_id || !camel_rss_store_summary_contains (store_summary, feed_id)) + return; + + session = em_folder_tree_model_get_session (model); + + if (!session) + return; + + service = camel_session_ref_service (CAMEL_SESSION (session), "rss"); + + if (service) { + GtkTreeRowReference *row; + + row = em_folder_tree_model_get_row_reference (model, CAMEL_STORE (service), feed_id); + if (row) { + GtkTreePath *path; + GtkTreeIter iter; + + path = gtk_tree_row_reference_get_path (row); + gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path); + gtk_tree_path_free (path); + + e_rss_update_custom_icon (store_summary, feed_id, model, &iter); + } + } + + g_clear_object (&service); +} + +static void +e_rss_folder_custom_icon_cb (EMFolderTreeModel *model, + GtkTreeIter *iter, + CamelStore *store, + const gchar *full_name, + ERssFolderTreeModelExtension *extension) +{ + CamelRssStoreSummary *store_summary = NULL; + const gchar *uid = camel_service_get_uid (CAMEL_SERVICE (store)); + + g_return_if_fail (extension != NULL); + + if (g_strcmp0 (uid, "rss") != 0 || !full_name) + return; + + if (g_strcmp0 (full_name, CAMEL_VJUNK_NAME) == 0 || + g_strcmp0 (full_name, CAMEL_VTRASH_NAME) == 0) + return; + + g_object_get (store, "summary", &store_summary, NULL); + + if (!store_summary) + return; + + if (!extension->listens_feed_changed) { + extension->listens_feed_changed = TRUE; + + g_signal_connect_object (store_summary, "feed-changed", + G_CALLBACK (e_rss_folder_custom_icon_feed_changed_cb), model, 0); + } + + e_rss_update_custom_icon (store_summary, full_name, model, iter); + + g_clear_object (&store_summary); +} + +static gint +e_rss_compare_folders_cb (EMFolderTreeModel *model, + const gchar *store_uid, + GtkTreeIter *iter1, + GtkTreeIter *iter2) +{ + gint rv = -2; + + /* Junk/Trash as the last, to not mix them with feed folders */ + if (g_strcmp0 (store_uid, "rss") == 0) { + gboolean a_is_vfolder, b_is_vfolder; + guint32 flags_a, flags_b; + + gtk_tree_model_get ( + GTK_TREE_MODEL (model), iter1, + COL_UINT_FLAGS, &flags_a, + -1); + + gtk_tree_model_get ( + GTK_TREE_MODEL (model), iter2, + COL_UINT_FLAGS, &flags_b, + -1); + + a_is_vfolder = (flags_a & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_JUNK || + (flags_a & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_TRASH; + b_is_vfolder = (flags_b & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_JUNK || + (flags_b & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_TRASH; + + if ((a_is_vfolder || b_is_vfolder) && (!a_is_vfolder || !b_is_vfolder)) { + if (a_is_vfolder) + rv = 1; + else + rv = -1; + } + } + + return rv; +} + +static void +e_rss_folder_tree_model_extension_constructed (GObject *object) +{ + static gboolean icon_dir_registered = FALSE; + + /* Chain up to parent's method */ + G_OBJECT_CLASS (e_rss_folder_tree_model_extension_parent_class)->constructed (object); + + g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "folder-custom-icon", + G_CALLBACK (e_rss_folder_custom_icon_cb), object, 0); + + g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "compare-folders", + G_CALLBACK (e_rss_compare_folders_cb), NULL, 0); + + if (!icon_dir_registered) { + gchar *icon_dir; + + icon_dir_registered = TRUE; + + icon_dir = g_build_filename (e_get_user_data_dir (), "mail", "rss", NULL); + + gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (), icon_dir); + + g_free (icon_dir); + } +} + +static void +e_rss_folder_tree_model_extension_class_init (ERssFolderTreeModelExtensionClass *klass) +{ + GObjectClass *object_class; + EExtensionClass *extension_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->constructed = e_rss_folder_tree_model_extension_constructed; + + extension_class = E_EXTENSION_CLASS (klass); + extension_class->extensible_type = EM_TYPE_FOLDER_TREE_MODEL; +} + +static void +e_rss_folder_tree_model_extension_class_finalize (ERssFolderTreeModelExtensionClass *klass) +{ +} + +static void +e_rss_folder_tree_model_extension_init (ERssFolderTreeModelExtension *extension) +{ +} + +void +e_rss_folder_tree_model_extension_type_register (GTypeModule *type_module) +{ + e_rss_folder_tree_model_extension_register_type (type_module); +} diff --git a/src/modules/rss/evolution/e-rss-preferences.c b/src/modules/rss/evolution/e-rss-preferences.c new file mode 100644 index 0000000000..8543b0756c --- /dev/null +++ b/src/modules/rss/evolution/e-rss-preferences.c @@ -0,0 +1,1437 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include + +#include "e-util/e-util.h" +#include "shell/e-shell.h" +#include "camel-rss-store-summary.h" +#include "e-rss-parser.h" + +#include "e-rss-preferences.h" + +enum { + COLUMN_STRING_ID = 0, + COLUMN_STRING_NAME, + COLUMN_STRING_HREF, + COLUMN_STRING_CONTENT_TYPE, + COLUMN_STRING_DESCRIPTION, + COLUMN_PIXBUF_ICON, + N_COLUMNS +}; + +static const gchar * +e_rss_preferences_content_type_to_string (CamelRssContentType content_type) +{ + switch (content_type) { + case CAMEL_RSS_CONTENT_TYPE_HTML: + break; + case CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT: + return "text"; + case CAMEL_RSS_CONTENT_TYPE_MARKDOWN: + return "markdown"; + } + + return "html"; +} + +static const gchar * +e_rss_preferences_content_type_to_locale_string (CamelRssContentType content_type) +{ + switch (content_type) { + case CAMEL_RSS_CONTENT_TYPE_HTML: + break; + case CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT: + return _("Plain Text"); + case CAMEL_RSS_CONTENT_TYPE_MARKDOWN: + return _("Markdown"); + } + + return _("HTML"); +} + +static CamelRssContentType +e_rss_preferences_content_type_from_string (const gchar *str) +{ + if (g_strcmp0 (str, "text") == 0) + return CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT; + + if (g_strcmp0 (str, "markdown") == 0) + return CAMEL_RSS_CONTENT_TYPE_MARKDOWN; + + return CAMEL_RSS_CONTENT_TYPE_HTML; +} + +static CamelService * +e_rss_preferences_ref_store (EShell *shell) +{ + EShellBackend *shell_backend; + CamelSession *session = NULL; + CamelService *service; + + g_return_val_if_fail (E_IS_SHELL (shell), NULL); + + shell_backend = e_shell_get_backend_by_name (shell, "mail"); + if (!shell_backend) + return NULL; + + g_object_get (G_OBJECT (shell_backend), "session", &session, NULL); + if (!session) + return NULL; + + service = camel_session_ref_service (session, "rss"); + + g_clear_object (&session); + + return service; +} + +static gchar * +e_rss_preferences_describe_feed (const gchar *href, + const gchar *name) +{ + return g_markup_printf_escaped ("%s\n%s", name, href); +} + +static GdkPixbuf * +e_rss_preferences_create_icon_pixbuf (const gchar *icon_filename) +{ + GdkPixbuf *pixbuf = NULL; + + if (icon_filename && *icon_filename) { + GError *error = NULL; + + pixbuf = gdk_pixbuf_new_from_file (icon_filename, &error); + + if (!pixbuf) + g_warning ("%s: Failed to load feed icon '%s': %s", G_STRFUNC, icon_filename, error ? error->message : "Unknown error"); + + g_clear_error (&error); + } + + if (!pixbuf) + pixbuf = e_icon_factory_get_icon ("rss", GTK_ICON_SIZE_DIALOG); + + return pixbuf; +} + +static void +e_rss_preferences_add_feed (GtkListStore *list_store, + CamelRssStoreSummary *store_summary, + const gchar *id) +{ + const gchar *href, *display_name, *icon_filename; + CamelRssContentType content_type; + gchar *description; + GdkPixbuf *pixbuf; + GtkTreeIter iter; + + href = camel_rss_store_summary_get_href (store_summary, id); + display_name = camel_rss_store_summary_get_display_name (store_summary, id); + content_type = camel_rss_store_summary_get_content_type (store_summary, id); + description = e_rss_preferences_describe_feed (href, display_name); + icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, id); + pixbuf = e_rss_preferences_create_icon_pixbuf (icon_filename); + + gtk_list_store_append (list_store, &iter); + gtk_list_store_set (list_store, &iter, + COLUMN_STRING_ID, id, + COLUMN_STRING_NAME, display_name, + COLUMN_STRING_HREF, href, + COLUMN_STRING_CONTENT_TYPE, e_rss_preferences_content_type_to_locale_string (content_type), + COLUMN_STRING_DESCRIPTION, description, + COLUMN_PIXBUF_ICON, pixbuf, + -1); + + g_clear_object (&pixbuf); + g_free (description); +} + +static void +e_rss_preferences_fill_list_store (GtkListStore *list_store, + CamelRssStoreSummary *store_summary) +{ + GSList *feeds, *link; + + gtk_list_store_clear (list_store); + + feeds = camel_rss_store_summary_dup_feeds (store_summary); + + for (link = feeds; link; link = g_slist_next (link)) { + const gchar *id = link->data; + + e_rss_preferences_add_feed (list_store, store_summary, id); + } + + g_slist_free_full (feeds, g_free); +} + +static void +e_rss_preferences_source_written_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + + if (!e_source_write_finish (E_SOURCE (source_object), result, &error)) + g_warning ("%s: Failed to save RSS changes: %s", G_STRFUNC, error ? error->message : "Unknown error"); + + g_clear_error (&error); +} + +static void +e_rss_preferences_source_changed_cb (ESource *source) +{ + e_source_write (source, NULL, e_rss_preferences_source_written_cb, NULL); +} + +static void +e_rss_preferences_three_state_toggled_cb (GtkToggleButton *widget, + gpointer user_data) +{ + gulong *phandler_id = user_data; + + g_return_if_fail (GTK_IS_TOGGLE_BUTTON (widget)); + g_return_if_fail (phandler_id != NULL); + + g_signal_handler_block (widget, *phandler_id); + + if (gtk_toggle_button_get_inconsistent (widget) && + gtk_toggle_button_get_active (widget)) { + gtk_toggle_button_set_active (widget, FALSE); + gtk_toggle_button_set_inconsistent (widget, FALSE); + } else if (!gtk_toggle_button_get_active (widget)) { + gtk_toggle_button_set_inconsistent (widget, TRUE); + gtk_toggle_button_set_active (widget, FALSE); + } + + g_signal_handler_unblock (widget, *phandler_id); +} + +static GtkWidget * +e_rss_preferences_new_three_state_check (const gchar *label) +{ + GtkWidget *widget; + gulong *phandler_id; + + widget = gtk_check_button_new_with_mnemonic (label); + + g_object_set (widget, + "inconsistent", TRUE, + "active", FALSE, + "visible", TRUE, + NULL); + + phandler_id = g_new (gulong, 1); + + *phandler_id = g_signal_connect_data (widget, "toggled", + G_CALLBACK (e_rss_preferences_three_state_toggled_cb), + phandler_id, (GClosureNotify) g_free, 0); + + return widget; +} + +static CamelThreeState +e_rss_preferences_three_state_from_widget (GtkToggleButton *button) +{ + g_return_val_if_fail (GTK_IS_TOGGLE_BUTTON (button), CAMEL_THREE_STATE_INCONSISTENT); + + if (gtk_toggle_button_get_inconsistent (button)) + return CAMEL_THREE_STATE_INCONSISTENT; + + if (gtk_toggle_button_get_active (button)) + return CAMEL_THREE_STATE_ON; + + return CAMEL_THREE_STATE_OFF; +} + +static void +e_rss_preferences_three_state_to_widget (GtkToggleButton *button, + CamelThreeState state) +{ + g_return_if_fail (GTK_IS_TOGGLE_BUTTON (button)); + + g_signal_handlers_block_matched (button, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, e_rss_preferences_three_state_toggled_cb, NULL); + + if (state == CAMEL_THREE_STATE_INCONSISTENT) { + gtk_toggle_button_set_active (button, FALSE); + gtk_toggle_button_set_inconsistent (button, TRUE); + } else { + gtk_toggle_button_set_inconsistent (button, FALSE); + gtk_toggle_button_set_active (button, state == CAMEL_THREE_STATE_ON); + } + + g_signal_handlers_unblock_matched (button, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, e_rss_preferences_three_state_toggled_cb, NULL); +} + +typedef struct _FolderOpts { + CamelThreeState complete_articles; + CamelThreeState feed_enclosures; +} FolderOpts; + +static void +e_rss_properties_got_folder_to_save_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + FolderOpts *fo = user_data; + CamelFolder *folder; + GError *error = NULL; + + folder = camel_store_get_folder_finish (CAMEL_STORE (source_object), result, &error); + + if (folder) { + g_object_set (folder, + "complete-articles", fo->complete_articles, + "feed-enclosures", fo->feed_enclosures, + NULL); + + g_object_unref (folder); + } else { + g_warning ("%s: Failed to get folder: %s", G_STRFUNC, error ? error->message : "Unknown error"); + } + + g_slice_free (FolderOpts, fo); +} + +typedef struct _PopoverData { + gchar *id; /* can be NULL */ + GtkEntry *href; + GtkWidget *fetch_button; + GtkEntry *name; + GtkButton *icon_button; + GtkImage *icon_image; + GtkComboBox *content_type; + GtkToggleButton *complete_articles; + GtkToggleButton *feed_enclosures; + GtkWidget *save_button; + gchar *icon_filename; + EActivityBar *activity_bar; + EActivity *activity; +} PopoverData; + +static void +popover_data_cancel_activity (PopoverData *pd) +{ + GCancellable *cancellable; + + if (!pd || !pd->activity) + return; + + cancellable = e_activity_get_cancellable (pd->activity); + g_cancellable_cancel (cancellable); + + e_activity_set_state (pd->activity, E_ACTIVITY_CANCELLED); + + g_clear_object (&pd->activity); +} + +static void +popover_data_free (gpointer ptr) +{ + PopoverData *pd = ptr; + + if (pd) { + popover_data_cancel_activity (pd); + + g_free (pd->id); + g_free (pd->icon_filename); + g_free (pd); + } +} + +static void +e_rss_preferences_entry_changed_cb (GtkEntry *entry, + gpointer user_data) +{ + GObject *popover = user_data; + PopoverData *pd; + const gchar *text; + gboolean sensitive; + + pd = g_object_get_data (popover, "e-rss-popover-data"); + + text = gtk_entry_get_text (pd->href); + sensitive = text && *text; + gtk_widget_set_sensitive (pd->fetch_button, sensitive); + + if (sensitive) { + text = gtk_entry_get_text (pd->name); + sensitive = text && *text; + } + + gtk_widget_set_sensitive (pd->save_button, sensitive); +} + +static void +e_rss_preferences_feed_icon_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GObject *popover = user_data; + GBytes *bytes; + GError *error = NULL; + + bytes = soup_session_send_and_read_finish (SOUP_SESSION (source_object), result, &error); + + if (bytes) { + PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data"); + SoupMessage *message = soup_session_get_async_result_message (SOUP_SESSION (source_object), result); + gboolean success = !error && g_bytes_get_size (bytes) > 0 && message && + SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message)); + + if (success) { + gchar *tmp_file; + + tmp_file = e_mktemp ("rss-feed-XXXXXX.png"); + success = g_file_set_contents (tmp_file, (const gchar *) g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), &error); + + if (success) { + gtk_image_set_from_file (pd->icon_image, tmp_file); + g_clear_pointer (&pd->icon_filename, g_free); + pd->icon_filename = g_steal_pointer (&tmp_file); + } + + g_free (tmp_file); + } + + if (success) { + e_activity_set_state (pd->activity, E_ACTIVITY_COMPLETED); + g_clear_object (&pd->activity); + } + } + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data"); + gchar *message; + + message = g_strdup_printf (_("Failed to fetch feed icon: %s"), error->message); + + e_activity_set_state (pd->activity, E_ACTIVITY_WAITING); + e_activity_set_text (pd->activity, message); + + g_free (message); + } + + g_clear_pointer (&bytes, g_bytes_unref); + g_clear_error (&error); +} + +static void +e_rss_preferences_feed_info_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GObject *popover = user_data; + GBytes *bytes; + GError *error = NULL; + + bytes = soup_session_send_and_read_finish (SOUP_SESSION (source_object), result, &error); + + if (bytes) { + PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data"); + GCancellable *cancellable = e_activity_get_cancellable (pd->activity); + SoupMessage *message = soup_session_get_async_result_message (SOUP_SESSION (source_object), result); + gboolean success = !error && g_bytes_get_size (bytes) > 0 && message && + SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message)); + + if (success) { + gchar *link = NULL, *alt_link = NULL, *title = NULL, *icon = NULL; + + success = e_rss_parser_parse ((const gchar *) g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), &link, &alt_link, &title, &icon, NULL); + if (success) { + if ((link && camel_strstrcase (link, "gitlab")) || + (alt_link && camel_strstrcase (alt_link, "gitlab"))) + gtk_combo_box_set_active_id (pd->content_type, e_rss_preferences_content_type_to_string (CAMEL_RSS_CONTENT_TYPE_MARKDOWN)); + else + gtk_combo_box_set_active_id (pd->content_type, e_rss_preferences_content_type_to_string (CAMEL_RSS_CONTENT_TYPE_HTML)); + + if (title && *title) + gtk_entry_set_text (pd->name, title); + + if (icon && *icon) { + SoupMessage *message; + + e_activity_set_text (pd->activity, _("Fetching feed icon…")); + + message = soup_message_new (SOUP_METHOD_GET, icon); + if (message) { + soup_session_send_and_read_async (SOUP_SESSION (source_object), message, G_PRIORITY_DEFAULT, cancellable, + e_rss_preferences_feed_icon_ready_cb, popover); + + g_object_unref (message); + + /* Not as a problem, but as a flag to not complete the activity */ + success = FALSE; + } + } + } else { + g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Failed to read feed information.")); + } + + g_free (link); + g_free (alt_link); + g_free (title); + g_free (icon); + } + + if (success) { + e_activity_set_state (pd->activity, E_ACTIVITY_COMPLETED); + g_clear_object (&pd->activity); + } + } + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data"); + gchar *message; + + message = g_strdup_printf (_("Failed to fetch feed information: %s"), error->message); + + e_activity_set_state (pd->activity, E_ACTIVITY_WAITING); + e_activity_set_text (pd->activity, message); + + g_free (message); + } + + g_clear_pointer (&bytes, g_bytes_unref); + g_clear_error (&error); +} + +static void +e_rss_preferences_fetch_clicked_cb (GtkWidget *button, + gpointer user_data) +{ + GObject *popover = user_data; + SoupSession *session; + SoupMessage *message; + GCancellable *cancellable; + PopoverData *pd; + + pd = g_object_get_data (popover, "e-rss-popover-data"); + cancellable = g_cancellable_new (); + + popover_data_cancel_activity (pd); + + pd->activity = e_activity_new (); + e_activity_set_cancellable (pd->activity, cancellable); + e_activity_set_state (pd->activity, E_ACTIVITY_RUNNING); + e_activity_set_text (pd->activity, _("Fetching feed information…")); + e_activity_bar_set_activity (pd->activity_bar, pd->activity); + + message = soup_message_new (SOUP_METHOD_GET, gtk_entry_get_text (pd->href)); + if (!message) { + e_activity_set_text (pd->activity, _("Invalid Feed URL")); + e_activity_set_state (pd->activity, E_ACTIVITY_WAITING); + g_clear_object (&cancellable); + + return; + } + + session = soup_session_new_with_options ( + "timeout", 30, + "user-agent", "Evolution/" VERSION, + NULL); + + if (camel_debug ("rss")) { + SoupLogger *logger; + + logger = soup_logger_new (SOUP_LOGGER_LOG_BODY); + soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger)); + g_object_unref (logger); + } + + soup_session_send_and_read_async (session, message, G_PRIORITY_DEFAULT, cancellable, + e_rss_preferences_feed_info_ready_cb, popover); + + g_clear_object (&message); + g_clear_object (&session); + g_clear_object (&cancellable); +} + +static void +e_rss_preferences_icon_clicked_cb (GtkWidget *button, + gpointer user_data) +{ + GObject *popover = user_data; + PopoverData *pd; + GtkWidget *dialog; + GtkWindow *parent; + GFile *file; + + pd = g_object_get_data (popover, "e-rss-popover-data"); + + dialog = gtk_widget_get_toplevel (button); + parent = GTK_IS_WINDOW (dialog) ? GTK_WINDOW (dialog) : NULL; + + dialog = e_image_chooser_dialog_new (_("Choose Feed Image"), parent); + file = e_image_chooser_dialog_run (E_IMAGE_CHOOSER_DIALOG (dialog)); + + g_clear_pointer (&pd->icon_filename, g_free); + + if (G_IS_FILE (file)) { + pd->icon_filename = g_file_get_path (file); + gtk_image_set_from_file (pd->icon_image, pd->icon_filename); + } else { + gtk_image_set_from_icon_name (pd->icon_image, "rss", GTK_ICON_SIZE_DIALOG); + } + + gtk_widget_destroy (dialog); +} + +/* Copy icon to the private directory */ +static gchar * +e_rss_preferences_maybe_copy_icon (const gchar *feed_id, + const gchar *icon_filename, + const gchar *user_data_dir) +{ + gchar *basename, *filename; + GFile *src, *des; + const gchar *ext; + GError *error = NULL; + + if (!icon_filename || !*icon_filename || !user_data_dir || !*user_data_dir || + g_str_has_prefix (icon_filename, user_data_dir)) + return NULL; + + basename = g_path_get_basename (icon_filename); + if (basename && *basename && (*basename == G_DIR_SEPARATOR || *basename == '.')) { + g_free (basename); + return NULL; + } + + ext = strrchr (basename, '.'); + if (!ext || !ext[1]) + ext = ".png"; + + filename = g_strconcat (user_data_dir, G_DIR_SEPARATOR_S, feed_id, ext, NULL); + + src = g_file_new_for_path (icon_filename); + des = g_file_new_for_path (filename); + + if (g_file_copy (src, des, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) + gtk_icon_theme_rescan_if_needed (gtk_icon_theme_get_default ()); + else + g_warning ("Failed to copy icon file '%s' to '%s': %s", icon_filename, filename, error ? error->message : "Unknown error"); + + g_clear_error (&error); + g_clear_object (&src); + g_clear_object (&des); + + g_free (basename); + + return filename; +} + +static void +e_rss_preferences_save_clicked_cb (GtkWidget *button, + gpointer user_data) +{ + GObject *popover = user_data; + CamelService *service; + CamelRssStoreSummary *store_summary = NULL; + CamelRssContentType content_type; + FolderOpts *fo; + gchar *icon_filename; + const gchar *user_data_dir; + PopoverData *pd; + GError *error = NULL; + + pd = g_object_get_data (popover, "e-rss-popover-data"); + + service = e_rss_preferences_ref_store (e_shell_get_default ()); + if (!service) { + g_warn_if_reached (); + return; + } + + g_object_get (service, "summary", &store_summary, NULL); + + if (!store_summary) { + g_clear_object (&service); + g_warn_if_reached (); + return; + } + + user_data_dir = camel_service_get_user_data_dir (service); + icon_filename = pd->icon_filename; + content_type = e_rss_preferences_content_type_from_string (gtk_combo_box_get_active_id (pd->content_type)); + + if (pd->id) { + const gchar *display_name; + gchar *old_display_name; + gchar *real_icon_filename; + + old_display_name = g_strdup (camel_rss_store_summary_get_display_name (store_summary, pd->id)); + display_name = gtk_entry_get_text (pd->name); + + real_icon_filename = e_rss_preferences_maybe_copy_icon (pd->id, icon_filename, user_data_dir); + + camel_rss_store_summary_set_display_name (store_summary, pd->id, display_name); + camel_rss_store_summary_set_icon_filename (store_summary, pd->id, real_icon_filename ? real_icon_filename : icon_filename); + camel_rss_store_summary_set_content_type (store_summary, pd->id, content_type); + + if (camel_rss_store_summary_save (store_summary, &error) && + g_strcmp0 (old_display_name, display_name) != 0) { + CamelFolderInfo *fi; + + fi = camel_rss_store_summary_dup_folder_info (store_summary, pd->id); + + camel_store_folder_renamed (CAMEL_STORE (service), pd->id, fi); + + camel_folder_info_free (fi); + } + + g_free (real_icon_filename); + g_free (old_display_name); + } else { + const gchar *new_id; + + new_id = camel_rss_store_summary_add (store_summary, + gtk_entry_get_text (pd->href), + gtk_entry_get_text (pd->name), + icon_filename, + content_type); + + if (new_id) { + gchar *real_icon_filename; + + pd->id = g_strdup (new_id); + + real_icon_filename = e_rss_preferences_maybe_copy_icon (pd->id, icon_filename, user_data_dir); + if (real_icon_filename) { + camel_rss_store_summary_set_icon_filename (store_summary, pd->id, real_icon_filename); + g_free (real_icon_filename); + } + + if (camel_rss_store_summary_save (store_summary, &error)) { + CamelFolderInfo *fi; + + fi = camel_rss_store_summary_dup_folder_info (store_summary, pd->id); + + camel_store_folder_created (CAMEL_STORE (service), fi); + + camel_folder_info_free (fi); + } + } + } + + fo = g_slice_new0 (FolderOpts); + fo->complete_articles = e_rss_preferences_three_state_from_widget (pd->complete_articles); + fo->feed_enclosures = e_rss_preferences_three_state_from_widget (pd->feed_enclosures); + + camel_store_get_folder (CAMEL_STORE (service), pd->id, CAMEL_STORE_FOLDER_NONE, G_PRIORITY_DEFAULT, NULL, + e_rss_properties_got_folder_to_save_cb, fo); + + if (error) { + g_warning ("Failed to store RSS settings: %s", error->message); + g_clear_error (&error); + } + + g_clear_object (&store_summary); + g_clear_object (&service); + + gtk_widget_hide (GTK_WIDGET (popover)); +} + +static GtkPopover * +e_rss_preferences_get_popover (GtkWidget *parent, + GtkTreeView *tree_view, + const gchar *id, + PopoverData **out_pd) +{ + GtkPopover *popover; + PopoverData *pd; + GtkGrid *grid; + GtkWidget *widget, *label; + + popover = g_object_get_data (G_OBJECT (tree_view), "e-rss-popover"); + + if (popover) { + pd = g_object_get_data (G_OBJECT (popover), "e-rss-popover-data"); + gtk_popover_set_relative_to (popover, parent); + g_clear_pointer (&pd->id, g_free); + g_clear_pointer (&pd->icon_filename, g_free); + pd->id = g_strdup (id); + + *out_pd = pd; + + return popover; + } + + pd = g_new0 (PopoverData, 1); + pd->id = g_strdup (id); + + popover = GTK_POPOVER (gtk_popover_new (parent)); + + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_column_spacing (grid, 6); + gtk_grid_set_row_spacing (grid, 6); + + widget = gtk_button_new (); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_START, + "valign", GTK_ALIGN_START, + NULL); + gtk_grid_attach (grid, widget, 0, 0, 1, 3); + pd->icon_button = GTK_BUTTON (widget); + + widget = gtk_image_new_from_icon_name ("rss", GTK_ICON_SIZE_DIALOG); + gtk_container_add (GTK_CONTAINER (pd->icon_button), widget); + pd->icon_image = GTK_IMAGE (widget); + + widget = gtk_label_new_with_mnemonic (_("Feed _URL:")); + gtk_widget_set_halign (widget, GTK_ALIGN_END); + gtk_grid_attach (grid, widget, 1, 0, 1, 1); + label = widget; + + widget = gtk_entry_new (); + gtk_widget_set_size_request (widget, 250, -1); + gtk_widget_set_halign (widget, GTK_ALIGN_FILL); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_grid_attach (grid, widget, 2, 0, 1, 1); + pd->href = GTK_ENTRY (widget); + + widget = gtk_button_new_with_mnemonic (_("_Fetch")); + gtk_grid_attach (grid, widget, 3, 0, 1, 1); + pd->fetch_button = widget; + + widget = gtk_label_new_with_mnemonic (_("_Name:")); + gtk_widget_set_halign (widget, GTK_ALIGN_END); + gtk_grid_attach (grid, widget, 1, 1, 1, 1); + label = widget; + + widget = gtk_entry_new (); + gtk_widget_set_halign (widget, GTK_ALIGN_FILL); + gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_grid_attach (grid, widget, 2, 1, 2, 1); + pd->name = GTK_ENTRY (widget); + + widget = gtk_label_new_with_mnemonic (_("C_ontent:")); + gtk_widget_set_halign (widget, GTK_ALIGN_END); + gtk_grid_attach (grid, widget, 1, 2, 1, 1); + label = widget; + + widget = gtk_combo_box_text_new (); + gtk_widget_set_size_request (widget, 250, -1); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "html", _("HTML")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "text", _("Plain Text")); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "markdown", _("Markdown")); + gtk_grid_attach (grid, widget, 2, 2, 2, 1); + pd->content_type = GTK_COMBO_BOX (widget); + + widget = e_rss_preferences_new_three_state_check (_("_Download complete articles")); + gtk_grid_attach (grid, widget, 2, 3, 2, 1); + pd->complete_articles = GTK_TOGGLE_BUTTON (widget); + + widget = e_rss_preferences_new_three_state_check (_("Download feed _enclosures")); + gtk_grid_attach (grid, widget, 2, 4, 2, 1); + pd->feed_enclosures = GTK_TOGGLE_BUTTON (widget); + + widget = gtk_button_new_with_mnemonic (_("_Save")); + gtk_widget_set_halign (widget, GTK_ALIGN_END); + gtk_grid_attach (grid, widget, 1, 5, 3, 1); + pd->save_button = widget; + + gtk_widget_show_all (GTK_WIDGET (grid)); + + widget = e_activity_bar_new (); + gtk_grid_attach (grid, widget, 0, 6, 4, 1); + pd->activity_bar = E_ACTIVITY_BAR (widget); + + gtk_popover_set_position (popover, GTK_POS_BOTTOM); + gtk_container_add (GTK_CONTAINER (popover), GTK_WIDGET (grid)); + gtk_container_set_border_width (GTK_CONTAINER (popover), 6); + + g_object_set_data_full (G_OBJECT (popover), "e-rss-popover-data", pd, popover_data_free); + g_object_set_data_full (G_OBJECT (tree_view), "e-rss-popover", g_object_ref_sink (popover), g_object_unref); + + g_signal_connect_object (pd->href, "changed", + G_CALLBACK (e_rss_preferences_entry_changed_cb), popover, 0); + + g_signal_connect_object (pd->name, "changed", + G_CALLBACK (e_rss_preferences_entry_changed_cb), popover, 0); + + g_signal_connect_object (pd->fetch_button, "clicked", + G_CALLBACK (e_rss_preferences_fetch_clicked_cb), popover, 0); + + g_signal_connect_object (pd->icon_button, "clicked", + G_CALLBACK (e_rss_preferences_icon_clicked_cb), popover, 0); + + g_signal_connect_object (pd->save_button, "clicked", + G_CALLBACK (e_rss_preferences_save_clicked_cb), popover, 0); + + e_rss_preferences_entry_changed_cb (pd->href, popover); + + *out_pd = pd; + + return popover; +} + +static void +e_rss_preferences_add_clicked_cb (GtkWidget *button, + GtkTreeView *tree_view) +{ + GtkPopover *popover; + PopoverData *pd = NULL; + + popover = e_rss_preferences_get_popover (button, tree_view, NULL, &pd); + + gtk_entry_set_text (pd->href, ""); + gtk_entry_set_text (pd->name, ""); + gtk_image_set_from_icon_name (pd->icon_image, "rss", GTK_ICON_SIZE_DIALOG); + gtk_combo_box_set_active_id (pd->content_type, "html"); + e_rss_preferences_three_state_to_widget (pd->complete_articles, CAMEL_THREE_STATE_INCONSISTENT); + e_rss_preferences_three_state_to_widget (pd->feed_enclosures, CAMEL_THREE_STATE_INCONSISTENT); + g_clear_pointer (&pd->icon_filename, g_free); + g_clear_pointer (&pd->id, g_free); + + gtk_widget_show (GTK_WIDGET (popover)); +} + +static gchar * +e_rss_preferences_dup_selected_id (GtkTreeView *tree_view, + CamelStore **out_store) +{ + CamelService *service; + GtkTreeSelection *selection; + GtkTreeModel *model = NULL; + GtkTreeIter iter; + gchar *id = NULL; + + if (out_store) + *out_store = NULL; + + selection = gtk_tree_view_get_selection (tree_view); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) + return NULL; + + gtk_tree_model_get (model, &iter, + COLUMN_STRING_ID, &id, + -1); + + if (!id) + return NULL; + + service = e_rss_preferences_ref_store (e_shell_get_default ()); + if (!service) { + g_warn_if_reached (); + g_free (id); + return NULL; + } + + if (out_store) + *out_store = CAMEL_STORE (service); + else + g_object_unref (service); + + return id; +} + +static void +e_rss_properties_got_folder_to_edit_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GtkTreeView *tree_view = user_data; + CamelFolder *folder; + GError *error = NULL; + + folder = camel_store_get_folder_finish (CAMEL_STORE (source_object), result, &error); + + if (folder) { + CamelRssStoreSummary *store_summary = NULL; + CamelThreeState state = CAMEL_THREE_STATE_INCONSISTENT; + GtkPopover *popover; + PopoverData *pd = NULL; + const gchar *icon_filename, *id; + + id = camel_folder_get_full_name (folder); + g_object_get (source_object, "summary", &store_summary, NULL); + popover = g_object_get_data (G_OBJECT (tree_view), "e-rss-popover"); + g_warn_if_fail (popover != NULL); + pd = g_object_get_data (G_OBJECT (popover), "e-rss-popover-data"); + g_warn_if_fail (pd != NULL); + g_warn_if_fail (g_strcmp0 (id, pd->id) == 0); + + icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, id); + + gtk_entry_set_text (pd->href, camel_rss_store_summary_get_href (store_summary, id)); + gtk_entry_set_text (pd->name, camel_rss_store_summary_get_display_name (store_summary, id)); + + if (icon_filename && g_file_test (icon_filename, G_FILE_TEST_IS_REGULAR)) + gtk_image_set_from_file (pd->icon_image, icon_filename); + else + gtk_image_set_from_icon_name (pd->icon_image, "rss", GTK_ICON_SIZE_DIALOG); + + gtk_combo_box_set_active_id (pd->content_type, e_rss_preferences_content_type_to_string ( + camel_rss_store_summary_get_content_type (store_summary, id))); + + g_clear_pointer (&pd->icon_filename, g_free); + pd->icon_filename = g_strdup (icon_filename); + + g_object_get (folder, "complete-articles", &state, NULL); + e_rss_preferences_three_state_to_widget (pd->complete_articles, state); + + g_object_get (folder, "feed-enclosures", &state, NULL); + e_rss_preferences_three_state_to_widget (pd->feed_enclosures, state); + + gtk_widget_show (GTK_WIDGET (popover)); + + g_clear_object (&store_summary); + g_object_unref (folder); + } else { + g_warning ("%s: Failed to get folder: %s", G_STRFUNC, error ? error->message : "Unknown error"); + } + + g_clear_object (&tree_view); +} + +static void +e_rss_preferences_edit_clicked_cb (GtkWidget *button, + GtkTreeView *tree_view) +{ + CamelStore *store = NULL; + gchar *id; + + id = e_rss_preferences_dup_selected_id (tree_view, &store); + if (id) { + PopoverData *pd = NULL; + + /* prepare the popover */ + g_warn_if_fail (e_rss_preferences_get_popover (button, tree_view, id, &pd) != NULL); + + camel_store_get_folder (store, id, CAMEL_STORE_FOLDER_NONE, G_PRIORITY_DEFAULT, NULL, + e_rss_properties_got_folder_to_edit_cb, g_object_ref (tree_view)); + } + + g_clear_object (&store); + g_free (id); +} + +static void +e_rss_preferences_delete_done_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + + if (!camel_store_delete_folder_finish (CAMEL_STORE (source_object), result, &error)) + g_warning ("%s: Failed to delete folder: %s", G_STRFUNC, error ? error->message : "Unknown error"); + + g_clear_error (&error); +} + +static void +e_rss_preferences_remove_clicked_cb (GtkButton *button, + GtkTreeView *tree_view) +{ + CamelStore *store = NULL; + gchar *id; + + id = e_rss_preferences_dup_selected_id (tree_view, &store); + if (id) + camel_store_delete_folder (store, id, G_PRIORITY_DEFAULT, NULL, e_rss_preferences_delete_done_cb, NULL); + + g_clear_object (&store); + g_free (id); +} + +static void +e_rss_pereferences_selection_changed_cb (GtkTreeSelection *selection, + GtkWidget *button) +{ + gtk_widget_set_sensitive (button, gtk_tree_selection_get_selected (selection, NULL, NULL)); +} + +static void +e_rss_preferences_map_cb (GtkTreeView *tree_view, + gpointer user_data) +{ + CamelRssStoreSummary *store_summary = user_data; + GtkTreeModel *model; + + model = gtk_tree_view_get_model (tree_view); + + e_rss_preferences_fill_list_store (GTK_LIST_STORE (model), store_summary); +} + +static void +e_rss_preferences_feed_changed_cb (CamelRssStoreSummary *store_summary, + const gchar *id, + gpointer user_data) +{ + GtkTreeView *tree_view = user_data; + GtkTreeIter iter; + GtkTreeModel *model; + GtkListStore *list_store; + gboolean found; + + if (!gtk_widget_get_mapped (GTK_WIDGET (tree_view))) + return; + + model = gtk_tree_view_get_model (tree_view); + list_store = GTK_LIST_STORE (model); + + found = gtk_tree_model_get_iter_first (model, &iter); + while (found) { + gchar *stored_id = NULL; + + gtk_tree_model_get (model, &iter, + COLUMN_STRING_ID, &stored_id, + -1); + + found = g_strcmp0 (id, stored_id) == 0; + + g_free (stored_id); + + if (found) + break; + + found = gtk_tree_model_iter_next (model, &iter); + } + + if (found) { + if (camel_rss_store_summary_contains (store_summary, id)) { + const gchar *href, *display_name, *icon_filename; + CamelRssContentType content_type; + gchar *description; + GdkPixbuf *pixbuf; + + href = camel_rss_store_summary_get_href (store_summary, id); + display_name = camel_rss_store_summary_get_display_name (store_summary, id); + content_type = camel_rss_store_summary_get_content_type (store_summary, id); + description = e_rss_preferences_describe_feed (href, display_name); + icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, id); + pixbuf = e_rss_preferences_create_icon_pixbuf (icon_filename); + + gtk_list_store_set (list_store, &iter, + COLUMN_STRING_NAME, display_name, + COLUMN_STRING_HREF, href, + COLUMN_STRING_CONTENT_TYPE, e_rss_preferences_content_type_to_locale_string (content_type), + COLUMN_STRING_DESCRIPTION, description, + COLUMN_PIXBUF_ICON, pixbuf, + -1); + + g_clear_object (&pixbuf); + g_free (description); + } else { + gtk_list_store_remove (list_store, &iter); + } + } else if (camel_rss_store_summary_contains (store_summary, id)) { + e_rss_preferences_add_feed (list_store, store_summary, id); + } +} + +static void +e_rss_preferences_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + gpointer user_data) +{ + GtkWidget *button = user_data; + + e_rss_preferences_edit_clicked_cb (button, tree_view); +} + +static GtkWidget * +e_rss_preferences_new (EPreferencesWindow *window) +{ + CamelService *service; + CamelSettings *settings; + CamelRssStoreSummary *store_summary = NULL; + EShell *shell; + ESource *source; + PangoAttrList *bold; + GtkGrid *grid; + GtkWidget *widget, *hbox, *spin, *scrolled_window, *button_box; + GtkListStore *list_store; + GtkTreeSelection *selection; + GtkTreeView *tree_view; + GtkTreeViewColumn *column; + GtkCellRenderer *cell_renderer; + gint row = 0; + + shell = e_preferences_window_get_shell (window); + service = e_rss_preferences_ref_store (shell); + if (!service) + return NULL; + + g_object_get (service, "summary", &store_summary, NULL); + + if (!store_summary) { + g_clear_object (&service); + g_warn_if_reached (); + return NULL; + } + + source = e_source_registry_ref_source (e_shell_get_registry (shell), "rss"); + if (source) { + /* Auto-save changes */ + g_signal_connect (source, "changed", + G_CALLBACK (e_rss_preferences_source_changed_cb), NULL); + g_clear_object (&source); + } else { + g_warn_if_reached (); + } + + settings = camel_service_ref_settings (service); + + bold = pango_attr_list_new (); + pango_attr_list_insert (bold, pango_attr_weight_new (PANGO_WEIGHT_BOLD)); + + grid = GTK_GRID (gtk_grid_new ()); + g_object_set (G_OBJECT (grid), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "border-width", 12, + NULL); + + widget = gtk_label_new (_("General")); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_START, + "hexpand", FALSE, + "attributes", bold, + NULL); + + gtk_grid_attach (grid, widget, 0, row, 2, 1); + row++; + + widget = gtk_check_button_new_with_mnemonic (_("_Download complete articles")); + g_object_set (G_OBJECT (widget), + "margin-start", 12, + NULL); + + e_binding_bind_property ( + settings, "complete-articles", + widget, "active", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + gtk_grid_attach (grid, widget, 0, row, 2, 1); + row++; + + widget = gtk_check_button_new_with_mnemonic (_("_Download feed enclosures")); + g_object_set (G_OBJECT (widget), + "margin-start", 12, + NULL); + + e_binding_bind_property ( + settings, "feed-enclosures", + widget, "active", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + gtk_grid_attach (grid, widget, 0, row, 2, 1); + row++; + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); + g_object_set (G_OBJECT (hbox), + "margin-start", 12, + NULL); + + /* Translators: This is part of "Do not download enclosures larger than [ nnn ] KB" */ + widget = gtk_check_button_new_with_mnemonic (_("Do not download enclosures larger than")); + gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0); + + spin = gtk_spin_button_new_with_range (1, 999999, 100); + gtk_box_pack_start (GTK_BOX (hbox), spin, FALSE, FALSE, 0); + + e_binding_bind_property ( + widget, "active", + spin, "sensitive", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + e_binding_bind_property ( + settings, "limit-feed-enclosure-size", + widget, "active", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + e_binding_bind_property ( + settings, "max-feed-enclosure-size", + spin, "value", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + + /* Translators: This is part of "Do not download enclosures larger than [ nnn ] KB" */ + widget = gtk_label_new (_("KB")); + gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0); + + gtk_grid_attach (grid, hbox, 0, row, 2, 1); + row++; + + widget = gtk_label_new (_("Feeds")); + g_object_set (G_OBJECT (widget), + "halign", GTK_ALIGN_START, + "hexpand", FALSE, + "attributes", bold, + NULL); + + gtk_grid_attach (grid, widget, 0, row, 2, 1); + row++; + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + g_object_set (G_OBJECT (scrolled_window), + "halign", GTK_ALIGN_FILL, + "hexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "margin-start", 12, + "hscrollbar-policy", GTK_POLICY_AUTOMATIC, + "vscrollbar-policy", GTK_POLICY_AUTOMATIC, + "shadow-type", GTK_SHADOW_IN, + NULL); + + list_store = gtk_list_store_new (N_COLUMNS, + G_TYPE_STRING, /* COLUMN_STRING_ID */ + G_TYPE_STRING, /* COLUMN_STRING_NAME */ + G_TYPE_STRING, /* COLUMN_STRING_HREF */ + G_TYPE_STRING, /* COLUMN_STRING_CONTENT_TYPE */ + G_TYPE_STRING, /* COLUMN_STRING_DESCRIPTION */ + GDK_TYPE_PIXBUF); /* COLUMN_PIXBUF_ICON */ + + widget = gtk_tree_view_new_with_model (GTK_TREE_MODEL (list_store)); + g_object_set (G_OBJECT (widget), + "hexpand", TRUE, + "halign", GTK_ALIGN_FILL, + "vexpand", TRUE, + "valign", GTK_ALIGN_FILL, + "visible", TRUE, + NULL); + g_object_unref (list_store); + gtk_container_add (GTK_CONTAINER (scrolled_window), widget); + + tree_view = GTK_TREE_VIEW (widget); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Name")); + gtk_tree_view_column_set_expand (column, TRUE); + + cell_renderer = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, cell_renderer, FALSE); + + gtk_tree_view_column_set_attributes (column, cell_renderer, + "pixbuf", COLUMN_PIXBUF_ICON, + NULL); + + cell_renderer = gtk_cell_renderer_text_new (); + g_object_set (cell_renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start (column, cell_renderer, FALSE); + + gtk_tree_view_column_set_attributes (column, cell_renderer, + "markup", COLUMN_STRING_DESCRIPTION, + NULL); + + gtk_tree_view_append_column (tree_view, column); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Content")); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED); + gtk_tree_view_column_set_fixed_width (column, 120); + gtk_tree_view_column_set_expand (column, FALSE); + + cell_renderer = gtk_cell_renderer_text_new (); + g_object_set (cell_renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start (column, cell_renderer, FALSE); + + gtk_tree_view_column_set_attributes (column, cell_renderer, + "text", COLUMN_STRING_CONTENT_TYPE, + NULL); + + gtk_tree_view_append_column (tree_view, column); + + g_signal_connect_object (tree_view, "map", + G_CALLBACK (e_rss_preferences_map_cb), store_summary, 0); + + g_signal_connect_object (store_summary, "feed-changed", + G_CALLBACK (e_rss_preferences_feed_changed_cb), tree_view, 0); + + selection = gtk_tree_view_get_selection (tree_view); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); + + button_box = gtk_button_box_new (GTK_ORIENTATION_VERTICAL); + g_object_set (G_OBJECT (button_box), + "layout-style", GTK_BUTTONBOX_START, + "margin-start", 6, + "spacing", 4, + NULL); + + widget = e_dialog_button_new_with_icon ("list-add", _("_Add")); + gtk_container_add (GTK_CONTAINER (button_box), widget); + + g_signal_connect_object (widget, "clicked", + G_CALLBACK (e_rss_preferences_add_clicked_cb), tree_view, 0); + + widget = e_dialog_button_new_with_icon (NULL, _("_Edit")); + gtk_widget_set_sensitive (widget, FALSE); + gtk_container_add (GTK_CONTAINER (button_box), widget); + + g_signal_connect_object (widget, "clicked", + G_CALLBACK (e_rss_preferences_edit_clicked_cb), tree_view, 0); + + g_signal_connect_object (selection, "changed", + G_CALLBACK (e_rss_pereferences_selection_changed_cb), widget, 0); + + g_signal_connect_object (tree_view, "row-activated", + G_CALLBACK (e_rss_preferences_row_activated_cb), widget, 0); + + widget = e_dialog_button_new_with_icon ("edit-delete", _("_Remove")); + gtk_widget_set_sensitive (widget, FALSE); + gtk_container_add (GTK_CONTAINER (button_box), widget); + + g_signal_connect_object (widget, "clicked", + G_CALLBACK (e_rss_preferences_remove_clicked_cb), tree_view, 0); + + g_signal_connect_object (selection, "changed", + G_CALLBACK (e_rss_pereferences_selection_changed_cb), widget, 0); + + gtk_grid_attach (grid, scrolled_window, 0, row, 1, 1); + gtk_grid_attach (grid, button_box, 1, row, 1, 1); + row++; + + pango_attr_list_unref (bold); + + widget = GTK_WIDGET (grid); + gtk_widget_show_all (widget); + + g_clear_object (&store_summary); + g_clear_object (&service); + g_clear_object (&settings); + + return widget; +} + +void +e_rss_preferences_init (EShell *shell) +{ + GtkWidget *preferences_window; + CamelService *service; + + g_return_if_fail (E_IS_SHELL (shell)); + + service = e_rss_preferences_ref_store (shell); + if (!service) + return; + + g_clear_object (&service); + + preferences_window = e_shell_get_preferences_window (shell); + + e_preferences_window_add_page ( + E_PREFERENCES_WINDOW (preferences_window), + "e-rss-page", + "rss", + _("News and Blogs"), + NULL, + e_rss_preferences_new, + 800); +} diff --git a/src/modules/rss/evolution/e-rss-preferences.h b/src/modules/rss/evolution/e-rss-preferences.h new file mode 100644 index 0000000000..bce32615dd --- /dev/null +++ b/src/modules/rss/evolution/e-rss-preferences.h @@ -0,0 +1,20 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef E_RSS_PREFERENCES_H +#define E_RSS_PREFERENCES_H + +#include + +#include + +G_BEGIN_DECLS + +void e_rss_preferences_init (EShell *shell); + +G_END_DECLS + +#endif /* E_RSS_PREFERENCES_H */ diff --git a/src/modules/rss/evolution/e-rss-shell-extension.c b/src/modules/rss/evolution/e-rss-shell-extension.c new file mode 100644 index 0000000000..9e83c2b230 --- /dev/null +++ b/src/modules/rss/evolution/e-rss-shell-extension.c @@ -0,0 +1,132 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include + +#include "shell/e-shell.h" + +#include "e-rss-preferences.h" + +#include "module-rss.h" + +#define E_TYPE_RSS_SHELL_EXTENSION (e_rss_shell_extension_get_type ()) + +GType e_rss_shell_extension_get_type (void); + +typedef struct _ERssShellExtension { + EExtension parent; +} ERssShellExtension; + +typedef struct _ERssShellExtensionClass { + EExtensionClass parent_class; +} ERssShellExtensionClass; + +G_DEFINE_DYNAMIC_TYPE (ERssShellExtension, e_rss_shell_extension, E_TYPE_EXTENSION) + +static void +e_rss_ensure_esource (EShell *shell) +{ + ESourceRegistry *registry; + ESource *rss_source; + + registry = e_shell_get_registry (shell); + rss_source = e_source_registry_ref_source (registry, "rss"); + + if (!rss_source) { + GError *error = NULL; + + rss_source = e_source_new_with_uid ("rss", NULL, &error); + + if (rss_source) { + ESourceMailAccount *mail_account; + + mail_account = e_source_get_extension (rss_source, E_SOURCE_EXTENSION_MAIL_ACCOUNT); + e_source_mail_account_set_builtin (mail_account, TRUE); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (mail_account), "rss"); + } else { + g_warning ("Failed to create RSS source: %s", error ? error->message : "Unknown error"); + } + + g_clear_error (&error); + } + + if (rss_source) { + GError *error = NULL; + + e_source_set_display_name (rss_source, _("News and Blogs")); + + if (!e_source_registry_commit_source_sync (registry, rss_source, NULL, &error)) + g_warning ("Failed to commit RSS source: %s", error ? error->message : "Unknown error"); + + g_clear_error (&error); + } + + g_clear_object (&rss_source); +} + +static gboolean +init_preferences_idle_cb (gpointer user_data) +{ + EShell *shell = g_weak_ref_get (user_data); + + if (shell) + e_rss_preferences_init (shell); + + g_clear_object (&shell); + + return G_SOURCE_REMOVE; +} + +static void +e_rss_shell_ready_to_start_cb (EShell *shell) +{ + e_rss_ensure_esource (shell); + + g_idle_add_full (G_PRIORITY_LOW, init_preferences_idle_cb, + e_weak_ref_new (shell), (GDestroyNotify) e_weak_ref_free); +} + +static void +e_rss_shell_extension_constructed (GObject *object) +{ + /* Chain up to parent's method */ + G_OBJECT_CLASS (e_rss_shell_extension_parent_class)->constructed (object); + + g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "event::ready-to-start", + G_CALLBACK (e_rss_shell_ready_to_start_cb), NULL, 0); +} + +static void +e_rss_shell_extension_class_init (ERssShellExtensionClass *klass) +{ + GObjectClass *object_class; + EExtensionClass *extension_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->constructed = e_rss_shell_extension_constructed; + + extension_class = E_EXTENSION_CLASS (klass); + extension_class->extensible_type = E_TYPE_SHELL; +} + +static void +e_rss_shell_extension_class_finalize (ERssShellExtensionClass *klass) +{ +} + +static void +e_rss_shell_extension_init (ERssShellExtension *extension) +{ +} + +void +e_rss_shell_extension_type_register (GTypeModule *type_module) +{ + e_rss_shell_extension_register_type (type_module); +} diff --git a/src/modules/rss/evolution/e-rss-shell-view-extension.c b/src/modules/rss/evolution/e-rss-shell-view-extension.c new file mode 100644 index 0000000000..ecb5ef6cdf --- /dev/null +++ b/src/modules/rss/evolution/e-rss-shell-view-extension.c @@ -0,0 +1,277 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include + +#include "mail/e-mail-reader-utils.h" +#include "mail/em-folder-tree.h" +#include "shell/e-shell-content.h" +#include "shell/e-shell-view.h" +#include "shell/e-shell-window.h" + +#include "../camel-rss-store-summary.h" + +#include "module-rss.h" + +static const gchar *mail_ui_def = + "\n" + " \n" + " \n" + " \n" + "\n"; + +#define E_TYPE_RSS_SHELL_VIEW_EXTENSION (e_rss_shell_view_extension_get_type ()) + +GType e_rss_shell_view_extension_get_type (void); + +typedef struct _ERssShellViewExtension { + EExtension parent; + guint current_ui_id; + gboolean actions_added; +} ERssShellViewExtension; + +typedef struct _ERssShellViewExtensionClass { + EExtensionClass parent_class; +} ERssShellViewExtensionClass; + +G_DEFINE_DYNAMIC_TYPE (ERssShellViewExtension, e_rss_shell_view_extension, E_TYPE_EXTENSION) + +static gboolean +e_rss_check_rss_folder_selected (EShellView *shell_view, + CamelStore **pstore, + gchar **pfolder_path) +{ + EShellSidebar *shell_sidebar; + EMFolderTree *folder_tree; + gchar *selected_path = NULL; + CamelStore *selected_store = NULL; + gboolean is_rss_folder = FALSE; + + shell_sidebar = e_shell_view_get_shell_sidebar (shell_view); + g_object_get (shell_sidebar, "folder-tree", &folder_tree, NULL); + if (em_folder_tree_get_selected (folder_tree, &selected_store, &selected_path)) { + if (selected_store) { + is_rss_folder = g_strcmp0 (camel_service_get_uid (CAMEL_SERVICE (selected_store)), "rss") == 0 && + g_strcmp0 (selected_path, CAMEL_VJUNK_NAME) != 0 && + g_strcmp0 (selected_path, CAMEL_VTRASH_NAME) != 0; + + if (is_rss_folder) { + if (pstore) + *pstore = g_object_ref (selected_store); + + if (pfolder_path) + *pfolder_path = selected_path; + else + g_free (selected_path); + + selected_path = NULL; + } + + g_object_unref (selected_store); + } + + g_free (selected_path); + } + + g_object_unref (folder_tree); + + return is_rss_folder; +} + +static void +e_rss_mail_folder_reload_got_folder_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EShellView *shell_view = user_data; + CamelFolder *folder; + GError *error = NULL; + + folder = camel_store_get_folder_finish (CAMEL_STORE (source_object), result, &error); + + if (folder) { + EShellContent *shell_content; + + shell_content = e_shell_view_get_shell_content (shell_view); + + e_mail_reader_refresh_folder (E_MAIL_READER (shell_content), folder); + + g_object_unref (folder); + } else { + g_warning ("%s: Failed to get folder: %s", G_STRFUNC, error ? error->message : "Unknown error"); + } +} + +static void +action_rss_mail_folder_reload_cb (GtkAction *action, + EShellView *shell_view) +{ + CamelStore *store = NULL; + CamelRssStoreSummary *store_summary = NULL; + gchar *folder_path = NULL; + + g_return_if_fail (E_IS_SHELL_VIEW (shell_view)); + + if (!e_rss_check_rss_folder_selected (shell_view, &store, &folder_path)) + return; + + g_object_get (store, "summary", &store_summary, NULL); + + camel_rss_store_summary_set_last_updated (store_summary, folder_path, 0); + + camel_store_get_folder (store, folder_path, CAMEL_STORE_FOLDER_NONE, G_PRIORITY_DEFAULT, NULL, + e_rss_mail_folder_reload_got_folder_cb, shell_view); + + g_clear_object (&store_summary); + g_clear_object (&store); + g_free (folder_path); +} + +static void +e_rss_shell_view_update_actions_cb (EShellView *shell_view, + GtkActionEntry *entries) +{ + CamelStore *store = NULL; + EShellWindow *shell_window; + GtkActionGroup *action_group; + GtkAction *action; + GtkUIManager *ui_manager; + gboolean is_rss_folder = FALSE; + + is_rss_folder = e_rss_check_rss_folder_selected (shell_view, &store, NULL); + + shell_window = e_shell_view_get_shell_window (shell_view); + ui_manager = e_shell_window_get_ui_manager (shell_window); + action_group = e_lookup_action_group (ui_manager, "mail"); + action = gtk_action_group_get_action (action_group, "e-rss-mail-folder-reload-action"); + + if (action) { + gtk_action_set_visible (action, is_rss_folder); + + if (store) { + CamelSession *session; + + session = camel_service_ref_session (CAMEL_SERVICE (store)); + gtk_action_set_sensitive (action, session && camel_session_get_online (session)); + g_clear_object (&session); + } else { + gtk_action_set_sensitive (action, FALSE); + } + } + + g_clear_object (&store); +} + +static void +e_rss_shell_view_toggled_cb (EShellView *shell_view, + ERssShellViewExtension *extension) +{ + EShellViewClass *shell_view_class; + EShellWindow *shell_window; + GtkUIManager *ui_manager; + gboolean is_active, need_update; + GError *error = NULL; + + g_return_if_fail (E_IS_SHELL_VIEW (shell_view)); + g_return_if_fail (extension != NULL); + + shell_view_class = E_SHELL_VIEW_GET_CLASS (shell_view); + g_return_if_fail (shell_view_class != NULL); + + shell_window = e_shell_view_get_shell_window (shell_view); + ui_manager = e_shell_window_get_ui_manager (shell_window); + + need_update = extension->current_ui_id != 0; + + if (extension->current_ui_id) { + gtk_ui_manager_remove_ui (ui_manager, extension->current_ui_id); + extension->current_ui_id = 0; + } + + is_active = e_shell_view_is_active (shell_view); + + if (!is_active || g_strcmp0 (shell_view_class->ui_manager_id, "org.gnome.evolution.mail") != 0) { + if (need_update) + gtk_ui_manager_ensure_update (ui_manager); + + return; + } + + if (!extension->actions_added) { + GtkActionEntry mail_folder_context_entries[] = { + { "e-rss-mail-folder-reload-action", + NULL, + N_("Re_load feed articles"), + NULL, + N_("Reload all feed articles from the server, updating existing and adding any missing"), + G_CALLBACK (action_rss_mail_folder_reload_cb) } + }; + + GtkActionGroup *action_group; + + action_group = e_shell_window_get_action_group (shell_window, "mail"); + + e_action_group_add_actions_localized ( + action_group, GETTEXT_PACKAGE, + mail_folder_context_entries, G_N_ELEMENTS (mail_folder_context_entries), shell_view); + + g_signal_connect (shell_view, "update-actions", + G_CALLBACK (e_rss_shell_view_update_actions_cb), NULL); + + extension->actions_added = TRUE; + } + + extension->current_ui_id = gtk_ui_manager_add_ui_from_string (ui_manager, mail_ui_def, -1, &error); + + if (error) { + g_warning ("%s: Failed to add ui definition: %s", G_STRFUNC, error->message); + g_error_free (error); + } + + gtk_ui_manager_ensure_update (ui_manager); +} + +static void +e_rss_shell_view_extension_constructed (GObject *object) +{ + /* Chain up to parent's method */ + G_OBJECT_CLASS (e_rss_shell_view_extension_parent_class)->constructed (object); + + g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "toggled", + G_CALLBACK (e_rss_shell_view_toggled_cb), object, 0); +} + +static void +e_rss_shell_view_extension_class_init (ERssShellViewExtensionClass *klass) +{ + GObjectClass *object_class; + EExtensionClass *extension_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->constructed = e_rss_shell_view_extension_constructed; + + extension_class = E_EXTENSION_CLASS (klass); + extension_class->extensible_type = E_TYPE_SHELL_VIEW; +} + +static void +e_rss_shell_view_extension_class_finalize (ERssShellViewExtensionClass *klass) +{ +} + +static void +e_rss_shell_view_extension_init (ERssShellViewExtension *extension) +{ +} + +void +e_rss_shell_view_extension_type_register (GTypeModule *type_module) +{ + e_rss_shell_view_extension_register_type (type_module); +} diff --git a/src/modules/rss/evolution/module-rss.c b/src/modules/rss/evolution/module-rss.c new file mode 100644 index 0000000000..aa1c1a432d --- /dev/null +++ b/src/modules/rss/evolution/module-rss.c @@ -0,0 +1,28 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include + +#include "module-rss.h" + +void e_module_load (GTypeModule *type_module); +void e_module_unload (GTypeModule *type_module); + +G_MODULE_EXPORT void +e_module_load (GTypeModule *type_module) +{ + e_rss_shell_extension_type_register (type_module); + e_rss_shell_view_extension_type_register (type_module); + e_rss_folder_tree_model_extension_type_register (type_module); +} + +G_MODULE_EXPORT void +e_module_unload (GTypeModule *type_module) +{ +} diff --git a/src/modules/rss/evolution/module-rss.h b/src/modules/rss/evolution/module-rss.h new file mode 100644 index 0000000000..18434fd947 --- /dev/null +++ b/src/modules/rss/evolution/module-rss.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef MODULE_RSS_H +#define MODULE_RSS_H + +#include + +G_BEGIN_DECLS + +void e_rss_shell_extension_type_register (GTypeModule *type_module); +void e_rss_shell_view_extension_type_register(GTypeModule *type_module); +void e_rss_folder_tree_model_extension_type_register + (GTypeModule *type_module); + +G_END_DECLS + +#endif /* MODULE_RSS_H */