/* ** Copyright (c) 2007 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/ ** ******************************************************************************* ** ** Setup pages associated with user management. The code in this ** file was formerly part of the "setup.c" module, but has been broken ** out into its own module to improve maintainability. ** ** Note: Do not confuse "Users" with "Subscribers". Code to deal with ** subscribers is over in the "alerts.c" source file. */ #include "config.h" #include #include "setupuser.h" /* ** WEBPAGE: setup_ulist ** ** Show a list of users. Clicking on any user jumps to the edit ** screen for that user. Requires Admin privileges. ** ** Query parameters: ** ** with=CAP Only show users that have one or more capabilities in CAP. ** ubg Color backgrounds by username hash */ void setup_ulist(void){ Stmt s; double rNow; const char *zWith = P("with"); int bUnusedOnly = P("unused")!=0; int bUbg = P("ubg")!=0; login_check_credentials(); if( !g.perm.Admin ){ login_needed(0); return; } style_submenu_element("Add", "setup_uedit"); style_submenu_element("Log", "access_log"); style_submenu_element("Help", "setup_ulist_notes"); if( alert_tables_exist() ){ style_submenu_element("Subscribers", "subscribers"); } style_set_current_feature("setup"); style_header("User List"); if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){ @ @ @ @ db_prepare(&s, "SELECT uid, login, cap, date(mtime,'unixepoch')" " FROM user" " WHERE login IN ('anonymous','nobody','developer','reader')" " ORDER BY login" ); while( db_step(&s)==SQLITE_ROW ){ int uid = db_column_int(&s, 0); const char *zLogin = db_column_text(&s, 1); const char *zCap = db_column_text(&s, 2); const char *zDate = db_column_text(&s, 4); @ @ } db_finalize(&s); @
Category @ Capabilities (key) @ Info Last Change
%h(zLogin) @ %h(zCap) if( fossil_strcmp(zLogin,"anonymous")==0 ){ @ All logged-in users }else if( fossil_strcmp(zLogin,"developer")==0 ){ @ Users with 'v' capability }else if( fossil_strcmp(zLogin,"nobody")==0 ){ @ All users without login }else if( fossil_strcmp(zLogin,"reader")==0 ){ @ Users with 'u' capability }else{ @ } if( zDate && zDate[0] ){ @ %h(zDate) }else{ @ } @
@
Users
}else{ style_submenu_element("All Users", "setup_ulist"); if( bUnusedOnly ){ @
Unused logins
}else if( zWith ){ if( zWith[1]==0 ){ @
Users with capability "%h(zWith)"
}else{ @
Users with any capability in "%h(zWith)"
} } } if( !bUnusedOnly ){ style_submenu_element("Unused", "setup_ulist?unused"); } @ @ @ @ db_multi_exec( "CREATE TEMP TABLE lastAccess(uname TEXT PRIMARY KEY, atime REAL)" "WITHOUT ROWID;" ); if( db_table_exists("repository","accesslog") ){ db_multi_exec( "INSERT INTO lastAccess(uname, atime)" " SELECT uname, max(mtime) FROM (" " SELECT uname, mtime FROM accesslog WHERE success" " UNION ALL" " SELECT login AS uname, rcvfrom.mtime AS mtime" " FROM rcvfrom JOIN user USING(uid))" " GROUP BY 1;" ); } if( !db_table_exists("repository","subscriber") ){ db_multi_exec( "CREATE TEMP TABLE subscriber(suname PRIMARY KEY, ssub, subscriberId)" "WITHOUT ROWID;" ); } if( bUnusedOnly ){ zWith = mprintf( " AND login NOT IN (" "SELECT user FROM event WHERE user NOT NULL " "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)" " AND uid NOT IN (SELECT uid FROM rcvfrom)", alert_tables_exist() ? " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":""); }else if( zWith && zWith[0] ){ zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith); }else{ zWith = ""; } db_prepare(&s, "SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," " lower(login) AS sortkey, " " CASE WHEN info LIKE '%%expires 20%%'" " THEN substr(info,instr(lower(info),'expires')+8,10)" " END AS exp," "atime," " subscriber.ssub, subscriber.subscriberId," " user.mtime AS sorttime" " FROM user LEFT JOIN lastAccess ON login=uname" " LEFT JOIN subscriber ON login=suname" " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" " ORDER BY sorttime DESC", zWith/*safe-for-%s*/ ); rNow = db_double(0.0, "SELECT julianday('now');"); while( db_step(&s)==SQLITE_ROW ){ int uid = db_column_int(&s, 0); const char *zLogin = db_column_text(&s, 1); const char *zCap = db_column_text(&s, 2); const char *zInfo = db_column_text(&s, 3); const char *zDate = db_column_text(&s, 4); const char *zSortKey = db_column_text(&s,5); const char *zExp = db_column_text(&s,6); double rATime = db_column_double(&s,7); char *zAge = 0; const char *zSub; int sid = db_column_int(&s,9); sqlite3_int64 sorttime = db_column_int64(&s, 10); if( rATime>0.0 ){ zAge = human_readable_age(rNow - rATime); } if( bUbg ){ @ }else{ @ } @ fossil_free(zAge); } @
Login NameCapsInfoDateExpireLast Login\ @ Alerts
\ @ %h(zLogin) @ %h(zCap) @ %h(zInfo) @ %h(zDate?zDate:"") @ %h(zExp?zExp:"") @ %s(zAge?zAge:"") if( db_column_type(&s,8)==SQLITE_NULL ){ @ }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){ @ off }else{ @ %h(zSub) } @
db_finalize(&s); style_table_sorter(); style_finish_page(); } /* ** WEBPAGE: setup_ulist_notes ** ** A documentation page showing notes about user configuration. This ** information used to be a side-bar on the user list page, but has been ** factored out for improved presentation. */ void setup_ulist_notes(void){ style_set_current_feature("setup"); style_header("User Configuration Notes"); @

User Configuration Notes:

@
    @
  1. @ Every user, logged in or not, inherits the privileges of @ nobody. @

  2. @ @
  3. @ Any human can login as anonymous since the @ password is clearly displayed on the login page for them to type. The @ purpose of requiring anonymous to log in is to prevent access by spiders. @ Every logged-in user inherits the combined privileges of @ anonymous and @ nobody. @

  4. @ @
  5. @ Users with privilege u inherit the combined @ privileges of reader, @ anonymous, and @ nobody. @

  6. @ @
  7. @ Users with privilege v inherit the combined @ privileges of developer, @ anonymous, and @ nobody. @

  8. @ @
  9. The permission flags are as follows:

    capabilities_table(CAPCLASS_ALL); @
  10. @
style_finish_page(); } /* ** WEBPAGE: setup_ucap_list ** ** A documentation page showing the meaning of the various user capabilities ** code letters. */ void setup_ucap_list(void){ style_set_current_feature("setup"); style_header("User Capability Codes"); @

All capabilities

capabilities_table(CAPCLASS_ALL); @

Capabilities associated with checked-in content

capabilities_table(CAPCLASS_CODE); @

Capabilities associated with data transfer and sync

capabilities_table(CAPCLASS_DATA); @

Capabilities associated with the forum

capabilities_table(CAPCLASS_FORUM); @

Capabilities associated with tickets

capabilities_table(CAPCLASS_TKT); @

Capabilities associated with wiki

capabilities_table(CAPCLASS_WIKI); @

Administrative capabilities

capabilities_table(CAPCLASS_SUPER); @

Miscellaneous capabilities

capabilities_table(CAPCLASS_OTHER); style_finish_page(); } /* ** Return true if zPw is a valid password string. A valid ** password string is: ** ** (1) A zero-length string, or ** (2) a string that contains a character other than '*'. */ static int isValidPwString(const char *zPw){ if( zPw==0 ) return 0; if( zPw[0]==0 ) return 1; while( zPw[0]=='*' ){ zPw++; } return zPw[0]!=0; } /* ** Return true if user capability string zNew contains any capability ** letter which is not in user capability string zOrig, else 0. This ** does not take inherited permissions into account. Either argument ** may be NULL. */ static int userHasNewCaps(const char *zOrig, const char *zNew){ for( ; zNew && *zNew; ++zNew ){ if( !zOrig || strchr(zOrig,*zNew)==0 ){ return *zNew; } } return 0; } /* ** Sends notification of user permission elevation changes to all ** subscribers with a "u" subscription. This is a no-op if alerts are ** not enabled. ** ** These subscriptions differ from most, in that: ** ** - They currently lack an "unsubscribe" link. ** ** - Only an admin can assign this subscription, but if a non-admin ** edits their subscriptions after an admin assigns them this one, ** this particular one will be lost. "Feature or bug?" is unclear, ** but it would be odd for a non-admin to be assigned this ** capability. */ static void alert_user_elevation(const char *zLogin, /*Affected user*/ int uid, /*[user].uid*/ int bIsNew, /*true if new user*/ const char *zOrigCaps,/*Old caps*/ const char *zNewCaps /*New caps*/){ Blob hdr, body; Stmt q; int nBody; AlertSender *pSender; char *zSubname; char *zURL; char * zSubject; if( !alert_enabled() ) return; zSubject = bIsNew ? mprintf("New user created: [%q]", zLogin) : mprintf("User [%q] capabilities changed", zLogin); zURL = db_get("email-url",0); zSubname = db_get("email-subname", "[Fossil Repo]"); blob_init(&body, 0, 0); blob_init(&hdr, 0, 0); if( bIsNew ){ blob_appendf(&body, "User [%q] was created with " "permissions [%q] by user [%q].\n", zLogin, zNewCaps, g.zLogin); } else { blob_appendf(&body, "Permissions for user [%q] where changed " "from [%q] to [%q] by user [%q].\n", zLogin, zOrigCaps, zNewCaps, g.zLogin); } if( zURL ){ blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid); } nBody = blob_size(&body); pSender = alert_sender_new(0, 0); db_prepare(&q, "SELECT semail, hex(subscriberCode)" " FROM subscriber, user " " WHERE sverified AND NOT sdonotcall" " AND suname=login" " AND ssub GLOB '*u*'"); while( !pSender->zErr && db_step(&q)==SQLITE_ROW ){ const char *zTo = db_column_text(&q, 0); blob_truncate(&hdr, 0); blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSubname, zSubject); if( zURL ){ const char *zCode = db_column_text(&q, 1); blob_truncate(&body, nBody); blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", zURL, zCode); } alert_send(pSender, &hdr, &body, 0); } db_finalize(&q); alert_sender_free(pSender); fossil_free(zURL); fossil_free(zSubname); fossil_free(zSubject); } /* ** WEBPAGE: setup_uedit ** ** Edit information about a user or create a new user. ** Requires Admin privileges. */ void user_edit(void){ const char *zId, *zLogin, *zInfo, *zCap, *zPw; const char *zGroup; const char *zOldLogin; int uid, i; char *zOldCaps = 0; /* Capabilities before edit */ char *zDeleteVerify = 0; /* Delete user verification text */ int higherUser = 0; /* True if user being edited is SETUP and the */ /* user doing the editing is ADMIN. Disallow editing */ const char *inherit[128]; int a[128]; const char *oa[128]; /* Must have ADMIN privileges to access this page */ login_check_credentials(); if( !g.perm.Admin ){ login_needed(0); return; } /* Check to see if an ADMIN user is trying to edit a SETUP account. ** Don't allow that. */ zId = PD("id", "0"); uid = atoi(zId); if( uid>0 ){ zOldCaps = db_text("", "SELECT cap FROM user WHERE uid=%d",uid); if( zId && !g.perm.Setup ){ higherUser = zOldCaps && strchr(zOldCaps,'s'); } } if( P("can") ){ /* User pressed the cancel button */ cgi_redirect(cgi_referer("setup_ulist")); return; } /* Check for requests to delete the user */ if( P("delete") && cgi_csrf_safe(2) ){ int n; if( P("verifydelete") ){ /* Verified delete user request */ db_unprotect(PROTECT_USER); if( alert_tables_exist() ){ /* Also delete any subscriptions associated with this user */ db_multi_exec("DELETE FROM subscriber WHERE suname=" "(SELECT login FROM user WHERE uid=%d)", uid); } db_multi_exec("DELETE FROM user WHERE uid=%d", uid); db_protect_pop(); moderation_disapprove_for_missing_users(); admin_log("Deleted user [%s] (uid %d).", PD("login","???")/*safe-for-%s*/, uid); cgi_redirect(cgi_referer("setup_ulist")); return; } n = db_int(0, "SELECT count(*) FROM event" " WHERE user=%Q AND objid NOT IN private", P("login")); if( n==0 ){ zDeleteVerify = mprintf("Check this box and press \"Delete User\" again"); }else{ zDeleteVerify = mprintf( "User \"%s\" has %d or more artifacts in the block-chain. " "Delete anyhow?", P("login")/*safe-for-%s*/, n); } } style_set_current_feature("setup"); /* If we have all the necessary information, write the new or ** modified user record. After writing the user record, redirect ** to the page that displays a list of users. */ if( !cgi_all("login","info","pw","apply") ){ /* need all of the above properties to make a change. Since one or ** more are missing, no-op */ }else if( higherUser ){ /* An Admin (a) user cannot edit a Superuser (s) */ }else if( zDeleteVerify!=0 ){ /* Need to verify a delete request */ }else if( !cgi_csrf_safe(2) ){ /* This might be a cross-site request forgery, so ignore it */ }else{ /* We have all the information we need to make the change to the user */ char c; int bHasNewCaps = 0 /* 1 if user's permissions are increased */; const int bIsNew = uid<=0; char aCap[70], zNm[4]; zNm[0] = 'a'; zNm[2] = 0; for(i=0, c='a'; c<='z'; c++){ zNm[1] = c; a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0; if( a[c&0x7f] ) aCap[i++] = c; } for(c='0'; c<='9'; c++){ zNm[1] = c; a[c&0x7f] = P(zNm)!=0; if( a[c&0x7f] ) aCap[i++] = c; } for(c='A'; c<='Z'; c++){ zNm[1] = c; a[c&0x7f] = P(zNm)!=0; if( a[c&0x7f] ) aCap[i++] = c; } aCap[i] = 0; bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]); zPw = P("pw"); zLogin = P("login"); if( strlen(zLogin)==0 ){ const char *zRef = cgi_referer("setup_ulist"); style_header("User Creation Error"); @ Empty login not allowed. @ @

@ [Bummer]

style_finish_page(); return; } if( isValidPwString(zPw) ){ zPw = sha1_shared_secret(zPw, zLogin, 0); }else{ zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid); } zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid); if( db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d",zLogin,uid) ){ const char *zRef = cgi_referer("setup_ulist"); style_header("User Creation Error"); @ Login "%h(zLogin)" is already used by @ a different user. @ @

@ [Bummer]

style_finish_page(); return; } cgi_csrf_verify(); db_unprotect(PROTECT_USER); uid = db_int(0, "REPLACE INTO user(uid,login,info,pw,cap,mtime) " "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now()) " "RETURNING uid", uid, zLogin, P("info"), zPw, &aCap[0]); assert( uid>0 ); if( zOldLogin && fossil_strcmp(zLogin, zOldLogin)!=0 ){ if( alert_tables_exist() ){ /* Rename matching subscriber entry, else the user cannot re-subscribe with their same email address. */ db_multi_exec("UPDATE subscriber SET suname=%Q WHERE suname=%Q", zLogin, zOldLogin); } admin_log( "Renamed user [%q] to [%q].", zOldLogin, zLogin ); } db_protect_pop(); setup_incr_cfgcnt(); admin_log( "%s user [%q] with capabilities [%q].", bIsNew ? "Added" : "Updated", zLogin, &aCap[0] ); if( atoi(PD("all","0"))>0 ){ Blob sql; char *zErr = 0; blob_zero(&sql); if( zOldLogin==0 ){ blob_appendf(&sql, "INSERT INTO user(login)" " SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);", zLogin, zLogin ); zOldLogin = zLogin; } #if 0 /* Problem: when renaming a user we need to update the subcriber ** names to match but we cannot know from here if each member of ** the login group has the subscriber tables, so we cannot blindly ** include this SQL. */ else if( fossil_strcmp(zLogin, zOldLogin)!=0 && alert_tables_exist() ){ /* Rename matching subscriber entry, else the user cannot re-subscribe with their same email address. */ blob_appendf(&sql, "UPDATE subscriber SET suname=%Q WHERE suname=%Q;", zLogin, zOldLogin); } #endif blob_appendf(&sql, "UPDATE user SET login=%Q," " pw=coalesce(shared_secret(%Q,%Q," "(SELECT value FROM config WHERE name='project-code')),pw)," " info=%Q," " cap=%Q," " mtime=now()" " WHERE login=%Q;", zLogin, P("pw"), zLogin, P("info"), &aCap[0], zOldLogin ); db_unprotect(PROTECT_USER); login_group_sql(blob_str(&sql), "
  • ", "
  • \n", &zErr); db_protect_pop(); blob_reset(&sql); admin_log( "Updated user [%q] in all login groups " "with capabilities [%q].", zLogin, &aCap[0] ); if( zErr ){ const char *zRef = cgi_referer("setup_ulist"); style_header("User Change Error"); admin_log( "Error updating user '%q': %s'.", zLogin, zErr ); @ %h(zErr) @ @

    @ [Bummer]

    style_finish_page(); if( bHasNewCaps ){ alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); } return; } } if( bHasNewCaps ){ alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); } cgi_redirect(cgi_referer("setup_ulist")); return; } /* Load the existing information about the user, if any */ zLogin = ""; zInfo = ""; zCap = zOldCaps; zPw = ""; for(i='a'; i<='z'; i++) oa[i] = ""; for(i='0'; i<='9'; i++) oa[i] = ""; for(i='A'; i<='Z'; i++) oa[i] = ""; if( uid ){ assert( zCap ); zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid); zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid); zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid); for(i=0; zCap[i]; i++){ char c = zCap[i]; if( (c>='a' && c<='z') || (c>='0' && c<='9') || (c>='A' && c<='Z') ){ oa[c&0x7f] = " checked=\"checked\""; } } } /* figure out inherited permissions */ memset((char *)inherit, 0, sizeof(inherit)); if( fossil_strcmp(zLogin, "developer") ){ char *z1, *z2; z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='developer'"); while( z1 && *z1 ){ inherit[0x7f & *(z1++)] = "[D]"; } free(z2); } if( fossil_strcmp(zLogin, "reader") ){ char *z1, *z2; z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='reader'"); while( z1 && *z1 ){ inherit[0x7f & *(z1++)] = "[R]"; } free(z2); } if( fossil_strcmp(zLogin, "anonymous") ){ char *z1, *z2; z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='anonymous'"); while( z1 && *z1 ){ inherit[0x7f & *(z1++)] = "[A]"; } free(z2); } if( fossil_strcmp(zLogin, "nobody") ){ char *z1, *z2; z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='nobody'"); while( z1 && *z1 ){ inherit[0x7f & *(z1++)] = "[N]"; } free(z2); } /* Begin generating the page */ style_submenu_element("Cancel", "%s", cgi_referer("setup_ulist")); if( uid ){ style_header("Edit User %h", zLogin); if( !login_is_special(zLogin) ){ style_submenu_element("Access Log", "%R/access_log?u=%t", zLogin); style_submenu_element("Timeline","%R/timeline?u=%t", zLogin); } }else{ style_header("Add A New User"); } @
    @
    login_insert_csrf_secret(); if( login_is_special(zLogin) ){ @ @ @ } @ @ @ @ if( uid ){ @ }else{ @ } @ @ @ if( login_is_special(zLogin) ){ @ }else{ @ @ @ @ } @ @ @ @ @ @ @ @ @ if( !login_is_special(zLogin) ){ @ @ if( zPw[0] ){ /* Obscure the password for all users */ @ }else{ /* Show an empty password as an empty input field */ char *zRPW = fossil_random_password(12); @ } @ } zGroup = login_group_name(); if( zGroup ){ @ @ @ } if( !higherUser ){ if( zDeleteVerify ){ @ @ @ @ } @ @ @ @ } @
    User ID:%d(uid) \ @ (new user)
    Login:%h(zLogin) if( alert_tables_exist() ){ int sid; sid = db_int(0, "SELECT subscriberId FROM subscriber" " WHERE suname=%Q", zLogin); if( sid>0 ){ @   \ @ (subscription info for %h(zLogin))\ } } @
    Contact Info:
    Capabilities: #define B(x) inherit[x] @
    @
      if( g.perm.Setup ){ @
    • } @
    • @
    • @
    • #if 0 /* Not Used */ @
    • #endif @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    • @
    @
    Selected Cap: @ (missing JS?) @ (key) @
    Password: @ (Leave unchanged to retain password) Password suggestion: %z(zRPW)
    Scope: @ @ Apply changes to this repository only.
    @ @ Apply changes to all repositories in the "%h(zGroup)" @ login group.
    Verify:
      if( !login_is_special(zLogin) ){ @ } @
    @
    @
    builtin_request_js("useredit.js"); @
    @

    Notes On Privileges And Capabilities:

    @ @ @

    Special Logins

    @ @ style_finish_page(); } /* ** WEBPAGE: setup_uinfo ** ** Detailed information about a user account, available to administrators ** only. ** ** u=UID ** l=LOGIN */ void setup_uinfo_page(void){ Stmt q; Blob sql; const char *zLogin; int uid; /* Must have ADMIN privileges to access this page */ login_check_credentials(); if( !g.perm.Admin ){ login_needed(0); return; } style_set_current_feature("setup"); zLogin = P("l"); uid = atoi(PD("u","0")); if( zLogin==0 && uid==0 ){ uid = db_int(1,"SELECT uid FROM user"); } blob_init(&sql, 0, 0); blob_append_sql(&sql, "SELECT " /* 0 */ "uid," /* 1 */ "login," /* 2 */ "cap," /* 3 */ "cookie," /* 4 */ "datetime(cexpire)," /* 5 */ "info," /* 6 */ "datetime(user.mtime,'unixepoch')," ); if( db_table_exists("repository","subscriber") ){ blob_append_sql(&sql, /* 7 */ "subscriberId," /* 8 */ "semail," /* 9 */ "sverified," /* 10 */ "date(lastContact+2440587.5)" " FROM user LEFT JOIN subscriber ON suname=login" ); }else{ blob_append_sql(&sql, /* 7 */ "NULL," /* 8 */ "NULL," /* 9 */ "NULL," /* 10 */ "NULL" " FROM user" ); } if( zLogin!=0 ){ blob_append_sql(&sql, " WHERE login=%Q", zLogin); }else{ blob_append_sql(&sql, " WHERE uid=%d", uid); } db_prepare(&q, "%s", blob_sql_text(&sql)); blob_zero(&sql); if( db_step(&q)!=SQLITE_ROW ){ style_header("No Such User"); if( zLogin ){ @

    Cannot find any information on user %h(zLogin). }else{ @

    Cannot find any information on userid %d(uid). } style_finish_page(); db_finalize(&q); return; } style_header("User %h", db_column_text(&q,1)); @ @ @ @ @ @ @ if( db_column_type(&q,7)!=SQLITE_NULL ){ @ @ @ @ } @
    uid:%d(db_column_int(&q,0)) @ (edit)
    login:%h(db_column_text(&q,1))
    capabilities:%h(db_column_text(&q,2))
    info:\ @ %h(db_column_text(&q,5))
    user.mtime:%h(db_column_text(&q,6))
    subscriberId:%d(db_column_int(&q,7)) @ (edit)
    semail:%h(db_column_text(&q,8))
    verified:%s(db_column_int(&q,9)?"yes":"no")
    lastContact:%h(db_column_text(&q,10))
    db_finalize(&q); style_finish_page(); }