/*
** Copyright (c) 2006 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)

** 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.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
**
** Commands and procedures used for creating, processing, editing, and
** querying information about users.
*/
#include "config.h"
#include "user.h"

/*
** Strip leading and trailing space from a string and add the string
** onto the end of a blob.
*/
static void strip_string(Blob *pBlob, char *z){
  int i;
  blob_reset(pBlob);
  while( fossil_isspace(*z) ){ z++; }
  for(i=0; z[i]; i++){
    if( z[i]=='\r' || z[i]=='\n' ){
       while( i>0 && fossil_isspace(z[i-1]) ){ i--; }
       z[i] = 0;
       break;
    }
    if( z[i]>0 && z[i]<' ' ) z[i] = ' ';
  }
  blob_append(pBlob, z, -1);
}

#if defined(_WIN32) || (defined(__BIONIC__) && !defined(FOSSIL_HAVE_GETPASS))
#ifdef _WIN32
#include <conio.h>
#endif

/*
** getpass() for Windows and Android.
*/
static char *zPwdBuffer = 0;
static size_t nPwdBuffer = 0;

static char *getpass(const char *prompt){
  char *zPwd;
  size_t nPwd;
  size_t i;
#if defined(_WIN32)
  int useGetch = _isatty(_fileno(stderr));
#endif

  if( zPwdBuffer==0 ){
    zPwdBuffer = fossil_secure_alloc_page(&nPwdBuffer);
    assert( zPwdBuffer );
  }else{
    fossil_secure_zero(zPwdBuffer, nPwdBuffer);
  }
  zPwd = zPwdBuffer;
  nPwd = nPwdBuffer;
  fputs(prompt,stderr);
  fflush(stderr);
  assert( zPwd!=0 );
  assert( nPwd>0 );
  for(i=0; i<nPwd-1; ++i){
#if defined(_WIN32)
    zPwd[i] = useGetch ? _getch() : getc(stdin);
#else
    zPwd[i] = getc(stdin);
#endif
    if(zPwd[i]=='\r' || zPwd[i]=='\n'){
      break;
    }
    /* BS or DEL */
    else if(i>0 && (zPwd[i]==8 || zPwd[i]==127)){
      i -= 2;
      continue;
    }
    /* CTRL-C */
    else if(zPwd[i]==3) {
      i=0;
      break;
    }
    /* ESC */
    else if(zPwd[i]==27){
      i=0;
      break;
    }
    else{
#if defined(_WIN32)
      if( useGetch )
#endif
      fputc('*',stderr);
    }
  }
  zPwd[i]='\0';
  fputs("\n", stderr);
  assert( zPwd==zPwdBuffer );
  return zPwd;
}
void freepass(){
  if( !zPwdBuffer ) return;
  assert( nPwdBuffer>0 );
  fossil_secure_free_page(zPwdBuffer, nPwdBuffer);
}
#endif

/*
** Scramble substitution matrix:
*/
static char aSubst[256];

/*
** Descramble the password
*/
static void userDescramble(char *z){
  int i;
  for(i=0; z[i]; i++) z[i] = aSubst[(unsigned char)z[i]];
}

/* Print a string in 5-letter groups */
static void printFive(const unsigned char *z){
  int i;
  for(i=0; z[i]; i++){
    if( i>0 && (i%5)==0 ) putchar(' ');
    putchar(z[i]);
  }
  putchar('\n');
}

/* Return a pseudo-random integer between 0 and N-1 */
static int randint(int N){
  unsigned char x;
  assert( N<256 );
  sqlite3_randomness(1, &x);
  return x % N;
}

/*
** Generate and print a random scrambling of letters a through z (omitting x)
** and set up the aSubst[] matrix to descramble.
*/
static void userGenerateScrambleCode(void){
  unsigned char zOrig[30];
  unsigned char zA[30];
  unsigned char zB[30];
  int nA = 25;
  int nB = 0;
  int i;
  memcpy(zOrig, "abcdefghijklmnopqrstuvwyz", nA+1);
  memcpy(zA, zOrig, nA+1);
  assert( nA==(int)strlen((char*)zA) );
  for(i=0; i<(int)sizeof(aSubst); i++) aSubst[i] = i;
  printFive(zA);
  while( nA>0 ){
    int x = randint(nA);
    zB[nB++] = zA[x];
    zA[x] = zA[--nA];
  }
  assert( nB==25 );
  zB[nB] = 0;
  printFive(zB);
  for(i=0; i<nB; i++) aSubst[zB[i]] = zOrig[i];
}

/*
** Return the value of the FOSSIL_SECURITY_LEVEL environment variable.
** Or return 0 if that variable does not exist.
*/
int fossil_security_level(void){
  const char *zLevel = fossil_getenv("FOSSIL_SECURITY_LEVEL");
  if( zLevel==0 ) return 0;
  return atoi(zLevel);
}


/*
** Do a single prompt for a passphrase.  Store the results in the blob.
**
**
** The return value is a pointer to a static buffer that is overwritten
** on subsequent calls to this same routine.
*/
static void prompt_for_passphrase(const char *zPrompt, Blob *pPassphrase){
  char *z;
#if 0
  */
  ** If the FOSSIL_PWREADER environment variable is set, then it will
  ** be the name of a program that prompts the user for their password/
  ** passphrase in a secure manner.  The program should take one or more
  ** arguments which are the prompts and should output the acquired
  ** passphrase as a single line on stdout.  This function will read the
  ** output using popen().
  **
  ** If FOSSIL_PWREADER is not set, or if it is not the name of an
  ** executable, then use the C-library getpass() routine.
  */
  const char *zProg = fossil_getenv("FOSSIL_PWREADER");
  if( zProg && zProg[0] ){
    static char zPass[100];
    Blob cmd;
    FILE *in;
    blob_zero(&cmd);
    blob_appendf(&cmd, "%s \"Fossil Passphrase\" \"%s\"", zProg, zPrompt);
    zPass[0] = 0;
    in = popen(blob_str(&cmd), "r");
    fgets(zPass, sizeof(zPass), in);
    pclose(in);
    blob_reset(&cmd);
    z = zPass;
  }else
#endif
  if( fossil_security_level()>=2 ){
    userGenerateScrambleCode();
    z = getpass(zPrompt);
    if( z ) userDescramble(z);
    printf("\033[3A\033[J");  /* Erase previous three lines */
    fflush(stdout);
  }else{
    z = getpass(zPrompt);
  }
  strip_string(pPassphrase, z);
}

/*
** Prompt the user for a password.  Store the result in the pPassphrase
** blob.
**
** Behavior is controlled by the verify parameter:
**
**     0     Just ask once.
**
**     1     If the first answer is a non-empty string, ask for
**           verification.  Repeat if the two strings do not match.
**
**     2     Ask twice, repeat if the strings do not match.
*/
void prompt_for_password(
  const char *zPrompt,
  Blob *pPassphrase,
  int verify
){
  Blob secondTry;
  blob_zero(pPassphrase);
  blob_zero(&secondTry);
  while(1){
    prompt_for_passphrase(zPrompt, pPassphrase);
    if( verify==0 ) break;
    if( verify==1 && blob_size(pPassphrase)==0 ) break;
    prompt_for_passphrase("Retype new password: ", &secondTry);
    if( blob_compare(pPassphrase, &secondTry) ){
      fossil_print("Passphrases do not match.  Try again...\n");
    }else{
      break;
    }
  }
  blob_reset(&secondTry);
}

/*
** Prompt to save Fossil user password
*/
int save_password_prompt(const char *passwd){
  Blob x;
  char c;
  if( fossil_security_level()>=1 ) return 0;
  prompt_user("remember password (Y/n)? ", &x);
  c = blob_str(&x)[0];
  blob_reset(&x);
  return ( c!='n' && c!='N' );
}

/*
** Prompt for Fossil user password
*/
char *prompt_for_user_password(const char *zUser){
  char *zPrompt = mprintf("\rpassword for %s: ", zUser);
  char *zPw;
  Blob x;
  fossil_force_newline();
  prompt_for_password(zPrompt, &x, 0);
  free(zPrompt);
  zPw = mprintf("%b", &x);
  blob_reset(&x);
  return zPw;
}

/*
** Prompt the user to enter a single line of text.
*/
void prompt_user(const char *zPrompt, Blob *pIn){
  char *z;
  char zLine[1000];
  blob_zero(pIn);
  fossil_force_newline();
  fossil_print("%s", zPrompt);
  fflush(stdout);
  z = fgets(zLine, sizeof(zLine), stdin);
  if( z ){
    int n = (int)strlen(z);
    if( n>0 && z[n-1]=='\n' ) fossil_new_line_started();
    strip_string(pIn, z);
  }
}

/*
** COMMAND: user*
**
** Usage: %fossil user SUBCOMMAND ...  ?-R|--repository REPO?
**
** Run various subcommands on users of the open repository or of
** the repository identified by the -R or --repository option.
**
** > fossil user capabilities USERNAME ?STRING?
**
**        Query or set the capabilities for user USERNAME
**
** > fossil user contact USERNAME ?CONTACT-INFO?
**
**        Query or set contact information for user USERNAME
**
** > fossil user default ?USERNAME?
**
**        Query or set the default user.  The default user is the
**        user for command-line interaction.
**
** > fossil user list
** > fossil user ls
**
**        List all users known to the repository
**
** > fossil user new ?USERNAME? ?CONTACT-INFO? ?PASSWORD?
**
**        Create a new user in the repository.  Users can never be
**        deleted.  They can be denied all access but they must continue
**        to exist in the database.
**
** > fossil user password USERNAME ?PASSWORD?
**
**        Change the web access password for a user.
*/
void user_cmd(void){
  int n;
  db_find_and_open_repository(0, 0);
  if( g.argc<3 ){
    usage("capabilities|contact|default|list|new|password ...");
  }
  n = strlen(g.argv[2]);
  if( n>=2 && strncmp(g.argv[2],"new",n)==0 ){
    Blob passwd, login, caps, contact;
    char *zPw;
    blob_init(&caps, db_get("default-perms", 0), -1);

    if( g.argc>=4 ){
      blob_init(&login, g.argv[3], -1);
    }else{
      prompt_user("login: ", &login);
    }
    if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){
      fossil_fatal("user %b already exists", &login);
    }
    if( g.argc>=5 ){
      blob_init(&contact, g.argv[4], -1);
    }else{
      prompt_user("contact-info: ", &contact);
    }
    if( g.argc>=6 ){
      blob_init(&passwd, g.argv[5], -1);
    }else{
      prompt_for_password("password: ", &passwd, 1);
    }
    zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
    db_unprotect(PROTECT_USER);
    db_multi_exec(
      "INSERT INTO user(login,pw,cap,info,mtime)"
      "VALUES(%B,%Q,%B,%B,now())",
      &login, zPw, &caps, &contact
    );
    db_protect_pop();
    free(zPw);
  }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
    if( g.argc==3 ){
      user_select();
      fossil_print("%s\n", g.zLogin);
    }else{
      if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
        fossil_fatal("no such user: %s", g.argv[3]);
      }
      if( g.localOpen ){
        db_lset("default-user", g.argv[3]);
      }else{
        db_set("default-user", g.argv[3], 0);
      }
    }
  }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) ||
           ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){
    Stmt q;
    db_prepare(&q, "SELECT login, info FROM user ORDER BY login");
    while( db_step(&q)==SQLITE_ROW ){
      fossil_print("%-12s %s\n", db_column_text(&q, 0), db_column_text(&q, 1));
    }
    db_finalize(&q);
  }else if( n>=2 && strncmp(g.argv[2],"password",2)==0 ){
    char *zPrompt;
    int uid;
    Blob pw;
    if( g.argc!=4 && g.argc!=5 ) usage("password USERNAME ?NEW-PASSWORD?");
    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]);
    if( uid==0 ){
      fossil_fatal("no such user: %s", g.argv[3]);
    }
    if( g.argc==5 ){
      blob_init(&pw, g.argv[4], -1);
    }else{
      zPrompt = mprintf("New password for %s: ", g.argv[3]);
      prompt_for_password(zPrompt, &pw, 1);
    }
    if( blob_size(&pw)==0 ){
      fossil_print("password unchanged\n");
    }else{
      char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
      db_unprotect(PROTECT_USER);
      db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
                    zSecret, uid);
      db_protect_pop();
      free(zSecret);
    }
  }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
    int uid;
    if( g.argc!=4 && g.argc!=5 ){
      usage("capabilities USERNAME ?PERMISSIONS?");
    }
    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]);
    if( uid==0 ){
      fossil_fatal("no such user: %s", g.argv[3]);
    }
    if( g.argc==5 ){
      db_unprotect(PROTECT_USER);
      db_multi_exec(
        "UPDATE user SET cap=%Q, mtime=now() WHERE uid=%d",
        g.argv[4], uid
      );
      db_protect_pop();
    }
    fossil_print("%s\n", db_text(0, "SELECT cap FROM user WHERE uid=%d", uid));
  }else if( n>=2 && strncmp(g.argv[2], "contact", 2)==0 ){
    int uid;
    if( g.argc!=4 && g.argc!=5 ){
      usage("contact USERNAME ?CONTACT-INFO?");
    }
    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]);
    if( uid==0 ){
      fossil_fatal("no such user: %s", g.argv[3]);
    }
    if( g.argc==5 ){
      db_unprotect(PROTECT_USER);
      db_multi_exec(
        "UPDATE user SET info=%Q, mtime=now() WHERE uid=%d",
        g.argv[4], uid
      );
      db_protect_pop();
    }
    fossil_print("%s\n", db_text(0, "SELECT info FROM user WHERE uid=%d", uid));
  }else{
    fossil_fatal("user subcommand should be one of: "
                 "capabilities contact default list new password");
  }
}

/*
** Attempt to set the user to zLogin
*/
static int attempt_user(const char *zLogin){
  int uid;

  if( zLogin==0 ){
    return 0;
  }
  uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
  if( uid ){
    g.userUid = uid;
    g.zLogin = mprintf("%s", zLogin);
    return 1;
  }
  return 0;
}

/*
** Figure out what user is at the controls.
**
**   (1)  Use the --user and -U command-line options.
**
**   (2)  If the local database is open, check in VVAR.
**
**   (3)  Check the default user in the repository
**
**   (4)  Try the FOSSIL_USER environment variable.
**
**   (5)  Try the USER environment variable.
**
**   (6)  Try the LOGNAME environment variable.
**
**   (7)  Try the USERNAME environment variable.
**
**   (8)  Check if the user can be extracted from the remote URL.
**
** The user name is stored in g.zLogin.  The uid is in g.userUid.
*/
void user_select(void){
  UrlData url;
  if( g.userUid ) return;
  if( g.zLogin ){
    if( attempt_user(g.zLogin)==0 ){
      fossil_fatal("no such user: %s", g.zLogin);
    }else{
      return;
    }
  }

  if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return;

  if( attempt_user(db_get("default-user", 0)) ) return;

  if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return;

  if( attempt_user(fossil_getenv("USER")) ) return;

  if( attempt_user(fossil_getenv("LOGNAME")) ) return;

  if( attempt_user(fossil_getenv("USERNAME")) ) return;

  memset(&url, 0, sizeof(url));
  url_parse_local(0, URL_USE_CONFIG, &url);
  if( url.user && attempt_user(url.user) ) return;

  fossil_print(
    "Cannot figure out who you are!  Consider using the --user\n"
    "command line option, setting your USER environment variable,\n"
    "or setting a default user with \"fossil user default USER\".\n"
  );
  fossil_fatal("cannot determine user");
}

/*
** COMMAND: test-usernames
**
** Usage: %fossil test-usernames
**
** Print details about sources of fossil usernames.
*/
void test_usernames_cmd(void){
  db_find_and_open_repository(0, 0);

  fossil_print("Initial g.zLogin: %s\n", g.zLogin);
  fossil_print("Initial g.userUid: %d\n", g.userUid);
  fossil_print("check-out default-user: %s\n", g.localOpen ?
               db_lget("default-user","") : "<<no open check-out>>");
  fossil_print("default-user: %s\n", db_get("default-user",""));
  fossil_print("FOSSIL_USER: %s\n", fossil_getenv("FOSSIL_USER"));
  fossil_print("USER: %s\n", fossil_getenv("USER"));
  fossil_print("LOGNAME: %s\n", fossil_getenv("LOGNAME"));
  fossil_print("USERNAME: %s\n", fossil_getenv("USERNAME"));
  url_parse(0, URL_USE_CONFIG);
  fossil_print("URL user: %s\n", g.url.user);
  user_select();
  fossil_print("Final g.zLogin: %s\n", g.zLogin);
  fossil_print("Final g.userUid: %d\n", g.userUid);
}


/*
** Make sure the USER table is up-to-date.  It should contain
** the "JX" column (as of version 2.21).  If it does not, add it.
**
** The "JX" column is intended to hold a JSON object containing optional
** key-value pairs.
*/
void user_update_user_table(void){
  if( db_table_has_column("repository","user","jx")==0 ){
    db_multi_exec("ALTER TABLE repository.user"
                  " ADD COLUMN jx TEXT DEFAULT '{}';");
  }
}

/*
** COMMAND: test-hash-passwords
**
** Usage: %fossil test-hash-passwords REPOSITORY
**
** Convert all local password storage to use a SHA1 hash of the password
** rather than cleartext.  Passwords that are already stored as the SHA1
** has are unchanged.
*/
void user_hash_passwords_cmd(void){
  if( g.argc!=3 ) usage("REPOSITORY");
  db_open_repository(g.argv[2]);
  sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0,
                          sha1_shared_secret_sql_function, 0, 0);
  db_unprotect(PROTECT_ALL);
  db_multi_exec(
    "UPDATE user SET pw=shared_secret(pw,login), mtime=now()"
    " WHERE length(pw)>0 AND length(pw)!=40"
  );
}

/*
** Ensure that the password for a user is hashed.
*/
void hash_user_password(const char *zUser){
  sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0,
                          sha1_shared_secret_sql_function, 0, 0);
  db_unprotect(PROTECT_USER);
  db_multi_exec(
    "UPDATE user SET pw=shared_secret(pw,login), mtime=now()"
    " WHERE login=%Q AND length(pw)>0 AND length(pw)!=40", zUser
  );
  db_protect_pop();
}

/*
** COMMAND: test-prompt-user
**
** Usage: %fossil test-prompt-user PROMPT
**
** Prompts the user for input and then prints it verbatim (i.e. without
** a trailing line terminator).
*/
void test_prompt_user_cmd(void){
  Blob answer;
  if( g.argc!=3 ) usage("PROMPT");
  prompt_user(g.argv[2], &answer);
  fossil_print("%s\n", blob_str(&answer));
}

/*
** COMMAND: test-prompt-password
**
** Usage: %fossil test-prompt-password PROMPT VERIFY
**
** Prompts the user for a password and then prints it verbatim.
**
** Behavior is controlled by the VERIFY parameter:
**
**     0     Just ask once.
**
**     1     If the first answer is a non-empty string, ask for
**           verification.  Repeat if the two strings do not match.
**
**     2     Ask twice, repeat if the strings do not match.

*/
void test_prompt_password_cmd(void){
  Blob answer;
  int iVerify = 0;
  if( g.argc!=4 ) usage("PROMPT VERIFY");
  iVerify = atoi(g.argv[3]);
  prompt_for_password(g.argv[2], &answer, iVerify);
  fossil_print("[%s]\n", blob_str(&answer));
}

/*
** WEBPAGE: access_log
** WEBPAGE: user_log
**
** Show login attempts, including timestamp and IP address.
** Requires Admin privileges.
**
** Query parameters:
**
**    y=N      1: success only.  2: failure only.  3: both (default: 3)
**    n=N      Number of entries to show (default: 200)
**    o=N      Skip this many entries (default: 0)
*/
void user_log_page(void){
  int y = atoi(PD("y","3"));
  int n = atoi(PD("n","200"));
  int skip = atoi(PD("o","0"));
  const char *zUser = P("u");
  Blob sql;
  Stmt q;
  int cnt = 0;
  int rc;
  int fLogEnabled;

  login_check_credentials();
  if( !g.perm.Admin ){ login_needed(0); return; }
  create_accesslog_table();


  if( P("delall") && P("delallbtn") ){
    db_multi_exec("DELETE FROM accesslog");
    cgi_redirectf("%R/user_log?y=%d&n=%d&o=%o", y, n, skip);
    return;
  }
  if( P("delanon") && P("delanonbtn") ){
    db_multi_exec("DELETE FROM accesslog WHERE uname='anonymous'");
    cgi_redirectf("%R/user_log?y=%d&n=%d&o=%o", y, n, skip);
    return;
  }
  if( P("delfail") && P("delfailbtn") ){
    db_multi_exec("DELETE FROM accesslog WHERE NOT success");
    cgi_redirectf("%R/user_log?y=%d&n=%d&o=%o", y, n, skip);
    return;
  }
  if( P("delold") && P("deloldbtn") ){
    db_multi_exec("DELETE FROM accesslog WHERE rowid in"
                  "(SELECT rowid FROM accesslog ORDER BY rowid DESC"
                  " LIMIT -1 OFFSET 200)");
    cgi_redirectf("%R/user_log?y=%d&n=%d", y, n);
    return;
  }
  style_header("User Log");
  style_submenu_element("Log-Menu", "setup-logmenu");

  blob_zero(&sql);
  blob_append_sql(&sql,
    "SELECT uname, ipaddr, datetime(mtime,toLocal()), success"
    "  FROM accesslog"
  );
  if( zUser ){
    blob_append_sql(&sql, "  WHERE uname=%Q", zUser);
    n = 1000000000;
    skip = 0;
  }else if( y==1 ){
    blob_append(&sql, "  WHERE success", -1);
  }else if( y==2 ){
    blob_append(&sql, "  WHERE NOT success", -1);
  }
  blob_append_sql(&sql,"  ORDER BY rowid DESC LIMIT %d OFFSET %d", n+1, skip);
  if( skip ){
    style_submenu_element("Newer", "%R/user_log?o=%d&n=%d&y=%d",
              skip>=n ? skip-n : 0, n, y);
  }
  rc = db_prepare_ignore_error(&q, "%s", blob_sql_text(&sql));
  fLogEnabled = db_get_boolean("access-log", 0);
  @ <div align="center">User logging is %s(fLogEnabled?"on":"off").
  @ (Change this on the <a href="setup_settings">settings</a> page.)</div>
  @ <table border="1" cellpadding="5" class="sortable" align="center" \
  @  data-column-types='Ttt' data-init-sort='1'>
  @ <thead><tr><th width="33%%">Date</th><th width="34%%">User</th>
  @ <th width="33%%">IP Address</th></tr></thead><tbody>
  while( rc==SQLITE_OK && db_step(&q)==SQLITE_ROW ){
    const char *zName = db_column_text(&q, 0);
    const char *zIP = db_column_text(&q, 1);
    const char *zDate = db_column_text(&q, 2);
    int bSuccess = db_column_int(&q, 3);
    cnt++;
    if( cnt>n ){
      style_submenu_element("Older", "%R/user_log?o=%d&n=%d&y=%d",
                  skip+n, n, y);
      break;
    }
    if( bSuccess ){
      @ <tr>
    }else{
      @ <tr bgcolor="#ffacc0">
    }
    @ <td>%s(zDate)</td><td>%h(zName)</td><td>%h(zIP)</td></tr>
  }
  if( skip>0 || cnt>n ){
    style_submenu_element("All", "%R/user_log?n=10000000");
  }
  @ </tbody></table>
  db_finalize(&q);
  @ <hr>
  @ <form method="post" action="%R/user_log">
  @ <label><input type="checkbox" name="delold">
  @ Delete all but the most recent 200 entries</input></label>
  @ <input type="submit" name="deloldbtn" value="Delete"></input>
  @ </form>
  @ <form method="post" action="%R/user_log">
  @ <label><input type="checkbox" name="delanon">
  @ Delete all entries for user "anonymous"</input></label>
  @ <input type="submit" name="delanonbtn" value="Delete"></input>
  @ </form>
  @ <form method="post" action="%R/user_log">
  @ <label><input type="checkbox" name="delfail">
  @ Delete all failed login attempts</input></label>
  @ <input type="submit" name="delfailbtn" value="Delete"></input>
  @ </form>
  @ <form method="post" action="%R/user_log">
  @ <label><input type="checkbox" name="delall">
  @ Delete all entries</input></label>
  @ <input type="submit" name="delallbtn" value="Delete"></input>
  @ </form>
  style_table_sorter();
  style_finish_page();
}
