/* * Copyright (C) 2002 Marco Pesenti Gritti * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "ephy-bookmarks.h" #include "ephy-file-helpers.h" #include "ephy-shell.h" #include "ephy-history.h" #include "ephy-debug.h" #include #include #define EPHY_BOOKMARKS_XML_VERSION "0.1" #define MAX_FAVORITES_NUM 10 struct EphyBookmarksPrivate { char *xml_file; EphyNode *bookmarks; EphyNode *keywords; EphyNode *favorites; EphyNode *lower_fav; double lower_score; GHashTable *keywords_hash; GStaticRWLock *keywords_hash_lock; }; static void ephy_bookmarks_class_init (EphyBookmarksClass *klass); static void ephy_bookmarks_init (EphyBookmarks *tab); static void ephy_bookmarks_finalize (GObject *object); static void ephy_bookmarks_autocompletion_source_init (EphyAutocompletionSourceIface *iface); static GObjectClass *parent_class = NULL; GType ephy_bookmarks_get_type (void) { static GType ephy_bookmarks_type = 0; if (ephy_bookmarks_type == 0) { static const GTypeInfo our_info = { sizeof (EphyBookmarksClass), NULL, /* base_init */ NULL, /* base_finalize */ (GClassInitFunc) ephy_bookmarks_class_init, NULL, NULL, /* class_data */ sizeof (EphyBookmarks), 0, /* n_preallocs */ (GInstanceInitFunc) ephy_bookmarks_init }; static const GInterfaceInfo autocompletion_source_info = { (GInterfaceInitFunc) ephy_bookmarks_autocompletion_source_init, NULL, NULL }; ephy_bookmarks_type = g_type_register_static (G_TYPE_OBJECT, "EphyBookmarks", &our_info, 0); g_type_add_interface_static (ephy_bookmarks_type, EPHY_TYPE_AUTOCOMPLETION_SOURCE, &autocompletion_source_info); } return ephy_bookmarks_type; } static void ephy_bookmarks_autocompletion_source_set_basic_key (EphyAutocompletionSource *source, const gchar *basic_key) { /* nothing to do here */ } static void ephy_bookmarks_autocompletion_source_foreach (EphyAutocompletionSource *source, const gchar *current_text, EphyAutocompletionSourceForeachFunc func, gpointer data) { GPtrArray *children; int i; EphyBookmarks *eb = EPHY_BOOKMARKS (source); children = ephy_node_get_children (eb->priv->bookmarks); for (i = 0; i < children->len; i++) { EphyNode *kid; const char *url, *smart_url, *title, *keywords; kid = g_ptr_array_index (children, i); url = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_LOCATION); smart_url = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_SMART_LOCATION); title = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_TITLE); keywords = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_KEYWORDS); if (smart_url == NULL || g_utf8_strlen (smart_url, -1) == 0) { smart_url = NULL; } func (source, smart_url ? NULL : keywords, title, smart_url ? smart_url : url, (smart_url != NULL), TRUE, 0, data); } ephy_node_thaw (eb->priv->bookmarks); } static void ephy_bookmarks_emit_data_changed (EphyBookmarks *eb) { g_signal_emit_by_name (eb, "data-changed"); } static void ephy_bookmarks_autocompletion_source_init (EphyAutocompletionSourceIface *iface) { iface->foreach = ephy_bookmarks_autocompletion_source_foreach; iface->set_basic_key = ephy_bookmarks_autocompletion_source_set_basic_key; } static void ephy_bookmarks_class_init (EphyBookmarksClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); parent_class = g_type_class_peek_parent (klass); object_class->finalize = ephy_bookmarks_finalize; } static gboolean ephy_bookmarks_clean_empty_keywords (EphyBookmarks *eb) { GPtrArray *children; int i; GList *l = NULL, *tmp; children = ephy_node_get_children (eb->priv->keywords); ephy_node_thaw (eb->priv->keywords); for (i = 0; i < children->len; i++) { EphyNode *kid; kid = g_ptr_array_index (children, i); if (ephy_node_get_n_children (kid) == 0) { l = g_list_append (l, kid); } } if (l == NULL) return FALSE; for (tmp = l; tmp != NULL; tmp = tmp->next) { EphyNode *node = EPHY_NODE (tmp->data); LOG ("Remove empty keyword: %s", ephy_node_get_property_string (node, EPHY_NODE_KEYWORD_PROP_NAME)) g_object_unref (node); } g_list_free (l); return FALSE; } static void ephy_bookmarks_load (EphyBookmarks *eb) { xmlDocPtr doc; xmlNodePtr root, child; char *tmp; if (g_file_test (eb->priv->xml_file, G_FILE_TEST_EXISTS) == FALSE) return; doc = xmlParseFile (eb->priv->xml_file); g_assert (doc != NULL); root = xmlDocGetRootElement (doc); tmp = xmlGetProp (root, "version"); g_assert (tmp != NULL && strcmp (tmp, EPHY_BOOKMARKS_XML_VERSION) == 0); g_free (tmp); for (child = root->children; child != NULL; child = child->next) { EphyNode *node; node = ephy_node_new_from_xml (child); } xmlFreeDoc (doc); } static void ephy_bookmarks_save (EphyBookmarks *eb) { xmlDocPtr doc; xmlNodePtr root; GPtrArray *children; int i; LOG ("Saving bookmarks") /* save nodes to xml */ xmlIndentTreeOutput = TRUE; doc = xmlNewDoc ("1.0"); root = xmlNewDocNode (doc, NULL, "ephy_bookmarks", NULL); xmlSetProp (root, "version", EPHY_BOOKMARKS_XML_VERSION); xmlDocSetRootElement (doc, root); children = ephy_node_get_children (eb->priv->keywords); for (i = 0; i < children->len; i++) { EphyNode *kid; kid = g_ptr_array_index (children, i); if (kid != eb->priv->bookmarks) { ephy_node_save_to_xml (kid, root); } } ephy_node_thaw (eb->priv->keywords); children = ephy_node_get_children (eb->priv->bookmarks); for (i = 0; i < children->len; i++) { EphyNode *kid; kid = g_ptr_array_index (children, i); ephy_node_save_to_xml (kid, root); } ephy_node_thaw (eb->priv->bookmarks); xmlSaveFormatFile (eb->priv->xml_file, doc, 1); } static double get_history_item_score (EphyHistory *eh, const char *page) { return ephy_history_get_page_visits (eh, page); } static EphyNode * compute_lower_fav (EphyNode *favorites, double *score) { GPtrArray *children; int i; EphyEmbedShell *embed_shell; EphyHistory *history; EphyNode *result = NULL; embed_shell = EPHY_EMBED_SHELL (ephy_shell); history = ephy_embed_shell_get_global_history (embed_shell); *score = DBL_MAX; children = ephy_node_get_children (favorites); for (i = 0; i < children->len; i++) { const char *url; EphyNode *child; double item_score; child = g_ptr_array_index (children, i); url = ephy_node_get_property_string (child, EPHY_NODE_BMK_PROP_LOCATION); item_score = get_history_item_score (history, url); if (*score > item_score) { *score = item_score; result = child; } } ephy_node_thaw (favorites); if (result == NULL) *score = 0; return result; } static void ephy_bookmarks_update_favorites (EphyBookmarks *eb) { eb->priv->lower_fav = compute_lower_fav (eb->priv->favorites, &eb->priv->lower_score); } static gboolean add_to_favorites (EphyBookmarks *eb, EphyNode *node, EphyHistory *eh) { const char *url; gboolean full_menu; double score; if (ephy_node_has_child (eb->priv->favorites, node)) return FALSE; url = ephy_node_get_property_string (node, EPHY_NODE_BMK_PROP_LOCATION); score = get_history_item_score (eh, url); full_menu = ephy_node_get_n_children (eb->priv->favorites) > MAX_FAVORITES_NUM; if (full_menu && score < eb->priv->lower_score) return FALSE; if (eb->priv->lower_fav && full_menu) { ephy_node_remove_child (eb->priv->favorites, eb->priv->lower_fav); } ephy_node_add_child (eb->priv->favorites, node); ephy_bookmarks_update_favorites (eb); return TRUE; } static void update_favorites_menus () { Session *session; const GList *l; session = ephy_shell_get_session (ephy_shell); l = session_get_windows (session); for (; l != NULL; l = l->next) { EphyWindow *window = EPHY_WINDOW (l->data); ephy_window_update_control (window, FavoritesControl); } } static void history_site_visited_cb (EphyHistory *gh, const char *url, EphyBookmarks *eb) { EphyNode *node; guint id; id = ephy_bookmarks_get_bookmark_id (eb, url); if (id == 0) return; node = ephy_node_get_from_id (id); if (add_to_favorites (eb, node, gh)) { update_favorites_menus (); } } static void ephy_setup_history_notifiers (EphyBookmarks *eb) { EphyEmbedShell *embed_shell; EphyHistory *history; embed_shell = EPHY_EMBED_SHELL (ephy_shell); history = ephy_embed_shell_get_global_history (embed_shell); g_signal_connect (history, "visited", G_CALLBACK (history_site_visited_cb), eb); } static void keywords_added_cb (EphyNode *node, EphyNode *child, EphyBookmarks *eb) { g_static_rw_lock_writer_lock (eb->priv->keywords_hash_lock); g_hash_table_insert (eb->priv->keywords_hash, (char *) ephy_node_get_property_string (child, EPHY_NODE_KEYWORD_PROP_NAME), child); g_static_rw_lock_writer_unlock (eb->priv->keywords_hash_lock); } static void keywords_removed_cb (EphyNode *node, EphyNode *child, EphyBookmarks *eb) { g_static_rw_lock_writer_lock (eb->priv->keywords_hash_lock); g_hash_table_remove (eb->priv->keywords_hash, ephy_node_get_property_string (child, EPHY_NODE_KEYWORD_PROP_NAME)); g_static_rw_lock_writer_unlock (eb->priv->keywords_hash_lock); } static void bookmarks_changed_cb (EphyNode *node, EphyNode *child, EphyBookmarks *eb) { ephy_bookmarks_emit_data_changed (eb); } static void bookmarks_removed_cb (EphyNode *node, EphyNode *child, EphyBookmarks *eb) { ephy_bookmarks_emit_data_changed (eb); g_idle_add ((GSourceFunc)ephy_bookmarks_clean_empty_keywords, eb); } static void ephy_bookmarks_init (EphyBookmarks *eb) { GValue value = { 0, }; eb->priv = g_new0 (EphyBookmarksPrivate, 1); eb->priv->xml_file = g_build_filename (ephy_dot_dir (), "bookmarks.xml", NULL); eb->priv->keywords_hash = g_hash_table_new (g_str_hash, g_str_equal); eb->priv->keywords_hash_lock = g_new0 (GStaticRWLock, 1); g_static_rw_lock_init (eb->priv->keywords_hash_lock); /* Bookmarks */ eb->priv->bookmarks = ephy_node_new_with_id (BOOKMARKS_NODE_ID); ephy_node_ref (eb->priv->bookmarks); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, _("All")); ephy_node_set_property (eb->priv->bookmarks, EPHY_NODE_KEYWORD_PROP_NAME, &value); g_value_unset (&value); g_signal_connect_object (G_OBJECT (eb->priv->bookmarks), "child_removed", G_CALLBACK (bookmarks_removed_cb), G_OBJECT (eb), 0); g_signal_connect_object (G_OBJECT (eb->priv->bookmarks), "child_changed", G_CALLBACK (bookmarks_changed_cb), G_OBJECT (eb), 0); /* Keywords */ eb->priv->keywords = ephy_node_new_with_id (KEYWORDS_NODE_ID); ephy_node_ref (eb->priv->keywords); g_value_init (&value, G_TYPE_BOOLEAN); g_value_set_boolean (&value, TRUE); ephy_node_set_property (eb->priv->bookmarks, EPHY_NODE_KEYWORD_PROP_ALL_PRIORITY, &value); g_value_unset (&value); ephy_node_add_child (eb->priv->keywords, eb->priv->bookmarks); g_signal_connect_object (G_OBJECT (eb->priv->keywords), "child_added", G_CALLBACK (keywords_added_cb), G_OBJECT (eb), 0); g_signal_connect_object (G_OBJECT (eb->priv->keywords), "child_removed", G_CALLBACK (keywords_removed_cb), G_OBJECT (eb), 0); eb->priv->favorites = ephy_node_new_with_id (FAVORITES_NODE_ID); ephy_node_ref (eb->priv->favorites); ephy_bookmarks_load (eb); ephy_bookmarks_emit_data_changed (eb); ephy_setup_history_notifiers (eb); ephy_bookmarks_update_favorites (eb); } static void ephy_bookmarks_finalize (GObject *object) { EphyBookmarks *eb; g_return_if_fail (IS_EPHY_BOOKMARKS (object)); eb = EPHY_BOOKMARKS (object); g_return_if_fail (eb->priv != NULL); ephy_bookmarks_save (eb); ephy_node_unref (eb->priv->bookmarks); ephy_node_unref (eb->priv->keywords); ephy_node_unref (eb->priv->favorites); g_hash_table_destroy (eb->priv->keywords_hash); g_static_rw_lock_free (eb->priv->keywords_hash_lock); g_free (eb->priv); LOG ("Bookmarks finalized") G_OBJECT_CLASS (parent_class)->finalize (object); } EphyBookmarks * ephy_bookmarks_new () { EphyBookmarks *tab; tab = EPHY_BOOKMARKS (g_object_new (EPHY_BOOKMARKS_TYPE, NULL)); return tab; } EphyNode * ephy_bookmarks_add (EphyBookmarks *eb, const char *title, const char *url, const char *smart_url, const char *keywords) { EphyNode *bm; GValue value = { 0, }; bm = ephy_node_new (); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, title); ephy_node_set_property (bm, EPHY_NODE_BMK_PROP_TITLE, &value); g_value_unset (&value); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, url); ephy_node_set_property (bm, EPHY_NODE_BMK_PROP_LOCATION, &value); g_value_unset (&value); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, smart_url); ephy_node_set_property (bm, EPHY_NODE_BMK_PROP_SMART_LOCATION, &value); g_value_unset (&value); ephy_bookmarks_update_keywords (eb, keywords, bm); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, keywords); ephy_node_set_property (bm, EPHY_NODE_BMK_PROP_KEYWORDS, &value); g_value_unset (&value); ephy_node_add_child (eb->priv->bookmarks, bm); ephy_bookmarks_emit_data_changed (eb); ephy_bookmarks_save (eb); return bm; } guint ephy_bookmarks_get_bookmark_id (EphyBookmarks *eb, const char *url) { GPtrArray *children; int i; children = ephy_node_get_children (eb->priv->bookmarks); for (i = 0; i < children->len; i++) { EphyNode *kid; const char *location; kid = g_ptr_array_index (children, i); location = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_LOCATION); if (strcmp (url, location) == 0) { ephy_node_thaw (eb->priv->bookmarks); return ephy_node_get_id (kid); } } ephy_node_thaw (eb->priv->bookmarks); return 0; } void ephy_bookmarks_set_icon (EphyBookmarks *eb, const char *url, const char *icon) { EphyNode *node; GValue value = { 0, }; guint id; g_return_if_fail (icon != NULL); id = ephy_bookmarks_get_bookmark_id (eb, url); if (id == 0) return; node = ephy_node_get_from_id (id); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, icon); ephy_node_set_property (node, EPHY_NODE_BMK_PROP_ICON, &value); g_value_unset (&value); } static gchar * options_skip_spaces (const gchar *str) { const gchar *ret = str; while (*ret && g_ascii_isspace (*ret)) { ++ret; } return (gchar *) ret; } static char * options_find_value_end (const gchar *value_start) { const gchar *value_end; if (*value_start == '"') { for (value_end = value_start + 1; *value_end && (*value_end != '"' || *(value_end - 1) == '\\'); ++value_end) ; } else { for (value_end = value_start; *value_end && ! (g_ascii_isspace (*value_end) || *value_end == ',' || *value_end == ';'); ++value_end) ; } return (gchar *) value_end; } static char * options_find_next_option (const char *current) { const gchar *value_start; const gchar *value_end; const gchar *ret; value_start = strchr (current, '='); if (!value_start) return NULL; value_start = options_skip_spaces (value_start + 1); value_end = options_find_value_end (value_start); if (! (*value_end)) return NULL; for (ret = value_end + 1; *ret && (g_ascii_isspace (*ret) || *ret == ',' || *ret == ';'); ++ret); return (char *) ret; } /** * Very simple parser for option strings in the * form a=b,c=d,e="f g",... */ static gchar * smart_url_options_get (const gchar *options, const gchar *option) { gchar *ret = NULL; gsize optionlen = strlen (option); const gchar *current = options_skip_spaces (options); while (current) { if (!strncmp (option, current, optionlen)) { if (g_ascii_isspace (*(current + optionlen)) || *(current + optionlen) == '=') { const gchar *value_start; const gchar *value_end; value_start = strchr (current + optionlen, '='); if (!value_start) continue; value_start = options_skip_spaces (value_start + 1); value_end = options_find_value_end (value_start); if (*value_start == '"') value_start++; if (value_end >= value_start) { ret = g_strndup (value_start, value_end - value_start); break; } } } current = options_find_next_option (current); } return ret; } static char * get_smarturl_only (const char *smarturl) { const gchar *openbrace; const gchar *closebrace; const gchar *c; openbrace = strchr (smarturl, '{'); if (!openbrace) return g_strdup (smarturl); for (c = smarturl; c < openbrace; ++c) { if (!strchr (" \t\n", *c)) return g_strdup (smarturl); } closebrace = strchr (openbrace + 1, '}'); if (!closebrace) return g_strdup (smarturl); return g_strdup (closebrace + 1); } char * ephy_bookmarks_solve_smart_url (EphyBookmarks *eb, const char *smart_url, const char *content) { gchar *ret; GString *s; gchar *t1; gchar *t2; gchar *encoding; gchar *smarturl_only; gchar *arg; g_return_val_if_fail (content != NULL, NULL); smarturl_only = get_smarturl_only (smart_url); s = g_string_new (""); encoding = smart_url_options_get (smart_url, "encoding"); if (!encoding) { encoding = g_strdup ("UTF-8"); } arg = g_convert (content, strlen (content), encoding, "UTF-8", NULL, NULL, NULL); t1 = smarturl_only; t2 = strstr (t1, "%s"); g_return_val_if_fail (t2 != NULL, NULL); g_string_append_len (s, t1, t2 - t1); g_string_append (s, arg); t1 = t2 + 2; g_string_append (s, t1); ret = g_string_free (s, FALSE); g_free (arg); g_free (encoding); g_free (smarturl_only); return ret; } EphyNode * ephy_bookmarks_add_keyword (EphyBookmarks *eb, const char *name) { EphyNode *key; GValue value = { 0, }; key = ephy_node_new (); g_value_init (&value, G_TYPE_STRING); g_value_set_string (&value, name); ephy_node_set_property (key, EPHY_NODE_KEYWORD_PROP_NAME, &value); g_value_unset (&value); ephy_node_add_child (eb->priv->keywords, key); return key; } EphyNode * ephy_bookmarks_find_keyword (EphyBookmarks *eb, const char *name, gboolean partial_match) { EphyNode *node; g_return_val_if_fail (name != NULL, NULL); if (!partial_match) { g_static_rw_lock_reader_lock (eb->priv->keywords_hash_lock); node = g_hash_table_lookup (eb->priv->keywords_hash, name); g_static_rw_lock_reader_unlock (eb->priv->keywords_hash_lock); } else { GPtrArray *children; int i; if (g_utf8_strlen (name, -1) == 0) { LOG ("Empty name, no keyword matches.") return NULL; } children = ephy_node_get_children (eb->priv->keywords); node = NULL; for (i = 0; i < children->len; i++) { EphyNode *kid; const char *key; kid = g_ptr_array_index (children, i); key = ephy_node_get_property_string (kid, EPHY_NODE_KEYWORD_PROP_NAME); if (g_str_has_prefix (key, name) > 0) { node = kid; } } ephy_node_thaw (eb->priv->keywords); } return node; } void ephy_bookmarks_set_keyword (EphyBookmarks *eb, EphyNode *keyword, EphyNode *bookmark) { ephy_node_add_child (keyword, bookmark); } void ephy_bookmarks_unset_keyword (EphyBookmarks *eb, EphyNode *keyword, EphyNode *bookmark) { ephy_node_remove_child (keyword, bookmark); ephy_bookmarks_clean_empty_keywords (eb); } static GList * diff_keywords (char **ks1, char **ks2) { GList *result = NULL; int i, j; for (i = 0; ks1 != NULL && ks1[i] != NULL; i++) { gboolean found = FALSE; LOG ("Diff keywords, keyword:\"%s\"", ks1[i]) for (j = 0; ks2 != NULL && ks2[j] != NULL; j++) { if (strcmp (ks1[i], ks2[j]) == 0) { found = TRUE; } } if (!found && g_utf8_strlen (ks1[i], -1) > 0) { result = g_list_append (result, ks1[i]); } } return result; } void ephy_bookmarks_update_keywords (EphyBookmarks *eb, const char *keywords, EphyNode *node) { const char *prop; char **ks, **old_ks = NULL; GList *diffs, *l; EphyNode *keyword; prop = ephy_node_get_property_string (node, EPHY_NODE_BMK_PROP_KEYWORDS); ks = g_strsplit (keywords, " ", 10); if (prop != NULL) { old_ks = g_strsplit (prop, " ", 10); } diffs = diff_keywords (ks, old_ks); for (l = diffs; l != NULL; l = l->next) { char *word = (char *)l->data; keyword = ephy_bookmarks_find_keyword (eb, word, FALSE); if (!keyword) { keyword = ephy_bookmarks_add_keyword (eb, word); } ephy_bookmarks_set_keyword (eb, keyword, node); } g_list_free (diffs); diffs = diff_keywords (old_ks, ks); for (l = diffs; l != NULL; l = l->next) { keyword = ephy_bookmarks_find_keyword (eb, (char *)l->data, FALSE); g_return_if_fail (keyword != NULL); ephy_bookmarks_unset_keyword (eb, keyword, node); } g_list_free (diffs); g_strfreev (ks); g_strfreev (old_ks); } EphyNode * ephy_bookmarks_get_keywords (EphyBookmarks *eb) { return eb->priv->keywords; } EphyNode * ephy_bookmarks_get_bookmarks (EphyBookmarks *eb) { return eb->priv->bookmarks; } EphyNode * ephy_bookmarks_get_favorites (EphyBookmarks *eb) { return eb->priv->favorites; }