/* ** 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/ ** ******************************************************************************* ** ** Implementation of the Setup page */ #include "config.h" #include #include "setup.h" /* ** Increment the "cfgcnt" variable, so that ETags will know that ** the configuration has changed. */ void setup_incr_cfgcnt(void){ static int once = 1; if( once ){ once = 0; db_multi_exec("UPDATE config SET value=value+1 WHERE name='cfgcnt'"); if( db_changes()==0 ){ db_multi_exec("INSERT INTO config(name,value) VALUES('cfgcnt',1)"); } } } /* ** Output a single entry for a menu generated using an HTML table. ** If zLink is not NULL or an empty string, then it is the page that ** the menu entry will hyperlink to. If zLink is NULL or "", then ** the menu entry has no hyperlink - it is disabled. */ void setup_menu_entry( const char *zTitle, const char *zLink, const char *zDesc ){ @ if( zLink && zLink[0] ){ @ %h(zTitle) }else{ @ %h(zTitle) } @ %h(zDesc) } /* ** WEBPAGE: setup ** ** Main menu for the administrative pages. Requires Admin privileges. */ void setup_page(void){ login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); } style_header("Server Administration"); /* Make sure the header contains . Issue a warning ** if it does not. */ if( !cgi_header_contains("Configuration Error: Please add @ <base href="$secureurl/$current_page"> after @ <head> in the @ HTML header!

} #if !defined(_WIN32) /* Check for /dev/null and /dev/urandom. We want both devices to be present, ** but they are sometimes omitted (by mistake) from chroot jails. */ if( access("/dev/null", R_OK|W_OK) ){ @

WARNING: Device "/dev/null" is not available @ for reading and writing.

} if( access("/dev/urandom", R_OK) ){ @

WARNING: Device "/dev/urandom" is not available @ for reading. This means that the pseudo-random number generator used @ by SQLite will be poorly seeded.

} #endif @ setup_menu_entry("Users", "setup_ulist", "Grant privileges to individual users."); setup_menu_entry("Access", "setup_access", "Control access settings."); setup_menu_entry("Configuration", "setup_config", "Configure the WWW components of the repository"); setup_menu_entry("Security-Audit", "secaudit0", "Analyze the current configuration for security problems"); setup_menu_entry("Settings", "setup_settings", "Web interface to the \"fossil settings\" command"); setup_menu_entry("Timeline", "setup_timeline", "Timeline display preferences"); setup_menu_entry("Login-Group", "setup_login_group", "Manage single sign-on between this repository and others" " on the same server"); setup_menu_entry("Tickets", "tktsetup", "Configure the trouble-ticketing system for this repository"); setup_menu_entry("Search","srchsetup", "Configure the built-in search engine"); setup_menu_entry("URL Aliases", "waliassetup", "Configure URL aliases"); setup_menu_entry("Notification", "setup_notification", "Automatic notifications of changes via outbound email"); setup_menu_entry("Email-Server", "setup_smtp", "Activate and configure the built-in email server"); setup_menu_entry("Transfers", "xfersetup", "Configure the transfer system for this repository"); setup_menu_entry("Skins", "setup_skin", "Select and/or modify the web interface \"skins\""); setup_menu_entry("Moderation", "setup_modreq", "Enable/Disable requiring moderator approval of Wiki and/or Ticket" " changes and attachments."); setup_menu_entry("Ad-Unit", "setup_adunit", "Edit HTML text for an ad unit inserted after the menu bar"); setup_menu_entry("URLs & Checkouts", "urllist", "Show URLs used to access this repo and known check-outs"); setup_menu_entry("Web-Cache", "cachestat", "View the status of the expensive-page cache"); setup_menu_entry("Logo", "setup_logo", "Change the logo and background images for the server"); setup_menu_entry("Shunned", "shun", "Show artifacts that are shunned by this repository"); setup_menu_entry("Artifact Receipts Log", "rcvfromlist", "A record of received artifacts and their sources"); setup_menu_entry("User Log", "access_log", "A record of login attempts"); setup_menu_entry("Administrative Log", "admin_log", "View the admin_log entries"); setup_menu_entry("Error Log", "errorlog", "View the Fossil server error log"); setup_menu_entry("Unversioned Files", "uvlist?byage=1", "Show all unversioned files held"); setup_menu_entry("Stats", "stat", "Repository Status Reports"); setup_menu_entry("Sitemap", "sitemap", "Links to miscellaneous pages"); setup_menu_entry("SQL", "admin_sql", "Enter raw SQL commands"); setup_menu_entry("TH1", "admin_th1", "Enter raw TH1 commands"); @
style_footer(); } /* ** 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. */ void setup_ulist(void){ Stmt s; double rNow; const char *zWith = P("with"); login_check_credentials(); if( !g.perm.Admin ){ login_needed(0); return; } if( zWith==0 || zWith[0]==0 ){ style_submenu_element("Add", "setup_uedit"); style_submenu_element("Log", "access_log"); style_submenu_element("Help", "setup_ulist_notes"); style_header("User List"); @ @ @ @ 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); }else{ style_header("Users With Capabilities \"%h\"", zWith); } @
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
@ @ @ @ 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( zWith && zWith[0] ){ zWith = mprintf(" AND cap GLOB '*[%q]*'", zWith); }else{ zWith = ""; } db_prepare(&s, "SELECT uid, login, cap, info, date(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" " FROM user LEFT JOIN lastAccess ON login=uname" " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" " ORDER BY sortkey", 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; if( rATime>0.0 ){ zAge = human_readable_age(rNow - rATime); } @ @ fossil_free(zAge); } @
Login NameCapsInfoDateExpireLast Login
\ @ %h(zLogin) @ %h(zCap) @ %h(zInfo) @ %h(zDate?zDate:"") @ %h(zExp?zExp:"") @ %s(zAge?zAge:"") @
db_finalize(&s); style_table_sorter(); style_footer(); } /* ** Render the user-capability table */ static void setup_usercap_table(void){ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
aAdmin: Create and delete users
bAttach: Add attachments to wiki or tickets
cAppend-Tkt: Append to tickets
dDelete: Delete wiki and tickets
eView-PII: \ @ View sensitive data such as email addresses
fNew-Wiki: Create new wiki pages
gClone: Clone the repository
hHyperlinks: Show hyperlinks to detailed @ repository history
iCheck-In: Commit new versions in the repository
jRead-Wiki: View wiki pages
kWrite-Wiki: Edit wiki pages
lMod-Wiki: Moderator for wiki pages
mAppend-Wiki: Append to wiki pages
nNew-Tkt: Create new tickets
oCheck-Out: Check out versions
pPassword: Change your own password
qMod-Tkt: Moderator for tickets
rRead-Tkt: View tickets
sSetup/Super-user: Setup and configure this website
tTkt-Report: Create new bug summary reports
uReader: Inherit privileges of @ user reader
vDeveloper: Inherit privileges of @ user developer
wWrite-Tkt: Edit tickets
xPrivate: Push and/or pull private branches
yWrite-Unver: Push unversioned files
zZip download: Download a ZIP archive or tarball
2Forum-Read: Read forum posts by others
3Forum-Append: Add new forum posts
4Forum-Trusted: Add pre-approved forum posts
5Forum-Moderator: Approve or disapprove forum posts
6Forum-Supervisor: \ @ Forum administrator @
7Email-Alerts: Sign up for email nofications
AAnnounce: Send announcements
} /* ** 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_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:

    setup_usercap_table(); @
  10. @
style_footer(); } /* ** WEBPAGE: setup_ucap_list ** ** A documentation page showing the meaning of the various user capabilities ** code letters. */ void setup_ucap_list(void){ style_header("User Capability Codes"); setup_usercap_table(); style_footer(); } /* ** 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; } /* ** 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 doWrite; int uid, i; 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( zId && !g.perm.Setup && uid>0 ){ char *zOldCaps; zOldCaps = db_text(0, "SELECT cap FROM user WHERE uid=%d",uid); higherUser = zOldCaps && strchr(zOldCaps,'s'); } if( P("can") ){ /* User pressed the cancel button */ cgi_redirect(cgi_referer("setup_ulist")); return; } /* 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. */ doWrite = cgi_all("login","info","pw") && !higherUser && cgi_csrf_safe(1); if( doWrite ){ char c; char zCap[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' || g.perm.Setup) && P(zNm)!=0; if( a[c&0x7f] ) zCap[i++] = c; } for(c='0'; c<='9'; c++){ zNm[1] = c; a[c&0x7f] = P(zNm)!=0; if( a[c&0x7f] ) zCap[i++] = c; } for(c='A'; c<='Z'; c++){ zNm[1] = c; a[c&0x7f] = P(zNm)!=0; if( a[c&0x7f] ) zCap[i++] = c; } zCap[i] = 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_footer(); 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_footer(); return; } login_verify_csrf_secret(); db_multi_exec( "REPLACE INTO user(uid,login,info,pw,cap,mtime) " "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now())", uid, zLogin, P("info"), zPw, zCap ); setup_incr_cfgcnt(); admin_log( "Updated user [%q] with capabilities [%q].", zLogin, zCap ); 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; } 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"), zCap, zOldLogin ); login_group_sql(blob_str(&sql), "
  • ", "
  • \n", &zErr); blob_reset(&sql); admin_log( "Updated user [%q] in all login groups " "with capabilities [%q].", zLogin, zCap ); 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_footer(); return; } } cgi_redirect(cgi_referer("setup_ulist")); return; } /* Load the existing information about the user, if any */ zLogin = ""; zInfo = ""; zCap = ""; 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 ){ zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid); zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid); zCap = db_text("", "SELECT cap 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); style_submenu_element("Access Log", "%R/access_log?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 */ @ } @ } zGroup = login_group_name(); if( zGroup ){ @ @ @ } if( !higherUser ){ @ @ @ @ } @
    User ID:%d(uid) (new user)
    Login:%h(zLogin)
    Contact Info:
    Capabilities: #define B(x) inherit[x] @ @
    if( g.perm.Setup ){ @
    } @
    @
    @
    @
    @
    @
    @
    @
    @
    @ @
    @
    @
    @
    @
    @
    @
    @
    @
    @
    @
    @ @
    @
    @
    @
    @
    @
    @
    @
    @
    @
    @
    @ @
    @
    Selected Cap.: @ (missing JS?) @ (key) @
    Password:
    Scope: @ @ Apply changes to this repository only.
    @ @ Apply changes to all repositories in the "%h(zGroup)" @ login group.
     
    @
    @
    style_load_one_js_file("useredit.js"); @

    Privileges And Capabilities:

    @ @ @

    Special Logins

    @ @ style_footer(); } /* ** Generate a checkbox for an attribute. */ void onoff_attribute( const char *zLabel, /* The text label on the checkbox */ const char *zVar, /* The corresponding row in the VAR table */ const char *zQParm, /* The query parameter */ int dfltVal, /* Default value if VAR table entry does not exist */ int disabled /* 1 if disabled */ ){ const char *zQ = P(zQParm); int iVal = db_get_boolean(zVar, dfltVal); if( zQ==0 && !disabled && P("submit") ){ zQ = "off"; } if( zQ ){ int iQ = fossil_strcmp(zQ,"on")==0 || atoi(zQ); if( iQ!=iVal ){ login_verify_csrf_secret(); db_set(zVar, iQ ? "1" : "0", 0); admin_log("Set option [%q] to [%q].", zVar, iQ ? "on" : "off"); iVal = iQ; } } @ } /* ** Generate an entry box for an attribute. */ void entry_attribute( const char *zLabel, /* The text label on the entry box */ int width, /* Width of the entry box */ const char *zVar, /* The corresponding row in the VAR table */ const char *zQParm, /* The query parameter */ const char *zDflt, /* Default value if VAR table entry does not exist */ int disabled /* 1 if disabled */ ){ const char *zVal = db_get(zVar, zDflt); const char *zQ = P(zQParm); if( zQ && fossil_strcmp(zQ,zVal)!=0 ){ const int nZQ = (int)strlen(zQ); login_verify_csrf_secret(); db_set(zVar, zQ, 0); admin_log("Set entry_attribute %Q to: %.*s%s", zVar, 20, zQ, (nZQ>20 ? "..." : "")); zVal = zQ; } @ %s(zLabel) } /* ** Generate a text box for an attribute. */ const char *textarea_attribute( const char *zLabel, /* The text label on the textarea */ int rows, /* Rows in the textarea */ int cols, /* Columns in the textarea */ const char *zVar, /* The corresponding row in the VAR table */ const char *zQP, /* The query parameter */ const char *zDflt, /* Default value if VAR table entry does not exist */ int disabled /* 1 if the textarea should not be editable */ ){ const char *z = db_get(zVar, zDflt); const char *zQ = P(zQP); if( zQ && !disabled && fossil_strcmp(zQ,z)!=0){ const int nZQ = (int)strlen(zQ); login_verify_csrf_secret(); db_set(zVar, zQ, 0); admin_log("Set textarea_attribute %Q to: %.*s%s", zVar, 20, zQ, (nZQ>20 ? "..." : "")); z = zQ; } if( rows>0 && cols>0 ){ @ if( zLabel && *zLabel ){ @ %s(zLabel) } } return z; } /* ** Generate a text box for an attribute. */ void multiple_choice_attribute( const char *zLabel, /* The text label on the menu */ const char *zVar, /* The corresponding row in the VAR table */ const char *zQP, /* The query parameter */ const char *zDflt, /* Default value if VAR table entry does not exist */ int nChoice, /* Number of choices */ const char *const *azChoice /* Choices. 2 per choice: (VAR value, Display) */ ){ const char *z = db_get(zVar, zDflt); const char *zQ = P(zQP); int i; if( zQ && fossil_strcmp(zQ,z)!=0){ const int nZQ = (int)strlen(zQ); login_verify_csrf_secret(); db_set(zVar, zQ, 0); admin_log("Set multiple_choice_attribute %Q to: %.*s%s", zVar, 20, zQ, (nZQ>20 ? "..." : "")); z = zQ; } @ %h(zLabel) } /* ** WEBPAGE: setup_access ** ** The access-control settings page. Requires Admin privileges. */ void setup_access(void){ login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } style_header("Access Control Settings"); db_begin_transaction(); @
    login_insert_csrf_secret(); @

    @
    onoff_attribute("Redirect to HTTPS on the Login page", "redirect-to-https", "redirhttps", 0, 0); @

    When selected, force the use of HTTPS for the Login page. @

    Details: When enabled, this option causes the $secureurl TH1 @ variable is set to an "https:" variant of $baseurl. Otherwise, @ $secureurl is just an alias for $baseurl. Also when enabled, the @ Login page redirects to https if accessed via http. @ (Property: "redirhttps") @


    onoff_attribute("Require password for local access", "localauth", "localauth", 0, 0); @

    When enabled, the password sign-in is always required for @ web access. When disabled, unrestricted web access from 127.0.0.1 @ is allowed for the fossil ui command or @ from the fossil server, @ fossil http commands when the @ "--localauth" command line options is used, or from the @ fossil cgi if a line containing @ the word "localauth" appears in the CGI script. @ @

    A password is always required if any one or more @ of the following are true: @

      @
    1. This button is checked @
    2. The inbound TCP/IP connection is not from 127.0.0.1 @
    3. The server is started using either of the @ fossil server or @ fossil http commands @ without the "--localauth" option. @
    4. The server is started from CGI without the "localauth" keyword @ in the CGI script. @
    @ (Property: "localauth") @ @
    onoff_attribute("Enable /test_env", "test_env_enable", "test_env_enable", 0, 0); @

    When enabled, the %h(g.zBaseURL)/test_env URL is available to all @ users. When disabled (the default) only users Admin and Setup can visit @ the /test_env page. @ (Property: "test_env_enable") @

    @ @
    onoff_attribute("Allow REMOTE_USER authentication", "remote_user_ok", "remote_user_ok", 0, 0); @

    When enabled, if the REMOTE_USER environment variable is set to the @ login name of a valid user and no other login credentials are available, @ then the REMOTE_USER is accepted as an authenticated user. @ (Property: "remote_user_ok") @

    @ @
    onoff_attribute("Allow HTTP_AUTHENTICATION authentication", "http_authentication_ok", "http_authentication_ok", 0, 0); @

    When enabled, allow the use of the HTTP_AUTHENTICATION environment @ variable or the "Authentication:" HTTP header to find the username and @ password. This is another way of supporting Basic Authenitication. @ (Property: "http_authentication_ok") @

    @ @
    entry_attribute("IP address terms used in login cookie", 3, "ip-prefix-terms", "ipt", "2", 0); @

    The number of octets of of the IP address used in the login cookie. @ Set to zero to omit the IP address from the login cookie. A value of @ 2 is recommended. @ (Property: "ip-prefix-terms") @

    @ @
    entry_attribute("Login expiration time", 6, "cookie-expire", "cex", "8766", 0); @

    The number of hours for which a login is valid. This must be a @ positive number. The default is 8766 hours which is approximately equal @ to a year. @ (Property: "cookie-expire")

    @
    entry_attribute("Download packet limit", 10, "max-download", "mxdwn", "5000000", 0); @

    Fossil tries to limit out-bound sync, clone, and pull packets @ to this many bytes, uncompressed. If the client requires more data @ than this, then the client will issue multiple HTTP requests. @ Values below 1 million are not recommended. 5 million is a @ reasonable number. (Property: "max-download")

    @
    entry_attribute("Download time limit", 11, "max-download-time", "mxdwnt", "30", 0); @

    Fossil tries to spend less than this many seconds gathering @ the out-bound data of sync, clone, and pull packets. @ If the client request takes longer, a partial reply is given similar @ to the download packet limit. 30s is a reasonable default. @ (Property: "max-download-time")

    @
    entry_attribute("Server Load Average Limit", 11, "max-loadavg", "mxldavg", "0.0", 0); @

    Some expensive operations (such as computing tarballs, zip archives, @ or annotation/blame pages) are prohibited if the load average on the host @ computer is too large. Set the threshold for disallowing expensive @ computations here. Set this to 0.0 to disable the load average limit. @ This limit is only enforced on Unix servers. On Linux systems, @ access to the /proc virtual filesystem is required, which means this limit @ might not work inside a chroot() jail. @ (Property: "max-loadavg")

    @
    onoff_attribute( "Enable hyperlinks for \"nobody\" based on User-Agent and Javascript", "auto-hyperlink", "autohyperlink", 1, 0); @

    Enable hyperlinks (the equivalent of the "h" permission) for all users, @ including user "nobody", as long as @

    1. the User-Agent string in the @ HTTP header indicates that the request is coming from an actual human @ being, and @
    2. the user agent is able to @ run Javascript in order to set the href= attribute of hyperlinks, and @
    3. mouse movement is detected (optional - see the checkbox below), and @
    4. a number of milliseconds have passed since the page loaded.
    @ @

    This setting is designed to give easy access to humans while @ keeping out robots and spiders. @ You do not normally want a robot to walk your entire repository because @ if it does, your server will end up computing diffs and annotations for @ every historical version of every file and creating ZIPs and tarballs of @ every historical check-in, which can use a lot of CPU and bandwidth @ even for relatively small projects.

    @ @

    Additional parameters that control this behavior:

    @
    onoff_attribute("Require mouse movement before enabling hyperlinks", "auto-hyperlink-mouseover", "ahmo", 0, 0); @
    entry_attribute("Delay in milliseconds before enabling hyperlinks", 5, "auto-hyperlink-delay", "ah-delay", "50", 0); @
    @

    For maximum robot defense, the "require mouse movement" should @ be turned on and the "Delay" should be at least 50 milliseconds.

    @ (Properties: "auto-hyperlink", @ "auto-hyperlink-mouseover", and "auto-hyperlink-delay")

    @
    onoff_attribute("Require a CAPTCHA if not logged in", "require-captcha", "reqcapt", 1, 0); @

    Require a CAPTCHA for edit operations (appending, creating, or @ editing wiki or tickets or adding attachments to wiki or tickets) @ for users who are not logged in. (Property: "require-captcha")

    @
    entry_attribute("Public pages", 30, "public-pages", "pubpage", "", 0); @

    A comma-separated list of glob patterns for pages that are accessible @ without needing a login and using the privileges given by the @ "Default privileges" setting below. Example use case: Set this field @ to "/doc/trunk/www/*" to give anonymous users read-only permission to the @ latest version of the embedded documentation in the www/ folder without @ allowing them to see the rest of the source code. @ (Property: "public-pages") @

    @
    onoff_attribute("Allow users to register themselves", "self-register", "selfregister", 0, 0); @

    Allow users to register themselves through the HTTP UI. @ The registration form always requires filling in a CAPTCHA @ (auto-captcha setting is ignored). Still, bear in mind that anyone @ can register under any user name. This option is useful for public projects @ where you do not want everyone in any ticket discussion to be named @ "Anonymous". (Property: "self-register")

    @
    entry_attribute("Default privileges", 10, "default-perms", "defaultperms", "u", 0); @

    Permissions given to users that...

    • register themselves using @ the self-registration procedure (if enabled), or
    • access "public" @ pages identified by the public-pages glob pattern above, or
    • @ are users newly created by the administrator.
    @ (Property: "default-perms") @

    @
    onoff_attribute("Show javascript button to fill in CAPTCHA", "auto-captcha", "autocaptcha", 0, 0); @

    When enabled, a button appears on the login screen for user @ "anonymous" that will automatically fill in the CAPTCHA password. @ This is less secure than forcing the user to do it manually, but is @ probably secure enough and it is certainly more convenient for @ anonymous users. (Property: "auto-captcha")

    @
    @

    @
    db_end_transaction(0); style_footer(); } /* ** WEBPAGE: setup_login_group ** ** Change how the current repository participates in a login ** group. */ void setup_login_group(void){ const char *zGroup; char *zErrMsg = 0; Blob fullName; char *zSelfRepo; const char *zRepo = PD("repo", ""); const char *zLogin = PD("login", ""); const char *zPw = PD("pw", ""); const char *zNewName = PD("newname", "New Login Group"); login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } file_canonical_name(g.zRepositoryName, &fullName, 0); zSelfRepo = fossil_strdup(blob_str(&fullName)); blob_reset(&fullName); if( P("join")!=0 ){ login_group_join(zRepo, zLogin, zPw, zNewName, &zErrMsg); }else if( P("leave") ){ login_group_leave(&zErrMsg); } style_header("Login Group Configuration"); if( zErrMsg ){ @

    %s(zErrMsg)

    } zGroup = login_group_name(); if( zGroup==0 ){ @

    This repository (in the file named "%h(zSelfRepo)") @ is not currently part of any login-group. @ To join a login group, fill out the form below.

    @ @
    login_insert_csrf_secret(); @
    @ @ @ @ @ @ @ @ @ @ @ @ @ @ @
    Repository filename in group to join: @
    Login on the above repo: @
    Password: @
    Name of login-group: @ @ (only used if creating a new login-group).
    @
    }else{ Stmt q; int n = 0; @

    This repository (in the file "%h(zSelfRepo)") @ is currently part of the "%h(zGroup)" login group. @ Other repositories in that group are:

    @ @ db_prepare(&q, "SELECT value," " (SELECT value FROM config" " WHERE name=('peer-name-' || substr(x.name,11)))" " FROM config AS x" " WHERE name GLOB 'peer-repo-*'" " ORDER BY value" ); while( db_step(&q)==SQLITE_ROW ){ const char *zRepo = db_column_text(&q, 0); const char *zTitle = db_column_text(&q, 1); n++; @ } db_finalize(&q); @
    Project Name @ Repository File
    %d(n). @ %h(zTitle)%h(zRepo)
    @ @

    login_insert_csrf_secret(); @ To leave this login group press @ @

    @
    For best results, use the same number of @ IP octets in the login cookie across all repositories in the @ same Login Group. @

    Implementation Details

    @

    The following are fields from the CONFIG table related to login-groups, @ provided here for instructional and debugging purposes:

    @ @ @ @ db_prepare(&q, "SELECT name, value, datetime(mtime,'unixepoch') FROM config" " WHERE name GLOB 'peer-*'" " OR name GLOB 'project-*'" " OR name GLOB 'login-group-*'" " ORDER BY name"); while( db_step(&q)==SQLITE_ROW ){ @ @ @ } db_finalize(&q); @
    Config.NameConfig.ValueConfig.mtime
    %h(db_column_text(&q,0))%h(db_column_text(&q,1))%h(db_column_text(&q,2))
    style_table_sorter(); } style_footer(); } /* ** WEBPAGE: setup_timeline ** ** Edit administrative settings controlling the display of ** timelines. */ void setup_timeline(void){ double tmDiff; char zTmDiff[20]; static const char *const azTimeFormats[] = { "0", "HH:MM", "1", "HH:MM:SS", "2", "YYYY-MM-DD HH:MM", "3", "YYMMDD HH:MM", "4", "(off)" }; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } style_header("Timeline Display Preferences"); db_begin_transaction(); @
    login_insert_csrf_secret(); @

    @
    onoff_attribute("Allow block-markup in timeline", "timeline-block-markup", "tbm", 0, 0); @

    In timeline displays, check-in comments can be displayed with or @ without block markup such as paragraphs, tables, etc. @ (Property: "timeline-block-markup")

    @
    onoff_attribute("Plaintext comments on timelines", "timeline-plaintext", "tpt", 0, 0); @

    In timeline displays, check-in comments are displayed literally, @ without any wiki or HTML interpretation. Use CSS to change @ display formatting features such as fonts and line-wrapping behavior. @ (Property: "timeline-plaintext")

    @
    onoff_attribute("Truncate comment at first blank line", "timeline-truncate-at-blank", "ttb", 0, 0); @

    In timeline displays, check-in comments are displayed only through @ the first blank line. (Property: "timeline-truncate-at-blank")

    @
    onoff_attribute("Use Universal Coordinated Time (UTC)", "timeline-utc", "utc", 1, 0); @

    Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or @ Zulu) instead of in local time. On this server, local time is currently tmDiff = db_double(0.0, "SELECT julianday('now')"); tmDiff = db_double(0.0, "SELECT (julianday(%.17g,'localtime')-julianday(%.17g))*24.0", tmDiff, tmDiff); sqlite3_snprintf(sizeof(zTmDiff), zTmDiff, "%.1f", tmDiff); if( strcmp(zTmDiff, "0.0")==0 ){ @ the same as UTC and so this setting will make no difference in @ the display.

    }else if( tmDiff<0.0 ){ sqlite3_snprintf(sizeof(zTmDiff), zTmDiff, "%.1f", -tmDiff); @ %s(zTmDiff) hours behind UTC.

    }else{ @ %s(zTmDiff) hours ahead of UTC.

    } @

    (Property: "timeline-utc") @


    multiple_choice_attribute("Per-Item Time Format", "timeline-date-format", "tdf", "0", count(azTimeFormats)/2, azTimeFormats); @

    If the "HH:MM" or "HH:MM:SS" format is selected, then the date is shown @ in a separate box (using CSS class "timelineDate") whenever the date @ changes. With the "YYYY-MM-DD HH:MM" and "YYMMDD ..." formats, @ the complete date and time is shown on every timeline entry using the @ CSS class "timelineTime". (Property: "timeline-date-format")

    @
    entry_attribute("Max timeline comment length", 6, "timeline-max-comment", "tmc", "0", 0); @

    The maximum length of a comment to be displayed in a timeline. @ "0" there is no length limit. @ (Property: "timeline-max-comment")

    @
    @

    @
    db_end_transaction(0); style_footer(); } /* ** WEBPAGE: setup_settings ** ** Change or view miscellaneous settings. Part of the ** Admin pages requiring Admin privileges. */ void setup_settings(void){ int nSetting; int i; Setting const *pSet; const Setting *aSetting = setting_info(&nSetting); login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } style_header("Settings"); if(!g.repositoryOpen){ /* Provide read-only access to versioned settings, but only if no repo file was explicitly provided. */ db_open_local(0); } db_begin_transaction(); @

    Settings marked with (v) are "versionable" and will be overridden @ by the contents of managed files named @ ".fossil-settings/SETTING-NAME". @ If the file for a versionable setting exists, the value cannot be @ changed on this screen.


    @ @

    @
    login_insert_csrf_secret(); for(i=0, pSet=aSetting; iwidth==0 ){ int hasVersionableValue = pSet->versionable && (db_get_versioned(pSet->name, NULL)!=0); onoff_attribute("", pSet->name, pSet->var!=0 ? pSet->var : pSet->name, is_truth(pSet->def), hasVersionableValue); @ %h(pSet->name) if( pSet->versionable ){ @ (v)
    } else { @
    } } } @
    @
    for(i=0, pSet=aSetting; iwidth!=0 && !pSet->forceTextArea ){ int hasVersionableValue = pSet->versionable && (db_get_versioned(pSet->name, NULL)!=0); entry_attribute("", /*pSet->width*/ 25, pSet->name, pSet->var!=0 ? pSet->var : pSet->name, (char*)pSet->def, hasVersionableValue); @ %h(pSet->name) if( pSet->versionable ){ @ (v)
    } else { @
    } } } @
    for(i=0, pSet=aSetting; iwidth!=0 && pSet->forceTextArea ){ int hasVersionableValue = db_get_versioned(pSet->name, NULL)!=0; @ %s(pSet->name) if( pSet->versionable ){ @ (v)
    } else { @
    } textarea_attribute("", /*rows*/ 2, /*cols*/ 35, pSet->name, pSet->var!=0 ? pSet->var : pSet->name, (char*)pSet->def, hasVersionableValue); @
    } } @
    @
    db_end_transaction(0); style_footer(); } /* ** WEBPAGE: setup_config ** ** The "Admin/Configuration" page. Requires Admin privilege. */ void setup_config(void){ login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } style_header("WWW Configuration"); db_begin_transaction(); @
    login_insert_csrf_secret(); @

    @
    entry_attribute("Project Name", 60, "project-name", "pn", "", 0); @

    A brief project name so visitors know what this site is about. @ The project name will also be used as the RSS feed title. @ (Property: "project-name") @

    @
    textarea_attribute("Project Description", 3, 80, "project-description", "pd", "", 0); @

    Describe your project. This will be used in page headers for search @ engines as well as a short RSS description. @ (Property: "project-description")

    @
    entry_attribute("Tarball and ZIP-archive Prefix", 20, "short-project-name", "spn", "", 0); @

    This is used as a prefix on the names of generated tarballs and @ ZIP archive. For best results, keep this prefix brief and avoid special @ characters such as "/" and "\". @ If no tarball prefix is specified, then the full Project Name above is used. @ (Property: "short-project-name") @

    @
    entry_attribute("Download Tag", 20, "download-tag", "dlt", "trunk", 0); @

    The /download page is designed to provide @ a convenient place for newbies @ to download a ZIP archive or a tarball of the project. By default, @ the latest trunk check-in is downloaded. Change this tag to something @ else (ex: release) to alter the behavior of the /download page. @ (Property: "download-tag") @

    @
    onoff_attribute("Enable WYSIWYG Wiki Editing", "wysiwyg-wiki", "wysiwyg-wiki", 0, 0); @

    Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages. @ The WYSIWYG editor generates HTML instead of markup, which makes @ subsequent manual editing more difficult. @ (Property: "wysiwyg-wiki")

    @
    entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0); @

    Enter the pathname of the page to display when the "Home" menu @ option is selected and when no pathname is @ specified in the URL. For example, if you visit the url:

    @ @

    %h(g.zBaseURL)

    @ @

    And you have specified an index page of "/home" the above will @ automatically redirect to:

    @ @

    %h(g.zBaseURL)/home

    @ @

    The default "/home" page displays a Wiki page with the same name @ as the Project Name specified above. Some sites prefer to redirect @ to a documentation page (ex: "/doc/tip/index.wiki") or to "/timeline".

    @ @

    Note: To avoid a redirect loop or other problems, this entry must @ begin with "/" and it must specify a valid page. For example, @ "/home" will work but "home" will not, since it omits the @ leading "/".

    @

    (Property: "index-page") @


    onoff_attribute("Use HTML as wiki markup language", "wiki-use-html", "wiki-use-html", 0, 0); @

    Use HTML as the wiki markup language. Wiki links will still be parsed @ but all other wiki formatting will be ignored. This option is helpful @ if you have chosen to use a rich HTML editor for wiki markup such as @ TinyMCE.

    @

    CAUTION: when @ enabling, all HTML tags and attributes are accepted in the wiki. @ No sanitization is done. This means that it is very possible for malicious @ users to inject dangerous HTML, CSS and JavaScript code into your wiki.

    @

    This should only be enabled when wiki editing is limited @ to trusted users. It should not be used on a publicly @ editable wiki.

    @ (Property: "wiki-use-html") @
    @

    @
    db_end_transaction(0); style_footer(); } /* ** WEBPAGE: setup_modreq ** ** Admin page for setting up moderation of tickets and wiki. */ void setup_modreq(void){ login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } style_header("Moderator For Wiki And Tickets"); db_begin_transaction(); @
    login_insert_csrf_secret(); @
    onoff_attribute("Moderate ticket changes", "modreq-tkt", "modreq-tkt", 0, 0); @

    When enabled, any change to tickets is subject to the approval @ by a ticket moderator - a user with the "q" or Mod-Tkt privilege. @ Ticket changes enter the system and are shown locally, but are not @ synced until they are approved. The moderator has the option to @ delete the change rather than approve it. Ticket changes made by @ a user who has the Mod-Tkt privilege are never subject to @ moderation. (Property: "modreq-tkt") @ @


    onoff_attribute("Moderate wiki changes", "modreq-wiki", "modreq-wiki", 0, 0); @

    When enabled, any change to wiki is subject to the approval @ by a wiki moderator - a user with the "l" or Mod-Wiki privilege. @ Wiki changes enter the system and are shown locally, but are not @ synced until they are approved. The moderator has the option to @ delete the change rather than approve it. Wiki changes made by @ a user who has the Mod-Wiki privilege are never subject to @ moderation. (Property: "modreq-wiki") @

    @
    @

    @
    db_end_transaction(0); style_footer(); } /* ** WEBPAGE: setup_adunit ** ** Administrative page for configuring and controlling ad units ** and how they are displayed. */ void setup_adunit(void){ login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } db_begin_transaction(); if( P("clear")!=0 && cgi_csrf_safe(1) ){ db_multi_exec("DELETE FROM config WHERE name GLOB 'adunit*'"); cgi_replace_parameter("adunit",""); } style_header("Edit Ad Unit"); @
    login_insert_csrf_secret(); @ Banner Ad-Unit:
    textarea_attribute("", 6, 80, "adunit", "adunit", "", 0); @
    @ Right-Column Ad-Unit:
    textarea_attribute("", 6, 80, "adunit-right", "adright", "", 0); @
    onoff_attribute("Omit ads to administrator", "adunit-omit-if-admin", "oia", 0, 0); @
    onoff_attribute("Omit ads to logged-in users", "adunit-omit-if-user", "oiu", 0, 0); @
    onoff_attribute("Temporarily disable all ads", "adunit-disable", "oall", 0, 0); @
    @ @ @
    @
    @ Ad-Unit Notes:
      @
    • Leave both Ad-Units blank to disable all advertising. @
    • The "Banner Ad-Unit" is used for wide pages. @
    • The "Right-Column Ad-Unit" is used on pages with tall, narrow content. @
    • If the "Right-Column Ad-Unit" is blank, the "Banner Ad-Unit" is @ used on all pages. @
    • Properties: "adunit", "adunit-right", "adunit-omit-if-admin", and @ "adunit-omit-if-user". @
    • Suggested CSS changes: @
        @ div.adunit_banner {
        @   margin: auto;
        @   width: 100%%;
        @ }
        @ div.adunit_right {
        @   float: right;
        @ }
        @ div.adunit_right_container {
        @   min-height: height-of-right-column-ad-unit;
        @ }
        @ 
      @
    • For a place-holder Ad-Unit for testing, Copy/Paste the following @ with appropriate adjustments to "width:" and "height:". @
        @ <div style='
        @   margin: 0 auto;
        @   width: 600px;
        @   height: 90px;
        @   border: 1px solid #f11;
        @   background-color: #fcc;
        @ '>Demo Ad</div>
        @ 
      @
    • style_footer(); db_end_transaction(0); } /* ** WEBPAGE: setup_logo ** ** Administrative page for changing the logo image. */ void setup_logo(void){ const char *zLogoMtime = db_get_mtime("logo-image", 0, 0); const char *zLogoMime = db_get("logo-mimetype","image/gif"); const char *aLogoImg = P("logoim"); int szLogoImg = atoi(PD("logoim:bytes","0")); const char *zBgMtime = db_get_mtime("background-image", 0, 0); const char *zBgMime = db_get("background-mimetype","image/gif"); const char *aBgImg = P("bgim"); int szBgImg = atoi(PD("bgim:bytes","0")); if( szLogoImg>0 ){ zLogoMime = PD("logoim:mimetype","image/gif"); } if( szBgImg>0 ){ zBgMime = PD("bgim:mimetype","image/gif"); } login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } db_begin_transaction(); if( !cgi_csrf_safe(1) ){ /* Allow no state changes if not safe from CSRF */ }else if( P("setlogo")!=0 && zLogoMime && zLogoMime[0] && szLogoImg>0 ){ Blob img; Stmt ins; blob_init(&img, aLogoImg, szLogoImg); db_prepare(&ins, "REPLACE INTO config(name,value,mtime)" " VALUES('logo-image',:bytes,now())" ); db_bind_blob(&ins, ":bytes", &img); db_step(&ins); db_finalize(&ins); db_multi_exec( "REPLACE INTO config(name,value,mtime) VALUES('logo-mimetype',%Q,now())", zLogoMime ); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("clrlogo")!=0 ){ db_multi_exec( "DELETE FROM config WHERE name IN " "('logo-image','logo-mimetype')" ); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("setbg")!=0 && zBgMime && zBgMime[0] && szBgImg>0 ){ Blob img; Stmt ins; blob_init(&img, aBgImg, szBgImg); db_prepare(&ins, "REPLACE INTO config(name,value,mtime)" " VALUES('background-image',:bytes,now())" ); db_bind_blob(&ins, ":bytes", &img); db_step(&ins); db_finalize(&ins); db_multi_exec( "REPLACE INTO config(name,value,mtime)" " VALUES('background-mimetype',%Q,now())", zBgMime ); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("clrbg")!=0 ){ db_multi_exec( "DELETE FROM config WHERE name IN " "('background-image','background-mimetype')" ); db_end_transaction(0); cgi_redirect("setup_logo"); } style_header("Edit Project Logo And Background"); @

      The current project logo has a MIME-Type of %h(zLogoMime) @ and looks like this:

      @

      logo @

      @ @
      @

      The logo is accessible to all users at this URL: @ %s(g.zBaseURL)/logo. @ The logo may or may not appear on each @ page depending on the CSS and @ header setup. @ To change the logo image, use the following form:

      login_insert_csrf_secret(); @ Logo Image file: @ @

      @ @

      @

      (Properties: "logo-image" and "logo-mimetype") @

      @
      @ @

      The current background image has a MIME-Type of %h(zBgMime) @ and looks like this:

      @

      background @

      @ @
      @

      The background image is accessible to all users at this URL: @ %s(g.zBaseURL)/background. @ The background image may or may not appear on each @ page depending on the CSS and @ header setup. @ To change the background image, use the following form:

      login_insert_csrf_secret(); @ Background image file: @ @

      @ @

      @
      @

      (Properties: "background-image" and "background-mimetype") @


      @ @

      Note: Your browser has probably cached these @ images, so you may need to press the Reload button before changes will @ take effect.

      style_footer(); db_end_transaction(0); } /* ** Prevent the RAW SQL feature from being used to ATTACH a different ** database and query it. ** ** Actually, the RAW SQL feature only does a single statement per request. ** So it is not possible to ATTACH and then do a separate query. This ** routine is not strictly necessary, therefore. But it does not hurt ** to be paranoid. */ int raw_sql_query_authorizer( void *pError, int code, const char *zArg1, const char *zArg2, const char *zArg3, const char *zArg4 ){ if( code==SQLITE_ATTACH ){ return SQLITE_DENY; } return SQLITE_OK; } /* ** WEBPAGE: admin_sql ** ** Run raw SQL commands against the database file using the web interface. ** Requires Admin privileges. */ void sql_page(void){ const char *zQ; int go = P("go")!=0; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } add_content_sql_commands(g.db); db_begin_transaction(); zQ = cgi_csrf_safe(1) ? P("q") : 0; style_header("Raw SQL Commands"); @

      Caution: There are no restrictions on the SQL that can be @ run by this page. You can do serious and irrepairable damage to the @ repository. Proceed with extreme caution.

      @ #if 0 @

      Only the first statement in the entry box will be run. @ Any subsequent statements will be silently ignored.

      @ @

      Database names:

      • repository if( g.zConfigDbName ){ @
      • configdb } if( g.localOpen ){ @
      • localdb } @

      #endif if( P("configtab") ){ /* If the user presses the "CONFIG Table Query" button, populate the ** query text with a pre-packaged query against the CONFIG table */ zQ = "SELECT\n" " CASE WHEN length(name)<50 THEN name ELSE printf('%.50s...',name)" " END AS name,\n" " CASE WHEN typeof(value)<>'blob' AND length(value)<80 THEN value\n" " ELSE '...' END AS value,\n" " datetime(mtime, 'unixepoch') AS mtime\n" "FROM config\n" "-- ORDER BY mtime DESC; -- optional"; go = 1; } @ @
      login_insert_csrf_secret(); @ SQL:
      @
      @ @ @ @ @
      if( P("schema") ){ zQ = sqlite3_mprintf( "SELECT sql FROM repository.sqlite_master" " WHERE sql IS NOT NULL ORDER BY name"); go = 1; }else if( P("tablelist") ){ zQ = sqlite3_mprintf( "SELECT name FROM repository.sqlite_master WHERE type='table'" " ORDER BY name"); go = 1; } if( go ){ sqlite3_stmt *pStmt; int rc; const char *zTail; int nCol; int nRow = 0; int i; @
      login_verify_csrf_secret(); sqlite3_set_authorizer(g.db, raw_sql_query_authorizer, 0); rc = sqlite3_prepare_v2(g.db, zQ, -1, &pStmt, &zTail); if( rc!=SQLITE_OK ){ @
      %h(sqlite3_errmsg(g.db))
      sqlite3_finalize(pStmt); }else if( pStmt==0 ){ /* No-op */ }else if( (nCol = sqlite3_column_count(pStmt))==0 ){ sqlite3_step(pStmt); rc = sqlite3_finalize(pStmt); if( rc ){ @
      %h(sqlite3_errmsg(g.db))
      } }else{ @ while( sqlite3_step(pStmt)==SQLITE_ROW ){ if( nRow==0 ){ @ for(i=0; i%h(sqlite3_column_name(pStmt, i)) } @ } nRow++; @ for(i=0; i @ %s(sqlite3_column_text(pStmt, i)) break; } case SQLITE_NULL: { @ break; } case SQLITE_TEXT: { const char *zText = (const char*)sqlite3_column_text(pStmt, i); @ break; } case SQLITE_BLOB: { @ break; } } } @ } sqlite3_finalize(pStmt); @
      NULL%h(zText) @ %d(sqlite3_column_bytes(pStmt, i))-byte BLOB
      } } style_footer(); } /* ** WEBPAGE: admin_th1 ** ** Run raw TH1 commands using the web interface. If Tcl integration was ** enabled at compile-time and the "tcl" setting is enabled, Tcl commands ** may be run as well. Requires Admin privilege. */ void th1_page(void){ const char *zQ = P("q"); int go = P("go")!=0; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } db_begin_transaction(); style_header("Raw TH1 Commands"); @

      Caution: There are no restrictions on the TH1 that can be @ run by this page. If Tcl integration was enabled at compile-time and @ the "tcl" setting is enabled, Tcl commands may be run as well.

      @ @
      login_insert_csrf_secret(); @ TH1:
      @
      @ @
      if( go ){ const char *zR; int rc; int n; @
      login_verify_csrf_secret(); rc = Th_Eval(g.interp, 0, zQ, -1); zR = Th_GetResult(g.interp, &n); if( rc==TH_OK ){ @
      %h(zR)
      }else{ @
      %h(zR)
      } } style_footer(); } /* ** WEBPAGE: admin_log ** ** Shows the contents of the admin_log table, which is only created if ** the admin-log setting is enabled. Requires Admin or Setup ('a' or ** 's') permissions. */ void page_admin_log(){ Stmt stLog; int limit; /* How many entries to show */ int ofst; /* Offset to the first entry */ int fLogEnabled; int counter = 0; login_check_credentials(); if( !g.perm.Setup && !g.perm.Admin ){ login_needed(0); return; } style_header("Admin Log"); create_admin_log_table(); limit = atoi(PD("n","200")); ofst = atoi(PD("x","0")); fLogEnabled = db_get_boolean("admin-log", 0); @
      Admin logging is %s(fLogEnabled?"on":"off"). @ (Change this on the settings page.)
      if( ofst>0 ){ int prevx = ofst - limit; if( prevx<0 ) prevx = 0; @

      [Newer]

      } db_prepare(&stLog, "SELECT datetime(time,'unixepoch'), who, page, what " "FROM admin_log " "ORDER BY time DESC"); style_table_sorter(); @ @ @ @ @ @ @ while( SQLITE_ROW == db_step(&stLog) ){ const char *zTime = db_column_text(&stLog, 0); const char *zUser = db_column_text(&stLog, 1); const char *zPage = db_column_text(&stLog, 2); const char *zMessage = db_column_text(&stLog, 3); counter++; if( counterofst+limit ) break; @ @ @ @ @ @ } @
      TimeUserPageMessage
      %s(zTime)%s(zUser)%s(zPage)%h(zMessage)
      if( counter>ofst+limit ){ @

      [Older]

      } style_footer(); } /* ** WEBPAGE: srchsetup ** ** Configure the search engine. Requires Admin privilege. */ void page_srchsetup(){ login_check_credentials(); if( !g.perm.Setup && !g.perm.Admin ){ login_needed(0); return; } style_header("Search Configuration"); @
      login_insert_csrf_secret(); @
      @ Server-specific settings that affect the @ /search webpage. @
      @
      textarea_attribute("Document Glob List", 3, 35, "doc-glob", "dg", "", 0); @

      The "Document Glob List" is a comma- or newline-separated list @ of GLOB expressions that identify all documents within the source @ tree that are to be searched when "Document Search" is enabled. @ Some examples: @ @ @ @ @ @
      *.wiki,*.html,*.md,*.txt @ Search all wiki, HTML, Markdown, and Text files
      doc/*.md,*/README.txt,README.txt @ Search all Markdown files in the doc/ subfolder and all README.txt @ files.
      *Search all checked-in files
      (blank) @ Search nothing. (Disables document search).
      @


      entry_attribute("Document Branch", 20, "doc-branch", "db", "trunk", 0); @

      When searching documents, use the versions of the files found at the @ type of the "Document Branch" branch. Recommended value: "trunk". @ Document search is disabled if blank. @


      onoff_attribute("Search Check-in Comments", "search-ci", "sc", 0, 0); @
      onoff_attribute("Search Documents", "search-doc", "sd", 0, 0); @
      onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0); @
      onoff_attribute("Search Wiki", "search-wiki", "sw", 0, 0); @
      onoff_attribute("Search Tech Notes", "search-technote", "se", 0, 0); @
      @

      @
      if( P("fts0") ){ search_drop_index(); }else if( P("fts1") ){ search_drop_index(); search_create_index(); search_fill_index(); search_update_index(search_restrict(SRCH_ALL)); } if( search_index_exists() ){ @

      Currently using an SQLite FTS4 search index. This makes search @ run faster, especially on large repositories, but takes up space.

      onoff_attribute("Use Porter Stemmer","search-stemmer","ss",0,0); @

      @ }else{ @

      The SQLite FTS4 search index is disabled. All searching will be @ a full-text scan. This usually works fine, but can be slow for @ larger repositories.

      onoff_attribute("Use Porter Stemmer","search-stemmer","ss",0,0); @

      } @

      style_footer(); } /* ** A URL Alias originally called zOldName is now zNewName/zValue. ** Write SQL to make this change into pSql. ** ** If zNewName or zValue is an empty string, then delete the entry. ** ** If zOldName is an empty string, create a new entry. */ static void setup_update_url_alias( Blob *pSql, const char *zOldName, const char *zNewName, const char *zValue ){ if( !cgi_csrf_safe(1) ) return; if( zNewName[0]==0 || zValue[0]==0 ){ if( zOldName[0] ){ blob_append_sql(pSql, "DELETE FROM config WHERE name='walias:%q';\n", zOldName); } return; } if( zOldName[0]==0 ){ blob_append_sql(pSql, "INSERT INTO config(name,value,mtime) VALUES('walias:%q',%Q,now());\n", zNewName, zValue); return; } if( strcmp(zOldName, zNewName)!=0 ){ blob_append_sql(pSql, "UPDATE config SET name='walias:%q', value=%Q, mtime=now()" " WHERE name='walias:%q';\n", zNewName, zValue, zOldName); }else{ blob_append_sql(pSql, "UPDATE config SET value=%Q, mtime=now()" " WHERE name='walias:%q' AND value<>%Q;\n", zValue, zOldName, zValue); } } /* ** WEBPAGE: waliassetup ** ** Configure the URL aliases */ void page_waliassetup(){ Stmt q; int cnt = 0; Blob namelist; login_check_credentials(); if( !g.perm.Setup && !g.perm.Admin ){ login_needed(0); return; } style_header("URL Alias Configuration"); if( P("submit")!=0 ){ Blob token; Blob sql; const char *zNewName; const char *zValue; char zCnt[10]; login_verify_csrf_secret(); blob_init(&namelist, PD("namelist",""), -1); blob_init(&sql, 0, 0); while( blob_token(&namelist, &token) ){ const char *zOldName = blob_str(&token); sqlite3_snprintf(sizeof(zCnt), zCnt, "n%d", cnt); zNewName = PD(zCnt, ""); sqlite3_snprintf(sizeof(zCnt), zCnt, "v%d", cnt); zValue = PD(zCnt, ""); setup_update_url_alias(&sql, zOldName, zNewName, zValue); cnt++; blob_reset(&token); } sqlite3_snprintf(sizeof(zCnt), zCnt, "n%d", cnt); zNewName = PD(zCnt,""); sqlite3_snprintf(sizeof(zCnt), zCnt, "v%d", cnt); zValue = PD(zCnt,""); setup_update_url_alias(&sql, "", zNewName, zValue); db_multi_exec("%s", blob_sql_text(&sql)); blob_reset(&sql); blob_reset(&namelist); cnt = 0; } db_prepare(&q, "SELECT substr(name,8), value FROM config WHERE name GLOB 'walias:/*'" " UNION ALL SELECT '', ''" ); @
      login_insert_csrf_secret(); @ @ cnt++; if( blob_size(&namelist)>0 ) blob_append(&namelist, " ", 1); blob_append(&namelist, zName, -1); } db_finalize(&q); @ @
      AliasURI That The Alias Maps Into blob_init(&namelist, 0, 0); while( db_step(&q)==SQLITE_ROW ){ const char *zName = db_column_text(&q, 0); const char *zValue = db_column_text(&q, 1); @
      @ @ @ @
      @ @ @
      @
      @

      When the first term of an incoming URL exactly matches one of @ the "Aliases" on the left-hand side (LHS) above, the URL is @ converted into the corresponding form on the right-hand side (RHS). @

        @
      • @ The LHS is compared against only the first term of the incoming URL. @ All LHS entries in the alias table should therefore begin with a @ single "/" followed by a single path element. @

      • @ The RHS entries in the alias table should begin with a single "/" @ followed by a path element, and optionally followed by "?" and a @ list of query parameters. @

      • @ Query parameters on the RHS are added to the set of query parameters @ in the incoming URL. @

      • @ If the same query parameter appears in both the incoming URL and @ on the RHS of the alias, the RHS query parameter value overwrites @ the value on the incoming URL. @

      • @ If a query parameter on the RHS of the alias is of the form "X!" @ (a name followed by "!") then the X query parameter is removed @ from the incoming URL if @ it exists. @

      • @ Only a single alias operation occurs. It is not possible to nest aliases. @ The RHS entries must be built-in webpage names. @

      • @ The alias table is only checked if no built-in webpage matches @ the incoming URL. @ Hence, it is not possible to override a built-in webpage using aliases. @ This is by design. @

      @ @

      To delete an entry from the alias table, change its name or value to an @ empty string and press "Apply Changes". @ @

      To add a new alias, fill in the name and value in the bottom row @ of the table above and press "Apply Changes". style_footer(); }