time before a played track can be picked at random. The default is
8 hours (which matches the earlier behaviour).
+** Disobedience
+
+There is now a new user management window. From here you can add and
+remove users or modify their settings.
+
* Changes up to version 3.0.2
Builds --without-server should work again.
where PORT is the same as 'listen PORT' on the server.
- 3. Copy the password file for each user to ~USER/.disorder/passwd, the
+ 3. Copy the password file for each user to ~USERNAME/.disorder/passwd, the
contents being:
password PASSWORD
int (*isarg)(const char *);
const char *argstr, *desc;
} commands[] = {
- { "adduser", 2, 3, cf_adduser, isarg_rights, "USER PASSWORD [RIGHTS]",
+ { "adduser", 2, 3, cf_adduser, isarg_rights, "USERNAME PASSWORD [RIGHTS]",
"Create a new user" },
{ "allfiles", 1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]",
"List all files and directories in DIR" },
- { "authorize", 1, 2, cf_authorize, isarg_rights, "USER [RIGHTS]",
- "Authorize USER to connect to the server" },
- { "deluser", 1, 1, cf_deluser, 0, "USER",
- "Delete a user" },
+ { "authorize", 1, 2, cf_authorize, isarg_rights, "USERNAME [RIGHTS]",
+ "Authorize user USERNAME to connect to the server" },
+ { "deluser", 1, 1, cf_deluser, 0, "USERNAME",
+ "Delete user USERNAME" },
{ "dirs", 1, 2, cf_dirs, isarg_regexp, "DIR [~REGEXP]",
"List directories in DIR" },
{ "disable", 0, 0, cf_disable, 0, "",
"Disable play" },
{ "disable-random", 0, 0, cf_random_disable, 0, "",
"Disable random play" },
- { "edituser", 3, 3, cf_edituser, 0, "USER PROPERTY VALUE",
- "Set a property of a user" },
+ { "edituser", 3, 3, cf_edituser, 0, "USERNAME PROPERTY VALUE",
+ "Set a property of user USERNAME" },
{ "enable", 0, 0, cf_enable, 0, "",
"Enable play" },
{ "enable-random", 0, 0, cf_random_enable, 0, "",
"Unset a preference" },
{ "unset-global", 1, 1, cf_unset_global, 0, "NAME",
"Unset a global preference" },
- { "userinfo", 2, 2, cf_userinfo, 0, "USER PROPERTY",
- "Get a property of as user" },
+ { "userinfo", 2, 2, cf_userinfo, 0, "USERNAME PROPERTY",
+ "Get a property of a user" },
{ "users", 0, 0, cf_users, 0, "",
"List all users" },
{ "version", 0, 0, cf_version, 0, "",
+
# Process this file with autoconf to produce a configure script.
#
# This file is part of DisOrder.
if test ! -z "$missing_functions"; then
AC_MSG_ERROR([missing functions:$missing_functions])
fi
+
+# Functions we can take or leave
+AC_CHECK_FUNCS([fls])
+
if test $want_server = yes; then
# <db.h> had better be version 3 or later
AC_CACHE_CHECK([db.h version],[rjk_cv_db_version],[
disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c \
choose.c misc.c control.c properties.c menu.c \
log.c progress.c login.c rtp.c help.c \
- ../lib/memgc.c settings.c
+ ../lib/memgc.c settings.c users.c
disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
$(LIBASOUND) $(COREAUDIO) $(LIBDB)
disobedience_LDFLAGS=$(GTK_LIBS)
struct choosenode *choosenode; /* gtkchoose.c got_files/got_dirs */
struct queuelike *ql; /* gtkqueue.c queuelike_completed */
struct prefdata *f; /* properties.c */
+ const char *user; /* users.c */
+ struct {
+ const char *user, *email; /* users.c */
+ } edituser;
} u;
};
const gchar *stock;
void (*clicked)(GtkButton *button, gpointer userdata);
const char *tip;
+ GtkWidget *widget;
};
/* Variables --------------------------------------------------------------- */
GtkWidget *scroll_widget(GtkWidget *child);
/* Wrap a widget up for scrolling */
+GtkWidget *frame_widget(GtkWidget *w, const char *title);
+
GdkPixbuf *find_image(const char *name);
/* Get the pixbuf for an image. Returns a null pointer if it cannot be
* found. */
void popup_msg(GtkMessageType mt, const char *msg);
-/* Pop up a message */
+void popup_submsg(GtkWidget *parent, GtkMessageType mt, const char *msg);
void fpopup_msg(GtkMessageType mt, const char *fmt, ...);
GtkWidget *iconbutton(const char *path, const char *tip);
-GtkWidget *create_buttons(const struct button *buttons,
+GtkWidget *create_buttons(struct button *buttons,
size_t nbuttons);
+GtkWidget *create_buttons_box(struct button *buttons,
+ size_t nbuttons,
+ GtkWidget *box);
void register_monitor(monitor_callback *callback,
void *u,
/* Called whenever the main menu might need to change. PAGE is the current
* page if known or -1 otherwise. */
+void users_set_sensitive(int sensitive);
/* Controls */
GtkWidget *login_window;
+/* User management */
+
+void manage_users(void);
+
/* Help */
void popup_help(void);
}
/* Buttons that appear at the bottom of the window */
-static const struct button buttons[] = {
+static struct button buttons[] = {
{
"Login",
login_ok,
"(Re-)connect using these settings",
+ 0
},
{
GTK_STOCK_CLOSE,
login_cancel,
- "Discard changes and close window"
+ "Discard changes and close window",
+ 0
},
};
TRUE/*expand*/, TRUE/*fill*/, 1/*padding*/);
gtk_box_pack_start(GTK_BOX(vbox), buttonbox,
FALSE/*expand*/, FALSE/*fill*/, 1/*padding*/);
- gtk_container_add(GTK_CONTAINER(login_window), vbox);
+ gtk_container_add(GTK_CONTAINER(login_window), frame_widget(vbox, NULL));
gtk_window_set_transient_for(GTK_WINDOW(login_window),
GTK_WINDOW(toplevel));
gtk_widget_show_all(login_window);
login_box();
}
+/** @brief Called when the login option is activated */
+static void users(gpointer attribute((unused)) callback_data,
+ guint attribute((unused)) callback_action,
+ GtkWidget attribute((unused)) *menu_item) {
+ manage_users();
+}
+
#if 0
/** @brief Called when the settings option is activated */
static void settings(gpointer attribute((unused)) callback_data,
t->selectall_sensitive(tab));
gtk_widget_set_sensitive(selectnone_widget,
t->selectnone_sensitive(tab));
+ /* TODO Users should only be sensitive if have RIGHT_ADMIN */
}
/** @brief Fetch version in order to display the about... popup */
gtk_widget_destroy(w);
}
+/** @brief Set 'Manage Users' menu item sensitivity */
+void users_set_sensitive(int sensitive) {
+ GtkWidget *w = gtk_item_factory_get_widget(mainmenufactory,
+ "<GdisorderMain>/Server/Manage users");
+ gtk_widget_set_sensitive(w, sensitive);
+}
+
+/** @brief Called with current user's rights string */
+static void menu_got_rights(void attribute((unused)) *v, const char *value) {
+ rights_type r;
+
+ if(parse_rights(value, &r, 0))
+ r = 0;
+ users_set_sensitive(!!(r & RIGHT_ADMIN));
+}
+
+/** @brief Called when we need to reset state */
+static void menu_reset(void) {
+ users_set_sensitive(0); /* until we know better */
+ disorder_eclient_userinfo(client, menu_got_rights, config->username, "rights",
+ 0);
+}
+
/** @brief Create the menu bar widget */
GtkWidget *menubar(GtkWidget *w) {
GtkWidget *m;
static const GtkItemFactoryEntry entries[] = {
{
- (char *)"/File", /* path */
+ (char *)"/Server", /* path */
0, /* accelerator */
0, /* callback */
0, /* callback_action */
0 /* extra_data */
},
{
- (char *)"/File/Login", /* path */
+ (char *)"/Server/Login", /* path */
(char *)"<CTRL>L", /* accelerator */
login, /* callback */
0, /* callback_action */
0, /* item_type */
0 /* extra_data */
},
+ {
+ (char *)"/Server/Manage users", /* path */
+ 0, /* accelerator */
+ users, /* callback */
+ 0, /* callback_action */
+ 0, /* item_type */
+ 0 /* extra_data */
+ },
#if 0
{
- (char *)"/File/Settings", /* path */
+ (char *)"/Server/Settings", /* path */
0, /* accelerator */
settings, /* callback */
0, /* callback_action */
},
#endif
{
- (char *)"/File/Quit Disobedience", /* path */
+ (char *)"/Server/Quit Disobedience", /* path */
(char *)"<CTRL>Q", /* accelerator */
quit_program, /* callback */
0, /* callback_action */
assert(selectall_widget != 0);
assert(selectnone_widget != 0);
assert(properties_widget != 0);
+ register_reset(menu_reset);
+ menu_reset();
m = gtk_item_factory_get_widget(mainmenufactory,
"<GdisorderMain>");
set_tool_colors(m);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller),
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
- if(GTK_IS_LAYOUT(child)) {
+ if(GTK_IS_LAYOUT(child)
+ || GTK_IS_TREE_VIEW(child)) {
/* Child widget has native scroll support */
gtk_container_add(GTK_CONTAINER(scroller), child);
/* Fix up the step increments if they are 0 (seems like an odd default?) */
return scroller;
}
+/** @brief Put a frame round a widget
+ * @param w Widget
+ * @param label Label or NULL
+ * @return Frame widget
+ */
+GtkWidget *frame_widget(GtkWidget *w, const char *label) {
+ GtkWidget *const frame = gtk_frame_new(label);
+ GtkWidget *const hbox = gtk_hbox_new(FALSE, 0);
+ GtkWidget *const vbox = gtk_vbox_new(FALSE, 0);
+ /* We want 4 pixels outside the frame boundary... */
+ gtk_container_set_border_width(GTK_CONTAINER(frame), 4);
+ /* ...and 4 pixels inside */
+ gtk_box_pack_start(GTK_BOX(hbox), w, TRUE/*expand*/, TRUE/*fill*/, 4);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE/*expand*/, TRUE/*fill*/, 4);
+ gtk_container_add(GTK_CONTAINER(frame), vbox);
+ return frame;
+}
+
/** @brief Find an image
* @param name Relative path to image
* @return pixbuf containing image
/** @brief Pop up a message */
void popup_msg(GtkMessageType mt, const char *msg) {
+ popup_submsg(toplevel, mt, msg);
+}
+
+void popup_submsg(GtkWidget *parent, GtkMessageType mt, const char *msg) {
GtkWidget *w;
- w = gtk_message_dialog_new(GTK_WINDOW(toplevel),
+ w = gtk_message_dialog_new(GTK_WINDOW(parent),
GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
mt,
GTK_BUTTONS_CLOSE,
return button;
}
-/** @brief Create buttons and pack them into an hbox */
-GtkWidget *create_buttons(const struct button *buttons,
- size_t nbuttons) {
+/** @brief Create buttons and pack them into a box, which is returned */
+GtkWidget *create_buttons_box(struct button *buttons,
+ size_t nbuttons,
+ GtkWidget *box) {
size_t n;
- GtkWidget *const hbox = gtk_hbox_new(FALSE, 1);
for(n = 0; n < nbuttons; ++n) {
- GtkWidget *const button = gtk_button_new_from_stock(buttons[n].stock);
- gtk_widget_set_style(button, tool_style);
- g_signal_connect(G_OBJECT(button), "clicked",
+ buttons[n].widget = gtk_button_new_from_stock(buttons[n].stock);
+ gtk_widget_set_style(buttons[n].widget, tool_style);
+ g_signal_connect(G_OBJECT(buttons[n].widget), "clicked",
G_CALLBACK(buttons[n].clicked), 0);
- gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 1);
- gtk_tooltips_set_tip(tips, button, buttons[n].tip, "");
+ gtk_box_pack_start(GTK_BOX(box), buttons[n].widget, FALSE, FALSE, 1);
+ gtk_tooltips_set_tip(tips, buttons[n].widget, buttons[n].tip, "");
}
- return hbox;
+ return box;
}
+/** @brief Create buttons and pack them into an hbox */
+GtkWidget *create_buttons(struct button *buttons,
+ size_t nbuttons) {
+ return create_buttons_box(buttons, nbuttons,
+ gtk_hbox_new(FALSE, 1));
+}
+
+
+
/*
Local Variables:
c-basic-offset:2
#define NPREFS (int)(sizeof prefs / sizeof *prefs)
/* Buttons that appear at the bottom of the window */
-static const struct button buttons[] = {
+static struct button buttons[] = {
{
GTK_STOCK_OK,
properties_ok,
- "Apply all changes and close window"
+ "Apply all changes and close window",
+ 0
},
{
GTK_STOCK_APPLY,
properties_apply,
- "Apply all changes and keep window open"
+ "Apply all changes and keep window open",
+ 0
},
{
GTK_STOCK_CANCEL,
properties_cancel,
- "Discard all changes and close window"
+ "Discard all changes and close window",
+ 0
},
};
scroll_widget(properties_table),
TRUE, TRUE, 1);
gtk_box_pack_start(GTK_BOX(vbox), buttonbox, FALSE, FALSE, 1);
- gtk_container_add(GTK_CONTAINER(properties_window), vbox);
+ gtk_container_add(GTK_CONTAINER(properties_window), frame_widget(vbox, NULL));
/* The table only really wants to be vertically scrollable */
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(GTK_WIDGET(properties_table)->parent->parent),
GTK_POLICY_NEVER,
1, 1);
}
}
- gtk_container_add(GTK_CONTAINER(settings_window), table);
+ gtk_container_add(GTK_CONTAINER(settings_window), frame_widget(table, NULL));
gtk_widget_show_all(settings_window);
/* TODO: save settings
TODO: web browser
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file disobedience/users.c
+ * @brief User management for Disobedience
+ *
+ * The user management window contains:
+ * - a list of all the users
+ * - an add button
+ * - a delete button
+ * - a user details panel
+ * - an apply button
+ *
+ * When you select a user that user's details are displayed to the right of the
+ * list. Hit the Apply button and any changes are applied.
+ *
+ * When you select 'add' a new empty set of details are displayed to be edited.
+ * Again Apply will commit them.
+ *
+ * TODO: it would be really nice if the Username entry could be removed and new
+ * user names entered in the list, rather off in the details panel. This may
+ * be possible with a sufficiently clever GtkCellRenderer.
+ */
+
+#include "disobedience.h"
+#include "bits.h"
+
+static GtkWidget *users_window;
+static GtkListStore *users_list;
+static GtkTreeSelection *users_selection;
+
+static GtkWidget *users_details_table;
+static GtkWidget *users_apply_button;
+static GtkWidget *users_delete_button;
+static GtkWidget *users_details_name;
+static GtkWidget *users_details_email;
+static GtkWidget *users_details_password;
+static GtkWidget *users_details_password2;
+static GtkWidget *users_details_rights[32];
+static int users_details_row;
+static const char *users_selected;
+static const char *users_deferred_select;
+
+static int users_mode;
+#define MODE_NONE 0
+#define MODE_ADD 1
+#define MODE_EDIT 2
+
+#define mode(X) do { \
+ users_mode = MODE_##X; \
+ if(0) fprintf(stderr, "%s:%d: %s(): mode -> %s\n", \
+ __FILE__, __LINE__, __FUNCTION__, #X); \
+ users_details_sensitize_all(); \
+} while(0)
+
+static const char *users_email, *users_rights, *users_password;
+
+/** @brief qsort() callback for username comparison */
+static int usercmp(const void *a, const void *b) {
+ return strcmp(*(char **)a, *(char **)b);
+}
+
+/** @brief Find a user
+ * @param user User to find
+ * @param iter Iterator to point at user
+ * @return 0 on success, -1 if not found
+ */
+static int users_find_user(const char *user,
+ GtkTreeIter *iter) {
+ char *who;
+
+ /* Find the user */
+ if(!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(users_list), iter))
+ return -1;
+ do {
+ gtk_tree_model_get(GTK_TREE_MODEL(users_list), iter,
+ 0, &who, -1);
+ if(!strcmp(who, user)) {
+ g_free(who);
+ return 0;
+ }
+ g_free(who);
+ } while(gtk_tree_model_iter_next(GTK_TREE_MODEL(users_list), iter));
+ return -1;
+}
+
+/** @brief Called with the list of users
+ *
+ * Called:
+ * - at startup to populate the initial list
+ * - when we add a user
+ * - maybe in the future when we delete a user
+ *
+ * If users_deferred_select is set then that user is selected.
+ */
+static void users_got_list(void attribute((unused)) *v, int nvec, char **vec) {
+ int n;
+ GtkTreeIter iter;
+
+ /* Present users in alphabetical order */
+ qsort(vec, nvec, sizeof (char *), usercmp);
+ /* Set the list contents */
+ gtk_list_store_clear(users_list);
+ for(n = 0; n < nvec; ++n)
+ gtk_list_store_insert_with_values(users_list, &iter, n/*position*/,
+ 0, vec[n], /* column 0 */
+ -1); /* no more columns */
+ /* Only show the window when the list is populated */
+ gtk_widget_show_all(users_window);
+ if(users_deferred_select) {
+ if(!users_find_user(users_deferred_select, &iter))
+ gtk_tree_selection_select_iter(users_selection, &iter);
+ users_deferred_select = 0;
+ }
+}
+
+/** @brief Text should be visible */
+#define DETAIL_VISIBLE 1
+
+/** @brief Text should be editable */
+#define DETAIL_EDITABLE 2
+
+/** @brief Add a row to the user detail table */
+static void users_detail_generic(const char *title,
+ GtkWidget *selector) {
+ const int row = users_details_row++;
+ GtkWidget *const label = gtk_label_new(title);
+ gtk_misc_set_alignment(GTK_MISC(label), 1, 0);
+ gtk_table_attach(GTK_TABLE(users_details_table),
+ label,
+ 0, 1, /* left/right_attach */
+ row, row+1, /* top/bottom_attach */
+ GTK_FILL, /* xoptions */
+ 0, /* yoptions */
+ 1, 1); /* x/ypadding */
+ gtk_table_attach(GTK_TABLE(users_details_table),
+ selector,
+ 1, 2, /* left/right_attach */
+ row, row + 1, /* top/bottom_attach */
+ GTK_EXPAND|GTK_FILL, /* xoptions */
+ GTK_FILL, /* yoptions */
+ 1, 1); /* x/ypadding */
+}
+
+/** @brief Add a row to the user details table
+ * @param entryp Where to put GtkEntry
+ * @param title Label for this row
+ * @param value Initial value or NULL
+ * @param flags Flags word
+ */
+static void users_add_detail(GtkWidget **entryp,
+ const char *title,
+ const char *value,
+ unsigned flags) {
+ GtkWidget *entry;
+
+ if(!(entry = *entryp)) {
+ *entryp = entry = gtk_entry_new();
+ users_detail_generic(title, entry);
+ }
+ gtk_entry_set_visibility(GTK_ENTRY(entry),
+ !!(flags & DETAIL_VISIBLE));
+ gtk_editable_set_editable(GTK_EDITABLE(entry),
+ !!(flags & DETAIL_EDITABLE));
+ gtk_entry_set_text(GTK_ENTRY(entry), value ? value : "");
+}
+
+/** @brief Add a checkbox for a right
+ * @param title Label for this row
+ * @param value Current value
+ * @param right Right bit
+ */
+static void users_add_right(const char *title,
+ rights_type value,
+ rights_type right) {
+ GtkWidget *check;
+ GtkWidget **checkp = &users_details_rights[leftmost_bit(right)];
+
+ if(!(check = *checkp)) {
+ *checkp = check = gtk_check_button_new_with_label("");
+ users_detail_generic(title, check);
+ }
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), !!(value & right));
+}
+
+/** @brief Set sensitivity of particular mine/random rights bits */
+static void users_details_sensitize(rights_type r) {
+ const int bit = leftmost_bit(r);
+ const GtkWidget *all = users_details_rights[bit];
+ const int sensitive = (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(all))
+ && users_mode != MODE_NONE);
+
+ gtk_widget_set_sensitive(users_details_rights[bit + 1], sensitive);
+ gtk_widget_set_sensitive(users_details_rights[bit + 2], sensitive);
+}
+
+/** @brief Set sensitivity of everything in sight */
+static void users_details_sensitize_all(void) {
+ int n;
+
+ for(n = 0; n < 32; ++n)
+ if(users_details_rights[n])
+ gtk_widget_set_sensitive(users_details_rights[n], users_mode != MODE_NONE);
+ gtk_widget_set_sensitive(users_details_name, users_mode != MODE_NONE);
+ gtk_widget_set_sensitive(users_details_email, users_mode != MODE_NONE);
+ gtk_widget_set_sensitive(users_details_password, users_mode != MODE_NONE);
+ gtk_widget_set_sensitive(users_details_password2, users_mode != MODE_NONE);
+ users_details_sensitize(RIGHT_MOVE_ANY);
+ users_details_sensitize(RIGHT_REMOVE_ANY);
+ users_details_sensitize(RIGHT_SCRATCH_ANY);
+ gtk_widget_set_sensitive(users_apply_button, users_mode != MODE_NONE);
+ gtk_widget_set_sensitive(users_delete_button, !!users_selected);
+}
+
+/** @brief Called when an _ALL widget is toggled
+ *
+ * Modifies sensitivity of the corresponding _MINE and _RANDOM widgets. We
+ * just do the lot rather than trying to figure out which one changed,
+ */
+static void users_any_toggled(GtkToggleButton attribute((unused)) *togglebutton,
+ gpointer attribute((unused)) user_data) {
+ users_details_sensitize_all();
+}
+
+/** @brief Add a checkbox for a three-right group
+ * @param title Label for this row
+ * @param bits Rights bits (not masked or normalized)
+ * @param mask Mask for this group (must be 7*2^n)
+ */
+static void users_add_right_group(const char *title,
+ rights_type bits,
+ rights_type mask) {
+ const uint32_t first = mask / 7;
+ const int bit = leftmost_bit(first);
+ GtkWidget **widgets = &users_details_rights[bit], *any, *mine, *random;
+
+ if(!*widgets) {
+ GtkWidget *hbox = gtk_hbox_new(FALSE, 2);
+
+ any = widgets[0] = gtk_check_button_new_with_label("Any");
+ mine = widgets[1] = gtk_check_button_new_with_label("Own");
+ random = widgets[2] = gtk_check_button_new_with_label("Random");
+ gtk_box_pack_start(GTK_BOX(hbox), any, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), mine, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), random, FALSE, FALSE, 0);
+ users_detail_generic(title, hbox);
+ g_signal_connect(any, "toggled", G_CALLBACK(users_any_toggled), NULL);
+ users_details_rights[bit] = any;
+ users_details_rights[bit + 1] = mine;
+ users_details_rights[bit + 2] = random;
+ } else {
+ any = widgets[0];
+ mine = widgets[1];
+ random = widgets[2];
+ }
+ /* Discard irrelevant bits */
+ bits &= mask;
+ /* Shift down to bits 0-2; the mask is always 3 contiguous bits */
+ bits >>= bit;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(any), !!(bits & 1));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(mine), !!(bits & 2));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(random), !!(bits & 4));
+}
+
+/** @brief Called when the details table is destroyed */
+static void users_details_destroyed(GtkWidget attribute((unused)) *widget,
+ GtkWidget attribute((unused)) **wp) {
+ users_details_table = 0;
+ g_object_unref(users_list);
+ users_list = 0;
+ users_details_name = 0;
+ users_details_email = 0;
+ users_details_password = 0;
+ users_details_password2 = 0;
+ memset(users_details_rights, 0, sizeof users_details_rights);
+ /* also users_selection? Not AFAICT; _get_selection does upref */
+}
+
+/** @brief Create or modify the user details table
+ * @param name User name (users_edit()) or NULL (users_add())
+ * @param email Email address
+ * @param rights User rights string
+ * @param password Password
+ */
+static void users_makedetails(const char *name,
+ const char *email,
+ const char *rights,
+ const char *password,
+ unsigned nameflags,
+ unsigned flags) {
+ rights_type r = 0;
+
+ /* Create the table if it doesn't already exist */
+ if(!users_details_table) {
+ users_details_table = gtk_table_new(4, 2, FALSE/*!homogeneous*/);
+ g_signal_connect(users_details_table, "destroy",
+ G_CALLBACK(users_details_destroyed), 0);
+ }
+
+ /* Create or update the widgets */
+ users_add_detail(&users_details_name, "Username", name,
+ (DETAIL_EDITABLE|DETAIL_VISIBLE) & nameflags);
+
+ users_add_detail(&users_details_email, "Email", email,
+ (DETAIL_EDITABLE|DETAIL_VISIBLE) & flags);
+
+ users_add_detail(&users_details_password, "Password", password,
+ DETAIL_EDITABLE & flags);
+ users_add_detail(&users_details_password2, "Password", password,
+ DETAIL_EDITABLE & flags);
+
+ parse_rights(rights, &r, 0);
+ users_add_right("Read operations", r, RIGHT_READ);
+ users_add_right("Play track", r, RIGHT_PLAY);
+ users_add_right_group("Move", r, RIGHT_MOVE__MASK);
+ users_add_right_group("Remove", r, RIGHT_REMOVE__MASK);
+ users_add_right_group("Scratch", r, RIGHT_SCRATCH__MASK);
+ users_add_right("Set volume", r, RIGHT_VOLUME);
+ users_add_right("Admin operations", r, RIGHT_ADMIN);
+ users_add_right("Rescan", r, RIGHT_RESCAN);
+ users_add_right("Register new users", r, RIGHT_REGISTER);
+ users_add_right("Modify own userinfo", r, RIGHT_USERINFO);
+ users_add_right("Modify track preferences", r, RIGHT_PREFS);
+ users_add_right("Modify global preferences", r, RIGHT_GLOBAL_PREFS);
+ users_add_right("Pause/resume tracks", r, RIGHT_PAUSE);
+ users_details_sensitize_all();
+}
+
+/** @brief Called when the 'add' button is pressed */
+static void users_add(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ /* Unselect whatever is selected */
+ gtk_tree_selection_unselect_all(users_selection);
+ /* Reset the form */
+ /* TODO it would be better to use the server default_rights if there's no
+ * client setting. */
+ users_makedetails("",
+ "",
+ config->default_rights,
+ "",
+ DETAIL_EDITABLE|DETAIL_VISIBLE,
+ DETAIL_EDITABLE|DETAIL_VISIBLE);
+ /* Remember we're adding a user */
+ mode(ADD);
+}
+
+static rights_type users_get_rights(void) {
+ rights_type r = 0;
+ int n;
+
+ /* Extract the rights value */
+ for(n = 0; n < 32; ++n) {
+ if(users_details_rights[n])
+ if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(users_details_rights[n])))
+ r |= 1 << n;
+ }
+ /* Throw out redundant bits */
+ if(r & RIGHT_REMOVE_ANY)
+ r &= ~(rights_type)(RIGHT_REMOVE_MINE|RIGHT_REMOVE_RANDOM);
+ if(r & RIGHT_MOVE_ANY)
+ r &= ~(rights_type)(RIGHT_MOVE_MINE|RIGHT_MOVE_RANDOM);
+ if(r & RIGHT_SCRATCH_ANY)
+ r &= ~(rights_type)(RIGHT_SCRATCH_MINE|RIGHT_SCRATCH_RANDOM);
+ return r;
+}
+
+/** @brief Called when various things fail */
+static void users_op_failed(struct callbackdata attribute((unused)) *cbd,
+ int attribute((unused)) code,
+ const char *msg) {
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, msg);
+}
+
+/** @brief Called when a new user has been created */
+static void users_adduser_completed(void *v) {
+ struct callbackdata *cbd = v;
+
+ /* Now the user is created we can go ahead and set the email address */
+ if(*cbd->u.edituser.email) {
+ struct callbackdata *ncbd = xmalloc(sizeof *cbd);
+ ncbd->onerror = users_op_failed;
+ disorder_eclient_edituser(client, NULL, cbd->u.edituser.user,
+ "email", cbd->u.edituser.email, ncbd);
+ }
+ /* Refresh the list of users */
+ disorder_eclient_users(client, users_got_list, 0);
+ /* We'll select the newly created user */
+ users_deferred_select = cbd->u.edituser.user;
+}
+
+/** @brief Called if creating a new user fails */
+static void users_adduser_failed(struct callbackdata attribute((unused)) *cbd,
+ int attribute((unused)) code,
+ const char *msg) {
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, msg);
+ mode(ADD); /* Let the user try again */
+}
+
+/** @brief Called when the 'Apply' button is pressed */
+static void users_apply(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ struct callbackdata *cbd;
+ const char *password;
+ const char *password2;
+ const char *name;
+ const char *email;
+
+ switch(users_mode) {
+ case MODE_NONE:
+ return;
+ case MODE_ADD:
+ name = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_name)));
+ email = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_email)));
+ password = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_password)));
+ password2 = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_password2)));
+ if(!*name) {
+ /* No username. Really we wanted to desensitize the Apply button when
+ * there's no userame but there doesn't seem to be a signal to detect
+ * changes to the entry text. Consequently we have error messages
+ * instead. */
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, "Must enter a username");
+ return;
+ }
+ if(strcmp(password, password2)) {
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, "Passwords do not match");
+ return;
+ }
+ if(*email && !strchr(email, '@')) {
+ /* The server will complain about this but we can give a better error
+ * message this way */
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, "Invalid email address");
+ return;
+ }
+ cbd = xmalloc(sizeof *cbd);
+ cbd->onerror = users_adduser_failed;
+ cbd->u.edituser.user = name;
+ cbd->u.edituser.email = email;
+ disorder_eclient_adduser(client, users_adduser_completed,
+ name,
+ password,
+ rights_string(users_get_rights()),
+ cbd);
+ /* We switch to no-op mode while creating the user */
+ mode(NONE);
+ break;
+ case MODE_EDIT:
+ /* Ugh, can we de-dupe with above? */
+ email = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_email)));
+ password = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_password)));
+ password2 = xstrdup(gtk_entry_get_text(GTK_ENTRY(users_details_password2)));
+ if(strcmp(password, password2)) {
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, "Passwords do not match");
+ return;
+ }
+ if(*email && !strchr(email, '@')) {
+ popup_submsg(users_window, GTK_MESSAGE_ERROR, "Invalid email address");
+ return;
+ }
+ cbd = xmalloc(sizeof *cbd);
+ cbd->onerror = users_op_failed;
+ disorder_eclient_edituser(client, NULL, users_selected,
+ "email", email, cbd);
+ disorder_eclient_edituser(client, NULL, users_selected,
+ "password", password, cbd);
+ disorder_eclient_edituser(client, NULL, users_selected,
+ "rights", rights_string(users_get_rights()), cbd);
+ /* We remain in edit mode */
+ break;
+ }
+}
+
+/** @brief Called when a user has been deleted */
+static void users_deleted(void *v) {
+ const struct callbackdata *const cbd = v;
+ GtkTreeIter iter[1];
+
+ if(!users_find_user(cbd->u.user, iter)) /* Find the user... */
+ gtk_list_store_remove(users_list, iter); /* ...and remove them */
+}
+
+/** @brief Called when the 'Delete' button is pressed */
+static void users_delete(GtkButton attribute((unused)) *button,
+ gpointer attribute((unused)) userdata) {
+ GtkWidget *yesno;
+ int res;
+ struct callbackdata *cbd;
+
+ if(!users_selected)
+ return;
+ yesno = gtk_message_dialog_new(GTK_WINDOW(users_window),
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ "Do you really want to delete user %s?"
+ " This action cannot be undone.",
+ users_selected);
+ res = gtk_dialog_run(GTK_DIALOG(yesno));
+ gtk_widget_destroy(yesno);
+ if(res == GTK_RESPONSE_YES) {
+ cbd = xmalloc(sizeof *cbd);
+ cbd->onerror = users_op_failed;
+ cbd->u.user = users_selected;
+ disorder_eclient_deluser(client, users_deleted, cbd->u.user, cbd);
+ }
+}
+
+static void users_got_email(void attribute((unused)) *v, const char *value) {
+ users_email = value;
+}
+
+static void users_got_rights(void attribute((unused)) *v, const char *value) {
+ users_rights = value;
+}
+
+static void users_got_password(void attribute((unused)) *v, const char *value) {
+ users_password = value;
+ users_makedetails(users_selected,
+ users_email,
+ users_rights,
+ users_password,
+ DETAIL_VISIBLE,
+ DETAIL_EDITABLE|DETAIL_VISIBLE);
+ mode(EDIT);
+}
+
+/** @brief Called when the selection MIGHT have changed */
+static void users_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
+ gpointer attribute((unused)) user_data) {
+ GtkTreeIter iter;
+ char *gselected, *selected;
+
+ /* Identify the current selection */
+ if(gtk_tree_selection_get_selected(users_selection, 0, &iter)) {
+ gtk_tree_model_get(GTK_TREE_MODEL(users_list), &iter,
+ 0, &gselected, -1);
+ selected = xstrdup(gselected);
+ g_free(gselected);
+ } else
+ selected = 0;
+ /* Eliminate no-change cases */
+ if(!selected && !users_selected)
+ return;
+ if(selected && users_selected && !strcmp(selected, users_selected))
+ return;
+ /* There's been a change; junk the old data and fetch new data in
+ * background. */
+ users_selected = selected;
+ users_makedetails("", "", "", "",
+ DETAIL_VISIBLE,
+ DETAIL_VISIBLE);
+ if(users_selected) {
+ disorder_eclient_userinfo(client, users_got_email, users_selected,
+ "email", 0);
+ disorder_eclient_userinfo(client, users_got_rights, users_selected,
+ "rights", 0);
+ disorder_eclient_userinfo(client, users_got_password, users_selected,
+ "password", 0);
+ }
+ mode(NONE); /* not editing *yet* */
+}
+
+/** @brief Table of buttons below the user list */
+static struct button users_buttons[] = {
+ {
+ GTK_STOCK_ADD,
+ users_add,
+ "Create a new user",
+ 0
+ },
+ {
+ GTK_STOCK_REMOVE,
+ users_delete,
+ "Delete a user",
+ 0
+ },
+};
+#define NUSERS_BUTTONS (sizeof users_buttons / sizeof *users_buttons)
+
+/** @brief Pop up the user management window */
+void manage_users(void) {
+ GtkWidget *tree, *buttons, *hbox, *hbox2, *vbox, *vbox2;
+ GtkCellRenderer *cr;
+ GtkTreeViewColumn *col;
+
+ /* If the window already exists just raise it */
+ if(users_window) {
+ gtk_window_present(GTK_WINDOW(users_window));
+ return;
+ }
+ /* Create the window */
+ users_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_widget_set_style(users_window, tool_style);
+ g_signal_connect(users_window, "destroy",
+ G_CALLBACK(gtk_widget_destroyed), &users_window);
+ gtk_window_set_title(GTK_WINDOW(users_window), "User Management");
+ /* default size is too small */
+ gtk_window_set_default_size(GTK_WINDOW(users_window), 240, 240);
+
+ /* Create the list of users and populate it asynchronously */
+ users_list = gtk_list_store_new(1, G_TYPE_STRING);
+ disorder_eclient_users(client, users_got_list, 0);
+ /* Create the view */
+ tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(users_list));
+ /* ...and the renderers for it */
+ cr = gtk_cell_renderer_text_new();
+ col = gtk_tree_view_column_new_with_attributes("Username",
+ cr,
+ "text", 0,
+ NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
+ /* Get the selection for the view; set its mode; arrange for a callback when
+ * it changes */
+ users_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_set_mode(users_selection, GTK_SELECTION_BROWSE);
+ g_signal_connect(users_selection, "changed",
+ G_CALLBACK(users_selection_changed), NULL);
+
+ /* Create the control buttons */
+ buttons = create_buttons_box(users_buttons,
+ NUSERS_BUTTONS,
+ gtk_hbox_new(FALSE, 1));
+ users_delete_button = users_buttons[1].widget;
+
+ /* Buttons live below the list */
+ vbox = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+
+ /* Create an empty user details table, and put an apply button below it */
+ users_apply_button = gtk_button_new_from_stock(GTK_STOCK_APPLY);
+ users_makedetails("", "", "", "",
+ DETAIL_VISIBLE,
+ DETAIL_VISIBLE);
+ g_signal_connect(users_apply_button, "clicked",
+ G_CALLBACK(users_apply), NULL);
+ hbox2 = gtk_hbox_new(FALSE, 0);
+ gtk_box_pack_end(GTK_BOX(hbox2), users_apply_button,
+ FALSE/*expand*/, FALSE, 0);
+
+ vbox2 = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox2), users_details_table,
+ TRUE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(vbox2), hbox2,
+ FALSE/*expand*/, FALSE, 0);
+
+ /* User details are to the right of the list. We put in a pointless event
+ * box as as spacer, so that the longest label in the user details isn't
+ * cuddled up to the user list. */
+ hbox = gtk_hbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE/*expand*/, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(), FALSE/*expand*/, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(hbox), vbox2, TRUE/*expand*/, TRUE/*fill*/, 0);
+ gtk_container_add(GTK_CONTAINER(users_window), frame_widget(hbox, NULL));
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
.B disobedience
is a graphical client for DisOrder.
.SH "WINDOWS AND ICONS"
-.SS "File Menu"
+.SS "Server Menu"
This has the following options:
.TP
.B Login
Brings up the \fBLogin Details Window\fR; see below.
.TP
+.B "Manage Users"
+Brings up the \fBUser Management Window\fR; see below.
+.TP
.B Quit
Terminates the program.
.SS "Edit Menu"
Press "OK" to confirm all changes and close the window, "Apply" to confirm
changes but keep the window open and "Cancel" to close the window and discard
all changes.
+.SS "User Management Window"
+This window is primarily of interest to adminstrators, and will not be
+available to users without admin rights. The left hand side is a list of all
+users; the right hand side contains the editable details of the currently
+selected user.
+.PP
+When you select any user you can edit their email address or change their
+password. It is also possible to edit the individual user rights. Click on
+the "Apply" button to commit any changes you make.
+.PP
+The "Add" button creates a new user. You must enter at least a username.
+Default rights are setting according to local configuration, \fInot\fR server
+configuration (but this may be changed in the future). As above, click on
+"Apply" to actually create the new user.
+.PP
+The "Delete" button deletes the selected user. This operation cannot be
+undone.
.SH "KEYBOARD SHORTCUTS"
.TP
.B CTRL+A
List all known commands.
.SH COMMANDS
.TP
-.B adduser \fIUSER PASSWORD\fR [\fIRIGHTS\fR]
+.B adduser \fIUSERNAME PASSWORD\fR [\fIRIGHTS\fR]
Create a new user.
If \fIRIGHTS\fR is not specified then the \fBdefault_rights\fR
setting from the server's configuration file applies.
.TP
-.B authorize \fIUSER\fR [\fIRIGHTS\fR]
-Create \fIUSER\fR with a random password.
-\fIUSER\fR must be a UNIX login user (not just any old string).
+.B authorize \fIUSERNAME\fR [\fIRIGHTS\fR]
+Create user \fIUSERNAME\fR with a random password.
+User \fIUSERNAME\fR must be a UNIX login user (not just any old string).
If \fIRIGHTS\fR is not specified then the \fBdefault_rights\fR
setting from the server's configuration file applies.
.IP
-\fI~USER/.disorder/passwd\fR is created with the password in it, so the new
+\fI~USERNAME/.disorder/passwd\fR is created with the password in it, so the new
user should be able to log in immediately.
.IP
If writing the \fIpasswd\fR file fails then the user will already have been
created in DisOrder's user database.
Use \fBdisorder deluser\fR to remove them before trying again.
.TP
-.B deluser \fIUSER\fR
+.B deluser \fIUSERNAME\fR
Delete a user.
.TP
.B dirs \fIDIRECTORY\fR [\fB~\fIREGEXP\fR]
.B disable
Disable playing after the current track finishes.
.TP
-.B edituser \fIUSER PROPERTY VALUE
+.B edituser \fIUSERNAME PROPERTY VALUE
Set some property of a user.
.TP
.B enable
.B unset\-global \fIKEY\fR
Unset the global preference \fIKEY\fR.
.TP
-.B userinfo \fIUSER PROPERTY
+.B userinfo \fIUSERNAME PROPERTY
Get some property of a user.
.TP
.B users
Should be readable only by the jukebox group.
Not really useful any more and will be abolished in future.
.TP
-.I ~\fRUSER\fI/.disorder/passwd
+.I ~\fRUSERNAME\fI/.disorder/passwd
Per-user client configuration.
Optional but if it exists must be readable only by the relevant user.
Would normally contain a \fBpassword\fR directive.
.TP
-.I pkgconfdir/config.\fRUSER
+.I pkgconfdir/config.\fRUSERNAME
Per-user system-controlled client configuration.
Optional but if it exists must be readable only by the relevant user.
Would normally contain a \fBpassword\fR directive.
If \fBtracklength\fR is used without arguments then the list of modules is
cleared.
.TP
-.B user \fIUSER\fR
+.B user \fIUSERNAME\fR
Specifies the user to run as.
Only makes sense if invoked as root (or the target user).
.SS "Client Configuration"
This must be the full URL, e.g. \fBhttp://myhost/cgi-bin/jukebox\fR and not
\fB/cgi-bin/jukebox\fR.
.SS "Authentication Configuration"
-These options would normally be used in \fI~\fRUSER\fI/.disorder/passwd\fR or
-\fIpkgconfdir/config.\fRUSER.
+These options would normally be used in \fI~\fRUSERNAME\fI/.disorder/passwd\fR
+or
+\fIpkgconfdir/config.\fRUSERNAME.
.TP
.B password \fIPASSWORD\fR
Specify password.
With the \fBadmin\fR right any username and property may be specified.
Otherwise the \fBuserinfo\fR right is required and only the
\fBemail\fR and \fBpassword\fR properties may be set.
+.IP
+User properties are syntax-checked before setting. For instance \fBemail\fR
+must contain an "@" sign or you will get an error. (Setting an empty value for
+\fBemail\fR is allowed and removes the property.)
.TP
.B enable
Re-enable further playing, and is the opposite of \fBdisable\fR.
Request that DisOrder reconfigure itself.
Requires the \fBadmin\fR right.
.TP
-.B register \fIUSER PASSWORD EMAIL
+.B register \fIUSERNAME PASSWORD EMAIL
Register a new user.
Requires the \fBregister\fR right.
The result contains a confirmation string; the user will be be able
to log in until this has been presented back to the server via the
\fBconfirm\fR command.
.TP
-.B reminder \fIUSER\fR
-Send a password reminder to \fIUSER\fR.
+.B reminder \fIUSERNAME\fR
+Send a password reminder to user \fIUSERNAME\fR.
If the user has no valid email address, or no password, or a
reminder has been sent too recently, then no reminder will be sent.
.TP
Unset a global preference.
Requires the \fBglobal prefs\fR right.
.TP
-.B user \fIUSER\fR \fIRESPONSE\fR
-Authenticate as \fIUSER\fR.
+.B user \fIUSERNAME\fR \fIRESPONSE\fR
+Authenticate as user \fIUSERNAME\fR.
See
.B AUTHENTICATION
below.
.TP
+.B userinfo \fIUSERNAME PROPERTY
+Get a user property.
+.TP
.B users
Send the list of currently known users in a response body.
.TP
.B failed \fITRACK\fR \fIERROR\fR
Completed playing \fITRACK\fR with an error status
.TP
-.B moved \fIUSER\fR
-User \fIUSER\fR moved some track(s).
+.B moved \fIUSERNAME\fR
+User \fIUSERNAME\fR moved some track(s).
Further details aren't included any more.
.TP
-.B playing \fITRACK\fR [\fIUSER\fR]
+.B playing \fITRACK\fR [\fIUSERNAME\fR]
Started playing \fITRACK\fR.
.TP
.B queue \fIQUEUE-ENTRY\fR...
.B recent_removed \fIID\fR
Removed \fIID\fR from the recently played list.
.TP
-.B removed \fIID\fR [\fIUSER\fR]
+.B removed \fIID\fR [\fIUSERNAME\fR]
Queue entry \fIID\fR was removed.
-This is used both for explicit removal (when \fIUSER\fR is present)
+This is used both for explicit removal (when \fIUSERNAME\fR is present)
and when playing a track (when it is absent).
.TP
.B rescanned
A rescan completed.
.TP
-.B scratched \fITRACK\fR \fIUSER\fR
-\fITRACK\fR was scratched by \fIUSER\fR.
+.B scratched \fITRACK\fR \fIUSERNAME\fR
+\fITRACK\fR was scratched by \fIUSERNAME\fR.
.TP
.B state \fIKEYWORD\fR
Some state change occurred.
authhash.c authhash.h \
basen.c basen.h \
base64.c base64.h \
+ bits.c bits.h \
cache.c cache.h \
client.c client.h \
client-common.c client-common.h \
t-casefold.c t-cookies.c t-filepart.c t-hash.c t-heap.c \
t-hex.c t-kvp.c t-mime.c t-printf.c t-regsub.c t-selection.c \
t-signame.c t-sink.c t-split.c t-unicode.c t-url.c t-utf8.c \
- t-words.c t-wstat.c
+ t-words.c t-wstat.c t-bits.c
test_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
test_DEPENDENCIES=libdisorder.a
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/bits.c
+ * @brief Bit operations
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <math.h>
+
+#include "bits.h"
+
+#if !HAVE_FLS
+/** @brief Compute index of leftmost 1 bit
+ * @param n Integer
+ * @return Index of leftmost 1 bit or -1
+ *
+ * For positive @p n we return the index of the leftmost bit of @p n. For
+ * instance @c leftmost_bit(1) returns 0, @c leftmost_bit(15) returns 3, etc.
+ *
+ * If @p n is zero then -1 is returned.
+ */
+int leftmost_bit(uint32_t n) {
+ /* See e.g. Hacker's Delight s5-3 (p81) for where the idea comes from.
+ * Warren is computing the number of leading zeroes, but that's not quite
+ * what I wanted. Also this version should be more portable than his, which
+ * inspects the bytes of the floating point number directly.
+ */
+ int x;
+ frexp((double)n, &x);
+ /* This gives: n = m * 2^x, where 0.5 <= m < 1 and x is an integer.
+ *
+ * If we take log2 of either side then we have:
+ * log2(n) = x + log2 m
+ *
+ * We know that 0.5 <= m < 1 => -1 <= log2 m < 0. So we floor either side:
+ *
+ * floor(log2(n)) = x - 1
+ *
+ * What is floor(log2(n))? Well, consider that:
+ *
+ * 2^k <= z < 2^(k+1) => floor(log2(z)) = k.
+ *
+ * But 2^k <= z < 2^(k+1) is the same as saying that the leftmost bit of z is
+ * bit k.
+ *
+ *
+ * Warren adds 0.5 first, to deal with the case when n=0. However frexp()
+ * guarantees to return x=0 when n=0, so we get the right answer without that
+ * step.
+ */
+ return x - 1;
+}
+#endif
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/bits.h
+ * @brief Bit operations
+ */
+
+#ifndef BITS_H
+#define BITS_H
+
+#include <string.h> /* for fls() */
+
+#if HAVE_FLS
+static inline int leftmost_bit(uint32_t n) {
+ return fls(n) - 1;
+}
+#else
+int leftmost_bit(uint32_t n);
+#endif
+
+#endif /* BITS_H */
+
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
static void string_response_opcallback(disorder_eclient *c,
struct operation *op) {
D(("string_response_callback"));
- if(c->rc / 100 == 2) {
+ if(c->rc / 100 == 2 || c->rc == 555) {
if(op->completed) {
- if(c->protocol >= 2) {
+ if(c->rc == 555)
+ ((disorder_eclient_string_response *)op->completed)(op->v, NULL);
+ else if(c->protocol >= 2) {
char **rr = split(c->line + 4, 0, SPLIT_QUOTES, 0, 0);
if(rr && *rr)
"rtp-address", (char *)0);
}
+/** @brief Get the list of users
+ * @param c Client
+ * @param completed Called with list of users
+ * @param v Passed to @p completed
+ *
+ * The user list is not sorted in any particular order.
+ */
+int disorder_eclient_users(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ void *v) {
+ return simple(c, list_response_opcallback, (void (*)())completed, v,
+ "users", (char *)0);
+}
+
+/** @brief Delete a user
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to delete
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_deluser(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *user,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "deluser", user, (char *)0);
+}
+
+/** @brief Get a user property
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to look up
+ * @param property Property to look up
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_userinfo(disorder_eclient *c,
+ disorder_eclient_string_response *completed,
+ const char *user,
+ const char *property,
+ void *v) {
+ return simple(c, string_response_opcallback, (void (*)())completed, v,
+ "userinfo", user, property, (char *)0);
+}
+
+/** @brief Modify a user property
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to modify
+ * @param property Property to modify
+ * @param value New property value
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_edituser(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *user,
+ const char *property,
+ const char *value,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "edituser", user, property, value, (char *)0);
+}
+
+/** @brief Create a new user
+ * @param c Client
+ * @param completed Called on completion
+ * @param user User to create
+ * @param password Initial password
+ * @param rights Initial rights or NULL
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_adduser(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *user,
+ const char *password,
+ const char *rights,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "adduser", user, password, rights, (char *)0);
+}
+
/* Log clients ***************************************************************/
/** @brief Monitor the server log
typedef void disorder_eclient_no_response(void *v);
/* completion callback with no data */
+/** @brief String result completion callback
+ * @param v User data
+ * @param value or NULL
+ *
+ * @p value can be NULL for disorder_eclient_get(),
+ * disorder_eclient_get_global() and disorder_eclient_userinfo().
+ */
typedef void disorder_eclient_string_response(void *v, const char *value);
-/* completion callback with a string result */
typedef void disorder_eclient_integer_response(void *v, long value);
/* completion callback with a integer result */
disorder_eclient_list_response *completed,
void *v);
+int disorder_eclient_users(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ void *v);
+int disorder_eclient_deluser(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *user,
+ void *v);
+int disorder_eclient_userinfo(disorder_eclient *c,
+ disorder_eclient_string_response *completed,
+ const char *user,
+ const char *property,
+ void *v);
+int disorder_eclient_edituser(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *user,
+ const char *property,
+ const char *value,
+ void *v);
+int disorder_eclient_adduser(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *user,
+ const char *password,
+ const char *rights,
+ void *v);
+
#endif
/*
--- /dev/null
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "test.h"
+#include "bits.h"
+
+void test_bits(void) {
+ int n;
+
+ printf("test_bits\n");
+ check_integer(leftmost_bit(0), -1);
+ check_integer(leftmost_bit(0x80000000), 31);
+ check_integer(leftmost_bit(0xffffffff), 31);
+ for(n = 0; n < 28; ++n) {
+ const uint32_t b = 1 << n, limit = 2 * b;
+ uint32_t v;
+
+ for(v = b; v < limit; ++v)
+ check_integer(leftmost_bit(v), n);
+ }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
#include "test.h"
-int tests, errors;
+long long tests, errors;
int fail_first;
void count_error(void) {
test_hash();
test_url();
test_regsub();
- fprintf(stderr, "%d errors out of %d tests\n", errors, tests);
+ test_bits();
+ fprintf(stderr, "%lld errors out of %lld tests\n", errors, tests);
return !!errors;
}
#include "url.h"
#include "regsub.h"
-extern int tests, errors;
+extern long long tests, errors;
extern int fail_first;
/** @brief Checks that @p expr is nonzero */
void test_utf8(void);
void test_words(void);
void test_wstat(void);
+void test_bits(void);
#endif /* TEST_H */
return -1;
}
} else if(!strcmp(key, "email")) {
- if(!strchr(value, '@')) {
- error(0, "invalid email address '%s' for user '%s'", user, value);
- return -1;
- }
+ if(*value) {
+ if(!strchr(value, '@')) {
+ error(0, "invalid email address '%s' for user '%s'", user, value);
+ return -1;
+ }
+ } else
+ value = 0; /* no email -> remove key */
} else if(!strcmp(key, "created")) {
error(0, "cannot change creation date for user '%s'", user);
return -1;
const char *value;
/* RIGHT_ADMIN allows anything; otherwise you can only get your own email
- * address and righst list. */
+ * address and rights list. */
if((c->rights & RIGHT_ADMIN)
|| (!strcmp(c->who, vec[0])
&& (!strcmp(vec[1], "email")
print " checking confirmed user can log in"
jc = disorder.client(user="joe", password="joepass")
jc.version()
+ print " checking new user's email address"
+ assert c.userinfo("joe", "email") == "joe@nowhere.invalid"
+ print " checking can change user email address"
+ c.edituser("joe", "email", "joe@another.invalid")
+ assert c.userinfo("joe", "email") == "joe@another.invalid"
+ print " checking bad email address rejected"
+ try:
+ c.edituser("joe", "email", "invalid")
+ print "*** should not be able to set invalid email address ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ assert c.userinfo("joe", "email") == "joe@another.invalid"
+ print " checking removal of email address"
+ c.edituser("joe", "email", "")
+ assert c.userinfo("joe", "email") == None
if __name__ == '__main__':
dtest.run()