summaryrefslogtreecommitdiff
path: root/src/user.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/user.c
downloadrelic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/user.c')
-rw-r--r--src/user.c835
1 files changed, 835 insertions, 0 deletions
diff --git a/src/user.c b/src/user.c
new file mode 100644
index 0000000..e5a393f
--- /dev/null
+++ b/src/user.c
@@ -0,0 +1,835 @@
+/*
+** 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);
+ zPwdBuffer = 0;
+ nPwdBuffer = 0;
+}
+#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);
+ fossil_free(zPrompt);
+ zPw = blob_str(&x)/*transfer ownership*/;
+ 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_init(pIn, 0, 0);
+ 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 ?OPTIONS? ?USERNAME?
+**
+** Query or set the default user. The default user is the
+** user for command-line interaction. If USERNAME is an
+** empty string, then the default user is unset from the
+** repository and will subsequently be determined by the -U
+** command-line option or by environment variables
+** FOSSIL_USER, USER, LOGNAME, or USERNAME, in that order.
+** OPTIONS:
+**
+** -v|--verbose Show how the default user is computed
+**
+** > fossil user list | 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 ){
+ int eVerbose = find_option("verbose","v",0)!=0;
+ verify_all_options();
+ if( g.argc>3 ){
+ const char *zUser = g.argv[3];
+ if( fossil_strcmp(zUser,"")==0 || fossil_stricmp(zUser,"nobody")==0 ){
+ db_begin_transaction();
+ if( g.localOpen ){
+ db_multi_exec("DELETE FROM vvar WHERE name='default-user'");
+ }
+ db_unset("default-user",0);
+ db_commit_transaction();
+ }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);
+ }
+ }
+ }
+ if( g.argc==3 || eVerbose ){
+ int eHow = user_select();
+ const char *zHow = "???";
+ switch( eHow ){
+ case 1: zHow = "-U option"; break;
+ case 2: zHow = "previously set"; break;
+ case 3: zHow = "local check-out"; break;
+ case 4: zHow = "repository"; break;
+ case 5: zHow = "FOSSIL_USER"; break;
+ case 6: zHow = "USER"; break;
+ case 7: zHow = "LOGNAME"; break;
+ case 8: zHow = "USERNAME"; break;
+ case 9: zHow = "URL"; break;
+ }
+ if( eVerbose ){
+ fossil_print("%s (determined by %s)\n", g.zLogin, zHow);
+ }else{
+ fossil_print("%s\n", g.zLogin);
+ }
+ }
+ }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();
+ fossil_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 = fossil_strdup(zLogin);
+ return 1;
+ }
+ return 0;
+}
+
+/*
+** Figure out what user is at the controls.
+**
+** (1) Use the --user and -U command-line options.
+**
+** (2) The name used for login (if there was a login).
+**
+** (3) If the local database is open, check in VVAR.
+**
+** (4) Check the default-user in the repository
+**
+** (5) Try the FOSSIL_USER environment variable.
+**
+** (6) Try the USER environment variable.
+**
+** (7) Try the LOGNAME environment variable.
+**
+** (8) Try the USERNAME environment variable.
+**
+** (9) 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.
+*/
+int user_select(void){
+ UrlData url;
+ if( g.userUid ) return 1;
+ if( g.zLogin ){
+ if( attempt_user(g.zLogin)==0 ){
+ fossil_fatal("no such user: %s", g.zLogin);
+ }else{
+ return 2;
+ }
+ }
+
+ if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return 3;
+
+ if( attempt_user(db_get("default-user", 0)) ) return 4;
+
+ if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return 5;
+
+ if( attempt_user(fossil_getenv("USER")) ) return 6;
+
+ if( attempt_user(fossil_getenv("LOGNAME")) ) return 7;
+
+ if( attempt_user(fossil_getenv("USERNAME")) ) return 8;
+
+ memset(&url, 0, sizeof(url));
+ url_parse_local(0, URL_USE_CONFIG, &url);
+ if( url.user && attempt_user(url.user) ) return 9;
+
+ 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", 1);
+ @ <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();
+}