chiark / gitweb /
Merge disorder.userman branch
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 20 Apr 2008 15:01:45 +0000 (16:01 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 20 Apr 2008 15:01:45 +0000 (16:01 +0100)
27 files changed:
CHANGES
README.client
clients/disorder.c
configure.ac
disobedience/Makefile.am
disobedience/disobedience.h
disobedience/login.c
disobedience/menu.c
disobedience/misc.c
disobedience/properties.c
disobedience/settings.c
disobedience/users.c [new file with mode: 0644]
doc/disobedience.1.in
doc/disorder.1.in
doc/disorder_config.5.in
doc/disorder_protocol.5.in
lib/Makefile.am
lib/bits.c [new file with mode: 0644]
lib/bits.h [new file with mode: 0644]
lib/eclient.c
lib/eclient.h
lib/t-bits.c [new file with mode: 0644]
lib/test.c
lib/test.h
lib/trackdb.c
server/server.c
tests/user.py

diff --git a/CHANGES b/CHANGES
index e638f434cdb630b2010b347d9e0d0bfdfbf74696..773115330e041288ee788b7ec30701b1c04756b9 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -16,6 +16,11 @@ This has been completely rewritten to support new features:
      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.
index cec4e8c42113dd69749cfe588fcbdc00732364af..04bdee7bddc8b0f1fd297b6259f3af8844831f14 100644 (file)
@@ -17,7 +17,7 @@ There is currently no standard DisOrder port number.
 
     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
index fa42558dd59ed838b6c5050a6fbfa9eeb81a621c..2088bc4a9fed9224dfd605008b79439d7a4ea160 100644 (file)
@@ -475,22 +475,22 @@ static const struct command {
   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, "",
@@ -566,8 +566,8 @@ static const struct command {
                       "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, "",
index 2be631f7bf2d340ebd49613cb512be0d940ee59d..1de3613ff903daa6601b4a0294fa0b0a51ab4d5e 100644 (file)
@@ -1,3 +1,4 @@
+
 # Process this file with autoconf to produce a configure script.
 #
 # This file is part of DisOrder.
@@ -427,6 +428,10 @@ AC_CHECK_FUNCS([fdatasync],[:],[
 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],[
index 031d68d7ed29c651022600f2e1cc505cf0bb0fff..0b1433b1124fa43d5c2eee3ebae1b50fb3c0953f 100644 (file)
@@ -28,7 +28,7 @@ PNGS:=$(shell export LC_COLLATE=C;echo ${top_srcdir}/images/*.png)
 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)
index a9c801cf1f8eabefeaaf7c5d0b5dd50574726471..ccb9acc3d7258b4df8fe3c8e8d40284343aa98db 100644 (file)
@@ -75,6 +75,10 @@ struct callbackdata {
     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;
 };
 
@@ -97,6 +101,7 @@ struct button {
   const gchar *stock;
   void (*clicked)(GtkButton *button, gpointer userdata);
   const char *tip;
+  GtkWidget *widget;
 };
 
 /* Variables --------------------------------------------------------------- */
@@ -138,12 +143,14 @@ void properties_reset(void);
 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, ...);
 
@@ -157,8 +164,11 @@ void progress_window_progress(struct progress_window *pw,
 
 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,
@@ -185,6 +195,7 @@ void menu_update(int page);
 /* 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 */
 
@@ -242,6 +253,10 @@ void login_box(void);
 
 GtkWidget *login_window;
 
+/* User management */
+
+void manage_users(void);
+
 /* Help */
 
 void popup_help(void);
index edefe29cd36b329b311333b4effdfaeaf7635656..18afce461642b353c49cc6fc6a9bfe06ddd0ca44 100644 (file)
@@ -170,16 +170,18 @@ static void login_cancel(GtkButton attribute((unused)) *button,
 }
 
 /* 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
   },
 };
 
@@ -232,7 +234,7 @@ void login_box(void) {
                      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);
index f93e2f7a2dfce973d509cba04780c41435167b86..f24569af7d481cf115f802dac57a0522253be41f 100644 (file)
@@ -94,6 +94,13 @@ static void login(gpointer attribute((unused)) callback_data,
   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,
@@ -123,6 +130,7 @@ void menu_update(int page) {
                            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 */
@@ -207,13 +215,36 @@ static void about_popup_got_version(void attribute((unused)) *v,
   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 */
@@ -221,16 +252,24 @@ GtkWidget *menubar(GtkWidget *w) {
       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 */
@@ -239,7 +278,7 @@ GtkWidget *menubar(GtkWidget *w) {
     },
 #endif
     {
-      (char *)"/File/Quit Disobedience", /* path */
+      (char *)"/Server/Quit Disobedience", /* path */
       (char *)"<CTRL>Q",                /* accelerator */
       quit_program,                     /* callback */
       0,                                /* callback_action */
@@ -367,6 +406,8 @@ GtkWidget *menubar(GtkWidget *w) {
   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);
index db1f6ab9b891b6895c72627679dc4b2d7fc24f09..1e5ee22c3c720ed965ef50105e5f2ebe707c05b4 100644 (file)
@@ -51,7 +51,8 @@ GtkWidget *scroll_widget(GtkWidget *child) {
   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?) */
@@ -72,6 +73,24 @@ GtkWidget *scroll_widget(GtkWidget *child) {
   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
@@ -112,9 +131,13 @@ GdkPixbuf *find_image(const char *name) {
 
 /** @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,
@@ -161,23 +184,32 @@ GtkWidget *iconbutton(const char *path, const char *tip) {
   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
index 44379f835af4d6e4fb5c52431bd960322dcde65a..96d5c489ddb615bad734ea5860962984f869b265 100644 (file)
@@ -128,21 +128,24 @@ static const struct pref {
 #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
   },
 };
 
@@ -268,7 +271,7 @@ void properties(int ntracks, const char **tracks) {
                      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,
index ae8990e3d95c4e26907e757f1b2231eeda592b6b..3cc418011a962ef02528ecc94578f0abae2a3a44 100644 (file)
@@ -338,7 +338,7 @@ void popup_settings(void) {
                        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
diff --git a/disobedience/users.c b/disobedience/users.c
new file mode 100644 (file)
index 0000000..80f3bf2
--- /dev/null
@@ -0,0 +1,679 @@
+/*
+ * 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:
+*/
index 6ac4202442246d0fbe5372ae4810c7189dfe9551..57e3addc1263679f42416bfb38cf2e9ffe2d9fda 100644 (file)
@@ -26,12 +26,15 @@ disobedience \- GUI client for DisOrder jukebox
 .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"
@@ -274,6 +277,23 @@ fields, or bulk-disable random play for many tracks.
 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
index 61f6f02d3b95ecd5f92e918d74c697a1afcaafbc..e1981f62df5b218181f1791dd1e8005fe3aaac36 100644 (file)
@@ -55,25 +55,25 @@ Display version number.
 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]
@@ -85,7 +85,7 @@ Only directories with a basename matching the regexp will be returned.
 .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
@@ -238,7 +238,7 @@ Unset the preference \fIKEY\fR for \fITRACK\fR.
 .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
index b0f20aed8e9f83b8177f4e44efc15688d116211a..a9549ed61f2b5a2548f42483b9e3490590279114 100644 (file)
@@ -214,12 +214,12 @@ Configuration files are read in the following order:
 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.
@@ -691,7 +691,7 @@ Specifies the module used to calculate the length of files matching
 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"
@@ -768,8 +768,9 @@ longer needs to be specified.
 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.
index cc88541a44878864ea490e523a719ff52d5d2796..5a2bf839a25e5601ad66b4e84824b48979ee3f4b 100644 (file)
@@ -91,6 +91,10 @@ Set a user property.
 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.
@@ -233,15 +237,15 @@ See below for the track information syntax.
 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
@@ -317,12 +321,15 @@ Requires the \fBprefs\fR right.
 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
@@ -487,11 +494,11 @@ Completed playing \fITRACK\fR
 .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...
@@ -503,16 +510,16 @@ Added \fIID\fR to the recently played list.
 .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.
index 5402be3954d4bcd0d0c9deefce4272e833d1f462..ecd106f43272299e1f12ae03b80626eaad69220f 100644 (file)
@@ -34,6 +34,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        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                 \
@@ -118,7 +119,7 @@ test_SOURCES=test.c memgc.c test.h t-addr.c t-basen.c t-cache.c             \
        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
 
diff --git a/lib/bits.c b/lib/bits.c
new file mode 100644 (file)
index 0000000..6b5273b
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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:
+*/
diff --git a/lib/bits.h b/lib/bits.h
new file mode 100644 (file)
index 0000000..0965cb6
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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:
+*/
index 98f60a899d917fd5d1ed4c843b071285f54754f5..a8ca322e071bed886259f8d26d74e5f663713516 100644 (file)
@@ -841,9 +841,11 @@ static void stash_command(disorder_eclient *c,
 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)
@@ -1263,6 +1265,86 @@ int disorder_eclient_rtp_address(disorder_eclient *c,
                 "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
index 37ec9cbd9edfd80ade9df2b8ffd5f635c3b2b429..157ad59301095a19556e0a2a9121de6e1c5cbf94 100644 (file)
@@ -141,8 +141,14 @@ struct sink;
 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 */
@@ -326,6 +332,31 @@ int disorder_eclient_rtp_address(disorder_eclient *c,
                                  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
 
 /*
diff --git a/lib/t-bits.c b/lib/t-bits.c
new file mode 100644 (file)
index 0000000..846abb8
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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:
+*/
index c205a5733a18e172ff4576f4e6ac15fda508dd55..30af3a53d0684328bf3656fd21655c0d58ab30ca 100644 (file)
@@ -21,7 +21,7 @@
 
 #include "test.h"
 
-int tests, errors;
+long long tests, errors;
 int fail_first;
 
 void count_error(void) {
@@ -158,7 +158,8 @@ int main(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;
 }
   
index 2dac63433097827959b5f466fea5c7547d499cdf..32eaa6f87a71e6eff58188e1d61f7979228c92a5 100644 (file)
@@ -70,7 +70,7 @@
 #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 */
@@ -153,6 +153,7 @@ void test_url(void);
 void test_utf8(void);
 void test_words(void);
 void test_wstat(void);
+void test_bits(void);
 
 #endif /* TEST_H */
 
index 6a93fbaba33562676ea7fd0f828ca64855d2dd10..80bfa01e0647e402b4658eb1fd24e6a6b8618bea 100644 (file)
@@ -2630,10 +2630,13 @@ int trackdb_edituserinfo(const char *user,
       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;
index 79ac7b114d13d0f7ddf9410a2079e24598bda4cd..e03ff9c3ea1a4c5b935b7f3723eed704ced337ec 100644 (file)
@@ -1173,7 +1173,7 @@ static int c_userinfo(struct conn *c,
   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")
index 2e5f86e85de8f3060faab9098e89e39d701abb22..1f3a39fe568775f767ed7b2ee74d512fd7cc6c4c 100755 (executable)
@@ -79,6 +79,22 @@ def test():
     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()