Index: src/attach.c ================================================================== --- src/attach.c +++ src/attach.c @@ -377,11 +377,11 @@ } if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop); if( P("cancel") ){ cgi_redirect(zFrom); } - if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){ + if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){ int needModerator = (zTkt!=0 && ticket_need_moderation(0)) || (zPage!=0 && wiki_need_moderation(0)); const char *zComment = PD("comment", ""); attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); cgi_redirect(zFrom); Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -480,10 +480,17 @@ ** Rewind the cursor on a blob back to the beginning. */ void blob_rewind(Blob *p){ p->iCursor = 0; } + +/* +** Truncate a blob back to zero length +*/ +void blob_truncate(Blob *p){ + p->nUsed = 0; +} /* ** Seek the cursor in a blob to the indicated offset. */ int blob_seek(Blob *p, int offset, int whence){ @@ -653,10 +660,20 @@ if( pTo ){ blob_append(pTo, &pFrom->aData[pFrom->iCursor], i - pFrom->iCursor); } pFrom->iCursor = i; } + +/* +** Ensure that the text in pBlob ends with '\n' +*/ +void blob_add_final_newline(Blob *pBlob){ + if( pBlob->nUsed<=0 ) return; + if( pBlob->aData[pBlob->nUsed-1]!='\n' ){ + blob_append(pBlob, "\n", 1); + } +} /* ** Return true if the blob contains a valid base16 identifier artifact hash. ** ** The value returned is actually one of HNAME_SHA1 OR HNAME_K256 if the Index: src/captcha.c ================================================================== --- src/captcha.c +++ src/captcha.c @@ -497,17 +497,17 @@ ** true (non-zero). ** ** The query parameters examined are "captchaseed" for the seed value and ** "captcha" for text that the user types in response to the captcha prompt. */ -int captcha_is_correct(void){ +int captcha_is_correct(int bAlwaysNeeded){ const char *zSeed; const char *zEntered; const char *zDecode; char z[30]; int i; - if( !captcha_needed() ){ + if( !bAlwaysNeeded && !captcha_needed() ){ return 1; /* No captcha needed */ } zSeed = P("captchaseed"); if( zSeed==0 ) return 0; zEntered = P("captcha"); @@ -593,11 +593,11 @@ } #endif zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x")); zCookieValue = P(zCookieName); if( zCookieValue && atoi(zCookieValue)==1 ) return 0; - if( captcha_is_correct() ){ + if( captcha_is_correct(0) ){ cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600); return 0; } /* This appears to be a spider. Offer the captcha */ Index: src/cgi.c ================================================================== --- src/cgi.c +++ src/cgi.c @@ -56,10 +56,12 @@ #define P(x) cgi_parameter((x),0) #define PD(x,y) cgi_parameter((x),(y)) #define PT(x) cgi_parameter_trimmed((x),0) #define PDT(x,y) cgi_parameter_trimmed((x),(y)) #define PB(x) cgi_parameter_boolean(x) +#define PCK(x) cgi_parameter_checked(x,1) +#define PIF(x,y) cgi_parameter_checked(x,y) /* ** Destinations for output text. */ @@ -1118,10 +1120,33 @@ int cgi_parameter_boolean(const char *zName){ const char *zIn = cgi_parameter(zName, 0); if( zIn==0 ) return 0; return zIn[0]==0 || is_truth(zIn); } + +/* +** Return either an empty string "" or the string "checked" depending +** on whether or not parameter zName has value iValue. If parameter +** zName does not exist, that is assumed to be the same as value 0. +** +** This routine implements the PCK(x) and PIF(x,y) macros. The PIF(x,y) +** macro generateds " checked" if the value of parameter x equals integer y. +** PCK(x) is the same as PIF(x,1). These macros are used to generate +** the "checked" attribute on checkbox and radio controls of forms. +*/ +const char *cgi_parameter_checked(const char *zName, int iValue){ + const char *zIn = cgi_parameter(zName,0); + int x; + if( zIn==0 ){ + x = 0; + }else if( !fossil_isdigit(zIn[0]) ){ + x = is_truth(zIn); + }else{ + x = atoi(zIn); + } + return x==iValue ? "checked" : ""; +} /* ** Return the name of the i-th CGI parameter. Return NULL if there ** are fewer than i registered CGI parameters. */ Index: src/codecheck1.c ================================================================== --- src/codecheck1.c +++ src/codecheck1.c @@ -331,11 +331,15 @@ ** Return true if the input is an argument that is never safe for use ** with %s. */ static int never_safe(const char *z){ if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0; - if( z[0]=='P' ) return 1; /* CGI macros like P() and PD() */ + if( z[0]=='P' ){ + if( strncmp(z,"PIF(",4)==0 ) return 0; + if( strncmp(z,"PCK(",4)==0 ) return 0; + return 1; + } if( strncmp(z,"cgi_param",9)==0 ) return 1; return 0; } /* Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -310,10 +310,31 @@ db.pAllStmt = pStmt; va_end(ap); } return rc; } + +/* Prepare a statement using text placed inside a Blob +** using blob_append_sql(). +*/ +int db_prepare_blob(Stmt *pStmt, Blob *pSql){ + int rc; + char *zSql; + pStmt->sql = *pSql; + blob_init(pSql, 0, 0); + zSql = blob_sql_text(&pStmt->sql); + db.nPrepare++; + rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0); + if( rc!=0 ){ + db_err("%s\n%s", sqlite3_errmsg(g.db), zSql); + } + pStmt->pNext = pStmt->pPrev = 0; + pStmt->nStep = 0; + pStmt->rc = rc; + return rc; +} + /* ** Return the index of a bind parameter */ static int paramIdx(Stmt *pStmt, const char *zParamName){ @@ -875,10 +896,33 @@ }else{ sqlite3_result_text(context, "utc", -1, SQLITE_STATIC); } } +/* +** If the input is a hexadecimal string, convert that string into a BLOB. +** If the input is not a hexadecimal string, return NULL. +*/ +void db_hextoblob( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const unsigned char *zIn = sqlite3_value_text(argv[0]); + int nIn = sqlite3_value_bytes(argv[0]); + unsigned char *zOut; + if( zIn==0 ) return; + if( nIn&1 ) return; + if( !validate16((const char*)zIn, nIn) ) return; + zOut = sqlite3_malloc64( nIn/2 ); + if( zOut==0 ){ + sqlite3_result_error_nomem(context); + return; + } + decode16(zIn, zOut, nIn); + sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free); +} /* ** Register the SQL functions that are useful both to the internal ** representation and to the "fossil sql" command. */ @@ -893,10 +937,12 @@ db_now_function, 0, 0); sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0, db_tolocal_function, 0, 0); sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, db_fromlocal_function, 0, 0); + sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0, + db_hextoblob, 0, 0); } #if USE_SEE /* ** This is a pointer to the saved database encryption key string. Index: src/email.c ================================================================== --- src/email.c +++ src/email.c @@ -19,10 +19,15 @@ */ #include "config.h" #include "email.h" #include +/* +** Maximum size of the subscriberCode blob, in bytes +*/ +#define SUBSCRIBER_CODE_SZ 32 + /* ** SQL code to implement the tables needed by the email notification ** system. */ static const char zEmailInit[] = @@ -42,34 +47,34 @@ @ -- we might also add a separate table that allows subscribing to email @ -- notifications for specific branches or tags or tickets. @ -- @ CREATE TABLE repository.subscriber( @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use -@ subscriberCode TEXT UNIQUE, -- UUID for subscriber. External use -@ sname TEXT, -- Human readable name -@ suname TEXT, -- Corresponding USER or NULL -@ semail TEXT, -- email address -@ sverify BOOLEAN, -- email address verified +@ subscriberCode BLOB UNIQUE, -- UUID for subscriber. External use +@ semail TEXT UNIQUE COLLATE nocase,-- email address +@ suname TEXT, -- corresponding USER entry +@ sverified BOOLEAN, -- email address verified @ sdonotcall BOOLEAN, -- true for Do Not Call @ sdigest BOOLEAN, -- true for daily digests only @ ssub TEXT, -- baseline subscriptions @ sctime DATE, -- When this entry was created. JulianDay @ smtime DATE, -- Last change. JulianDay -@ sipaddr TEXT, -- IP address for last change -@ spswdHash TEXT -- SHA3 hash of password +@ smip TEXT -- IP address of last change @ ); +@ CREATE INDEX repository.subscriberUname +@ ON subscriber(suname) WHERE suname IS NOT NULL; @ @ -- Email notifications that need to be sent. @ -- -@ -- If the eventid key is an integer, then it corresponds to the -@ -- EVENT.OBJID table. Other kinds of eventids are reserved for -@ -- future expansion. +@ -- The first character of the eventid determines the event type. +@ -- Remaining characters determine the specific event. For example, +@ -- 'c4413' means check-in with rid=4413. @ -- -@ CREATE TABLE repository.email_pending( -@ eventid ANY PRIMARY KEY, -- Object that changed +@ CREATE TABLE repository.pending_alert( +@ eventid TEXT PRIMARY KEY, -- Object that changed @ sentSep BOOLEAN DEFAULT false, -- individual emails sent -@ sentDigest BOOLEAN DEFAULT false -- digest emails sent +@ sendDigest BOOLEAN DEFAULT false -- digest emails sent @ ) WITHOUT ROWID; @ @ -- Record bounced emails. If too many bounces are received within @ -- some defined time range, then cancel the subscription. Older @ -- entries are periodically purged. @@ -85,10 +90,66 @@ ** Make sure the unversioned table exists in the repository. */ void email_schema(void){ if( !db_table_exists("repository", "subscriber") ){ db_multi_exec(zEmailInit/*works-like:""*/); + email_triggers_enable(); + } +} + +/* +** Enable triggers that automatically populate the event_pending +** table. +*/ +void email_triggers_enable(void){ + if( !db_table_exists("repository","pending_alert") ) return; + db_multi_exec( + "CREATE TRIGGER IF NOT EXISTS repository.email_trigger1\n" + "AFTER INSERT ON event BEGIN\n" + " INSERT INTO pending_alert(eventid,mtime)\n" + " SELECT printf('%%.1c%%d',new.type,new.objid)," + " julianday('now') WHERE true\n" + " ON CONFLICT(eventId) DO NOTHING;\n" + "END;" + ); +} + +/* +** Disable triggers the event_pending triggers. +** +** This must be called before rebuilding the EVENT table, for example +** via the "fossil rebuild" command. +*/ +void email_triggers_disable(void){ + db_multi_exec( + "DROP TRIGGER IF EXISTS repository.email_trigger1;\n" + ); +} + +/* +** Return true if email alerts are active. +*/ +int email_enabled(void){ + if( !db_table_exists("repository", "subscriber") ) return 0; + if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0; + return 1; +} + + + +/* +** Insert a "Subscriber List" submenu link if the current user +** is an administrator. +*/ +void email_submenu_common(void){ + if( g.perm.Admin ){ + if( fossil_strcmp(g.zPath,"subscribers") ){ + style_submenu_element("List Subscribers","%R/subscribers"); + } + if( fossil_strcmp(g.zPath,"subscribe") ){ + style_submenu_element("Add New Subscriber","%R/subscribe"); + } } } /* @@ -99,64 +160,96 @@ void setup_email(void){ static const char *const azSendMethods[] = { "off", "Disabled", "pipe", "Pipe to a command", "db", "Store in a database", - "file", "Store in a directory" + "dir", "Store in a directory" }; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; } db_begin_transaction(); + email_submenu_common(); style_header("Email Notification Setup"); @
@
login_insert_csrf_secret(); - multiple_choice_attribute("Email Send Method","email-send-method", - "esm", "off", count(azSendMethods)/2, azSendMethods); + + entry_attribute("Canonical Server URL", 40, "email-url", + "eurl", "", 0); + @

Required. + @ This is URL used as the basename for hyperlinks included in + @ email alert text. Omit the trailing "/". + @ Suggested value: "%h(g.zBaseURL)" + @ (Property: "email-url")

+ @
+ + entry_attribute("\"From\" email address", 20, "email-self", + "eself", "", 0); + @

Required. + @ This is the email from which email notifications are sent. The + @ system administrator should arrange for emails sent to this address + @ to be handed off to the "fossil email incoming" command so that Fossil + @ can handle bounces. (Property: "email-self")

+ @
+ + entry_attribute("Repository Nickname", 16, "email-subname", + "enn", "", 0); + @

Required. + @ This is short name used to identifies the repository in the + @ Subject: line of email alerts. Traditionally this name is + @ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]". + @ (Property: "email-subname")

+ @
+ + multiple_choice_attribute("Email Send Method", "email-send-method", "esm", + "off", count(azSendMethods)/2, azSendMethods); @

How to send email. The "Pipe to a command" @ method is the usual choice in production. @ (Property: "email-send-method")

@
- entry_attribute("Command To Pipe Email To", 80, "esc", - "email-send-command", "sendmail -t", 0); + + + entry_attribute("Command To Pipe Email To", 80, "email-send-command", + "ecmd", "sendmail -t", 0); @

When the send method is "pipe to a command", this is the command @ that is run. Email messages are piped into the standard input of this @ command. The command is expected to extract the sender address, @ recepient addresses, and subject from the header of the piped email @ text. (Property: "email-send-command")

- entry_attribute("Database In Which To Store Email", 60, "esdb", - "email-send-db", "", 0); + entry_attribute("Database In Which To Store Email", 60, "email-send-db", + "esdb", "", 0); @

When the send method is "store in a databaes", each email message is @ stored in an SQLite database file with the name given here. @ (Property: "email-send-db")

- entry_attribute("Directory In Which To Store Email", 60, "esdir", - "email-send-dir", "", 0); + entry_attribute("Directory In Which To Store Email", 60, "email-send-dir", + "esdir", "", 0); @

When the send method is "store in a directory", each email message is @ stored as a separate file in the directory shown here. @ (Property: "email-send-dir")

@
- entry_attribute("\"From\" email address", 40, "ef", - "email-self", "", 0); - @

This is the email from which email notifications are sent. The - @ system administrator should arrange for emails sent to this address - @ to be handed off to the "fossil email incoming" command so that Fossil - @ can handle bounces. (Property: "email-self")

- @
- - entry_attribute("Administrator email address", 40, "ea", - "email-admin", "", 0); + entry_attribute("Administrator email address", 40, "email-admin", + "eadmin", "", 0); @

This is the email for the human administrator for the system. @ Abuse and trouble reports are send here. @ (Property: "email-admin")

@
+ + entry_attribute("Inbound email directory", 40, "email-receive-dir", + "erdir", "", 0); + @

Inbound emails can be stored in a directory for analysis as + @ a debugging aid. Put the name of that directory in this entry box. + @ Disable saving of inbound email by making this an empty string. + @ Abuse and trouble reports are send here. + @ (Property: "email-receive-dir")

+ @
@

@
db_end_transaction(0); style_footer(); } @@ -172,10 +265,23 @@ k = translateBase64(blob_buffer(pMsg)+i, i+54=3 ? g.argv[2] : "x"; nCmd = (int)strlen(zCmd); + if( strncmp(zCmd, "exec", nCmd)==0 ){ + u32 eFlags = 0; + if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; + verify_all_options(); + email_send_alerts(eFlags); + }else + if( strncmp(zCmd, "inbound", nCmd)==0 ){ + Blob email; + const char *zInboundDir = db_get("email-receive-dir",""); + verify_all_options(); + if( g.argc!=3 && g.argc!=4 ){ + usage("inbound [FILE]"); + } + blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE); + if( zInboundDir[0] ){ + char *zFN = emailTempFilename(zInboundDir); + blob_write_to_file(&email, zFN); + fossil_free(zFN); + } + email_receive(&email); + }else if( strncmp(zCmd, "reset", nCmd)==0 ){ - Blob yn; int c; - fossil_print( - "This will erase all content in the repository tables, thus\n" - "deleting all subscriber information. The information will be\n" - "unrecoverable.\n"); - prompt_user("Continue? (y/N) ", &yn); - c = blob_str(&yn)[0]; + int bForce = find_option("force","f",0)!=0; + verify_all_options(); + if( bForce ){ + c = 'y'; + }else{ + Blob yn; + fossil_print( + "This will erase all content in the repository tables, thus\n" + "deleting all subscriber information. The information will be\n" + "unrecoverable.\n"); + prompt_user("Continue? (y/N) ", &yn); + c = blob_str(&yn)[0]; + blob_zero(&yn); + } if( c=='y' ){ + email_triggers_disable(); db_multi_exec( "DROP TABLE IF EXISTS subscriber;\n" - "DROP TABLE IF EXISTS subscription;\n" + "DROP TABLE IF EXISTS pending_alert;\n" + "DROP TABLE IF EXISTS email_bounce;\n" + /* Legacy */ "DROP TABLE IF EXISTS email_pending;\n" - "DROP TABLE IF EXISTS email_bounce;\n" + "DROP TABLE IF EXISTS subscription;\n" ); email_schema(); } - blob_zero(&yn); }else if( strncmp(zCmd, "send", nCmd)==0 ){ Blob prompt, body, hdr; int sendAsBoth = find_option("both",0,0)!=0; int sendAsHtml = find_option("html",0,0)!=0; @@ -402,10 +574,11 @@ if( zSource ){ blob_read_from_file(&body, zSource, ExtFILE); }else{ prompt_for_user_comment(&body, &prompt); } + blob_add_final_newline(&body); if( sendAsHtml ){ email_send(&hdr, 0, &body, zDest); }else if( sendAsBoth ){ Blob html; blob_init(&html, 0, 0); @@ -416,14 +589,13 @@ email_send(&hdr, &body, 0, zDest); } blob_zero(&hdr); blob_zero(&body); blob_zero(&prompt); - } - else if( strncmp(zCmd, "settings", nCmd)==0 ){ + }else + if( strncmp(zCmd, "settings", nCmd)==0 ){ int isGlobal = find_option("global",0,0)!=0; - int i; int nSetting; const Setting *pSetting = setting_info(&nSetting); db_open_config(1, 0); verify_all_options(); if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]"); @@ -439,15 +611,133 @@ pSetting = setting_info(&nSetting); for(; nSetting>0; nSetting--, pSetting++ ){ if( strncmp(pSetting->name,"email-",6)!=0 ) continue; print_setting(pSetting); } + }else + if( strncmp(zCmd, "subscribers", nCmd)==0 ){ + Stmt q; + verify_all_options(); + if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); + if( g.argc==4 ){ + char *zPattern = g.argv[3]; + db_prepare(&q, + "SELECT semail FROM subscriber" + " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'" + " OR semail GLOB '*%q*' or suname GLOB '*%q*'" + " ORDER BY semail", + zPattern, zPattern, zPattern, zPattern); + }else{ + db_prepare(&q, + "SELECT semail FROM subscriber" + " ORDER BY semail"); + } + while( db_step(&q)==SQLITE_ROW ){ + fossil_print("%s\n", db_column_text(&q, 0)); + } + db_finalize(&q); + }else + if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ + verify_all_options(); + if( g.argc!=4 ) usage("unsubscribe EMAIL"); + db_multi_exec( + "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); + }else + { + usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); + } +} + +/* +** Do error checking on a submitted subscription form. Return TRUE +** if the submission is valid. Return false if any problems are seen. +*/ +static int subscribe_error_check( + int *peErr, /* Type of error */ + char **pzErr, /* Error message text */ + int needCaptcha /* True if captcha check needed */ +){ + const char *zEAddr; + int i, j, n; + char c; + + *peErr = 0; + *pzErr = 0; + + /* Check the validity of the email address. + ** + ** (1) Exactly one '@' character. + ** (2) No other characters besides [a-zA-Z0-9._-] + */ + zEAddr = P("e"); + if( zEAddr==0 ) return 0; + for(i=j=0; (c = zEAddr[i])!=0; i++){ + if( c=='@' ){ + n = i; + j++; + continue; + } + if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){ + *peErr = 1; + *pzErr = mprintf("illegal character in email address: 0x%x '%c'", + c, c); + return 0; + } + } + if( j!=1 ){ + *peErr = 1; + *pzErr = mprintf("email address should contain exactly one '@'"); + return 0; + } + if( n<1 ){ + *peErr = 1; + *pzErr = mprintf("name missing before '@' in email address"); + return 0; + } + if( n>i-5 ){ + *peErr = 1; + *pzErr = mprintf("email domain too short"); + return 0; + } + + /* Verify the captcha */ + if( needCaptcha && !captcha_is_correct(1) ){ + *peErr = 2; + *pzErr = mprintf("incorrect security code"); + return 0; } - else{ - usage("reset|send|setting"); + + /* Check to make sure the email address is available for reuse */ + if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){ + *peErr = 1; + *pzErr = mprintf("this email address is used by someone else"); + return 0; } + + /* If we reach this point, all is well */ + return 1; } + +/* +** Text of email message sent in order to confirm a subscription. +*/ +static const char zConfirmMsg[] = +@ Someone has signed you up for email alerts on the Fossil repository +@ at %s. +@ +@ To confirm your subscription and begin receiving alerts, click on +@ the following hyperlink: +@ +@ %s/alerts/%s +@ +@ Save the hyperlink above! You can reuse this same hyperlink to +@ unsubscribe or to change the kinds of alerts you receive. +@ +@ If you do not want to subscribe, you can simply ignore this message. +@ You will not be contacted again. +@ +; /* ** WEBPAGE: subscribe ** ** Allow users to subscribe to email notifications, or to change or @@ -455,65 +745,162 @@ */ void subscribe_page(void){ int needCaptcha; unsigned int uSeed; const char *zDecoded; - char *zCaptcha; + char *zCaptcha = 0; + char *zErr = 0; + int eErr = 0; login_check_credentials(); if( !g.perm.EmailAlert ){ login_needed(g.anon.EmailAlert); return; } - style_header("Email Subscription"); - needCaptcha = P("usecaptcha")!=0 || login_is_nobody() - || login_is_special(g.zLogin); + if( login_is_individual() + && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin) + ){ + /* This person is already signed up for email alerts. Jump + ** to the screen that lets them edit their alert preferences. + ** Except, administrators can create subscriptions for others so + ** do not jump for them. + */ + if( g.perm.Admin ){ + /* Admins get a link to admin their own account, but they + ** stay on this page so that they can create subscriptions + ** for other people. */ + style_submenu_element("My Subscription","%R/alerts"); + }else{ + /* Everybody else jumps to the page to administer their own + ** account only. */ + cgi_redirectf("%R/alerts"); + return; + } + } + email_submenu_common(); + needCaptcha = !login_is_individual(); + if( P("submit") + && cgi_csrf_safe(1) + && subscribe_error_check(&eErr,&zErr,needCaptcha) + ){ + /* A validated request for a new subscription has been received. */ + char ssub[20]; + const char *zEAddr = P("e"); + sqlite3_int64 id; /* New subscriber Id */ + const char *zCode; /* New subscriber code (in hex) */ + int nsub = 0; + const char *suname = PT("suname"); + if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin; + if( suname && suname[0]==0 ) suname = 0; + if( PB("sa") ) ssub[nsub++] = 'a'; + if( PB("sc") ) ssub[nsub++] = 'c'; + if( PB("st") ) ssub[nsub++] = 't'; + if( PB("sw") ) ssub[nsub++] = 'w'; + ssub[nsub] = 0; + db_multi_exec( + "INSERT INTO subscriber(subscriberCode,semail,suname," + " sverified,sdonotcall,sdigest,ssub,sctime,smtime,smip)" + "VALUES(randomblob(32),%Q,%Q,%d,0,%d,%Q," + " julianday('now'),julianday('now'),%Q)", + /* semail */ zEAddr, + /* suname */ suname, + /* sverified */ needCaptcha==0, + /* sdigest */ PB("di"), + /* ssub */ ssub, + /* smip */ g.zIpAddr + ); + id = db_last_insert_rowid(); + zCode = db_text(0, + "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld", + id); + if( !needCaptcha ){ + /* The new subscription has been added on behalf of a logged-in user. + ** No verification is required. Jump immediately to /alerts page. + */ + cgi_redirectf("%R/alerts/%s", zCode); + return; + }else{ + /* We need to send a verification email */ + Blob hdr, body; + blob_init(&hdr,0,0); + blob_init(&body,0,0); + blob_appendf(&hdr, "To: %s\n", zEAddr); + blob_appendf(&hdr, "Subject: Subscription verification\n"); + blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/, + g.zBaseURL, g.zBaseURL, zCode); + email_send(&hdr, &body, 0, 0); + style_header("Email Alert Verification"); + @

An email has been sent to "%h(zEAddr)". That email contains a + @ hyperlink that you must click on in order to activate your + @ subscription.

+ style_footer(); + } + return; + } + style_header("Signup For Email Alerts"); + @

To receive email notifications for changes to this + @ repository, fill out the form below and press "Submit" button.

form_begin(0, "%R/subscribe"); @ @ @ - @ - @ + @ + if( eErr==1 ){ + @ + } @ if( needCaptcha ){ uSeed = captcha_seed(); zDecoded = captcha_decode(uSeed); zCaptcha = captcha_render(zDecoded); @ @ @ - @ - @ - @ - } - @ - @ - @ - @ - @ - @ - @ - @ - @ - @ - @ - @ - @ + @ + if( eErr==2 ){ + @ + } + @ + } + if( g.perm.Admin ){ + @ + @ + @ + if( eErr==3 ){ + @ + } + @ + } + @ + @ + @ @ @ @ - @ + if( needCaptcha && !email_enabled() ){ + @ + }else{ + @ + } @ @ if( needCaptcha ){ @
     @ %h(zCaptcha)
@@ -520,7 +907,701 @@
     @ 
@ Enter the 8 characters above in the "Security Code" box @
} @ + fossil_free(zErr); + style_footer(); +} + +/* +** Either shutdown or completely delete a subscription entry given +** by the hex value zName. Then paint a webpage that explains that +** the entry has been removed. +*/ +static void email_unsubscribe(const char *zName){ + char *zEmail; + zEmail = db_text(0, "SELECT semail FROM subscriber" + " WHERE subscriberCode=hextoblob(%Q)", zName); + if( zEmail==0 ){ + style_header("Unsubscribe Fail"); + @

Unable to locate a subscriber with the requested key

+ }else{ + db_multi_exec( + "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)", + zName + ); + style_header("Unsubscribed"); + @

The "%h(zEmail)" email address has been delisted. + @ All traces of that email address have been removed

+ } + style_footer(); + return; +} + +/* +** WEBPAGE: alerts +** +** Edit email alert and notification settings. +** +** The subscriber entry is identified in either of two ways: +** +** (1) The name= query parameter contains the subscriberCode. +** +** (2) The user is logged into an account other than "nobody" or +** "anonymous". In that case the notification settings +** associated with that account can be edited without needing +** to know the subscriber code. +*/ +void alerts_page(void){ + const char *zName = P("name"); + Stmt q; + int sa, sc, st, sw; + int sdigest, sdonotcall, sverified; + const char *ssub; + const char *semail; + const char *smip; + const char *suname; + int eErr = 0; + char *zErr = 0; + + login_check_credentials(); + if( !g.perm.EmailAlert ){ + cgi_redirect("subscribe"); + return; + } + if( zName==0 && login_is_individual() ){ + zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber" + " WHERE suname=%Q", g.zLogin); + } + if( zName==0 || !validate16(zName, -1) ){ + cgi_redirect("subscribe"); + return; + } + email_submenu_common(); + if( P("submit")!=0 && cgi_csrf_safe(1) ){ + int sdonotcall = PB("sdonotcall"); + int sdigest = PB("sdigest"); + char ssub[10]; + int nsub = 0; + if( PB("sa") ) ssub[nsub++] = 'a'; + if( PB("sc") ) ssub[nsub++] = 'c'; + if( PB("st") ) ssub[nsub++] = 't'; + if( PB("sw") ) ssub[nsub++] = 'w'; + ssub[nsub] = 0; + if( g.perm.Admin ){ + const char *suname = PT("suname"); + if( suname && suname[0]==0 ) suname = 0; + int sverified = PB("sverified"); + db_multi_exec( + "UPDATE subscriber SET" + " sdonotcall=%d," + " sdigest=%d," + " ssub=%Q," + " smtime=julianday('now')," + " smip=%Q," + " suname=%Q," + " sverified=%d" + " WHERE subscriberCode=hextoblob(%Q)", + sdonotcall, + sdigest, + ssub, + g.zIpAddr, + suname, + sverified, + zName + ); + }else{ + db_multi_exec( + "UPDATE subscriber SET" + " sdonotcall=%d," + " sdigest=%d," + " ssub=%Q," + " smtime=julianday('now')," + " smip=%Q," + " WHERE subscriberCode=hextoblob(%Q)", + sdonotcall, + sdigest, + ssub, + g.zIpAddr, + zName + ); + } + } + if( P("delete")!=0 && cgi_csrf_safe(1) ){ + if( !PB("dodelete") ){ + eErr = 9; + zErr = mprintf("Select this checkbox and press \"Unsubscribe\" to" + " unsubscribe"); + }else{ + email_unsubscribe(zName); + return; + } + } + db_prepare(&q, + "SELECT" + " semail," + " sverified," + " sdonotcall," + " sdigest," + " ssub," + " smip," + " suname" + " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName); + if( db_step(&q)!=SQLITE_ROW ){ + db_finalize(&q); + cgi_redirect("subscribe"); + return; + } + style_header("Update Subscription"); + semail = db_column_text(&q, 0); + sverified = db_column_int(&q, 1); + sdonotcall = db_column_int(&q, 2); + sdigest = db_column_int(&q, 3); + ssub = db_column_text(&q, 4); + sa = strchr(ssub,'a')!=0; + sc = strchr(ssub,'c')!=0; + st = strchr(ssub,'t')!=0; + sw = strchr(ssub,'w')!=0; + smip = db_column_text(&q, 5); + suname = db_column_text(&q, 6); + if( !g.perm.Admin && !sverified ){ + db_multi_exec( + "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)", + zName); + @

Your email alert subscription has been verified!

+ @

Use the form below to update your subscription information.

+ @

Hint: Bookmark this page so that you can more easily update + @ your subscription information in the future

+ }else{ + @

Make changes to the email subscription shown below and + @ press "Submit".

+ } + form_begin(0, "%R/alerts"); + @ + @ + @ + @ + @ + @ + if( g.perm.Admin ){ + @ + @ + @ + @ + @ + @ + @ + @ + } + @ + @ + @ + @ + @ + @ + @ + @ + fossil_free(zErr); + db_finalize(&q); + style_footer(); +} + +/* This is the message that gets sent to describe how to change +** or modify a subscription +*/ +static const char zUnsubMsg[] = +@ To changes your subscription settings at %s visit this link: +@ +@ %s/alerts/%s +@ +@ To completely unsubscribe from %s, visit the following link: +@ +@ %s/unsubscribe/%s +; + +/* +** WEBPAGE: unsubscribe +** +** Users visit this page to be delisted from email alerts. +** +** If a valid subscriber code is supplied in the name= query parameter, +** then that subscriber is delisted. +** +** Otherwise, If the users is logged in, then they are redirected +** to the /alerts page where they have an unsubscribe button. +** +** Non-logged-in users with no name= query parameter are invited to enter +** an email address to which will be sent the unsubscribe link that +** contains the correct subscriber code. +*/ +void unsubscribe_page(void){ + const char *zName = P("name"); + char *zErr = 0; + int eErr = 0; + unsigned int uSeed; + const char *zDecoded; + char *zCaptcha = 0; + int dx; + int bSubmit; + const char *zEAddr; + char *zCode = 0; + + /* If a valid subscriber code is supplied, then unsubscribe immediately. + */ + if( zName + && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)", + zName) + ){ + email_unsubscribe(zName); + return; + } + + /* Logged in users are redirected to the /alerts page */ + login_check_credentials(); + if( login_is_individual() ){ + cgi_redirectf("%R/alerts"); + return; + } + + zEAddr = PD("e",""); + dx = atoi(PD("dx","0")); + bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(1); + if( bSubmit ){ + if( !captcha_is_correct(1) ){ + eErr = 2; + zErr = mprintf("enter the security code shown below"); + bSubmit = 0; + } + } + if( bSubmit ){ + zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber" + " WHERE semail=%Q", zEAddr); + if( zCode==0 ){ + eErr = 1; + zErr = mprintf("not a valid email address"); + bSubmit = 0; + } + } + if( bSubmit ){ + /* If we get this far, it means that a valid unsubscribe request has + ** been submitted. Send the appropriate email. */ + Blob hdr, body; + blob_init(&hdr,0,0); + blob_init(&body,0,0); + blob_appendf(&hdr, "To: %s\n", zEAddr); + blob_appendf(&hdr, "Subject: Unsubscribe Instructions\n"); + blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/, + g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode); + email_send(&hdr, &body, 0, 0); + style_header("Unsubscribe Instructions Sent"); + @

An email has been sent to "%h(zEAddr)" that explains how to + @ unsubscribe and/or modify your subscription settings

+ style_footer(); + return; + } + + /* Non-logged-in users have to enter an email address to which is + ** sent a message containing the unsubscribe link. + */ + style_header("Unsubscribe Request"); + @

Fill out the form below to request an email message that will + @ explain how to unsubscribe and/or change your subscription settings.

+ @ + form_begin(0, "%R/unsubscribe"); + @ + @ + @ + @ + if( eErr==1 ){ + @ + } + @ + uSeed = captcha_seed(); + zDecoded = captcha_decode(uSeed); + zCaptcha = captcha_render(zDecoded); + @ + @ + @ + if( eErr==2 ){ + @ + } + @ + @ + @ + @ + @ + @ + @ + @ + @
+  @ %h(zCaptcha)
+  @ 
+ @ Enter the 8 characters above in the "Security Code" box + @
+ @ + fossil_free(zErr); + style_footer(); +} + +/* +** WEBPAGE: subscribers +** +** This page, accessible to administrators only, +** shows a list of email notification email addresses with +** links to facilities for editing. +*/ +void subscriber_list_page(void){ + Blob sql; + Stmt q; + login_check_credentials(); + if( !g.perm.Admin ){ + fossil_redirect_home(); + return; + } + email_submenu_common(); + style_header("Subscriber List"); + blob_init(&sql, 0, 0); + blob_append_sql(&sql, + "SELECT hex(subscriberCode)," + " semail," + " ssub," + " suname," + " sverified," + " sdigest" + " FROM subscriber" + ); + db_prepare_blob(&q, &sql); + @ + @ + @ + while( db_step(&q)==SQLITE_ROW ){ + @ + @ + @ + @ + @ + @ + @ + } + @
Email + @ Events + @ Digest-Only? + @ User + @ Verified? + @
\ + @ %h(db_column_text(&q,1))%h(db_column_text(&q,2))%s(db_column_int(&q,5)?"digest":"")%h(db_column_text(&q,3))%s(db_column_int(&q,4)?"yes":"pending")
+ db_finalize(&q); style_footer(); } + +#if LOCAL_INTERFACE +/* +** A single event that might appear in an alert is recorded as an +** instance of the following object. +*/ +struct EmailEvent { + int type; /* 'c', 't', 'w', etc. */ + Blob txt; /* Text description to appear in an alert */ + EmailEvent *pNext; /* Next in chronological order */ +}; +#endif + +/* +** Free a linked list of EmailEvent objects +*/ +void email_free_eventlist(EmailEvent *p){ + while( p ){ + EmailEvent *pNext = p->pNext; + blob_zero(&p->txt); + fossil_free(p); + p = pNext; + } +} + +/* +** Compute and return a linked list of EmailEvent objects +** corresponding to the current content of the temp.wantalert +** table which should be defined as follows: +** +** CREATE TEMP TABLE wantalert(eventId TEXT); +*/ +EmailEvent *email_compute_event_text(int *pnEvent){ + Stmt q; + EmailEvent *p; + EmailEvent anchor; + EmailEvent *pLast; + const char *zUrl = db_get("email-url","http://localhost:8080"); + + db_prepare(&q, + "SELECT" + " blob.uuid," /* 0 */ + " datetime(event.mtime)," /* 1 */ + " coalesce(ecomment,comment)" + " || ' (user: ' || coalesce(euser,user,'?')" + " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" + " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" + " FROM tag, tagxref" + " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" + " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" + " || ')' as comment," /* 2 */ + " tagxref.value AS branch," /* 3 */ + " wantalert.eventId" /* 4 */ + " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" + " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND tagxref.rid=blob.rid" + " WHERE blob.rid=event.objid" + " AND tag.tagname='branch'" + " AND event.objid=substr(wantalert.eventId,2)+0" + " ORDER BY event.mtime" + ); + memset(&anchor, 0, sizeof(anchor)); + pLast = &anchor; + *pnEvent = 0; + while( db_step(&q)==SQLITE_ROW ){ + const char *zType = ""; + p = fossil_malloc( sizeof(EmailEvent) ); + pLast->pNext = p; + pLast = p; + p->type = db_column_text(&q, 4)[0]; + p->pNext = 0; + switch( p->type ){ + case 'c': zType = "Check-In"; break; + case 't': zType = "Wiki Edit"; break; + case 'w': zType = "Ticket Change"; break; + } + blob_init(&p->txt, 0, 0); + blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", + db_column_text(&q,1), + zType, + db_column_text(&q,2), + zUrl, + db_column_text(&q,0) + ); + (*pnEvent)++; + } + db_finalize(&q); + return anchor.pNext; +} + +/* +** Put a header on an alert email +*/ +void email_header(Blob *pOut){ + blob_appendf(pOut, + "This is an automated email reporting changes " + "on Fossil repository %s (%s/timeline)\n", + db_get("email-subname","(unknown)"), + db_get("email-url","http://localhost:8080")); +} + +/* +** Append the "unsubscribe" notification and other footer text to +** the end of an email alert being assemblied in pOut. +*/ +void email_footer(Blob *pOut){ + blob_appendf(pOut, "\n%.72c\nTo unsubscribe: %s/unsubscribe\n", + '-', db_get("email-url","http://localhost:8080")); +} + +/* +** COMMAND: test-generate-alert +** +** Usage: %fossil test-generate-alert [--html] [--actual] EVENTID ... +** +** Generate the text of an email alert for all of the EVENTIDs +** listed on the command-line. Write that text to standard +** output. If the --actual flag is present, then the EVENTIDs are +** the actual event-ids in the pending_alert table. +** +** This command is intended for testing and debugging the logic +** that generates email alert text. +*/ +void test_generate_alert_cmd(void){ + int bActual = find_option("actual",0,0)!=0; + Blob out; + int nEvent; + EmailEvent *pEvent, *p; + + db_find_and_open_repository(0, 0); + verify_all_options(); + db_begin_transaction(); + email_schema(); + db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); + if( bActual ){ + db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); + }else{ + int i; + for(i=2; ipNext){ + blob_append(&out, "\n", 1); + blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); + } + email_free_eventlist(pEvent); + email_footer(&out); + fossil_print("%s", blob_str(&out)); + blob_zero(&out); + db_end_transaction(0); +} + +/* +** COMMAND: test-add-alerts +** +** Usage: %fossil test-add-alerts EVENTID ... +** +** Add one or more events to the pending_alert queue. Use this +** command during testing to force email notifications for specific +** events. +*/ +void test_add_alert_cmd(void){ + int i; + db_find_and_open_repository(0, 0); + verify_all_options(); + db_begin_transaction(); + email_schema(); + for(i=2; ipNext){ + if( strchr(zSub,p->type)==0 ) continue; + if( nHit==0 ){ + blob_appendf(&hdr,"To: %s\n", zEmail); + blob_appendf(&hdr,"From: %s\n", zFrom); + blob_appendf(&hdr,"Subject: %s activity alert\n", zRepoName); + blob_appendf(&body, + "This is an automated email sent by the Fossil repository " + "at %s to alert you to changes.\n", + zUrl + ); + } + nHit++; + blob_append(&body, "\n", 1); + blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); + } + if( nHit==0 ) continue; + blob_appendf(&body,"\n%.72c\nSubscription info: %s/alerts/%s\n", + '-', zUrl, zCode); + email_send(&hdr,&body,0,0); + blob_truncate(&hdr); + blob_truncate(&body); + } + blob_zero(&hdr); + blob_zero(&body); + db_finalize(&q); + email_free_eventlist(pEvents); + if( (flags & SENDALERT_PRESERVE)==0 ){ + if( flags & SENDALERT_DIGEST ){ + db_multi_exec("UPDATE pending_alert SET sentDigest=true"); + }else{ + db_multi_exec("UPDATE pending_alert SET sentSep=true"); + } + db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); + } +send_alerts_done: + db_end_transaction(0); +} Index: src/encode.c ================================================================== --- src/encode.c +++ src/encode.c @@ -634,10 +634,11 @@ ** Return true if the input string contains only valid base-16 digits. ** If any invalid characters appear in the string, return false. */ int validate16(const char *zIn, int nIn){ int i; + if( nIn<0 ) nIn = (int)strlen(zIn); for(i=0; i63 ){ return zIn[0]==0; } } Index: src/forum.c ================================================================== --- src/forum.c +++ src/forum.c @@ -101,13 +101,11 @@ itemId ); while( db_step(&q)==SQLITE_ROW ){ int id = db_column_int(&q, 0); const char *zUser = db_column_text(&q, 1); - const char *zStat = db_column_text(&q, 2); const char *zMime = db_column_text(&q, 3); - const char *zIp = db_column_text(&q, 4); int iDepth = db_column_int(&q, 7); double rMTime = db_column_double(&q, 8); char *zAge = db_timespan_name(rNow - rMTime); Blob body; @ @@ -257,11 +255,10 @@ void forum_edit_page(void){ int itemId; int parentId; char *zErr = 0; login_check_credentials(); - const char *zBody; const char *zMime; const char *zSub; if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } forum_verify_schema(); itemId = atoi(PD("item","0")); Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -730,10 +730,15 @@ free(zCaptcha); } @ } if( g.zLogin && g.perm.Password ){ + if( email_enabled() ){ + @
+ @

Configure Email Alerts + @ for user %h(g.zLogin)

+ } @
@

Change Password for user %h(g.zLogin):

form_begin(0, "%R/login"); @ @ @@ -1348,10 +1353,19 @@ ** Return true if the user is "nobody" */ int login_is_nobody(void){ return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0; } + +/* +** Return true if the user is a specific individual, not "nobody" or +** "anonymous". +*/ +int login_is_individual(void){ + return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0 + && fossil_strcmp(g.zLogin,"anonymous")!=0; +} /* ** Return the login name. If no login name is specified, return "nobody". */ const char *login_name(void){ Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -144,11 +144,12 @@ ** Update the repository schema for Fossil version 2.0. (2017-02-28) ** (1) Change the CHECK constraint on BLOB.UUID so that the length ** is greater than or equal to 40, not exactly equal to 40. */ void rebuild_schema_update_2_0(void){ - char *z = db_text(0, "SELECT sql FROM repository.sqlite_master WHERE name='blob'"); + char *z = db_text(0, "SELECT sql FROM repository.sqlite_master" + " WHERE name='blob'"); if( z ){ /* Search for: length(uuid)==40 ** 0123456789 12345 */ int i; for(i=10; z[i]; i++){ @@ -357,19 +358,21 @@ ttyOutput = doOut; processCnt = 0; if (ttyOutput && !g.fQuiet) { percent_complete(0); } + email_triggers_disable(); rebuild_update_schema(); blob_init(&sql, 0, 0); db_prepare(&q, "SELECT name FROM sqlite_master /*scan*/" " WHERE type='table'" " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias'," "'config','shun','private','reportfmt'," "'concealed','accesslog','modreq'," - "'purgeevent','purgeitem','unversioned')" + "'purgeevent','purgeitem','unversioned'," + "'subscriber','pending_alert','email_bounce')" " AND name NOT GLOB 'sqlite_*'" " AND name NOT GLOB 'fx_*'" ); while( db_step(&q)==SQLITE_ROW ){ blob_appendf(&sql, "DROP TABLE IF EXISTS \"%w\";\n", db_column_text(&q,0)); @@ -446,10 +449,11 @@ if( doClustering ) create_cluster(); if( ttyOutput && !g.fQuiet && totalSize>0 ){ processCnt += incrSize; percent_complete((processCnt*1000)/totalSize); } + email_triggers_enable(); if(!g.fQuiet && ttyOutput ){ percent_complete(1000); fossil_print("\n"); } return errCnt; Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -1022,13 +1022,13 @@ admin_log("Set entry_attribute %Q to: %.*s%s", zVar, 20, zQ, (nZQ>20 ? "..." : "")); zVal = zQ; } @ %s(zLabel) } /* Index: src/timeline.c ================================================================== --- src/timeline.c +++ src/timeline.c @@ -2336,10 +2336,11 @@ ** N is negative, output the first -N lines. If N is ** zero, no limit. Default is -20 meaning 20 lines. ** -p|--path PATH Output items affecting PATH only. ** PATH can be a file or a sub directory. ** --offset P skip P changes +** --sql Show the SQL used to generate the timeline ** -t|--type TYPE Output items from the given types only, such as: ** ci = file commits only ** e = technical notes only ** t = tickets only ** w = wiki commits only @@ -2367,10 +2368,11 @@ int mode = TIMELINE_MODE_NONE; int verboseFlag = 0 ; int iOffset; const char *zFilePattern = 0; Blob treeName; + int showSql = 0; verboseFlag = find_option("verbose","v", 0)!=0; if( !verboseFlag){ verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */ } @@ -2377,10 +2379,11 @@ db_find_and_open_repository(0, 0); zLimit = find_option("limit","n",1); zWidth = find_option("width","W",1); zType = find_option("type","t",1); zFilePattern = find_option("path","p",1); + showSql = find_option("sql",0,0)!=0; if( !zLimit ){ zLimit = find_option("count",0,1); } if( zLimit ){ @@ -2520,11 +2523,14 @@ if( iOffset>0 ){ /* Don't handle LIMIT here, otherwise print_timeline() * will not determine the end-marker correctly! */ blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset); } - db_prepare(&q, "%s", blob_sql_text(&sql)); + if( showSql ){ + fossil_print("%s\n", blob_str(&sql)); + } + db_prepare_blob(&q, &sql); blob_reset(&sql); print_timeline(&q, n, width, verboseFlag); db_finalize(&q); } Index: src/tkt.c ================================================================== --- src/tkt.c +++ src/tkt.c @@ -594,11 +594,11 @@ int nJ = 0; Blob tktchng, cksum; int needMod; login_verify_csrf_secret(); - if( !captcha_is_correct() ){ + if( !captcha_is_correct(0) ){ @

Error: Incorrect security code.

return TH_OK; } zUuid = (const char *)pUuid; blob_zero(&tktchng); Index: src/wiki.c ================================================================== --- src/wiki.c +++ src/wiki.c @@ -534,11 +534,11 @@ zBody = pWiki->zWiki; zMimetype = pWiki->zMimetype; } } if( P("submit")!=0 && zBody!=0 - && (goodCaptcha = captcha_is_correct()) + && (goodCaptcha = captcha_is_correct(0)) ){ char *zDate; Blob cksum; blob_zero(&wiki); db_begin_transaction(); @@ -758,11 +758,11 @@ if( !g.perm.ApndWiki ){ login_needed(g.anon.ApndWiki); return; } if( P("submit")!=0 && P("r")!=0 && P("u")!=0 - && (goodCaptcha = captcha_is_correct()) + && (goodCaptcha = captcha_is_correct(0)) ){ char *zDate; Blob cksum; Blob body; Blob wiki;
Old Password: