diff options
Diffstat (limited to 'seaudit/message_view.c')
-rw-r--r-- | seaudit/message_view.c | 1341 |
1 files changed, 1341 insertions, 0 deletions
diff --git a/seaudit/message_view.c b/seaudit/message_view.c new file mode 100644 index 0000000..64d625f --- /dev/null +++ b/seaudit/message_view.c @@ -0,0 +1,1341 @@ +/** + * @file + * Implementation of the view for a libseaudit model. + * + * @author Jeremy A. Mowery jmowery@tresys.com + * @author Jason Tang jtang@tresys.com + * @author Jeremy Solt jsolt@tresys.com + * + * Copyright (C) 2003-2007 Tresys Technology, LLC + * + * 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 St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include <config.h> + +#include "message_view.h" +#include "modify_view.h" +#include "utilgui.h" + +#include <assert.h> +#include <errno.h> +#include <stdio.h> +#include <string.h> +#include <time.h> +#include <apol/util.h> + +/** + * A custom model that implements the interfaces GtkTreeModel and + * GtkTreeSortable. + */ +typedef struct message_view_store +{ + /** this must be the first field, to satisfy glib */ + GObject parent; + /** pointer to the store's controller */ + message_view_t *view; + /** vector of seaudit_message_t, as returned by + * seaudit_model_get_messages() */ + apol_vector_t *messages; + /** column that is currently being sorted; use OTHER_FIELD to + * indicate no sorting */ + gint sort_field; + /** current sort direction, either 1 or ascending or -1 for + * descending */ + int sort_dir; + /** unique integer for each instance of a model */ + gint stamp; +} message_view_store_t; + +typedef struct message_view_store_class +{ + GObjectClass parent_class; +} message_view_store_class_t; + +static GType message_view_store_get_type(void); +#define SEAUDIT_TYPE_MESSAGE_VIEW_STORE (message_view_store_get_type()) +#define SEAUDIT_IS_MESSAGE_VIEW_STORE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SEAUDIT_TYPE_MESSAGE_VIEW_STORE)) + +struct message_view +{ + seaudit_model_t *model; + toplevel_t *top; + /** toplevel of the view, currently a scrolled_window */ + GtkWidget *w; + /** actual GTK+ tree view widget that displays the rows and + * columns of message data */ + GtkTreeView *view; + /** GTK+ store that models messages within the tree */ + message_view_store_t *store; + /** filename for when this view was saved (could be NULL) */ + char *filename; + /** most recent filename for exported messages (could be NULL) */ + char *export_filename; +}; + +typedef seaudit_sort_t *(*sort_generator_fn_t) (int direction); + +struct view_column_record +{ + preference_field_e id; + const char *name; + const char *sample_text; + sort_generator_fn_t sort; +}; + +static const struct view_column_record column_data[] = { + {HOST_FIELD, "Hostname", "Hostname", seaudit_sort_by_host}, + {MESSAGE_FIELD, "Message", "Message", seaudit_sort_by_message_type}, + {DATE_FIELD, "Date", "Jan 01 00:00:00", seaudit_sort_by_date}, + {SUSER_FIELD, "Source\nUser", "Source", seaudit_sort_by_source_user}, + {SROLE_FIELD, "Source\nRole", "Source", seaudit_sort_by_source_role}, + {STYPE_FIELD, "Source\nType", "unlabeled_t", seaudit_sort_by_source_type}, + {SMLS_LVL_FIELD, "Source\nMLS Level", "MLS Level", seaudit_sort_by_source_mls_lvl}, + {SMLS_CLR_FIELD, "Source\nMLS Clearance", "MLS Clearance", seaudit_sort_by_source_mls_clr}, + {TUSER_FIELD, "Target\nUser", "Target", seaudit_sort_by_target_user}, + {TROLE_FIELD, "Target\nRole", "Target", seaudit_sort_by_target_role}, + {TTYPE_FIELD, "Target\nType", "unlabeled_t", seaudit_sort_by_target_type}, + {TMLS_LVL_FIELD, "Target\nMLS Level", "MLS Level", seaudit_sort_by_target_mls_lvl}, + {TMLS_CLR_FIELD, "Target\nMLS Clearance", "MLS Clearance", seaudit_sort_by_target_mls_clr}, + {OBJCLASS_FIELD, "Object\nClass", "Object", seaudit_sort_by_object_class}, + {PERM_FIELD, "Permission", "Permission", seaudit_sort_by_permission}, + {EXECUTABLE_FIELD, "Executable", "/usr/bin/cat", seaudit_sort_by_executable}, + {COMMAND_FIELD, "Command", "/usr/bin/cat", seaudit_sort_by_command}, + {NAME_FIELD, "Name", "iceweasel", seaudit_sort_by_name}, + {PID_FIELD, "PID", "12345", seaudit_sort_by_pid}, + {INODE_FIELD, "Inode", "123456", seaudit_sort_by_inode}, + {PATH_FIELD, "Path", "/home/gburdell/foo", seaudit_sort_by_path}, + {OTHER_FIELD, "Other", "Lorem ipsum dolor sit amet, consectetur", NULL} +}; + +static const size_t num_columns = sizeof(column_data) / sizeof(column_data[0]); + +/** + * (Re)sort the view based upon which column is clicked. If already + * sorting on this column, then reverse the sort direction. Also + * update the sort indicator for this column. + */ +static gboolean message_view_on_column_click(GtkTreeViewColumn * column, gpointer user_data) +{ + gint column_id = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(column), "column id")); + message_view_t *view = (message_view_t *) user_data; + int dir = 0; + seaudit_sort_t *sort; + GtkTreeViewColumn *prev_column; + if (column_id == view->store->sort_field) { + dir = view->store->sort_dir * -1; + } else { + dir = 1; + } + + if ((sort = column_data[(preference_field_e) column_id].sort(dir)) == NULL) { + toplevel_ERR(view->top, "%s", strerror(errno)); + return TRUE; + } + seaudit_model_clear_sorts(view->model); + if (seaudit_model_append_sort(view->model, sort) < 0) { + seaudit_sort_destroy(&sort); + toplevel_ERR(view->top, "%s", strerror(errno)); + } + prev_column = gtk_tree_view_get_column(view->view, view->store->sort_field); + if (prev_column != NULL) { + gtk_tree_view_column_set_sort_indicator(prev_column, FALSE); + } + gtk_tree_view_column_set_sort_indicator(column, TRUE); + if (dir > 0) { + gtk_tree_view_column_set_sort_order(column, GTK_SORT_ASCENDING); + } else { + gtk_tree_view_column_set_sort_order(column, GTK_SORT_DESCENDING); + } + + view->store->sort_field = column_id; + view->store->sort_dir = dir; + message_view_update_rows(view); + return TRUE; +} + +/*************** implementation of a custom GtkTreeModel ***************/ + +static GObjectClass *parent_class = NULL; + +static void message_view_store_init(message_view_store_t * m); +static void message_view_store_class_init(message_view_store_class_t * c); +static void message_view_store_tree_init(GtkTreeModelIface * iface); +static void message_view_store_finalize(GObject * object); +static GtkTreeModelFlags message_view_store_get_flags(GtkTreeModel * tree_model); +static gint message_view_store_get_n_columns(GtkTreeModel * tree_model); +static GType message_view_store_get_column_type(GtkTreeModel * tree_model, gint index); +static gboolean message_view_store_get_iter(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreePath * path); +static GtkTreePath *message_view_store_get_path(GtkTreeModel * tree_model, GtkTreeIter * iter); +static void message_view_store_get_value(GtkTreeModel * tree_model, GtkTreeIter * iter, gint column, GValue * value); +static gboolean message_view_store_iter_next(GtkTreeModel * tree_model, GtkTreeIter * iter); +static gboolean message_view_store_iter_children(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreeIter * parent); +static gboolean message_view_store_iter_has_child(GtkTreeModel * tree_model, GtkTreeIter * iter); +static gint message_view_store_iter_n_children(GtkTreeModel * tree_model, GtkTreeIter * iter); +static gboolean message_view_store_iter_nth_child(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreeIter * parent, gint n); +static gboolean message_view_store_iter_parent(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreeIter * child); + +static GType message_view_store_get_type(void) +{ + static GType store_type = 0; + static const GTypeInfo store_info = { + sizeof(message_view_store_class_t), + NULL, + NULL, + (GClassInitFunc) message_view_store_class_init, + NULL, + NULL, + sizeof(message_view_store_t), + 0, + (GInstanceInitFunc) message_view_store_init + }; + static const GInterfaceInfo tree_model_info = { + (GInterfaceInitFunc) message_view_store_tree_init, + NULL, + NULL + }; + + if (store_type) + return store_type; + + store_type = g_type_register_static(G_TYPE_OBJECT, "message_view_store", &store_info, (GTypeFlags) 0); + g_type_add_interface_static(store_type, GTK_TYPE_TREE_MODEL, &tree_model_info); + return store_type; +} + +static void message_view_store_init(message_view_store_t * m) +{ + static int next_stamp = 0; + m->messages = NULL; + m->sort_field = OTHER_FIELD; + m->sort_dir = 1; + m->stamp = next_stamp++; +} + +static void message_view_store_class_init(message_view_store_class_t * c) +{ + GObjectClass *object_class; + parent_class = g_type_class_peek_parent(c); + object_class = (GObjectClass *) c; + object_class->finalize = message_view_store_finalize; +} + +static void message_view_store_tree_init(GtkTreeModelIface * iface) +{ + iface->get_flags = message_view_store_get_flags; + iface->get_n_columns = message_view_store_get_n_columns; + iface->get_column_type = message_view_store_get_column_type; + iface->get_iter = message_view_store_get_iter; + iface->get_path = message_view_store_get_path; + iface->get_value = message_view_store_get_value; + iface->iter_next = message_view_store_iter_next; + iface->iter_children = message_view_store_iter_children; + iface->iter_has_child = message_view_store_iter_has_child; + iface->iter_n_children = message_view_store_iter_n_children; + iface->iter_nth_child = message_view_store_iter_nth_child; + iface->iter_parent = message_view_store_iter_parent; +} + +static void message_view_store_finalize(GObject * object) +{ + (*parent_class->finalize) (object); +} + +static GtkTreeModelFlags message_view_store_get_flags(GtkTreeModel * tree_model) +{ + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), 0); + return GTK_TREE_MODEL_ITERS_PERSIST | GTK_TREE_MODEL_LIST_ONLY; +} + +static gint message_view_store_get_n_columns(GtkTreeModel * tree_model __attribute__ ((unused))) +{ + return OTHER_FIELD + 1; +} + +static GType message_view_store_get_column_type(GtkTreeModel * tree_model, gint idx __attribute__ ((unused))) +{ + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), G_TYPE_INVALID); + /* everything is a string for now */ + return G_TYPE_STRING; +} + +static gboolean message_view_store_get_iter(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreePath * path) +{ + gint i; + message_view_store_t *store = (message_view_store_t *) tree_model; + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), FALSE); + g_return_val_if_fail(gtk_tree_path_get_depth(path) > 0, FALSE); + i = gtk_tree_path_get_indices(path)[0]; + if (i >= apol_vector_get_size(store->messages)) + return FALSE; + + iter->stamp = store->stamp; + iter->user_data = apol_vector_get_element(store->messages, i); + iter->user_data2 = GINT_TO_POINTER(i); + iter->user_data3 = store->view; + return TRUE; +} + +static GtkTreePath *message_view_store_get_path(GtkTreeModel * tree_model, GtkTreeIter * iter) +{ + GtkTreePath *retval; + message_view_store_t *store = (message_view_store_t *) tree_model; + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), NULL); + g_return_val_if_fail(iter->stamp == store->stamp, NULL); + retval = gtk_tree_path_new(); + gtk_tree_path_append_index(retval, GPOINTER_TO_INT(iter->user_data2)); + return retval; +} + +/** + * Given a string, check that it is UTF8 legal. If not, or if the + * string is NULL, then return an empty string. Otherwise return the + * original string. + */ +static void message_view_to_utf8(GValue * value, const char *s) +{ + if (s == NULL || !g_utf8_validate(s, -1, NULL)) { + g_value_set_string(value, ""); + } + g_value_set_string(value, s); +} + +static void message_view_store_get_value(GtkTreeModel * tree_model, GtkTreeIter * iter, gint column, GValue * value) +{ + message_view_store_t *store; + message_view_t *view; + seaudit_message_t *m; + seaudit_message_type_e type; + void *data; + seaudit_avc_message_t *avc; + g_return_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model)); + g_return_if_fail(iter != NULL); + g_return_if_fail(column <= OTHER_FIELD); + g_value_init(value, G_TYPE_STRING); + store = (message_view_store_t *) tree_model; + view = store->view; + m = (seaudit_message_t *) iter->user_data; + data = seaudit_message_get_data(m, &type); + preference_field_e field = column; + + switch (field) { + case HOST_FIELD: + { + message_view_to_utf8(value, seaudit_message_get_host(m)); + return; + } + case MESSAGE_FIELD: + { + char *message = "Invalid"; + switch (type) { + case SEAUDIT_MESSAGE_TYPE_BOOL: + { + message = "Boolean"; + break; + } + case SEAUDIT_MESSAGE_TYPE_LOAD: + { + message = "Load"; + break; + } + case SEAUDIT_MESSAGE_TYPE_AVC: + { + avc = (seaudit_avc_message_t *) data; + seaudit_avc_message_type_e avc_type; + avc_type = seaudit_avc_message_get_message_type(avc); + switch (avc_type) { + case SEAUDIT_AVC_DENIED: + { + message = "Denied"; + break; + } + case SEAUDIT_AVC_GRANTED: + { + message = "Granted"; + break; + } + default: + { + /* should never get here */ + toplevel_ERR(view->top, "Got an invalid AVC message type %d!", avc_type); + assert(0); + return; + } + } + break; + } + default: + { + /* should never get here */ + toplevel_ERR(view->top, "Got an invalid message type %d!", type); + assert(0); + return; + } + } + message_view_to_utf8(value, message); + return; + } + case DATE_FIELD: + { + const struct tm *tm = seaudit_message_get_time(m); + char date[256]; + /* check to see if we have been given a valid year, if + * so display, otherwise no year displayed */ + if (tm->tm_year == 0) { + strftime(date, 256, "%b %d %H:%M:%S", tm); + } else { + strftime(date, 256, "%b %d %H:%M:%S %Y", tm); + } + message_view_to_utf8(value, date); + return; + } + case OTHER_FIELD: + { + char *other = seaudit_message_to_misc_string(m);; + if (other == NULL) { + toplevel_ERR(view->top, "%s", strerror(errno)); + return; + } + message_view_to_utf8(value, other); + free(other); + return; + } + default: /* FALLTHROUGH */ + break; + } + + if (type != SEAUDIT_MESSAGE_TYPE_AVC) { + /* the rest of the columns are blank for non-AVC + * messages */ + message_view_to_utf8(value, ""); + return; + } + avc = (seaudit_avc_message_t *) data; + + switch (field) { + case SUSER_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_source_user(avc)); + return; + } + case SROLE_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_source_role(avc)); + return; + } + case STYPE_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_source_type(avc)); + return; + } + case SMLS_LVL_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_source_mls_lvl(avc)); + return; + } + case SMLS_CLR_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_source_mls_clr(avc)); + return; + } + case TUSER_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_target_user(avc)); + return; + } + case TROLE_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_target_role(avc)); + return; + } + case TTYPE_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_target_type(avc)); + return; + } + case TMLS_LVL_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_target_mls_lvl(avc)); + return; + } + case TMLS_CLR_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_target_mls_clr(avc)); + return; + } + case OBJCLASS_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_object_class(avc)); + return; + } + case PERM_FIELD: + { + const apol_vector_t *perms = seaudit_avc_message_get_perm(avc); + char *perm = NULL; + size_t i, len = 0; + for (i = 0; perms != NULL && i < apol_vector_get_size(perms); i++) { + char *p = apol_vector_get_element(perms, i); + if (apol_str_appendf(&perm, &len, "%s%s", (i > 0 ? "," : ""), p) < 0) { + toplevel_ERR(view->top, "%s", strerror(errno)); + return; + } + } + message_view_to_utf8(value, perm); + free(perm); + return; + } + case EXECUTABLE_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_exe(avc)); + return; + } + case COMMAND_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_comm(avc)); + return; + } + case NAME_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_name(avc)); + return; + } + case PID_FIELD: + { + char *s; + if (asprintf(&s, "%u", seaudit_avc_message_get_pid(avc)) < 0) { + toplevel_ERR(view->top, "%s", strerror(errno)); + return; + } + message_view_to_utf8(value, s); + free(s); + return; + } + case INODE_FIELD: + { + char *s; + if (asprintf(&s, "%lu", seaudit_avc_message_get_inode(avc)) < 0) { + toplevel_ERR(view->top, "%s", strerror(errno)); + return; + } + message_view_to_utf8(value, s); + free(s); + return; + } + case PATH_FIELD: + { + message_view_to_utf8(value, seaudit_avc_message_get_path(avc)); + return; + } + default: /* FALLTHROUGH */ + break; + } + /* should never get here */ + toplevel_ERR(view->top, "Got an invalid column %d!", field); + assert(0); +} + +static gboolean message_view_store_iter_next(GtkTreeModel * tree_model, GtkTreeIter * iter) +{ + gint i; + message_view_store_t *store = (message_view_store_t *) tree_model; + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), FALSE); + g_return_val_if_fail(iter->stamp == store->stamp, FALSE); + if (iter == NULL || iter->user_data == NULL) + return FALSE; + i = GPOINTER_TO_INT(iter->user_data2) + 1; + if (i >= apol_vector_get_size(store->messages)) { + return FALSE; + } + iter->user_data = apol_vector_get_element(store->messages, i); + iter->user_data2 = GINT_TO_POINTER(i); + iter->user_data3 = store->view; + return TRUE; +} + +static gboolean message_view_store_iter_children(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreeIter * parent) +{ + message_view_store_t *store; + g_return_val_if_fail(parent == NULL || parent->user_data != NULL, FALSE); + if (parent) + return FALSE; + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), FALSE); + + /* set iterator to first row, if possible */ + store = (message_view_store_t *) tree_model; + if (store->messages == NULL || apol_vector_get_size(store->messages) == 0) + return FALSE; + + iter->stamp = store->stamp; + iter->user_data = apol_vector_get_element(store->messages, 0); + iter->user_data2 = GINT_TO_POINTER(0); + iter->user_data3 = store->view; + return TRUE; +} + +static gboolean message_view_store_iter_has_child(GtkTreeModel * tree_model __attribute__ ((unused)), GtkTreeIter * iter + __attribute__ ((unused))) +{ + return FALSE; +} + +static gint message_view_store_iter_n_children(GtkTreeModel * tree_model, GtkTreeIter * iter) +{ + message_view_store_t *store; + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), -1); + g_return_val_if_fail(iter == NULL || iter->user_data != NULL, 0); + store = (message_view_store_t *) tree_model; + /* return the number of rows, if iterator is at the top; + * otherwise return 0 because this store is just a list */ + if (iter != NULL || store->messages == NULL) { + return 0; + } + return apol_vector_get_size(store->messages); +} + +static gboolean message_view_store_iter_nth_child(GtkTreeModel * tree_model, GtkTreeIter * iter, GtkTreeIter * parent, gint n) +{ + message_view_store_t *store; + g_return_val_if_fail(SEAUDIT_IS_MESSAGE_VIEW_STORE(tree_model), FALSE); + store = (message_view_store_t *) tree_model; + if (store->messages == NULL || parent != NULL) { + return FALSE; + } + if (n >= apol_vector_get_size(store->messages)) { + return FALSE; + } + iter->stamp = store->stamp; + iter->user_data = apol_vector_get_element(store->messages, n); + iter->user_data2 = GINT_TO_POINTER(n); + iter->user_data3 = store->view; + return TRUE; +} + +static gboolean message_view_store_iter_parent(GtkTreeModel * tree_model __attribute__ ((unused)), GtkTreeIter * iter + __attribute__ ((unused)), GtkTreeIter * child __attribute__ ((unused))) +{ + return FALSE; +} + +/*************** end of custom GtkTreeModel implementation ***************/ + +/*************** message_view_messages_vector() callbacks ******************/ +#define LBACK 1 +#define LFORWARD 2 +#define LNOOP 255 + +typedef struct _msg_user_data +{ + message_view_t *view; + apol_vector_t *messages; + GtkDialog *dialog; + GtkTextBuffer *buffer; + gint handle_id; +} _msg_user_data_t; + +static void message_view_dialog_change(GtkTreeModel * tree_model, + GtkTreePath * path __attribute__ ((unused)), + GtkTreeIter * iter __attribute__ ((unused)), gpointer user_data) +{ + _msg_user_data_t *d = (_msg_user_data_t *) user_data; + /* Disconnect this signal handler after it fires. It's one + * shot, nothing should be able to bring the next and previous + * buttons back from the dead if the view ever changes. + */ + g_signal_handler_disconnect(tree_model, d->handle_id); + d->view = NULL; + gtk_dialog_set_response_sensitive(d->dialog, LBACK, FALSE); + gtk_dialog_set_response_sensitive(d->dialog, LFORWARD, FALSE); +} + +static void message_view_dialog_response(GtkDialog * dialog, gint response, gpointer user_data) +{ + _msg_user_data_t *d = (_msg_user_data_t *) user_data; + GtkTreePath *p; + GtkTreeIter tree_iter; + GtkTextIter text_iter; + size_t i; + gboolean go_back = FALSE; + gboolean go_forward = FALSE; + + gtk_text_buffer_set_text(d->buffer, "", -1); + p = apol_vector_get_element(d->messages, 0); + assert(p != NULL); + switch (response) { + case LNOOP: + break; /* no-op response, display and test only */ + case LBACK: + gtk_tree_path_prev(p); + break; + case LFORWARD: + gtk_tree_path_next(p); + break; + case GTK_RESPONSE_DELETE_EVENT: + case GTK_RESPONSE_CLOSE: + apol_vector_destroy(&d->messages); + if (d->view != NULL) { + g_signal_handler_disconnect(d->view->store, d->handle_id); + } + free(d); + gtk_widget_destroy(GTK_WIDGET(dialog)); + return; + default: + /* should never get here */ + toplevel_ERR(d->view->top, "Unhandled response type (%d).\n", response); + assert(0); + return; + } + assert(d->view != NULL); + + /* determine if the forward and backward buttons should be + enabled or not */ + if (apol_vector_get_size(d->messages) == 1) { + GtkTreePath *dupe_p = gtk_tree_path_copy(p); + if (dupe_p == NULL) { + toplevel_ERR(d->view->top, "%s", strerror(errno)); + return; + } + go_back = gtk_tree_path_prev(dupe_p); + gtk_tree_path_free(dupe_p); + + message_view_store_get_iter(GTK_TREE_MODEL(d->view->store), &tree_iter, p); + go_forward = gtk_tree_model_iter_next(GTK_TREE_MODEL(d->view->store), &tree_iter); + } + gtk_dialog_set_response_sensitive(dialog, LBACK, go_back); + gtk_dialog_set_response_sensitive(dialog, LFORWARD, go_forward); + + gtk_text_buffer_get_start_iter(d->buffer, &text_iter); + for (i = 0; i < apol_vector_get_size(d->messages); i++) { + char *s; + p = apol_vector_get_element(d->messages, i); + message_view_store_get_iter(GTK_TREE_MODEL(d->view->store), &tree_iter, p); + if ((s = seaudit_message_to_string(tree_iter.user_data)) == NULL) { + toplevel_ERR(d->view->top, "%s", strerror(errno)); + continue; + } + + gtk_text_buffer_insert(d->buffer, &text_iter, s, -1); + gtk_text_buffer_insert(d->buffer, &text_iter, "\n", -1); + free(s); + } +} + +/** + * Show all messages within the messages vector (vector of + * GtkTreePaths into the view's store). + * + * Callback function cb_view_message takes ownership of messages + * vector and state. + */ +static void message_view_messages_vector(message_view_t * view, apol_vector_t * messages) +{ + GtkWidget *window = NULL; + GtkWidget *scroll; + GtkWidget *text_view; + GtkTextBuffer *buffer; + _msg_user_data_t *state; + + state = malloc(sizeof(_msg_user_data_t)); + if (state == NULL) { + toplevel_ERR(view->top, "%s", strerror(errno)); + apol_vector_destroy(&messages); + return; + } + + window = gtk_dialog_new_with_buttons("View Messages", + toplevel_get_window(view->top), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_GO_BACK, LBACK, + GTK_STOCK_GO_FORWARD, LFORWARD, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL); + gtk_dialog_set_default_response(GTK_DIALOG(window), GTK_RESPONSE_CLOSE); + gtk_window_set_modal(GTK_WINDOW(window), FALSE); + scroll = gtk_scrolled_window_new(NULL, NULL); + text_view = gtk_text_view_new(); + gtk_window_set_default_size(GTK_WINDOW(window), 480, 300); + gtk_container_add(GTK_CONTAINER(GTK_DIALOG(window)->vbox), scroll); + gtk_container_add(GTK_CONTAINER(scroll), text_view); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view), GTK_WRAP_WORD); + gtk_widget_show(text_view); + gtk_widget_show(scroll); + + gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view), FALSE); + gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT); + + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view)); + state->view = view; + state->messages = messages; + state->dialog = GTK_DIALOG(window); + state->buffer = buffer; + g_signal_connect(window, "response", G_CALLBACK(message_view_dialog_response), state); + state->handle_id = g_signal_connect(view->store, "row-changed", G_CALLBACK(message_view_dialog_change), state); + message_view_dialog_response(GTK_DIALOG(window), LNOOP, state); + + gtk_widget_show_all(GTK_WIDGET(window)); +} + +/******************** handlers for right click menu ********************/ + +static void message_view_popup_on_view_message_activate(GtkMenuItem * menuitem, gpointer user_data __attribute__ ((unused))) +{ + message_view_t *v = g_object_get_data(G_OBJECT(menuitem), "view-object"); + message_view_entire_message(v); +} + +static void message_view_popup_on_find_terules_activate(GtkMenuItem * menuitem, gpointer user_data) +{ + message_view_t *v = g_object_get_data(G_OBJECT(menuitem), "view-object"); + toplevel_find_terules(v->top, (seaudit_message_t *) user_data); +} + +static void message_view_popup_on_export_selected_messages_activate(GtkMenuItem * menuitem, gpointer user_data + __attribute__ ((unused))) +{ + message_view_t *v = g_object_get_data(G_OBJECT(menuitem), "view-object"); + message_view_export_selected_messages(v); +} + +static void message_view_popup_menu(GtkWidget * treeview, GdkEventButton * event, message_view_t * view, + seaudit_message_t * message) +{ + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); + gint num_selected_rows = gtk_tree_selection_count_selected_rows(selection); + GtkWidget *menu, *menuitem, *menuitem2, *menuitem3; + int button, event_time; + + menu = gtk_menu_new(); + if (num_selected_rows == 1) { + menuitem = gtk_menu_item_new_with_label("View Selected Message"); + menuitem3 = gtk_menu_item_new_with_label("Export Selected Message..."); + } else { + menuitem = gtk_menu_item_new_with_label("View Selected Messages"); + menuitem3 = gtk_menu_item_new_with_label("Export Selected Messages..."); + } + menuitem2 = gtk_menu_item_new_with_label("Find TERules using Message..."); + g_signal_connect(menuitem, "activate", (GCallback) message_view_popup_on_view_message_activate, message); + g_signal_connect(menuitem2, "activate", (GCallback) message_view_popup_on_find_terules_activate, message); + g_signal_connect(menuitem3, "activate", (GCallback) message_view_popup_on_export_selected_messages_activate, NULL); + g_object_set_data(G_OBJECT(menuitem), "view-object", view); + g_object_set_data(G_OBJECT(menuitem2), "view-object", view); + g_object_set_data(G_OBJECT(menuitem3), "view-object", view); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem2); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem3); + gtk_widget_show_all(menu); + if (toplevel_get_policy(view->top) == NULL) { + gtk_widget_set_sensitive(menuitem2, FALSE); + } + + if (event) { + button = event->button; + event_time = event->time; + } else { + button = 0; + event_time = gtk_get_current_event_time(); + } + gtk_menu_attach_to_widget(GTK_MENU(menu), treeview, NULL); + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, button, event_time); +} + +static gboolean message_view_delayed_selection_menu_item(gpointer data) +{ + message_view_t *view = (message_view_t *) data; + toplevel_update_selection_menu_item(view->top); + return FALSE; +} + +static gboolean message_view_on_button_press(GtkWidget * treeview, GdkEventButton * event, gpointer user_data) +{ + message_view_t *view = (message_view_t *) user_data; + if (event->type == GDK_BUTTON_PRESS && event->button == 3) { + GtkTreePath *path = NULL; + GtkTreeIter iter; + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); + if (!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeview), event->x, event->y, &path, NULL, NULL, NULL)) { + return FALSE; + } + /* if the right click occurred on an unselected row, remove + * all selections and select the item under the pointer */ + if (!gtk_tree_selection_path_is_selected(selection, path)) { + gtk_tree_selection_unselect_all(selection); + gtk_tree_selection_select_path(selection, path); + } + message_view_store_get_iter(GTK_TREE_MODEL(view->store), &iter, path); + /* popup a menu for the row that was clicked */ + message_view_popup_menu(treeview, event, view, (seaudit_message_t *) iter.user_data); + return TRUE; + } else if (event->type == GDK_BUTTON_PRESS && event->button == 1) { + /* n.b.: rows can be selected but never deselected. + * delay updating the menu, for upon the first click + * there is not a selection yet */ + g_idle_add(&message_view_delayed_selection_menu_item, view); + return FALSE; + } else if (event->type == GDK_2BUTTON_PRESS && event->button == 1){ + /* Show message on double click */ + message_view_entire_message(view); + } + return FALSE; +} + +static void message_view_gtk_tree_path_free(gpointer data, gpointer user_data __attribute__ ((unused))) +{ + gtk_tree_path_free((GtkTreePath *) data); +} + +static gboolean message_view_on_popup_menu(GtkWidget * treeview, gpointer user_data) +{ + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); + GList *glist = gtk_tree_selection_get_selected_rows(selection, NULL); + message_view_t *view = (message_view_t *) user_data; + GtkTreePath *path; + GtkTreeIter iter; + if (glist == NULL) { + return FALSE; + } + path = g_list_nth_data(glist, 0); + message_view_store_get_iter(GTK_TREE_MODEL(view->store), &iter, path); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + message_view_popup_menu(treeview, NULL, view, (seaudit_message_t *) iter.user_data); + return TRUE; +} + +static void message_view_on_row_activate(GtkTreeView * tree_view __attribute__ ((unused)), GtkTreePath * path + __attribute__ ((unused)), GtkTreeViewColumn * column + __attribute__ ((unused)), gpointer user_data) +{ + message_view_t *view = (message_view_t *) user_data; + toplevel_update_selection_menu_item(view->top); +} + +/******************** other public functions below ********************/ + +message_view_t *message_view_create(toplevel_t * top, seaudit_model_t * model, const char *filename) +{ + message_view_t *view; + GtkTreeSelection *selection; + GtkCellRenderer *renderer; + size_t i; + + if ((view = calloc(1, sizeof(*view))) == NULL || (filename != NULL && (view->filename = strdup(filename)) == NULL)) { + int error = errno; + toplevel_ERR(top, "%s", strerror(error)); + message_view_destroy(&view); + errno = error; + return NULL; + } + view->top = top; + view->model = model; + view->store = (message_view_store_t *) g_object_new(SEAUDIT_TYPE_MESSAGE_VIEW_STORE, NULL); + view->store->view = view; + view->store->sort_field = OTHER_FIELD; + view->store->sort_dir = 1; + view->w = gtk_scrolled_window_new(NULL, NULL); + view->view = GTK_TREE_VIEW(gtk_tree_view_new_with_model(GTK_TREE_MODEL(view->store))); + selection = gtk_tree_view_get_selection(view->view); + gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE); + gtk_container_add(GTK_CONTAINER(view->w), GTK_WIDGET(view->view)); + gtk_widget_show(GTK_WIDGET(view->view)); + gtk_widget_show(view->w); + + renderer = gtk_cell_renderer_text_new(); + for (i = 0; i < num_columns; i++) { + struct view_column_record r = column_data[i]; + PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(view->view), r.sample_text); + gint width; + GtkTreeViewColumn *column; + pango_layout_get_pixel_size(layout, &width, NULL); + g_object_unref(G_OBJECT(layout)); + width += 12; + column = gtk_tree_view_column_new_with_attributes(r.name, renderer, "text", r.id, NULL); + gtk_tree_view_column_set_clickable(column, TRUE); + gtk_tree_view_column_set_resizable(column, TRUE); + if (r.sort != NULL) { + g_object_set_data(G_OBJECT(column), "column id", GINT_TO_POINTER(r.id)); + g_signal_connect_after(G_OBJECT(column), "clicked", G_CALLBACK(message_view_on_column_click), view); + } + gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED); + gtk_tree_view_column_set_fixed_width(column, width); + gtk_tree_view_append_column(view->view, column); + } + + g_signal_connect(G_OBJECT(view->view), "button-press-event", G_CALLBACK(message_view_on_button_press), view); + g_signal_connect(G_OBJECT(view->view), "popup-menu", G_CALLBACK(message_view_on_popup_menu), view); + g_signal_connect(G_OBJECT(view->view), "row-activated", G_CALLBACK(message_view_on_row_activate), view); + message_view_update_visible_columns(view); + message_view_update_rows(view); + return view; +} + +void message_view_destroy(message_view_t ** view) +{ + if (view != NULL && *view != NULL) { + /* emit a signal to force all message view dialogs to + disable their scroll buttons. need to pass a + non-NULL path in the signal handler to make GTK + shut up */ + GtkTreePath *path = gtk_tree_path_new_first(); + GtkTreeIter iter; + gtk_tree_model_get_iter(GTK_TREE_MODEL((*view)->store), &iter, path); + g_signal_emit_by_name((*view)->store, "row-changed", path, &iter); + gtk_tree_path_free(path); + seaudit_model_destroy(&(*view)->model); + apol_vector_destroy(&((*view)->store->messages)); + g_free((*view)->filename); + g_free((*view)->export_filename); + /* let glib handle destruction of object */ + g_object_unref((*view)->store); + free(*view); + *view = NULL; + } +} + +seaudit_model_t *message_view_get_model(message_view_t * view) +{ + return view->model; +} + +void message_view_set_model(message_view_t * view, seaudit_model_t * model) +{ + seaudit_model_destroy(&view->model); + view->model = model; + toplevel_update_tabs(view->top); + message_view_update_rows(view); +} + +GtkWidget *message_view_get_view(message_view_t * view) +{ + return view->w; +} + +size_t message_view_get_num_log_messages(message_view_t * view) +{ + if (view->store->messages == NULL) { + return 0; + } + return apol_vector_get_size(view->store->messages); +} + +gboolean message_view_is_message_selected(message_view_t * view) +{ + GtkTreeSelection *selection = gtk_tree_view_get_selection(view->view); + GList *glist = gtk_tree_selection_get_selected_rows(selection, NULL); + if (glist == NULL) { + return FALSE; + } + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + return TRUE; +} + +void message_view_vector_gtk_tree_path_free(void *elem) +{ + GtkTreePath *path = (GtkTreePath *) elem; + gtk_tree_path_free(path); +} + +void message_view_entire_message(message_view_t * view) +{ + GtkTreeSelection *selection = gtk_tree_view_get_selection(view->view); + GList *glist = gtk_tree_selection_get_selected_rows(selection, NULL); + GList *l; + apol_vector_t *messages; + if (glist == NULL) { + return; + } + if ((messages = apol_vector_create(message_view_vector_gtk_tree_path_free)) == NULL) { + toplevel_ERR(view->top, "%s", strerror(errno)); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + return; + } + for (l = glist; l != NULL; l = l->next) { + GtkTreePath *path = gtk_tree_path_copy((GtkTreePath *) l->data); + if (path == NULL || apol_vector_append(messages, path) < 0) { + toplevel_ERR(view->top, "%s", strerror(errno)); + gtk_tree_path_free(path); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + apol_vector_destroy(&messages); + return; + } + } + /* the following function takes ownership of messages vector */ + message_view_messages_vector(view, messages); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); +} + +void message_view_save(message_view_t * view) +{ + if (view->filename == NULL) { + GtkWindow *parent = toplevel_get_window(view->top); + char *path = util_save_file(parent, "Save View", NULL); + if (path == NULL) { + return; + } + view->filename = path; + } + if (seaudit_model_save_to_file(view->model, view->filename) < 0) { + toplevel_ERR(view->top, "Error saving view: %s", strerror(errno)); + } +} + +void message_view_saveas(message_view_t * view) +{ + GtkWindow *parent = toplevel_get_window(view->top); + char *path = util_save_file(parent, "Save View As", view->filename); + if (path == NULL) { + return; + } + g_free(view->filename); + view->filename = path; + if (seaudit_model_save_to_file(view->model, view->filename) < 0) { + toplevel_ERR(view->top, "Error saving view: %s", strerror(errno)); + } +} + +void message_view_modify(message_view_t * view) +{ + if (modify_view_run(view->top, view)) { + toplevel_update_status_bar(view->top); + } +} + +void message_view_clear(message_view_t * view) +{ + size_t i; + for (i = 0; i < apol_vector_get_size(view->store->messages); i++) { + seaudit_message_t *m = apol_vector_get_element(view->store->messages, i); + seaudit_model_hide_message(view->model, m); + } + message_view_update_rows(view); +} + +/** + * Write to a file all messages in the given vector. Upon success, + * update the view object's export filename. + * + * @param view View containing messages to write. + * @param path Destination to write file, overwriting existing files + * as necessary. + * @param messages Vector of seaudit_message_t. + */ +static void message_view_export_messages_vector(message_view_t * view, char *path, apol_vector_t * messages) +{ + FILE *f = NULL; + size_t i; + g_free(view->export_filename); + view->export_filename = path; + if ((f = fopen(path, "w")) == NULL) { + toplevel_ERR(view->top, "Could not open %s for writing.", path); + goto cleanup; + } + for (i = 0; i < apol_vector_get_size(messages); i++) { + seaudit_message_t *m = apol_vector_get_element(messages, i); + char *s = seaudit_message_to_string(m); + if (s == NULL || fprintf(f, "%s\n", s) < 0) { + toplevel_ERR(view->top, "Error writing string: %s", strerror(errno)); + goto cleanup; + } + free(s); + } + cleanup: + if (f != NULL) { + fclose(f); + } +} + +void message_view_export_all_messages(message_view_t * view) +{ + GtkWindow *parent = toplevel_get_window(view->top); + char *path = util_save_file(parent, "Export Messages", view->export_filename); + apol_vector_t *messages = view->store->messages; + if (path == NULL) { + return; + } + message_view_export_messages_vector(view, path, messages); +} + +void message_view_export_selected_messages(message_view_t * view) +{ + GtkWindow *parent = toplevel_get_window(view->top); + char *path; + GtkTreeSelection *selection = gtk_tree_view_get_selection(view->view); + GList *glist = gtk_tree_selection_get_selected_rows(selection, NULL); + GList *l; + apol_vector_t *messages; + if (glist == NULL) { + return; + } + path = util_save_file(parent, "Export Selected Messages", view->export_filename); + if (path == NULL) { + return; + } + if ((messages = apol_vector_create(NULL)) == NULL) { + toplevel_ERR(view->top, "%s", strerror(errno)); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + return; + } + for (l = glist; l != NULL; l = l->next) { + GtkTreePath *tree_path = (GtkTreePath *) l->data; + GtkTreeIter iter; + message_view_store_get_iter(GTK_TREE_MODEL(view->store), &iter, tree_path); + if (apol_vector_append(messages, iter.user_data) < 0) { + toplevel_ERR(view->top, "%s", strerror(errno)); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + apol_vector_destroy(&messages); + return; + } + } + message_view_export_messages_vector(view, path, messages); + g_list_foreach(glist, message_view_gtk_tree_path_free, NULL); + g_list_free(glist); + apol_vector_destroy(&messages); +} + +/** + * Given the name of a column, return its column record data. + */ +static const struct view_column_record *get_record(const char *name) +{ + size_t i; + for (i = 0; i < num_columns; i++) { + const struct view_column_record *r = column_data + i; + if (strcmp(r->name, name) == 0) { + return r; + } + } + return NULL; +} + +void message_view_update_visible_columns(message_view_t * view) +{ + GList *columns, *c; + preferences_t *prefs = toplevel_get_prefs(view->top); + columns = gtk_tree_view_get_columns(view->view); + c = columns; + while (c != NULL) { + GtkTreeViewColumn *vc = GTK_TREE_VIEW_COLUMN(c->data); + const gchar *title = gtk_tree_view_column_get_title(vc); + const struct view_column_record *r = get_record(title); + if (preferences_is_column_visible(prefs, r->id)) { + gtk_tree_view_column_set_visible(vc, TRUE); + } else { + gtk_tree_view_column_set_visible(vc, FALSE); + } + c = g_list_next(c); + } + g_list_free(columns); +} + +void message_view_update_rows(message_view_t * view) +{ + /* remove all existing rows, then insert them back into the + * view according to the model. automatically scroll to the + * same seleceted row(s). */ + GtkTreeSelection *selection; + GList *rows, *r, *selected = NULL; + GtkTreePath *path; + GtkTreeIter iter; + seaudit_log_t *log; + size_t i, num_old_messages = 0, num_new_messages = 0, num_changed; + int first_scroll = 0; + + if (!seaudit_model_is_changed(view->model)) { + return; + } + + /* convert the current selection into a GList of message + * pointers */ + selection = gtk_tree_view_get_selection(view->view); + rows = gtk_tree_selection_get_selected_rows(selection, NULL); + for (r = rows; r != NULL; r = r->next) { + path = (GtkTreePath *) r->data; + message_view_store_get_iter(GTK_TREE_MODEL(view->store), &iter, path); + selected = g_list_prepend(selected, iter.user_data); + } + g_list_foreach(rows, message_view_gtk_tree_path_free, NULL); + g_list_free(rows); + + log = toplevel_get_log(view->top); + if (view->store->messages != NULL) { + num_old_messages = apol_vector_get_size(view->store->messages); + } + apol_vector_destroy(&view->store->messages); + if (log != NULL) { + view->store->messages = seaudit_model_get_messages(log, view->model); + num_new_messages = apol_vector_get_size(view->store->messages); + } + gtk_tree_selection_unselect_all(selection); + + /* mark which rows have been changed/removed/inserted. do + * this as a single pass, rather than a two pass + * mark-and-sweep, for GTK+ tree views can be somewhat slow */ + num_changed = num_old_messages; + if (num_new_messages < num_changed) { + num_changed = num_new_messages; + } + for (i = 0; i < num_changed; i++) { + path = gtk_tree_path_new(); + gtk_tree_path_append_index(path, i); + iter.user_data = apol_vector_get_element(view->store->messages, i); + iter.user_data2 = GINT_TO_POINTER(i); + iter.user_data3 = view; + gtk_tree_model_row_changed(GTK_TREE_MODEL(view->store), path, &iter); + for (r = selected; r != NULL; r = r->next) { + if (r->data == iter.user_data) { + gtk_tree_selection_select_iter(selection, &iter); + if (!first_scroll) { + gtk_tree_view_scroll_to_cell(view->view, path, NULL, FALSE, 0.0, 0.0); + first_scroll = 1; + } + break; + } + } + gtk_tree_path_free(path); + } + if (num_old_messages > num_changed) { + /* delete in reverse order, else indices get renumbered */ + for (i = num_old_messages; i > num_changed; i--) { + path = gtk_tree_path_new(); + gtk_tree_path_append_index(path, i - 1); + gtk_tree_model_row_deleted(GTK_TREE_MODEL(view->store), path); + gtk_tree_path_free(path); + } + } else { + for (; i < num_new_messages; i++) { + path = gtk_tree_path_new(); + gtk_tree_path_append_index(path, i); + iter.user_data = apol_vector_get_element(view->store->messages, i); + iter.user_data2 = GINT_TO_POINTER(i); + iter.user_data3 = view; + gtk_tree_model_row_inserted(GTK_TREE_MODEL(view->store), path, &iter); + for (r = selected; r != NULL; r = r->next) { + if (r->data == iter.user_data) { + gtk_tree_selection_select_iter(selection, &iter); + if (!first_scroll) { + gtk_tree_view_scroll_to_cell(view->view, path, NULL, FALSE, 0.0, 0.0); + first_scroll = 1; + } + break; + } + } + gtk_tree_path_free(path); + } + } + g_list_free(selected); +} |