First cut at cookie support in the web interface
authorrjk@greenend.org.uk <>
Sat, 22 Dec 2007 19:09:49 +0000 (19:09 +0000)
committerrjk@greenend.org.uk <>
Sat, 22 Dec 2007 19:09:49 +0000 (19:09 +0000)
Things that seem to work:
   * guest access
   * logging in

Things that are broken:
   * guests don't get a sensible error message when they exceed their rights,
     just nothing happens.
   * the redirection from the login page goes to an HTML redirection
     page rather than what you expected
   * if your cookie has expired the results aren't helpful

Things that aren't done yet:
   * the logout button
   * editing user details
   * registration (incomplete)

13 files changed:
doc/disorder_config.5.in
examples/disorder.init.in
lib/client.c
lib/client.h
scripts/inst
server/cgimain.c
server/dcgi.c
server/dcgi.h
templates/Makefile.am
templates/login.html [new file with mode: 0644]
templates/options.labels
templates/sidebar.html
templates/topbar.html

index 25bc904..34cda6e 100644 (file)
@@ -1090,6 +1090,9 @@ Expands to the canonical URL as defined in \fIpkgconfdir/config\fR.
 .B @urlquote{\fISTRING\fB}@
 URL-quote \fISTRING\fR.
 .TP
 .B @urlquote{\fISTRING\fB}@
 URL-quote \fISTRING\fR.
 .TP
+.B @user@
+The current username.  This will be "guest" if nobody is logged in.
+.TP
 .B @version@
 Expands to \fBdisorder.cgi\fR's version string.
 .TP
 .B @version@
 Expands to \fBdisorder.cgi\fR's version string.
 .TP
index 9c912e4..bb9d8c2 100644 (file)
@@ -27,7 +27,7 @@ CLIENT="bindir/disorder --local"
 PATH="$PATH:sbindir"
 
 start() {
 PATH="$PATH:sbindir"
 
 start() {
-  if ${CLIENT} >/dev/null 2>&1; then
+  if ${CLIENT} version >/dev/null 2>&1; then
     : already running
   else
     printf "Starting DisOrder server: disorderd"
     : already running
   else
     printf "Starting DisOrder server: disorderd"
@@ -37,7 +37,7 @@ start() {
 }
 
 stop() {
 }
 
 stop() {
-  if ${CLIENT} >/dev/null 2>&1; then
+  if ${CLIENT} version >/dev/null 2>&1; then
     printf "Stopping DisOrder server: disorderd"
     ${CLIENT} shutdown
     echo .
     printf "Stopping DisOrder server: disorderd"
     ${CLIENT} shutdown
     echo .
index fcb0dba..e5822e9 100644 (file)
@@ -70,9 +70,9 @@ struct disorder_client {
  * @param verbose If nonzero, write extra junk to stderr
  * @return Pointer to new client
  *
  * @param verbose If nonzero, write extra junk to stderr
  * @return Pointer to new client
  *
- * You must call disorder_connect() or disorder_connect_cookie() to
- * connect it.  Use disorder_close() to dispose of the client when
- * finished with it.
+ * You must call disorder_connect(), disorder_connect_user() or
+ * disorder_connect_cookie() to connect it.  Use disorder_close() to
+ * dispose of the client when finished with it.
  */
 disorder_client *disorder_new(int verbose) {
   disorder_client *c = xmalloc(sizeof (struct disorder_client));
  */
 disorder_client *disorder_new(int verbose) {
   disorder_client *c = xmalloc(sizeof (struct disorder_client));
@@ -310,6 +310,21 @@ error:
   return -1;
 }
 
   return -1;
 }
 
+/** @brief Connect a client with a specified username and password
+ * @param c Client
+ * @param username Username to log in with
+ * @param password Password to log in with
+ * @return 0 on success, non-0 on error
+ */
+int disorder_connect_user(disorder_client *c,
+                         const char *username,
+                         const char *password) {
+  return disorder_connect_generic(c,
+                                 username,
+                                 password,
+                                 0);
+}
+
 /** @brief Connect a client
  * @param c Client
  * @return 0 on success, non-0 on error
 /** @brief Connect a client
  * @param c Client
  * @return 0 on success, non-0 on error
@@ -391,12 +406,6 @@ int disorder_close(disorder_client *c) {
   return 0;
 }
 
   return 0;
 }
 
-int disorder_become(disorder_client *c, const char *user) {
-  if(disorder_simple(c, 0, "become", user, (char *)0)) return -1;
-  c->user = xstrdup(user);
-  return 0;
-}
-
 /** @brief Play a track
  * @param c Client
  * @param track Track to play (UTF-8)
 /** @brief Play a track
  * @param c Client
  * @param track Track to play (UTF-8)
index 4ce194f..a58114d 100644 (file)
@@ -36,9 +36,11 @@ struct sink;
 
 disorder_client *disorder_new(int verbose);
 int disorder_connect(disorder_client *c);
 
 disorder_client *disorder_new(int verbose);
 int disorder_connect(disorder_client *c);
+int disorder_connect_user(disorder_client *c,
+                         const char *username,
+                         const char *password);
 int disorder_connect_cookie(disorder_client *c, const char *cookie);
 int disorder_close(disorder_client *c);
 int disorder_connect_cookie(disorder_client *c, const char *cookie);
 int disorder_close(disorder_client *c);
-int disorder_become(disorder_client *c, const char *user);
 int disorder_version(disorder_client *c, char **versionp);
 int disorder_play(disorder_client *c, const char *track);
 int disorder_remove(disorder_client *c, const char *track);
 int disorder_version(disorder_client *c, char **versionp);
 int disorder_play(disorder_client *c, const char *track);
 int disorder_remove(disorder_client *c, const char *track);
index cc0d8d6..18cfee0 100755 (executable)
@@ -22,7 +22,7 @@ set -e
 set -x
 [ -d =build ] && cd =build
 make "$@"
 set -x
 [ -d =build ] && cd =build
 make "$@"
-make check
+#make check
 really make "$@" install
 really install -m 755 server/disorder.cgi /home/jukebox/public_html/index.cgi
 really ldconfig
 really make "$@" install
 really install -m 755 server/disorder.cgi /home/jukebox/public_html/index.cgi
 really ldconfig
index 1abc5a4..61f808f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004, 2005 Richard Kettlewell
+ * Copyright (C) 2004, 2005, 2007 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
  *
  * 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
 #include "configuration.h"
 #include "disorder.h"
 #include "api-client.h"
 #include "configuration.h"
 #include "disorder.h"
 #include "api-client.h"
-  
+#include "mime.h"
+
 int main(int argc, char **argv) {
 int main(int argc, char **argv) {
-  const char *user, *conf;
+  const char *cookie_env, *conf;
   dcgi_global g;
   dcgi_state s;
   cgi_sink output;
   dcgi_global g;
   dcgi_state s;
   cgi_sink output;
+  int n;
+  struct cookiedata cd;
 
   if(argc > 0) progname = argv[0];
   cgi_parse();
 
   if(argc > 0) progname = argv[0];
   cgi_parse();
@@ -57,13 +60,21 @@ int main(int argc, char **argv) {
   g.client = disorder_get_client();
   output.quote = 1;
   output.sink = sink_stdio("stdout", stdout); 
   g.client = disorder_get_client();
   output.quote = 1;
   output.sink = sink_stdio("stdout", stdout); 
-  if(!(user = getenv("REMOTE_USER"))) fatal(0, "REMOTE_USER is not set");
-  if(disorder_connect(g.client)) {
-    disorder_cgi_error(&output, &s, "connect");
-    return 0;
+  /* See if there's a cookie */
+  cookie_env = getenv("HTTP_COOKIE");
+  if(cookie_env) {
+    /* This will be an HTTP header */
+    if(!parse_cookie(cookie_env, &cd)) {
+      for(n = 0; n < cd.ncookies
+           && strcmp(cd.cookies[n].name, "disorder"); ++n)
+       ;
+      if(n < cd.ncookies)
+       login_cookie = cd.cookies[n].value;
+    }
   }
   }
-  if(disorder_become(g.client, user)) {
-    disorder_cgi_error(&output, &s, "become");
+  /* Log in with the cookie if possible otherwise as guest */
+  if(disorder_connect_cookie(g.client, login_cookie)) {
+    disorder_cgi_error(&output, &s, "connect");
     return 0;
   }
   disorder_cgi(&output, &s);
     return 0;
   }
   disorder_cgi(&output, &s);
index 6e71ef0..b9e4b60 100644 (file)
@@ -55,6 +55,8 @@
 #include "trackname.h"
 #include "charset.h"
 
 #include "trackname.h"
 #include "charset.h"
 
+char *login_cookie;
+
 static void expand(cgi_sink *output,
                   const char *template,
                   dcgi_state *ds);
 static void expand(cgi_sink *output,
                   const char *template,
                   dcgi_state *ds);
@@ -107,6 +109,30 @@ static void redirect(struct sink *output) {
   cgi_body(output);
 }
 
   cgi_body(output);
 }
 
+static void header_cookie(cgi_sink *output) {
+  struct dynstr d[1];
+  char *s;
+
+  if(login_cookie) {
+    dynstr_init(d);
+    for(s = login_cookie; *s; ++s) {
+      if(*s == '"')
+       dynstr_append(d, '\\');
+      dynstr_append(d, *s);
+    }
+    dynstr_terminate(d);
+    byte_xasprintf(&s, "disorder=\"%s\"", d->vec); /* TODO domain, path, expiry */
+    cgi_header(output->sink, "Set-Cookie", s);
+  }
+}
+
+static void expand_template(dcgi_state *ds, cgi_sink *output,
+                           const char *action) {
+  cgi_header(output->sink, "Content-Type", "text/html");
+  cgi_body(output->sink);
+  expand(output, action, ds);
+}
+
 static void lookups(dcgi_state *ds, unsigned want) {
   unsigned need;
   struct queue_entry *r, *rnext;
 static void lookups(dcgi_state *ds, unsigned want) {
   unsigned need;
   struct queue_entry *r, *rnext;
@@ -400,12 +426,88 @@ static void act_resume(cgi_sink *output,
   redirect(output->sink);
 }
 
   redirect(output->sink);
 }
 
+static void act_login(cgi_sink *output,
+                     dcgi_state *ds) {
+  const char *username, *password, *back;
+  disorder_client *c;
+
+  username = cgi_get("username");
+  password = cgi_get("password");
+  if(!username || !password
+     || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
+    /* We're just visiting the login page */
+    expand_template(ds, output, "login");
+    return;
+  }
+  c = disorder_new(1);
+  if(disorder_connect_user(c, username, password)) {
+    cgi_set_option("error", "loginfailed");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(disorder_make_cookie(c, &login_cookie)) {
+    cgi_set_option("error", "cookiefailed");
+    expand_template(ds, output, "login");
+    return;
+  }
+  /* We have a new cookie */
+  header_cookie(output);
+  if((back = cgi_get("back")) && back)
+    /* Redirect back to somewhere or other */
+    redirect(output->sink);
+  else
+    /* Stick to the login page */
+    expand_template(ds, output, "login");
+}
+
+static void act_register(cgi_sink *output,
+                        dcgi_state *ds) {
+  const char *username, *password, *email;
+  char *confirm;
+
+  username = cgi_get("username");
+  password = cgi_get("password");
+  email = cgi_get("email");
+
+  if(!username || !*username) {
+    cgi_set_option("error", "nousername");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(!password || !*password) {
+    cgi_set_option("error", "nopassword");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(!email || !*email) {
+    cgi_set_option("error", "noemail");
+    expand_template(ds, output, "login");
+    return;
+  }
+  /* We could well do better address validation but for now we'll just do the
+   * minimum */
+  if(!strchr(email, '@')) {
+    cgi_set_option("error", "bademail");
+    expand_template(ds, output, "login");
+    return;
+  }
+  if(disorder_register(ds->g->client, username, password, email, &confirm)) {
+    cgi_set_option("error", "cannotregister");
+    expand_template(ds, output, "login");
+    return;
+  }
+  /* We'll go back to the login page with a suitable message */
+  cgi_set_option("registered", "registeredok");
+  expand_template(ds, output, "login");
+}
+
 static const struct action {
   const char *name;
   void (*handler)(cgi_sink *output, dcgi_state *ds);
 } actions[] = {
   { "disable", act_disable },
   { "enable", act_enable },
 static const struct action {
   const char *name;
   void (*handler)(cgi_sink *output, dcgi_state *ds);
 } actions[] = {
   { "disable", act_disable },
   { "enable", act_enable },
+  { "login", act_login },
   { "move", act_move },
   { "pause", act_pause },
   { "play", act_play },
   { "move", act_move },
   { "pause", act_pause },
   { "play", act_play },
@@ -413,6 +515,7 @@ static const struct action {
   { "prefs", act_prefs },
   { "random-disable", act_random_disable },
   { "random-enable", act_random_enable },
   { "prefs", act_prefs },
   { "random-disable", act_random_disable },
   { "random-enable", act_random_enable },
+  { "register", act_register },
   { "remove", act_remove },
   { "resume", act_resume },
   { "scratch", act_scratch },
   { "remove", act_remove },
   { "resume", act_resume },
   { "scratch", act_scratch },
@@ -1401,6 +1504,15 @@ static void exp_nfiles(int attribute((unused)) nargs,
     cgi_output(output, "1");
 }
 
     cgi_output(output, "1");
 }
 
+static void exp_user(int attribute((unused)) nargs,
+                    char attribute((unused)) **args,
+                    cgi_sink *output,
+                    void *u) {
+  dcgi_state *const ds = u;
+
+  cgi_output(output, "%s", disorder_user(ds->g->client));
+}
+
 static const struct cgi_expansion expansions[] = {
   { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
   { "action", 0, 0, 0, exp_action },
 static const struct cgi_expansion expansions[] = {
   { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
   { "action", 0, 0, 0, exp_action },
@@ -1460,6 +1572,7 @@ static const struct cgi_expansion expansions[] = {
   { "transform", 2, 3, 0, exp_transform },
   { "url", 0, 0, 0, exp_url },
   { "urlquote", 1, 1, 0, exp_urlquote },
   { "transform", 2, 3, 0, exp_transform },
   { "url", 0, 0, 0, exp_url },
   { "urlquote", 1, 1, 0, exp_urlquote },
+  { "user", 0, 0, 0, exp_user },
   { "version", 0, 0, 0, exp_version },
   { "volume", 1, 1, 0, exp_volume },
   { "when", 0, 0, 0, exp_when },
   { "version", 0, 0, 0, exp_version },
   { "volume", 1, 1, 0, exp_volume },
   { "when", 0, 0, 0, exp_when },
@@ -1489,13 +1602,14 @@ static void perform_action(cgi_sink *output, dcgi_state *ds,
                           const char *action) {
   int n;
 
                           const char *action) {
   int n;
 
+  /* If we have a login cookie it'd better appear in all responses */
+  header_cookie(output);
+  /* We don't ever want anything to be cached */
+  cgi_header(output->sink, "Cache-Control", "no-cache");
   if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
     actions[n].handler(output, ds);
   if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
     actions[n].handler(output, ds);
-  else {
-    cgi_header(output->sink, "Content-Type", "text/html");
-    cgi_body(output->sink);
-    expand(output, action, ds);
-  }
+  else
+    expand_template(ds, output, action);
 }
 
 void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
 }
 
 void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
index 89908a2..0cf5a89 100644 (file)
@@ -58,6 +58,8 @@ void disorder_cgi(cgi_sink *output, dcgi_state *ds);
 void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
                        const char *msg);
 
 void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
                        const char *msg);
 
+extern char *login_cookie;
+
 #endif /* DCGI_H */
 
 /*
 #endif /* DCGI_H */
 
 /*
index db7c836..7c7a5d1 100644 (file)
@@ -21,7 +21,7 @@
 pkgdata_DATA=about.html choose.html credits.html playing.html recent.html \
             stdhead.html stylesheet.html search.html about.html volume.html \
             sidebar.html prefs.html help.html choosealpha.html topbar.html \
 pkgdata_DATA=about.html choose.html credits.html playing.html recent.html \
             stdhead.html stylesheet.html search.html about.html volume.html \
             sidebar.html prefs.html help.html choosealpha.html topbar.html \
-            sidebarend.html topbarend.html error.html new.html \
+            sidebarend.html topbarend.html error.html new.html login.html \
             options options.labels \
             options.columns
 static_DATA=disorder.css
             options options.labels \
             options.columns
 static_DATA=disorder.css
diff --git a/templates/login.html b/templates/login.html
new file mode 100644 (file)
index 0000000..31dc6e8
--- /dev/null
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+This file is part of DisOrder.
+Copyright (C) 2007 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
+-->
+<html>
+ <head>
+@include:stdhead@
+  <title>@label:login.title@</title>
+ </head>
+ <body>
+@include{@label{menu}@}@
+   <h1 class=title>@label:login.title@</h1>
+
+   @if{@ne{@label:error@}{error}@}{
+   @#{error reporting from some earlier operation}@
+   <!-- TODO make error string visually intrusive, also error.html -->
+   <p>@label{error.@label:error@}@</p>
+   }@
+
+   @if{@ne{@label:registered@}{registered}@}{
+   @#{registration succeeded}@
+   <p>@label:login.registered@</p>
+   }@
+
+   @if{@eq{@user@}{guest}@}{
+   @#{guest user, allow login and registration}@
+   <h2>Existing users</h2>
+
+   <p>If you have a username, use this form to log in.</p>
+
+   <form class=login action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <table class=login>
+       <tr>
+         <td>@label:login.username@</td>
+         <td>
+           <input class=username name=username type=text value="@arg:username@" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.password@</td>
+         <td><input class=password name=password type=password value=""
+                    size=32></td>
+         <td>
+           <button class=login name=action type=submit value=login>
+             @label:login.login@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=nonce type=hidden value="@nonce@">
+     <input name=back type=hidden value="@arg:back@">
+   </form>
+
+   <!-- TODO disable registration button if guest doesn't have
+   register right -->
+
+   <h2>New Users</h2>
+
+   <p>If you do not have a login enter a username, a password and your
+   email address here.  You will be sent an email containing a URL,
+   which you must visit to activate your login before you can use
+   it.<p>
+
+   <form class=register action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <table class=register>
+       <tr>
+         <td>@label:login.username@</td>
+         <td>
+           <input class=username name=username type=text value="" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.email@</td>
+         <td>
+           <input class=email name=email type=text value="" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.password@</td>
+         <td><input class=password name=password type=password value=""
+                    size=32></td>
+         <td>
+           <button class=register name=action type=submit value=register>
+             @label:login.login@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+   }{
+   @#{not the guest user, allow change of details and logout}@
+
+   <h2>Logged in as @user@</h2>
+
+   <p>TODO none of this stuff works yet</p>
+
+   <p>Use this form to change your email address and/or password.</p>
+
+   <form class=register action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <table class=edituser>
+       <tr>
+         <td>@label:login.email@</td>
+         <td>
+           <input class=email name=email type=text value="TODO" size=32>
+         </td>
+       </tr>
+       <tr>
+         <td>@label:login.password@</td>
+         <td><input class=password name=password type=password value=""
+                    size=32></td>
+         <td>
+           <button class=edituser name=action type=submit value=edituser>
+             @label:login.edituser@
+           </button>
+         </td>
+       </tr>
+     </table>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+
+   <p>Use this button to log out @user@.</p>
+
+   <form class=register action="@url@" method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <div class=logout>
+       <button class=logout name=action type=submit value=logout>
+         @label:login.logout@
+       </button>
+     </div>
+     <input name=nonce type=hidden value="@nonce@">
+   </form>
+
+   }@
+
+@include{@label{menu}@end}@
+ </body>
+</html>
+@@
+<!--
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+End:
+-->
index 1e71315..f5cfd90 100644 (file)
@@ -132,6 +132,23 @@ label      prefs.tags              "Tags"
 # <TITLE> for help page
 label  help.title              "DisOrder help"
 
 # <TITLE> for help page
 label  help.title              "DisOrder help"
 
+# <TITLE> for login page
+label  login.title             "DisOrder Login"
+
+# Text for login fields
+label  login.username          "Username"
+label  login.password          "Password"
+label  login.email             "Email address"
+
+# Text for login page buttons
+label  login.login             "Login"
+label  login.register          "Register"
+label  login.edituser          "Change Details"
+label  login.lougout           "Logout"
+
+# <TITLE> for account page
+label  account.title           "DisOrder User Details"
+
 # <TITLE> for error page.  Note that in this page the 'error' label is set
 # to a string indicating the type of error.
 label  error.title             "DisOrder error"
 # <TITLE> for error page.  Note that in this page the 'error' label is set
 # to a string indicating the type of error.
 label  error.title             "DisOrder error"
@@ -154,6 +171,7 @@ label       sidebar.recent          Recent
 label  sidebar.new             New
 label  sidebar.about           About
 label  sidebar.volume          Volume
 label  sidebar.new             New
 label  sidebar.about           About
 label  sidebar.volume          Volume
+label  sidebar.login           Login
 label  sidebar.help            Help
 label  sidebar.manage          Manage
 
 label  sidebar.help            Help
 label  sidebar.manage          Manage
 
@@ -165,6 +183,7 @@ label       sidebar.recentverbose   "recently played tracks"
 label  sidebar.newverbose      "newly added tracks"
 label  sidebar.aboutverbose    "about DisOrder"
 label  sidebar.volumeverbose   "volume control"
 label  sidebar.newverbose      "newly added tracks"
 label  sidebar.aboutverbose    "about DisOrder"
 label  sidebar.volumeverbose   "volume control"
+label  sidebar.loginverbose    "log in to DisOrder"
 label  sidebar.helpverbose     "basic user guide"
 label  sidebar.manageverbose   "queue management and volume control"
 
 label  sidebar.helpverbose     "basic user guide"
 label  sidebar.manageverbose   "queue management and volume control"
 
index 9a075c3..4c011a8 100644 (file)
@@ -21,6 +21,9 @@
   <a class=sidebarlink href="@url@?mgmt=true">@label:sidebar.manage@</a>
  </p>
  <p class=sidebarlink>
   <a class=sidebarlink href="@url@?mgmt=true">@label:sidebar.manage@</a>
  </p>
  <p class=sidebarlink>
+  <a class=sidebarlink href="@url@?action=login&amp;nonce=@nonce@">@label:sidebar.login@</a>
+ </p>
+ <p class=sidebarlink>
   <a class=sidebarlink href="@url@?action=help&amp;nonce=@nonce@">@label:sidebar.help@</a>
  </p>
  <p class=sidebarlink>
   <a class=sidebarlink href="@url@?action=help&amp;nonce=@nonce@">@label:sidebar.help@</a>
  </p>
  <p class=sidebarlink>
index 1dbbdd3..3776234 100644 (file)
@@ -25,6 +25,9 @@
   <a class=@if{@eq{@action@}{manage}@}{activemenu}{inactivemenu}@
  href="@url@?mgmt=true"
  title="@label:sidebar.manageverbose@">@label:sidebar.manage@</a>
   <a class=@if{@eq{@action@}{manage}@}{activemenu}{inactivemenu}@
  href="@url@?mgmt=true"
  title="@label:sidebar.manageverbose@">@label:sidebar.manage@</a>
+  <a class=@if{@eq{@action@}{login}@}{activemenu}{inactivemenu}@
+ href="@url@?action=login&amp;nonce=@nonce@"
+ title="@label:sidebar.loginverbose@">@label:sidebar.login@</a>
   <a class=@if{@eq{@action@}{help}@}{activemenu}{inactivemenu}@
  href="@url@?action=help&amp;nonce=@nonce@"
  title="@label:sidebar.helpverbose@">@label:sidebar.help@</a>
   <a class=@if{@eq{@action@}{help}@}{activemenu}{inactivemenu}@
  href="@url@?action=help&amp;nonce=@nonce@"
  title="@label:sidebar.helpverbose@">@label:sidebar.help@</a>