/* * Copyright © 2009 Benjamin Otte * * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include "ephy-history.h" #define EPHY_HISTORY_DEFAULT ".gnome2/epiphany/history.sqlite" #define EPHY_HISTORY_PRIORITY_SELECT G_PRIORITY_DEFAULT struct _EphyHistoryPrivate { /* data only touched upon creation and inside thread */ GMutex * lock; char * filename; sqlite3 * db; }; enum { PROP_0, PROP_FILENAME }; G_DEFINE_TYPE (EphyHistory, ephy_history, G_TYPE_OBJECT); /*** OBJECT ***/ static void ephy_history_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { EphyHistory *history = EPHY_HISTORY (object); EphyHistoryPrivate *priv = history->priv; const char *s; switch (prop_id) { case PROP_FILENAME: s = g_value_get_string (value); if (s == NULL) s = EPHY_HISTORY_DEFAULT; if (g_path_is_absolute (s)) priv->filename = g_strdup (s); else priv->filename = g_build_filename (g_get_home_dir (), s, NULL); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void ephy_history_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { EphyHistory *history = EPHY_HISTORY (object); EphyHistoryPrivate *priv = history->priv; switch (prop_id) { case PROP_FILENAME: g_value_set_string (value, priv->filename); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void ephy_history_finalize (GObject *object) { EphyHistory *history = EPHY_HISTORY (object); EphyHistoryPrivate *priv = history->priv; g_free (priv->filename); sqlite3_close (priv->db); g_mutex_free (priv->lock); G_OBJECT_CLASS (ephy_history_parent_class)->finalize (object); } static void ephy_history_class_init (EphyHistoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = ephy_history_finalize; object_class->get_property = ephy_history_get_property; object_class->set_property = ephy_history_set_property; g_object_class_install_property (object_class, PROP_FILENAME, g_param_spec_string ("filename", "filename", "name of history file", NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); g_type_class_add_private (object_class, sizeof (EphyHistoryPrivate)); } static void ephy_history_init (EphyHistory *history) { EphyHistoryPrivate *priv; priv = history->priv = G_TYPE_INSTANCE_GET_PRIVATE (history, EPHY_TYPE_HISTORY, EphyHistoryPrivate); priv->lock = g_mutex_new (); } EphyHistory * ephy_history_new (const char *filename) { return g_object_new (EPHY_TYPE_HISTORY, "filename", filename, NULL); } EphyHistory * ephy_history_get_default (void) { static EphyHistory *history = NULL; if (G_UNLIKELY (history == NULL)) history = ephy_history_new (NULL); return g_object_ref (history); } static gboolean check_sqlite_result (EphyHistory * history, GSimpleAsyncResult *res, int result) { EphyHistoryPrivate *priv = history->priv; if (result == SQLITE_OK) return TRUE; /* FIXME: improve */ g_simple_async_result_set_error (res, EPHY_SQLITE_ERROR, sqlite3_extended_errcode (priv->db), "%s", sqlite3_errmsg (priv->db)); return FALSE; } static void _g_string_append_escaped (GString *string, const char *text, gssize len) { char *s = g_markup_escape_text (text, len); g_string_append (string, s); g_free (s); } static void custom_function_markup (sqlite3_context *context, int argc, sqlite3_value** argv) { const char *text; GRegex *regex; GString *string; GMatchInfo *match; int i, current, end; if (argc < 2) { sqlite3_result_null (context); return; } text = (const char *) sqlite3_value_text (argv[0]); if (text == NULL || text[0] == 0) { sqlite3_result_null (context); return; } regex = sqlite3_get_auxdata (context, 0); if (regex == NULL) { regex = g_regex_new (text, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL); sqlite3_set_auxdata (context, 0, regex, (GDestroyNotify) g_regex_unref); } string = g_string_new (""); for (i = 1; i < argc; i++) { const char *argument = (const char *) sqlite3_value_text (argv[1]); current = end = 0; if (i > 1) g_string_append_c (string, '\n'); if (!g_regex_match (regex, argument, G_REGEX_MATCH_NOTEMPTY, &match)) { _g_string_append_escaped (string, argument, -1); continue; } do { int from, to; if (!g_match_info_fetch_pos (match, 0, &from, &to)) continue; g_assert (from >= 0 && to >= 0); if (from > end) { if (end > current) { g_string_append (string, ""); _g_string_append_escaped (string, argument + current, end - current); g_string_append (string, ""); } _g_string_append_escaped (string, argument + end, from - end); current = from; } end = MAX (end, to); } while (g_match_info_next (match, NULL)); if (end > current) { g_string_append (string, ""); _g_string_append_escaped (string, argument + current, end - current); g_string_append (string, ""); } _g_string_append_escaped (string, argument + end, -1); g_match_info_free (match); } sqlite3_result_text (context, string->str, string->len, g_free); g_string_free (string, FALSE); } static void custom_function_match (sqlite3_context *context, int argc, sqlite3_value** argv) { const char *has_match, *haystack, *needle; haystack = (char *) sqlite3_value_text (argv[1]); needle = (char *) sqlite3_value_text (argv[0]); #if 0 /* * - requires _GNU_SOURCE when including string.h * - the version below is 50% faster (huh?) */ has_match = strcasestr (haystack, needle); #else { gsize needle_len = strlen (needle); char find[3] = { g_ascii_tolower (*needle), g_ascii_toupper (*needle), 0 }; has_match = haystack; while ((has_match = strpbrk (has_match, find))) { if (strncasecmp (has_match, needle, needle_len) == 0) break; has_match++; } } #endif sqlite3_result_int (context, has_match ? 1 : 0); } static void custom_function_regexp (sqlite3_context *context, int argc, sqlite3_value** argv) { GRegex *regex; gboolean has_match; regex = sqlite3_get_auxdata (context, 0); if (regex == NULL) { regex = g_regex_new ((char *) sqlite3_value_text (argv[0]), G_REGEX_CASELESS | G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL); sqlite3_set_auxdata (context, 0, regex, (GDestroyNotify) g_regex_unref); } has_match = g_regex_match (regex, (char *) sqlite3_value_text (argv[1]), G_REGEX_MATCH_NOTEMPTY, NULL); sqlite3_result_int (context, has_match ? 1 : 0); } static gboolean ephy_history_open_database (EphyHistory * history, GSimpleAsyncResult *res) { static const struct { const char *name; int n_args; void (* func) (sqlite3_context *, int, sqlite3_value **); } custom_functions[] = { { "markup", -1, custom_function_markup }, { "match", 2, custom_function_match }, { "regexp", 2, custom_function_regexp } }; EphyHistoryPrivate *priv = history->priv; guint i; if (priv->db) return TRUE; if (!check_sqlite_result (history, res, sqlite3_open_v2 (priv->filename, &priv->db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL))) return FALSE; /* register custom functions */ for (i = 0; i < G_N_ELEMENTS (custom_functions); i++) { if (!check_sqlite_result (history, res, sqlite3_create_function (priv->db, custom_functions[i].name, custom_functions[i].n_args, SQLITE_UTF8, NULL, custom_functions[i].func, NULL, NULL))) goto fail; } /* FIXME: create database if it doesn't exist */ return TRUE; fail: sqlite3_close (priv->db); priv->db = NULL; return FALSE; } GQuark ephy_sqlite_error_quark (void) { GQuark quark = 0; if (G_UNLIKELY (quark == 0)) quark = g_quark_from_static_string ("epiphany-sqlite-error"); return quark; } /*** SELECT ***/ typedef struct { guint initial_results; guint progress_ms; char *sql; GSList *results; GCancellable *cancellable; /* need to store these because GSimpleAsyncResult doesn't allow querying */ GAsyncReadyCallback callback; gpointer user_data; } SelectData; static void select_data_free (gpointer data) { SelectData *select = data; if (select->sql) sqlite3_free (select->sql); if (select->cancellable) g_object_unref (select->cancellable); g_slist_foreach (select->results, (GFunc) g_value_array_free, NULL); g_slist_free (select->results); g_slice_free (SelectData, select); } static gboolean ephy_history_complete_in_idle_cb (gpointer _data) { GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (_data); SelectData *data = g_simple_async_result_get_op_res_gpointer (simple); /* * Don't complete the async result here, because then we'd * get multiple cancellation errors - once for every progress * update plus the final one. */ if (!g_cancellable_is_cancelled (data->cancellable)) g_simple_async_result_complete (simple); return FALSE; } static void ephy_history_select_emit_partial (EphyHistory *history, GSList *list, GAsyncReadyCallback callback, gpointer user_data, GCancellable *cancellable) { GSimpleAsyncResult *res; SelectData *select; GSource *source; g_assert (list); select = g_slice_new0 (SelectData); select->results = list; if (cancellable) select->cancellable = g_object_ref (cancellable); res = g_simple_async_result_new (G_OBJECT (history), callback, user_data, ephy_history_select_async); g_simple_async_result_set_op_res_gpointer (res, select, select_data_free); source = g_idle_source_new (); g_source_set_priority (source, G_PRIORITY_DEFAULT); g_source_set_callback (source, ephy_history_complete_in_idle_cb, res, g_object_unref); g_source_attach (source, NULL); g_source_unref (source); } static void ephy_history_select_thread (GSimpleAsyncResult *res, GObject * object, GCancellable * cancellable) { EphyHistory *history = EPHY_HISTORY (object); EphyHistoryPrivate *priv = history->priv; SelectData *select = g_simple_async_result_get_op_res_gpointer (res); GTimer *timer; sqlite3_stmt *stmt; guint counter; double timeout; g_mutex_lock (priv->lock); if (!ephy_history_open_database (history, res) || !check_sqlite_result (history, res, sqlite3_prepare_v2 (priv->db, select->sql, -1, &stmt, NULL))) /* we only run one SQL */ goto out; counter = select->initial_results; timeout = select->progress_ms ? select->progress_ms / 1000. : G_MAXDOUBLE; timer = g_timer_new (); g_timer_start (timer); while (sqlite3_step (stmt) == SQLITE_ROW && !g_cancellable_is_cancelled (cancellable)) { guint i, n_rows = sqlite3_data_count (stmt); GValueArray *array = g_value_array_new (n_rows); for (i = 0; i < n_rows; i++) { g_value_array_append (array, NULL); switch (sqlite3_column_type (stmt, i)) { case SQLITE_INTEGER: g_value_init (&array->values[i], G_TYPE_INT64); g_value_set_int64 (&array->values[i], sqlite3_column_int64 (stmt, i)); break; case SQLITE_TEXT: g_value_init (&array->values[i], G_TYPE_STRING); g_value_set_string (&array->values[i], (char *) sqlite3_column_text (stmt, i)); break; case SQLITE_FLOAT: g_value_init (&array->values[i], G_TYPE_DOUBLE); g_value_set_int64 (&array->values[i], sqlite3_column_double (stmt, i)); break; case SQLITE_NULL: g_value_init (&array->values[i], G_TYPE_STRING); /* leave values as NULL */ break; case SQLITE_BLOB: /* FIXME: implement */ default: g_warning ("unhandled column type %d", sqlite3_column_type (stmt, i)); break; } } select->results = g_slist_prepend (select->results, array); counter--; if (counter == 0 || g_timer_elapsed (timer, NULL) > timeout) { ephy_history_select_emit_partial (history, g_slist_reverse (select->results), select->callback, select->user_data, cancellable); select->results = NULL; g_timer_reset (timer); } } g_timer_destroy (timer); if (check_sqlite_result (history, res, sqlite3_finalize (stmt)) && select->results != NULL) { ephy_history_select_emit_partial (history, g_slist_reverse (select->results), select->callback, select->user_data, cancellable); select->results = NULL; } out: g_mutex_unlock (priv->lock); } void ephy_history_select_async (EphyHistory * history, guint initial_results, guint progress_ms, GAsyncReadyCallback callback, gpointer user_data, GCancellable * cancellable, const char * sql, ...) { GSimpleAsyncResult *res; SelectData *select; va_list args; g_return_if_fail (EPHY_IS_HISTORY (history)); g_return_if_fail (callback); g_return_if_fail (sql != NULL); select = g_slice_new0 (SelectData); select->callback = callback; select->user_data = user_data; select->initial_results = initial_results; select->progress_ms = progress_ms; if (cancellable) select->cancellable = g_object_ref (cancellable); va_start (args, sql); select->sql = sqlite3_vmprintf (sql, args); va_end (args); res = g_simple_async_result_new (G_OBJECT (history), callback, user_data, ephy_history_select_async); g_simple_async_result_set_op_res_gpointer (res, select, select_data_free); g_simple_async_result_run_in_thread (res, ephy_history_select_thread, EPHY_HISTORY_PRIORITY_SELECT, cancellable); g_object_unref (res); } GSList * ephy_history_select_finish (EphyHistory * history, GAsyncResult *res, GError ** error) { GSimpleAsyncResult *simple; SelectData *select; g_return_val_if_fail (EPHY_IS_HISTORY (history), NULL); g_return_val_if_fail (G_IS_ASYNC_RESULT (res), NULL); simple = G_SIMPLE_ASYNC_RESULT (res); g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == ephy_history_select_async); if (g_simple_async_result_propagate_error (simple, error)) return NULL; select = g_simple_async_result_get_op_res_gpointer (simple); return select->results; }