Index: src/attach.c ================================================================== --- src/attach.c +++ src/attach.c @@ -106,15 +106,13 @@ }else{ zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); } @
  • @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid) - if( moderation_pending(attachid) ){ - @ *** Awaiting Moderator Approval *** - } + moderation_pending_www(attachid); @
    %h(zFilename) - @ [download]
    + @ [download]
    if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; if( zComment && zComment[0] ){ @ %!W(zComment)
    } if( zPage==0 && zTkt==0 && zTechNote==0 ){ @@ -564,14 +562,11 @@ @ Artifact ID: @ %z(href("%R/artifact/%!S",zUuid))%s(zUuid) if( g.perm.Setup ){ @ (%d(rid)) } - modPending = moderation_pending(rid); - if( modPending ){ - @ *** Awaiting Moderator Approval *** - } + modPending = moderation_pending_www(rid); if( zTktUuid ){ @ Ticket: @ %z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid) } if( zTNUuid ){ Index: src/backoffice.c ================================================================== --- src/backoffice.c +++ src/backoffice.c @@ -78,18 +78,16 @@ ** otherwise taking a long time to complete. Set this when a user-visible ** process might need to wait for backoffice to complete. */ static int backofficeNoDelay = 0; - /* ** Disable the backoffice */ void backoffice_no_delay(void){ backofficeNoDelay = 1; } - /* ** Parse a unsigned 64-bit integer from a string. Return a pointer ** to the character of z[] that occurs after the integer. */ @@ -264,11 +262,11 @@ getpid()); } backoffice_work(); break; } - if( backofficeNoDelay ){ + if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",1) ){ /* If the no-delay flag is set, exit immediately rather than queuing ** up. Assume that some future request will come along and handle any ** necessary backoffice work. */ db_end_transaction(0); break; ADDED src/capabilities.c Index: src/capabilities.c ================================================================== --- /dev/null +++ src/capabilities.c @@ -0,0 +1,387 @@ +/* +** Copyright (c) 2018 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/ +** +******************************************************************************* +** +** This file contains code used managing user capability strings. +*/ +#include "config.h" +#include "capabilities.h" +#include + +#if INTERFACE +/* +** A capability string object holds all defined capabilities in a +** vector format that is subject to boolean operations. +*/ +struct CapabilityString { + unsigned char x[128]; +}; +#endif + +/* +** Add capabilities to a CapabilityString. If pIn is NULL, then create +** a new capability string. +** +** Call capability_free() on the allocated CapabilityString object to +** deallocate. +*/ +CapabilityString *capability_add(CapabilityString *pIn, const char *zCap){ + int c; + int i; + if( pIn==0 ){ + pIn = fossil_malloc( sizeof(*pIn) ); + memset(pIn, 0, sizeof(*pIn)); + } + if( zCap ){ + for(i=0; (c = zCap[i])!=0; i++){ + if( c>='0' && c<='z' ) pIn->x[c] = 1; + } + } + return pIn; +} + +/* +** Remove capabilities from a CapabilityString. +*/ +CapabilityString *capability_remove(CapabilityString *pIn, const char *zCap){ + int c; + int i; + if( pIn==0 ){ + pIn = fossil_malloc( sizeof(*pIn) ); + memset(pIn, 0, sizeof(*pIn)); + } + if( zCap ){ + for(i=0; (c = zCap[i])!=0; i++){ + if( c>='0' && c<='z' ) pIn->x[c] = 0; + } + } + return pIn; +} + +/* +** Return true if any of the capabilities in zNeeded are found in pCap +*/ +int capability_has_any(CapabilityString *p, const char *zNeeded){ + if( p==0 ) return 0; + if( zNeeded==0 ) return 0; + while( zNeeded[0] ){ + int c = zNeeded[0]; + if( fossil_isalnum(c) && p->x[c] ) return 1; + zNeeded++; + } + return 0; +} + +/* +** Delete a CapabilityString object. +*/ +void capability_free(CapabilityString *p){ + fossil_free(p); +} + +/* +** Expand the capability string by including all capabilities for +** special users "nobody" and "anonymous". Also include "reader" +** if "u" is present and "developer" if "v" is present. +*/ +void capability_expand(CapabilityString *pIn){ + static char *zNobody = 0; + static char *zAnon = 0; + static char *zReader = 0; + static char *zDev = 0; + + if( pIn==0 ){ + fossil_free(zNobody); zNobody = 0; + fossil_free(zAnon); zAnon = 0; + fossil_free(zReader); zReader = 0; + fossil_free(zDev); zDev = 0; + return; + } + if( pIn->x['v'] ){ + if( zDev==0 ){ + zDev = db_text(0, "SELECT cap FROM user WHERE login='developer'"); + } + pIn = capability_add(pIn, zDev); + } + if( pIn->x['u'] ){ + if( zReader==0 ){ + zReader = db_text(0, "SELECT cap FROM user WHERE login='reader'"); + } + pIn = capability_add(pIn, zReader); + } + if( zNobody==0 ){ + zNobody = db_text(0, "SELECT cap FROM user WHERE login='nobody'"); + zAnon = db_text(0, "SELECT cap FROM user WHERE login='anonymous'"); + } + pIn = capability_add(pIn, zAnon); + pIn = capability_add(pIn, zNobody); +} + +/* +** Render a capability string in canonical string format. Space to hold +** the returned string is obtained from fossil_malloc() can should be freed +** by the caller. +*/ +char *capability_string(CapabilityString *p){ + Blob out; + int i; + int j = 0; + char buf[100]; + blob_init(&out, 0, 0); + for(i='a'; i<='z'; i++){ + if( p->x[i] ) buf[j++] = i; + } + for(i='0'; i<='9'; i++){ + if( p->x[i] ) buf[j++] = i; + } + for(i='A'; i<='Z'; i++){ + if( p->x[i] ) buf[j++] = i; + } + buf[j] = 0; + return fossil_strdup(buf); +} + +/* +** The next two routines implement an aggregate SQL function that +** takes multiple capability strings and in the end returns their +** union. Example usage: +** +** SELECT capunion(cap) FROM user WHERE login IN ('nobody','anonymous'); +*/ +void capability_union_step( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + CapabilityString *p; + const char *zIn; + + zIn = (const char*)sqlite3_value_text(argv[0]); + if( zIn==0 ) return; + p = (CapabilityString*)sqlite3_aggregate_context(context, sizeof(*p)); + p = capability_add(p, zIn); +} +void capability_union_finalize(sqlite3_context *context){ + CapabilityString *p; + p = sqlite3_aggregate_context(context, 0); + if( p ){ + char *zOut = capability_string(p); + sqlite3_result_text(context, zOut, -1, fossil_free); + } +} + +/* +** The next routines takes the raw USER.CAP field and expands it with +** capabilities from special users. Example: +** +** SELECT fullcap(cap) FROM user WHERE login=?1 +*/ +void capability_fullcap( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + CapabilityString *p; + const char *zIn; + char *zOut; + + zIn = (const char*)sqlite3_value_text(argv[0]); + if( zIn==0 ) zIn = ""; + p = capability_add(0, zIn); + capability_expand(p); + zOut = capability_string(p); + sqlite3_result_text(context, zOut, -1, fossil_free); + capability_free(p); +} + +/* +** Generate HTML that lists all of the capability letters together with +** a brief summary of what each letter means. +*/ +void capabilities_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: Set or remove capability "4" for other users + @
    7Email-Alerts: Sign up for email nofications
    AAnnounce: Send announcements
    DDebug: Enable debugging features
    +} + +/* +** Generate a "capability summary table" that shows the major capabilities +** against the various user categories. +*/ +void capability_summary(void){ + Stmt q; + db_prepare(&q, + "WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3)," + "('developer',4))" + " SELECT id, fullcap(user.cap),seq,1" + " FROM t LEFT JOIN user ON t.id=user.login" + " UNION ALL" + " SELECT 'New User Default', fullcap(%Q), 10, 1" + " UNION ALL" + " SELECT 'Regular User', fullcap(capunion(cap)), 20, count(*) FROM user" + " WHERE cap NOT GLOB '*[as]*'" + " UNION ALL" + " SELECT 'Adminstator', fullcap(capunion(cap)), 30, count(*) FROM user" + " WHERE cap GLOB '*[as]*'" + " ORDER BY 3 ASC", + db_get("default-perms","") + ); + @ + @ + while( db_step(&q)==SQLITE_ROW ){ + const char *zId = db_column_text(&q, 0); + const char *zCap = db_column_text(&q, 1); + int n = db_column_int(&q, 3); + int eType; + static const char *azType[] = { "off", "read", "write" }; + static const char *azClass[] = { "capsumOff", "capsumRead", "capsumWrite" }; + + if( n==0 ) continue; + + /* Code */ + if( db_column_int(&q,2)<10 ){ + @ + }else if( n>1 ){ + @ + }else{ + @ + } + if( sqlite3_strglob("*[asi]*",zCap)==0 ){ + eType = 2; + }else if( sqlite3_strglob("*[oz]*",zCap)==0 ){ + eType = 1; + }else{ + eType = 0; + } + @ + + /* Forum */ + if( sqlite3_strglob("*[as3456]*",zCap)==0 ){ + eType = 2; + }else if( sqlite3_strglob("*2*",zCap)==0 ){ + eType = 1; + }else{ + eType = 0; + } + @ + + /* Ticket */ + if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){ + eType = 2; + }else if( sqlite3_strglob("*r*",zCap)==0 ){ + eType = 1; + }else{ + eType = 0; + } + @ + + /* Wiki */ + if( sqlite3_strglob("*[asdfklm]*",zCap)==0 ){ + eType = 2; + }else if( sqlite3_strglob("*j*",zCap)==0 ){ + eType = 1; + }else{ + eType = 0; + } + @ + + /* Unversioned */ + if( sqlite3_strglob("*y*",zCap)==0 ){ + eType = 2; + }else if( sqlite3_strglob("*[ioas]*",zCap)==0 ){ + eType = 1; + }else{ + eType = 0; + } + @ + + } + db_finalize(&q); + @
     CodeForumTicketsWiki\ + @ Unversioned Content
    "%h(zId)"
    %d(n) %h(zId)s
    %h(zId)%s(azType[eType])%s(azType[eType])%s(azType[eType])%s(azType[eType])%s(azType[eType])
    +} Index: src/cgi.c ================================================================== --- src/cgi.c +++ src/cgi.c @@ -344,11 +344,11 @@ /* After the webpage has been sent, do any useful background ** processing. */ g.cgiOutput = 2; - if( g.db!=0 && iReplyStatus==200 ){ + if( g.db!=0 && iReplyStatus==200 && !g.fSshClient ){ fclose(g.httpOut); #ifdef _WIN32 g.httpOut = fossil_fopen("NUL", "wb"); #else g.httpOut = fossil_fopen("/dev/null", "wb"); @@ -1120,23 +1120,27 @@ return zDefault; } /* ** Return the value of a CGI parameter with leading and trailing -** spaces removed. +** spaces removed and with internal \r\n changed to just \n */ char *cgi_parameter_trimmed(const char *zName, const char *zDefault){ const char *zIn; - char *zOut; - int i; + char *zOut, c; + int i, j; zIn = cgi_parameter(zName, 0); if( zIn==0 ) zIn = zDefault; if( zIn==0 ) return 0; while( fossil_isspace(zIn[0]) ) zIn++; zOut = fossil_strdup(zIn); - for(i=0; zOut[i]; i++){} - while( i>0 && fossil_isspace(zOut[i-1]) ) zOut[--i] = 0; + for(i=j=0; (c = zOut[i])!=0; i++){ + if( c=='\r' && zOut[i+1]=='\n' ) continue; + zOut[j++] = c; + } + zOut[j] = 0; + while( j>0 && fossil_isspace(zOut[j-1]) ) zOut[--j] = 0; return zOut; } /* ** Return true if the CGI parameter zName exists and is not equal to 0, Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -73,11 +73,10 @@ */ static void db_err(const char *zFormat, ...){ static int rcLooping = 0; va_list ap; char *z; - int rc = 1; if( rcLooping ) exit(rcLooping); va_start(ap, zFormat); z = vmprintf(zFormat, ap); va_end(ap); #ifdef FOSSIL_ENABLE_JSON @@ -87,26 +86,16 @@ rc = 0 /* avoid HTTP 500 */; } } else #endif /* FOSSIL_ENABLE_JSON */ - if( g.xferPanic ){ + if( g.xferPanic && g.cgiOutput==1 ){ cgi_reset_content(); @ error Database\serror:\s%F(z) - cgi_reply(); - } - else if( g.cgiOutput ){ - g.cgiOutput = 0; - cgi_printf("

    Database Error

    \n

    %h

    \n", z); - cgi_reply(); - }else{ - fprintf(stderr, "%s: %s\n", g.argv[0], z); - } - free(z); - rcLooping = rc; - db_force_rollback(); - fossil_exit(rc); + cgi_reply(); + } + fossil_panic("Database error: %s", z); } /* ** All static variable that a used by only this file are gathered into ** the following structure. @@ -492,15 +481,10 @@ db_check_result(rc); return rc; } int db_finalize(Stmt *pStmt){ int rc; - db_stats(pStmt); - blob_reset(&pStmt->sql); - rc = sqlite3_finalize(pStmt->pStmt); - db_check_result(rc); - pStmt->pStmt = 0; if( pStmt->pNext ){ pStmt->pNext->pPrev = pStmt->pPrev; } if( pStmt->pPrev ){ pStmt->pPrev->pNext = pStmt->pNext; @@ -507,10 +491,15 @@ }else if( db.pAllStmt==pStmt ){ db.pAllStmt = pStmt->pNext; } pStmt->pNext = 0; pStmt->pPrev = 0; + db_stats(pStmt); + blob_reset(&pStmt->sql); + rc = sqlite3_finalize(pStmt->pStmt); + db_check_result(rc); + pStmt->pStmt = 0; return rc; } /* ** Return the rowid of the most recent insert @@ -1012,10 +1001,14 @@ 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); + sqlite3_create_function(db, "capunion", 1, SQLITE_UTF8, 0, + 0, capability_union_step, capability_union_finalize); + sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, + capability_fullcap, 0, 0); } #if USE_SEE /* ** This is a pointer to the saved database encryption key string. @@ -1632,22 +1625,22 @@ if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){ if( file_access(zDbName, F_OK) ){ #ifdef FOSSIL_ENABLE_JSON g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; #endif - fossil_panic("repository does not exist or" + fossil_fatal("repository does not exist or" " is in an unreadable directory: %s", zDbName); }else if( file_access(zDbName, R_OK) ){ #ifdef FOSSIL_ENABLE_JSON g.json.resultCode = FSL_JSON_E_DENIED; #endif - fossil_panic("read permission denied for repository %s", zDbName); + fossil_fatal("read permission denied for repository %s", zDbName); }else{ #ifdef FOSSIL_ENABLE_JSON g.json.resultCode = FSL_JSON_E_DB_NOT_VALID; #endif - fossil_panic("not a valid repository: %s", zDbName); + fossil_fatal("not a valid repository: %s", zDbName); } } g.zRepositoryName = mprintf("%s", zDbName); db_open_or_attach(g.zRepositoryName, "repository"); g.repositoryOpen = 1; @@ -1725,13 +1718,13 @@ if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){ #ifdef FOSSIL_ENABLE_JSON g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; #endif if( nArgUsed==0 ){ - fossil_panic("use --repository or -R to specify the repository database"); + fossil_fatal("use --repository or -R to specify the repository database"); }else{ - fossil_panic("specify the repository name as a command-line argument"); + fossil_fatal("specify the repository name as a command-line argument"); } } } /* @@ -3033,10 +3026,16 @@ ** SETTING: autosync-tries width=16 default=1 ** If autosync is enabled setting this to a value greater ** than zero will cause autosync to try no more than this ** number of attempts if there is a sync failure. */ +/* +** SETTING: backoffice-nodelay boolean default=on +** If backoffice-nodelay is true, then the backoffice processing +** will never invoke sleep(). If it has nothing useful to do, +** it simply exits. +*/ /* ** SETTING: binary-glob width=40 versionable block-text ** The VALUE of this setting is a comma or newline-separated list of ** GLOB patterns that should be treated as binary files ** for committing and merging purposes. Example: *.jpg Index: src/default_css.txt ================================================================== --- src/default_css.txt +++ src/default_css.txt @@ -534,10 +534,14 @@ pre.th1error { white-space: pre-wrap; word-wrap: break-word; color: red; } +pre.textPlain { + white-space: pre-wrap; + word-wrap: break-word; +} .statistics-report-graph-line { background-color: #446979; } .statistics-report-table-events th { padding: 0 1em 0 1em; @@ -672,5 +676,46 @@ } td.form_label { vertical-align: top; text-align: right; } +.debug { + background-color: #ffc; + border: 2px solid #ff0; +} +div.forumEdit { + border: 1px solid black; + padding-left: 1ex; + padding-right: 1ex; +} +div.forumHier, div.forumTime { + border: 1px solid black; + padding-left: 1ex; + padding-right: 1ex; + margin-top: 1ex; +} +div.forumSel { + background-color: #cef; +} +div.forumObs { + color: #bbb; +} +#capabilitySummary { + text-align: center; +} +#capabilitySummary td { + padding-left: 3ex; + padding-right: 3ex; +} +#capabilitySummary th { + padding-left: 1ex; + padding-right: 1ex; +} +.capsumOff { + background-color: #bbb; +} +.capsumRead { + background-color: #bfb; +} +.capsumWrite { + background-color: #ffb; +} Index: src/dispatch.c ================================================================== --- src/dispatch.c +++ src/dispatch.c @@ -167,11 +167,11 @@ zQ = &z[i+1]; }else{ zQ = &z[i]; } if( dispatch_name_search(z, CMDFLAG_WEBPAGE, ppCmd) ){ - fossil_panic("\"%s\" aliased to \"%s\" but \"%s\" does not exist", + fossil_fatal("\"%s\" aliased to \"%s\" but \"%s\" does not exist", zName, z, z); } z = zQ; while( *z ){ char *zName = z; Index: src/email.c ================================================================== --- src/email.c +++ src/email.c @@ -1,7 +1,7 @@ /* -** Copyright (c) 2007 D. Richard Hipp +** Copyright (c) 2018 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".) ** @@ -14,11 +14,16 @@ ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** Logic for email notification, also known as "alerts". -*/ +** +** Are you looking for the code that reads and writes the internet +** email protocol? That is not here. See the "smtp.c" file instead. +** Yes, the choice of source code filenames is not the greatest, but +** it is not so bad that changing them seems justified. +*/ #include "config.h" #include "email.h" #include #include @@ -72,12 +77,13 @@ @ -- Remaining characters determine the specific event. For example, @ -- 'c4413' means check-in with rid=4413. @ -- @ 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 +@ sentSep BOOLEAN DEFAULT false, -- individual alert sent +@ sentDigest BOOLEAN DEFAULT false, -- digest alert sent +@ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent @ ) WITHOUT ROWID; @ @ DROP TABLE IF EXISTS repository.email_bounce; @ -- Record bounced emails. If too many bounces are received within @ -- some defined time range, then cancel the subscription. Older @@ -110,10 +116,15 @@ ){ return; /* Don't create table for disabled email */ } db_multi_exec(zEmailInit/*works-like:""*/); email_triggers_enable(); + }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ + db_multi_exec( + "ALTER TABLE repository.pending_alert" + " ADD COLUMN sentMod BOOLEAN DEFAULT false;" + ); } } /* ** Enable triggers that automatically populate the pending_alert @@ -289,18 +300,10 @@ @

    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(); } @@ -568,27 +571,27 @@ return 0; } /* ** Make a copy of the input string up to but not including the -** first ">" character. +** first cTerm character. ** ** Verify that the string really that is to be copied really is a ** valid email address. If it is not, then return NULL. ** ** This routine is more restrictive than necessary. It does not ** allow comments, IP address, quoted strings, or certain uncommon ** characters. The only non-alphanumerics allowed in the local ** part are "_", "+", "-" and "+". */ -char *email_copy_addr(const char *z){ +char *email_copy_addr(const char *z, char cTerm ){ int i; int nAt = 0; int nDot = 0; char c; if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ - for(i=0; (c = z[i])!=0 && c!='>'; i++){ + for(i=0; (c = z[i])!=0 && c!=cTerm; i++){ if( fossil_isalnum(c) ){ /* Alphanumerics are always ok */ }else if( c=='@' ){ if( nAt ) return 0; /* Only a single "@" allowed */ if( i>64 ) return 0; /* Local part too big */ @@ -598,22 +601,22 @@ if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ if( z[i+1]=='.' || z[i+1]=='-' ){ return 0; /* Domain cannot begin with "." or "-" */ } }else if( c=='-' ){ - if( z[i+1]=='>' ) return 0; /* Last character cannot be "-" */ + if( z[i+1]==cTerm ) return 0; /* Last character cannot be "-" */ }else if( c=='.' ){ if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ - if( z[i+1]=='>' ) return 0; /* Domain may not end with . */ + if( z[i+1]==cTerm ) return 0; /* Domain may not end with . */ nDot++; }else if( (c=='_' || c=='+') && nAt==0 ){ /* _ and + are ok in the local part */ }else{ return 0; /* Anything else is an error */ } } - if( c!='>' ) return 0; /* Missing final ">" */ + if( c!=cTerm ) return 0; /* Missing terminator */ if( nAt==0 ) return 0; /* No "@" found anywhere */ if( nDot==0 ) return 0; /* No "." in the domain */ /* If we reach this point, the email address is valid */ return mprintf("%.*s", i, z); @@ -631,11 +634,11 @@ int i; email_header_value(pMsg, "to", &v); z = blob_str(&v); for(i=0; z[i]; i++){ - if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){ + if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){ azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); azTo[nTo++] = zAddr; } } *pnTo = nTo; @@ -691,16 +694,18 @@ pOut = &all; } blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr)); blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); - /* Message-id format: "<$(date)x$(random).$(from)>" where $(date) is - ** the current unix-time in hex, $(random) is a 64-bit random number, - ** and $(from) is the sender. */ - sqlite3_randomness(sizeof(r1), &r1); - r2 = time(0); - blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom); + if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ + /* Message-id format: "<$(date)x$(random).$(from)>" where $(date) is + ** the current unix-time in hex, $(random) is a 64-bit random number, + ** and $(from) is the sender. */ + sqlite3_randomness(sizeof(r1), &r1); + r2 = time(0); + blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom); + } blob_add_final_newline(pBody); blob_appendf(pOut,"Content-Type: text/plain\r\n"); #if 0 blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n"); append_base64(pOut, pBody); @@ -752,24 +757,10 @@ fossil_print("%s", blob_str(&all)); } blob_reset(&all); } -/* -** Analyze and act on a received email. -** -** This routine takes ownership of the Blob parameter and is responsible -** for freeing that blob when it is done with it. -** -** This routine acts on all email messages received from the -** "fossil email inbound" command. -*/ -void email_receive(Blob *pMsg){ - /* To Do: Look for bounce messages and possibly disable subscriptions */ - blob_reset(pMsg); -} - /* ** SETTING: email-send-method width=5 default=off ** Determine the method used to send email. Allowed values are ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value ** means no email is ever sent. The "relay" value means emails are sent @@ -801,16 +792,10 @@ /* ** SETTING: email-self width=40 ** This is the email address for the repository. Outbound emails add ** this email address as the "From:" field. */ -/* -** SETTING: email-receive-dir width=40 -** Inbound email messages are saved as separate files in this directory, -** for debugging analysis. Disable saving of inbound emails omitting -** this setting, or making it an empty string. -*/ /* ** SETTING: email-send-relayhost width=40 ** This is the hostname and TCP port to which output email messages ** are sent when email-send-method is "relay". There should be an ** SMTP server configured as a Mail Submission Agent listening on the @@ -817,80 +802,72 @@ ** designated host and port and all times. */ /* -** COMMAND: email +** COMMAND: alerts ** -** Usage: %fossil email SUBCOMMAND ARGS... +** Usage: %fossil alerts SUBCOMMAND ARGS... ** ** Subcommands: ** -** exec Compose and send pending email alerts. +** pending Show all pending alerts. Useful for debugging. +** +** reset Hard reset of all email notification tables +** in the repository. This erases all subscription +** information. ** Use with extreme care ** +** +** send Compose and send pending email alerts. ** Some installations may want to do this via ** a cron-job to make sure alerts are sent ** in a timely manner. ** Options: ** ** --digest Send digests -** --test Resets to standard output -** -** inbound [FILE] Receive an inbound email message. This message -** is analyzed to see if it is a bounce, and if -** necessary, subscribers may be disabled. -** -** reset Hard reset of all email notification tables -** in the repository. This erases all subscription -** information. Use with extreme care. -** -** send TO [OPTIONS] Send a single email message using whatever +** --test Write to standard output +** +** settings [NAME VALUE] With no arguments, list all email settings. +** Or change the value of a single email setting. +** +** status Report on the status of the email alert +** subsystem +** +** subscribers [PATTERN] List all subscribers matching PATTERN. +** +** test-message TO [OPTS] Send a single email message using whatever ** email sending mechanism is currently configured. -** Use this for testing the email configuration. -** Options: +** Use this for testing the email notification +** configuration. Options: ** ** --body FILENAME ** --smtp-trace ** --stdout ** --subject|-S SUBJECT ** -** settings [NAME VALUE] With no arguments, list all email settings. -** Or change the value of a single email setting. -** -** subscribers [PATTERN] List all subscribers matching PATTERN. -** ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. */ void email_cmd(void){ const char *zCmd; int nCmd; db_find_and_open_repository(0, 0); email_schema(0); zCmd = g.argc>=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; - if( find_option("test",0,0)!=0 ){ - eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; - } - 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 = file_time_tempname(zInboundDir,".email"); - blob_write_to_file(&email, zFN); - fossil_free(zFN); - } - email_receive(&email); + if( strncmp(zCmd, "pending", nCmd)==0 ){ + Stmt q; + verify_all_options(); + if( g.argc!=3 ) usage("pending"); + db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" + " FROM pending_alert"); + while( db_step(&q)==SQLITE_ROW ){ + fossil_print("%10s %7s %10s %7s\n", + db_column_text(&q,0), + db_column_int(&q,1) ? "sentSep" : "", + db_column_int(&q,2) ? "sentDigest" : "", + db_column_int(&q,3) ? "sentMod" : ""); + } + db_finalize(&q); }else if( strncmp(zCmd, "reset", nCmd)==0 ){ int c; int bForce = find_option("force","f",0)!=0; verify_all_options(); @@ -918,43 +895,17 @@ ); email_schema(0); } }else if( strncmp(zCmd, "send", nCmd)==0 ){ - Blob prompt, body, hdr; - const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; - int i; - u32 mFlags = EMAIL_IMMEDIATE_FAIL; - const char *zSubject = find_option("subject", "S", 1); - const char *zSource = find_option("body", 0, 1); - EmailSender *pSender; - if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE; + u32 eFlags = 0; + if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; + if( find_option("test",0,0)!=0 ){ + eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; + } verify_all_options(); - blob_init(&prompt, 0, 0); - blob_init(&body, 0, 0); - blob_init(&hdr, 0, 0); - blob_appendf(&hdr,"To: "); - for(i=3; i3 ) blob_append(&hdr, ", ", 2); - blob_appendf(&hdr, "<%s>", g.argv[i]); - } - blob_append(&hdr,"\r\n",2); - if( zSubject ){ - blob_appendf(&hdr, "Subject: %s\r\n", zSubject); - } - if( zSource ){ - blob_read_from_file(&body, zSource, ExtFILE); - }else{ - prompt_for_user_comment(&body, &prompt); - } - blob_add_final_newline(&body); - pSender = email_sender_new(zDest, mFlags); - email_send(pSender, &hdr, &body); - email_sender_free(pSender); - blob_reset(&hdr); - blob_reset(&body); - blob_reset(&prompt); + email_send_alerts(eFlags); }else if( strncmp(zCmd, "settings", nCmd)==0 ){ int isGlobal = find_option("global",0,0)!=0; int nSetting; const Setting *pSetting = setting_info(&nSetting); @@ -974,10 +925,32 @@ for(; nSetting>0; nSetting--, pSetting++ ){ if( strncmp(pSetting->name,"email-",6)!=0 ) continue; print_setting(pSetting); } }else + if( strncmp(zCmd, "status", nCmd)==0 ){ + int nSetting, n; + static const char *zFmt = "%-29s %d\n"; + const Setting *pSetting = setting_info(&nSetting); + db_open_config(1, 0); + verify_all_options(); + if( g.argc!=3 ) usage("status"); + pSetting = setting_info(&nSetting); + for(; nSetting>0; nSetting--, pSetting++ ){ + if( strncmp(pSetting->name,"email-",6)!=0 ) continue; + print_setting(pSetting); + } + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); + n = db_int(0,"SELECT count(*) FROM subscriber"); + fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); + n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" + " AND NOT sdonotcall AND length(ssub)>1"); + fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); + }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 ){ @@ -995,19 +968,54 @@ } while( db_step(&q)==SQLITE_ROW ){ fossil_print("%s\n", db_column_text(&q, 0)); } db_finalize(&q); + }else + if( strncmp(zCmd, "test-message", nCmd)==0 ){ + Blob prompt, body, hdr; + const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; + int i; + u32 mFlags = EMAIL_IMMEDIATE_FAIL; + const char *zSubject = find_option("subject", "S", 1); + const char *zSource = find_option("body", 0, 1); + EmailSender *pSender; + if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE; + verify_all_options(); + blob_init(&prompt, 0, 0); + blob_init(&body, 0, 0); + blob_init(&hdr, 0, 0); + blob_appendf(&hdr,"To: "); + for(i=3; i3 ) blob_append(&hdr, ", ", 2); + blob_appendf(&hdr, "<%s>", g.argv[i]); + } + blob_append(&hdr,"\r\n",2); + if( zSubject==0 ) zSubject = "fossil alerts test-message"; + blob_appendf(&hdr, "Subject: %s\r\n", zSubject); + if( zSource ){ + blob_read_from_file(&body, zSource, ExtFILE); + }else{ + prompt_for_user_comment(&body, &prompt); + } + blob_add_final_newline(&body); + pSender = email_sender_new(zDest, mFlags); + email_send(pSender, &hdr, &body); + email_sender_free(pSender); + blob_reset(&hdr); + blob_reset(&body); + blob_reset(&prompt); }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"); + usage("pending|reset|send|setting|status|" + "subscribers|test-message|unsubscribe"); } } /* ** Do error checking on a submitted subscription form. Return TRUE @@ -1784,11 +1792,13 @@ /* ** 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. */ + int type; /* 'c', 'f', 'm', 't', 'w' */ + int needMod; /* Pending moderator approval */ + Blob hdr; /* Header content, for forum entries */ Blob txt; /* Text description to appear in an alert */ EmailEvent *pNext; /* Next in chronological order */ }; #endif @@ -1797,10 +1807,11 @@ */ void email_free_eventlist(EmailEvent *p){ while( p ){ EmailEvent *pNext = p->pNext; blob_reset(&p->txt); + blob_reset(&p->hdr); fossil_free(p); p = pNext; } } @@ -1807,68 +1818,152 @@ /* ** 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); +** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN); */ -EmailEvent *email_compute_event_text(int *pnEvent){ +EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){ Stmt q; EmailEvent *p; EmailEvent anchor; EmailEvent *pLast; const char *zUrl = db_get("email-url","http://localhost:8080"); + const char *zFrom; + const char *zSub; + + /* First do non-forum post events */ db_prepare(&q, "SELECT" - " blob.uuid," /* 0 */ - " datetime(event.mtime)," /* 1 */ + " 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" + " || ')' as comment," /* 2 */ + " wantalert.eventId," /* 3 */ + " wantalert.needMod" /* 4 */ + " FROM temp.wantalert CROSS JOIN event CROSS JOIN blob" " WHERE blob.rid=event.objid" - " AND tag.tagname='branch'" " AND event.objid=substr(wantalert.eventId,2)+0" - " ORDER BY event.mtime" + " AND (%d OR eventId NOT GLOB 'f*')" + " ORDER BY event.mtime", + doDigest ); 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->type = db_column_text(&q, 3)[0]; + p->needMod = db_column_int(&q, 4); p->pNext = 0; switch( p->type ){ case 'c': zType = "Check-In"; break; + case 'f': zType = "Forum post"; break; case 't': zType = "Wiki Edit"; break; case 'w': zType = "Ticket Change"; break; } + blob_init(&p->hdr, 0, 0); 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) ); + if( p->needMod ){ + blob_appendf(&p->txt, + "** Pending moderator approval (%s/modreq) **\n", + zUrl + ); + } + (*pnEvent)++; + } + db_finalize(&q); + + /* Early-out if forumpost is not a table in this repository */ + if( !db_table_exists("repository","forumpost") ){ + return anchor.pNext; + } + + /* For digests, the previous loop also handled forumposts already */ + if( doDigest ){ + return anchor.pNext; + } + + /* If we reach this point, it means that forumposts exist and this + ** is a normal email alert. Construct full-text forum post alerts + ** using a format that enables them to be sent as separate emails. + */ + db_prepare(&q, + "SELECT" + " forumpost.fpid," /* 0 */ + " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */ + " datetime(event.mtime)," /* 2 */ + " substr(comment,instr(comment,':')+2)," /* 3 */ + " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */ + " wantalert.needMod" /* 5 */ + " FROM temp.wantalert, event, forumpost" + " WHERE event.objid=substr(wantalert.eventId,2)+0" + " AND eventId GLOB 'f*'" + " AND forumpost.fpid=event.objid" + ); + zFrom = db_get("email-self",0); + zSub = db_get("email-subname",""); + while( db_step(&q)==SQLITE_ROW ){ + Manifest *pPost = manifest_get(db_column_int(&q,0), CFTYPE_FORUM, 0); + const char *zIrt; + const char *zUuid; + const char *zTitle; + if( pPost==0 ) continue; + p = fossil_malloc( sizeof(EmailEvent) ); + pLast->pNext = p; + pLast = p; + p->type = 'f'; + p->needMod = db_column_int(&q, 5); + p->pNext = 0; + blob_init(&p->hdr, 0, 0); + zUuid = db_column_text(&q, 1); + zTitle = db_column_text(&q, 3); + if( p->needMod ){ + blob_appendf(&p->hdr, "Subject: %s Pending Moderation: %s\r\n", + zSub, zTitle); + }else{ + blob_appendf(&p->hdr, "Subject: %s %s\r\n", zSub, zTitle); + blob_appendf(&p->hdr, "Message-Id: <%s.%s>\r\n", zUuid, zFrom); + zIrt = db_column_text(&q, 4); + if( zIrt && zIrt[0] ){ + blob_appendf(&p->hdr, "In-Reply-To: <%s.%s>\r\n", zIrt, zFrom); + } + } + blob_init(&p->txt, 0, 0); + if( p->needMod ){ + blob_appendf(&p->txt, + "** Pending moderator approval (%s/modreq) **\n", + zUrl + ); + } + blob_appendf(&p->txt, + "Forum post by %s on %s\n", + pPost->zUser, db_column_text(&q, 2)); + blob_appendf(&p->txt, "%s/forumpost/%S\n\n", zUrl, zUuid); + blob_append(&p->txt, pPost->zWiki, -1); + manifest_destroy(pPost); (*pnEvent)++; } db_finalize(&q); + return anchor.pNext; } /* ** Put a header on an alert email @@ -1900,34 +1995,50 @@ ** command line, generate text for all events named in the ** pending_alert table. ** ** This command is intended for testing and debugging the logic ** that generates email alert text. +** +** Options: +** +** --digest Generate digest alert text +** --needmod Assume all events are pending moderator approval */ void test_alert_cmd(void){ Blob out; int nEvent; + int needMod; + int doDigest; EmailEvent *pEvent, *p; + doDigest = find_option("digest",0,0)!=0; + needMod = find_option("needmod",0,0)!=0; db_find_and_open_repository(0, 0); verify_all_options(); db_begin_transaction(); email_schema(0); - db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); + db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)"); if( g.argc==2 ){ - db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); + db_multi_exec( + "INSERT INTO wantalert(eventId,needMod)" + " SELECT eventid, %d FROM pending_alert", needMod); }else{ int i; for(i=2; ipNext){ blob_append(&out, "\n", 1); + if( blob_size(&p->hdr) ){ + blob_append(&out, blob_buffer(&p->hdr), blob_size(&p->hdr)); + 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)); @@ -1936,38 +2047,54 @@ } /* ** COMMAND: test-add-alerts ** -** Usage: %fossil test-add-alerts [--backoffice] EVENTID ... +** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ... ** ** Add one or more events to the pending_alert queue. Use this ** command during testing to force email notifications for specific ** events. ** -** EVENTIDs are text. The first character is 'c', 'w', or 't' -** for check-in, wiki, or ticket. The remaining text is a +** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' +** for check-in, forum, ticket, or wiki. The remaining text is a ** integer that references the EVENT.OBJID value for the event. ** Run /timeline?showid to see these OBJID values. ** -** If the --backoffice option is included, then email_backoffice() is run -** after all alerts have been added. This will cause the alerts to -** be sent out with the SENDALERT_TRACE option. +** Options: +** +** --backoffice Run email_backoffice() after all alerts have +** been added. This will cause the alerts to be +** sent out with the SENDALERT_TRACE option. +** +** --debug Like --backoffice, but add the SENDALERT_STDOUT +** so that emails are printed to standard output +** rather than being sent. +** +** --digest Process emails using SENDALERT_DIGEST */ void test_add_alert_cmd(void){ int i; int doAuto = find_option("backoffice",0,0)!=0; + unsigned mFlags = 0; + if( find_option("debug",0,0)!=0 ){ + doAuto = 1; + mFlags = SENDALERT_STDOUT; + } + if( find_option("digest",0,0)!=0 ){ + mFlags |= SENDALERT_DIGEST; + } db_find_and_open_repository(0, 0); verify_all_options(); db_begin_write(); email_schema(0); for(i=2; ipNext){ if( strchr(zSub,p->type)==0 ) continue; - if( nHit==0 ){ - blob_appendf(&hdr,"To: <%s>\r\n", zEmail); - blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); - blob_appendf(&body, - "This is an automated email sent by the Fossil repository " - "at %s to report changes.\n", - zUrl - ); - } - nHit++; - blob_append(&body, "\n", 1); - blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); + if( p->needMod ){ + /* For events that require moderator approval, only send an alert + ** if the recipient is a moderator for that type of event */ + char xType = '*'; + switch( p->type ){ + case 'f': xType = '5'; break; + case 't': xType = 'q'; break; + case 'w': xType = 'l'; break; + } + if( strchr(zCap,xType)==0 ) continue; + }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ + /* Setup and admin users can get any notification that does not + ** require moderation */ + }else{ + /* Other users only see the alert if they have sufficient + ** privilege to view the event itself */ + char xType = '*'; + switch( p->type ){ + case 'c': xType = 'o'; break; + case 'f': xType = '2'; break; + case 't': xType = 'r'; break; + case 'w': xType = 'j'; break; + } + if( strchr(zCap,xType)==0 ) continue; + } + if( blob_size(&p->hdr)>0 ){ + /* This alert should be sent as a separate email */ + Blob fhdr, fbody; + blob_init(&fhdr, 0, 0); + blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); + blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); + blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); + blob_appendf(&fbody, "\n-- \nSubscription info: %s/alerts/%s\n", + zUrl, zCode); + email_send(pSender,&fhdr,&fbody); + blob_reset(&fhdr); + blob_reset(&fbody); + }else{ + /* Events other than forum posts are gathered together into + ** a single email message */ + if( nHit==0 ){ + blob_appendf(&hdr,"To: <%s>\r\n", zEmail); + blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); + blob_appendf(&body, + "This is an automated email sent by the Fossil repository " + "at %s to report 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-- \nSubscription info: %s/alerts/%s\n", zUrl, zCode); email_send(pSender,&hdr,&body); @@ -2070,15 +2276,24 @@ blob_reset(&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"); + db_multi_exec( + "UPDATE pending_alert SET sentDigest=true" + " WHERE eventid IN (SELECT eventid FROM wantalert);" + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" + ); }else{ - db_multi_exec("UPDATE pending_alert SET sentSep=true"); + db_multi_exec( + "UPDATE pending_alert SET sentSep=true" + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" + "UPDATE pending_alert SET sentMod=true" + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" + ); } - db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); } send_alerts_done: email_sender_free(pSender); if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); db_end_transaction(0); Index: src/forum.c ================================================================== --- src/forum.c +++ src/forum.c @@ -19,337 +19,932 @@ */ #include "config.h" #include #include "forum.h" -/* -** The schema for the tables that manage the forum, if forum is -** enabled. -*/ -static const char zForumInit[] = -@ CREATE TABLE repository.forumpost( -@ mpostid INTEGER PRIMARY KEY, -- unique id for each post (local) -@ mposthash TEXT, -- uuid for this post -@ mthreadid INTEGER, -- thread to which this post belongs -@ uname TEXT, -- name of user -@ mtime REAL, -- julian day number -@ mstatus TEXT, -- status. NULL=ok. 'mod'=pending moderation -@ mimetype TEXT, -- Mimetype for mbody -@ ipaddr TEXT, -- IP address of post origin -@ inreplyto INT, -- Parent posting -@ mbody TEXT -- Content of the post -@ ); -@ CREATE INDEX repository.forumpost_x1 ON -@ forumpost(inreplyto,mtime); -@ CREATE TABLE repository.forumthread( -@ mthreadid INTEGER PRIMARY KEY, -@ mthreadhash TEXT, -- uuid for this thread -@ mtitle TEXT, -- Title or subject line -@ mtime REAL, -- Most recent update -@ npost INT -- Number of posts on this thread -@ ); -; - -/* -** Create the forum tables in the schema if they do not already -** exist. -*/ -static void forum_verify_schema(void){ - if( !db_table_exists("repository","forumpost") ){ - db_multi_exec(zForumInit /*works-like:""*/); - } -} - -/* -** WEBPAGE: forum -** URL: /forum -** Query parameters: -** -** item=N Show post N and its replies -** -*/ -void forum_page(void){ - int itemId; - Stmt q; - int i; - - login_check_credentials(); - if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } - forum_verify_schema(); - style_header("Forum"); - itemId = atoi(PD("item","0")); - if( itemId>0 ){ - int iUp; - double rNow; - style_submenu_element("Topics", "%R/forum"); - iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId); - if( iUp ){ - style_submenu_element("Parent", "%R/forum?item=%d", iUp); - } - rNow = db_double(0.0, "SELECT julianday('now')"); - /* Show the post given by itemId and all its descendents */ - db_prepare(&q, - "WITH RECURSIVE" - " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS (" - " SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody," - " 0, mtime FROM forumpost WHERE mpostid=%d" - " UNION" - " SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr," - " f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime" - " FROM forumpost AS f, post AS p" - " WHERE f.inreplyto=p.id" - " ORDER BY xdepth DESC, xtime ASC" - ") SELECT * FROM post;", - itemId - ); - while( db_step(&q)==SQLITE_ROW ){ - int id = db_column_int(&q, 0); - const char *zUser = db_column_text(&q, 1); - const char *zMime = db_column_text(&q, 3); - int iDepth = db_column_int(&q, 7); - double rMTime = db_column_double(&q, 8); - char *zAge = db_timespan_name(rNow - rMTime); - Blob body; - @ - @ - @ - @ - @ - @
    - @ %h(zUser) - @ %s(zAge) ago - sqlite3_free(zAge); - if( g.perm.WrForum ){ - @ - if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){ - @ Edit - } - @ Reply - @ - } - @
    - blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6)); - wiki_render_by_mimetype(&body, zMime); - blob_reset(&body); - @
    - } - }else{ - /* If we reach this point, that means the users wants a list of - ** recent threads. - */ - i = 0; - db_prepare(&q, - "SELECT a.mtitle, a.npost, b.mpostid" - " FROM forumthread AS a, forumpost AS b " - " WHERE a.mthreadid=b.mthreadid" - " AND b.inreplyto IS NULL" - " ORDER BY a.mtime DESC LIMIT 40" - ); - if( g.perm.WrForum ){ - style_submenu_element("New", "%R/forumedit"); - } - @

    Recent Forum Threads

    - while( db_step(&q)==SQLITE_ROW ){ - int n = db_column_int(&q,1); - int itemid = db_column_int(&q,2); - const char *zTitle = db_column_text(&q,0); - if( (i++)==0 ){ - @
      - } - @
    1. - @ %z(href("%R/forum?item=%d",itemid))%h(zTitle) - @ %d(n) post%s(n==1?"":"s")
    2. - } - if( i ){ - @
    - } - } +#if INTERFACE +/* +** Each instance of the following object represents a single message - +** either the initial post, an edit to a post, a reply, or an edit to +** a reply. +*/ +struct ForumEntry { + int fpid; /* rid for this entry */ + int fprev; /* zero if initial entry. non-zero if an edit */ + int firt; /* This entry replies to firt */ + int mfirt; /* Root in-reply-to */ + char *zUuid; /* Artifact hash */ + ForumEntry *pLeaf; /* Most recent edit for this entry */ + ForumEntry *pEdit; /* This entry is an edit of pEditee */ + ForumEntry *pNext; /* Next in chronological order */ + ForumEntry *pPrev; /* Previous in chronological order */ + ForumEntry *pDisplay; /* Next in display order */ + int nIndent; /* Number of levels of indentation for this entry */ +}; + +/* +** A single instance of the following tracks all entries for a thread. +*/ +struct ForumThread { + ForumEntry *pFirst; /* First entry in chronological order */ + ForumEntry *pLast; /* Last entry in chronological order */ + ForumEntry *pDisplay; /* Entries in display order */ + ForumEntry *pTail; /* Last on the display list */ +}; +#endif /* INTERFACE */ + +/* +** Delete a complete ForumThread and all its entries. +*/ +static void forumthread_delete(ForumThread *pThread){ + ForumEntry *pEntry, *pNext; + for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){ + pNext = pEntry->pNext; + fossil_free(pEntry->zUuid); + fossil_free(pEntry); + } + fossil_free(pThread); +} + +#if 0 /* not used */ +/* +** Search a ForumEntry list forwards looking for the entry with fpid +*/ +static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){ + while( p && p->fpid!=fpid ) p = p->pNext; + return p; +} +#endif + +/* +** Search backwards for a ForumEntry +*/ +static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){ + while( p && p->fpid!=fpid ) p = p->pPrev; + return p; +} + +/* +** Add an entry to the display list +*/ +static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){ + if( pThread->pDisplay==0 ){ + pThread->pDisplay = p; + }else{ + pThread->pTail->pDisplay = p; + } + pThread->pTail = p; +} + +/* +** Extend the display list for pThread by adding all entries that +** reference fpid. The first such entry will be no earlier then +** entry "p". +*/ +static void forumthread_display_order( + ForumThread *pThread, + ForumEntry *p, + int fpid, + int nIndent +){ + while( p ){ + if( p->fprev==0 && p->mfirt==fpid ){ + p->nIndent = nIndent; + forumentry_add_to_display(pThread, p); + forumthread_display_order(pThread, p->pNext, p->fpid, nIndent+1); + } + p = p->pNext; + } +} + +/* +** Construct a ForumThread object given the root record id. +*/ +static ForumThread *forumthread_create(int froot, int computeHierarchy){ + ForumThread *pThread; + ForumEntry *pEntry; + Stmt q; + pThread = fossil_malloc( sizeof(*pThread) ); + memset(pThread, 0, sizeof(*pThread)); + db_prepare(&q, + "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)" + " FROM forumpost" + " WHERE froot=%d ORDER BY fmtime", + froot + ); + while( db_step(&q)==SQLITE_ROW ){ + pEntry = fossil_malloc( sizeof(*pEntry) ); + memset(pEntry, 0, sizeof(*pEntry)); + pEntry->fpid = db_column_int(&q, 0); + pEntry->firt = db_column_int(&q, 1); + pEntry->fprev = db_column_int(&q, 2); + pEntry->zUuid = fossil_strdup(db_column_text(&q,3)); + pEntry->mfirt = pEntry->firt; + pEntry->pPrev = pThread->pLast; + pEntry->pNext = 0; + if( pThread->pLast==0 ){ + pThread->pFirst = pEntry; + }else{ + pThread->pLast->pNext = pEntry; + } + pThread->pLast = pEntry; + } + db_finalize(&q); + + /* Establish which entries are the latest edit. After this loop + ** completes, entries that have non-NULL pLeaf should not be + ** displayed. + */ + for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){ + if( pEntry->fprev ){ + ForumEntry *pBase, *p; + p = forumentry_backward(pEntry->pPrev, pEntry->fprev); + pEntry->pEdit = p; + while( p ){ + pBase = p; + p->pLeaf = pEntry; + p = pBase->pEdit; + } + for(p=pEntry->pNext; p; p=p->pNext){ + if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid; + } + } + } + + if( computeHierarchy ){ + /* Compute the hierarchical display order */ + pEntry = pThread->pFirst; + pEntry->nIndent = 1; + forumentry_add_to_display(pThread, pEntry); + forumthread_display_order(pThread, pEntry, pEntry->fpid, 2); + } + + /* Return the result */ + return pThread; +} + +/* +** COMMAND: test-forumthread +** +** Usage: %fossil test-forumthread THREADID +** +** Display a summary of all messages on a thread. +*/ +void forumthread_cmd(void){ + int fpid; + int froot; + const char *zName; + ForumThread *pThread; + ForumEntry *p; + + db_find_and_open_repository(0,0); + verify_all_options(); + if( g.argc!=3 ) usage("THREADID"); + zName = g.argv[2]; + fpid = symbolic_name_to_rid(zName, "f"); + if( fpid<=0 ){ + fossil_fatal("Unknown or ambiguous forum id: \"%s\"", zName); + } + froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); + if( froot==0 ){ + fossil_fatal("Not a forum post: \"%s\"", zName); + } + fossil_print("fpid = %d\n", fpid); + fossil_print("froot = %d\n", froot); + pThread = forumthread_create(froot, 1); + fossil_print("Chronological:\n"); + /* 123456789 123456789 123456789 123456789 123456789 */ + fossil_print(" fpid firt fprev mfirt pLeaf\n"); + for(p=pThread->pFirst; p; p=p->pNext){ + fossil_print("%9d %9d %9d %9d %9d\n", + p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0); + } + fossil_print("\nDisplay\n"); + for(p=pThread->pDisplay; p; p=p->pDisplay){ + fossil_print("%*s", (p->nIndent-1)*3, ""); + if( p->pLeaf ){ + fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid); + }else{ + fossil_print("%d\n", p->fpid); + } + } + forumthread_delete(pThread); +} + +/* +** Render a forum post for display +*/ +void forum_render( + const char *zTitle, /* The title. Might be NULL for no title */ + const char *zMimetype, /* Mimetype of the message */ + const char *zContent, /* Content of the message */ + const char *zClass /* Put in a
    if not NULL */ +){ + if( zClass ){ + @
    + } + if( zTitle ){ + if( zTitle[0] ){ + @

    %h(zTitle)

    + }else{ + @

    Deleted

    + } + } + if( zContent && zContent[0] ){ + Blob x; + blob_init(&x, 0, 0); + blob_append(&x, zContent, -1); + wiki_render_by_mimetype(&x, zMimetype); + blob_reset(&x); + }else{ + @ Deleted + } + if( zClass ){ + @
    + } +} + +/* +** Display all posts in a forum thread in chronological order +*/ +static void forum_display_chronological(int froot, int target){ + ForumThread *pThread = forumthread_create(froot, 0); + ForumEntry *p; + for(p=pThread->pFirst; p; p=p->pNext){ + char *zDate; + Manifest *pPost; + + pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); + if( pPost==0 ) continue; + if( p->fpid==target ){ + @
    + }else if( p->pLeaf!=0 ){ + @
    + }else{ + @
    + } + if( pPost->zThreadTitle ){ + @

    %h(pPost->zThreadTitle)

    + } + zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); + @

    By %h(pPost->zUser) on %h(zDate) (%d(p->fpid)) + fossil_free(zDate); + if( p->pEdit ){ + @ edit of %z(href("%R/forumpost/%S?t",p->pEdit->zUuid))%d(p->fprev) + } + if( p->firt ){ + ForumEntry *pIrt = p->pPrev; + while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev; + if( pIrt ){ + @ reply to %z(href("%R/forumpost/%S?t",pIrt->zUuid))%d(p->firt) + } + } + if( p->pLeaf ){ + @ updated by %z(href("%R/forumpost/%S?t",p->pLeaf->zUuid))\ + @ %d(p->pLeaf->fpid) + } + if( g.perm.Debug ){ + @ \ + @ artifact + } + if( p->fpid!=target ){ + @ %z(href("%R/forumpost/%S?t",p->zUuid))[link] + } + forum_render(0, pPost->zMimetype, pPost->zWiki, 0); + if( g.perm.WrForum && p->pLeaf==0 ){ + int sameUser = login_is_individual() + && fossil_strcmp(pPost->zUser, g.zLogin)==0; + int isPrivate = content_is_private(p->fpid); + @

    + @ + if( !isPrivate ){ + /* Reply and Edit are only available if the post has already + ** been approved */ + @ + if( g.perm.Admin || sameUser ){ + @ + @ + } + }else if( g.perm.ModForum ){ + /* Provide moderators with moderation buttons for posts that + ** are pending moderation */ + @ + @ + }else if( sameUser ){ + /* A post that is pending moderation can be deleted by the + ** person who originally submitted the post */ + @ + } + @

    + } + manifest_destroy(pPost); + @
    + } + forumthread_delete(pThread); +} + +/* +** Display all messages in a forumthread with indentation. +*/ +static int forum_display_hierarchical(int froot, int target){ + ForumThread *pThread; + ForumEntry *p; + Manifest *pPost, *pOPost; + int fpid; + const char *zUuid; + char *zDate; + const char *zSel; + + pThread = forumthread_create(froot, 1); + for(p=pThread->pFirst; p; p=p->pNext){ + if( p->fpid==target ){ + while( p->pEdit ) p = p->pEdit; + target = p->fpid; + break; + } + } + for(p=pThread->pDisplay; p; p=p->pDisplay){ + pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); + if( p->pLeaf ){ + fpid = p->pLeaf->fpid; + zUuid = p->pLeaf->zUuid; + pPost = manifest_get(fpid, CFTYPE_FORUM, 0); + }else{ + fpid = p->fpid; + zUuid = p->zUuid; + pPost = pOPost; + } + zSel = p->fpid==target ? " forumSel" : ""; + if( p->nIndent==1 ){ + @
    + }else{ + @
    + } + pPost = manifest_get(fpid, CFTYPE_FORUM, 0); + if( pPost==0 ) continue; + if( pPost->zThreadTitle ){ + @

    %h(pPost->zThreadTitle)

    + } + zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate); + @

    By %h(pOPost->zUser) on %h(zDate) + fossil_free(zDate); + if( g.perm.Debug ){ + @ \ + @ (%d(p->fpid)) + } + if( p->pLeaf ){ + zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); + if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){ + @ and edited on %h(zDate) + }else{ + @ as edited by %h(pPost->zUser) on %h(zDate) + } + fossil_free(zDate); + if( g.perm.Debug ){ + @ \ + @ (%d(fpid)) + } + manifest_destroy(pOPost); + } + if( fpid!=target ){ + @ %z(href("%R/forumpost/%S",zUuid))[link] + } + forum_render(0, pPost->zMimetype, pPost->zWiki, 0); + if( g.perm.WrForum ){ + int sameUser = login_is_individual() + && fossil_strcmp(pPost->zUser, g.zLogin)==0; + int isPrivate = content_is_private(fpid); + @

    + @ + if( !isPrivate ){ + /* Reply and Edit are only available if the post has already + ** been approved */ + @ + if( g.perm.Admin || sameUser ){ + @ + @ + } + }else if( g.perm.ModForum ){ + /* Provide moderators with moderation buttons for posts that + ** are pending moderation */ + @ + @ + }else if( sameUser ){ + /* A post that is pending moderation can be deleted by the + ** person who originally submitted the post */ + @ + } + @

    + } + manifest_destroy(pPost); + @
    + } + forumthread_delete(pThread); + return target; +} + +/* +** WEBPAGE: forumpost +** +** Show a single forum posting. The posting is shown in context with +** it's entire thread. The selected posting is enclosed within +**
    ...
    . Javascript is used to move the +** selected posting into view after the page loads. +** +** Query parameters: +** +** name=X REQUIRED. The hash of the post to display +** t Show a chronologic listing instead of hierarchical +*/ +void forumpost_page(void){ + forumthread_page(); +} + +/* +** WEBPAGE: forumthread +** +** Show all forum messages associated with a particular message thread. +** The result is basically the same as /forumpost except that none of +** the postings in the thread are selected. +** +** Query parameters: +** +** name=X REQUIRED. The hash of any post of the thread. +** t Show a chronologic listing instead of hierarchical +*/ +void forumthread_page(void){ + int fpid; + int froot; + const char *zName = P("name"); + login_check_credentials(); + if( !g.perm.RdForum ){ + login_needed(g.anon.RdForum); + return; + } + if( zName==0 ){ + webpage_error("Missing \"name=\" query parameter"); + } + fpid = symbolic_name_to_rid(zName, "f"); + if( fpid<=0 ){ + webpage_error("Unknown or ambiguous forum id: \"%s\"", zName); + } + style_header("Forum"); + froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); + if( froot==0 ){ + webpage_error("Not a forum post: \"%s\"", zName); + } + if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0; + if( P("t") ){ + if( g.perm.Debug ){ + style_submenu_element("Hierarchical", "%R/%s/%s", g.zPath, zName); + } + forum_display_chronological(froot, fpid); + }else{ + if( g.perm.Debug ){ + style_submenu_element("Chronological", "%R/%s/%s?t", g.zPath, zName); + } + forum_display_hierarchical(froot, fpid); + } + style_load_js("forum.js"); + style_footer(); +} + +/* +** Return true if a forum post should be moderated. +*/ +static int forum_need_moderation(void){ + if( P("domod") ) return 1; + if( g.perm.WrTForum ) return 0; + if( g.perm.ModForum ) return 0; + return 1; +} + +/* +** Add a new Forum Post artifact to the repository. +** +** Return true if a redirect occurs. +*/ +static int forum_post( + const char *zTitle, /* Title. NULL for replies */ + int iInReplyTo, /* Post replying to. 0 for new threads */ + int iEdit, /* Post being edited, or zero for a new post */ + const char *zUser, /* Username. NULL means use login name */ + const char *zMimetype, /* Mimetype of content. */ + const char *zContent /* Content */ +){ + char *zDate; + char *zI; + char *zG; + int iBasis; + Blob x, cksum, formatCheck, errMsg; + Manifest *pPost; + + schema_forum(); + if( iInReplyTo==0 && iEdit>0 ){ + iBasis = iEdit; + iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); + }else{ + iBasis = iInReplyTo; + } + webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); + blob_init(&x, 0, 0); + zDate = date_in_standard_format("now"); + blob_appendf(&x, "D %s\n", zDate); + fossil_free(zDate); + zG = db_text(0, + "SELECT uuid FROM blob, forumpost" + " WHERE blob.rid==forumpost.froot" + " AND forumpost.fpid=%d", iBasis); + if( zG ){ + blob_appendf(&x, "G %s\n", zG); + fossil_free(zG); + } + if( zTitle ){ + blob_appendf(&x, "H %F\n", zTitle); + } + zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo); + if( zI ){ + blob_appendf(&x, "I %s\n", zI); + fossil_free(zI); + } + if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ + blob_appendf(&x, "N %s\n", zMimetype); + } + if( iEdit>0 ){ + char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); + if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); + blob_appendf(&x, "P %s\n", zP); + fossil_free(zP); + } + if( zUser==0 ){ + if( login_is_nobody() ){ + zUser = "anonymous"; + }else{ + zUser = login_name(); + } + } + blob_appendf(&x, "U %F\n", zUser); + blob_appendf(&x, "W %d\n%s\n", strlen(zContent), zContent); + md5sum_blob(&x, &cksum); + blob_appendf(&x, "Z %b\n", &cksum); + blob_reset(&cksum); + + /* Verify that the artifact we are creating is well-formed */ + blob_init(&formatCheck, 0, 0); + blob_init(&errMsg, 0, 0); + blob_copy(&formatCheck, &x); + pPost = manifest_parse(&formatCheck, 0, &errMsg); + if( pPost==0 ){ + webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); + } + webpage_assert( pPost->type==CFTYPE_FORUM ); + manifest_destroy(pPost); + + if( P("dryrun") ){ + @
    + @ This is the artifact that would have been generated: + @
    %h(blob_str(&x))
    + @
    + blob_reset(&x); + return 0; + }else{ + int nrid = wiki_put(&x, 0, forum_need_moderation()); + cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid)); + return 1; + } +} + +/* +** Paint the form elements for entering a Forum post +*/ +static void forum_entry_widget( + const char *zTitle, + const char *zMimetype, + const char *zContent +){ + if( zTitle ){ + @ Title:
    + } + @ Markup style: + mimetype_option_menu(zMimetype); + @

    +} + +/* +** WEBPAGE: forumnew +** WEBPAGE: forumedit +** +** Start a new thread on the forum or reply to an existing thread. +** But first prompt to see if the user would like to log in. +*/ +void forum_page_init(void){ + int isEdit; + char *zGoto; + login_check_credentials(); + if( !g.perm.WrForum ){ + login_needed(g.anon.WrForum); + return; + } + if( sqlite3_strglob("*edit*", g.zPath)==0 ){ + zGoto = mprintf("%R/forume2?fpid=%S",PD("fpid","")); + isEdit = 1; + }else{ + zGoto = mprintf("%R/forume1"); + isEdit = 0; + } + if( login_is_individual() ){ + if( isEdit ){ + forumedit_page(); + }else{ + forumnew_page(); + } + return; + } + style_header("%h As Anonymous?", isEdit ? "Reply" : "Post"); + @

    You are not logged in. + @

    + @
    + @
    + @ + @
    + @
    Post to the forum anonymously + if( login_self_register_available(0) ){ + @
    + @
    + @ + @ + @
    + @
    Create a new account and post using that new account + } + @
    + @
    + @ + @ + @ + @
    + @
    Log into an existing account + @
    + style_footer(); + fossil_free(zGoto); +} + +/* +** Write the "From: USER" line on the webpage. +*/ +static void forum_from_line(void){ + if( login_is_nobody() ){ + @ From: anonymous
    + }else{ + @ From: %h(login_name())
    + } +} + +/* +** WEBPAGE: forume1 +** +** Start a new forum thread. +*/ +void forumnew_page(void){ + const char *zTitle = PDT("title",""); + const char *zMimetype = PD("mimetype","text/x-fossil-wiki"); + const char *zContent = PDT("content",""); + login_check_credentials(); + if( !g.perm.WrForum ){ + login_needed(g.anon.WrForum); + return; + } + if( P("submit") ){ + if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent) ) return; + } + if( P("preview") ){ + @

    Preview:

    + forum_render(zTitle, zMimetype, zContent, "forumEdit"); + } + style_header("New Forum Thread"); + @
    + @

    New Message:

    + forum_from_line(); + forum_entry_widget(zTitle, zMimetype, zContent); + @ + if( P("preview") ){ + @ + }else{ + @ + } + if( g.perm.Debug ){ + /* For the test-forumnew page add these extra debugging controls */ + @
    + @ + @
    + @
    + @
    + } + @
    + style_footer(); +} + +/* +** WEBPAGE: forume2 +** +** Edit an existing forum message. +** Query parameters: +** +** fpid=X Hash of the post to be editted. REQUIRED +*/ +void forumedit_page(void){ + int fpid; + Manifest *pPost; + const char *zMimetype = 0; + const char *zContent = 0; + const char *zTitle = 0; + int isCsrfSafe; + int isDelete = 0; + + login_check_credentials(); + if( !g.perm.WrForum ){ + login_needed(g.anon.WrForum); + return; + } + fpid = symbolic_name_to_rid(PD("fpid",""), "f"); + if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){ + webpage_error("Missing or invalid fpid query parameter"); + } + if( P("cancel") ){ + cgi_redirectf("%R/forumpost/%S",P("fpid")); + return; + } + isCsrfSafe = cgi_csrf_safe(1); + if( g.perm.ModForum && isCsrfSafe ){ + if( P("approve") ){ + moderation_approve(fpid); + cgi_redirectf("%R/forumpost/%S",P("fpid")); + return; + } + if( P("reject") ){ + char *zParent = + db_text(0, + "SELECT uuid FROM forumpost, blob" + " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt", + fpid + ); + moderation_disapprove(fpid); + if( zParent ){ + cgi_redirectf("%R/forumpost/%S",zParent); + }else{ + cgi_redirectf("%R/forum"); + } + return; + } + } + isDelete = P("nullout")!=0; + if( P("submit") && isCsrfSafe ){ + int done = 1; + const char *zMimetype = PD("mimetype","text/x-fossil-wiki"); + const char *zContent = PDT("content",""); + if( P("reply") ){ + done = forum_post(0, fpid, 0, 0, zMimetype, zContent); + }else if( P("edit") || isDelete ){ + done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent); + }else{ + webpage_error("Missing 'reply' query parameter"); + } + if( done ) return; + } + if( isDelete ){ + zMimetype = "text/x-fossil-wiki"; + zContent = ""; + if( pPost->zThreadTitle ) zTitle = ""; + style_header("Delete %s", zTitle ? "Post" : "Reply"); + @

    Original Post:

    + forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, + "forumEdit"); + @

    Change Into:

    + forum_render(zTitle, zMimetype, zContent,"forumEdit"); + @
    + @ + @ + @ + @ + if( zTitle ){ + @ + } + }else if( P("edit") ){ + /* Provide an edit to the fpid post */ + zMimetype = P("mimetype"); + zContent = PT("content"); + zTitle = P("title"); + if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki); + if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype); + if( zTitle==0 && pPost->zThreadTitle!=0 ){ + zTitle = fossil_strdup(pPost->zThreadTitle); + } + style_header("Edit %s", zTitle ? "Post" : "Reply"); + @

    Original Post:

    + forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, + "forumEdit"); + if( P("preview") ){ + @

    Preview Of Editted Post:

    + forum_render(zTitle, zMimetype, zContent,"forumEdit"); + } + @

    Revised Message:

    + @ + @ + @ + forum_from_line(); + forum_entry_widget(zTitle, zMimetype, zContent); + }else{ + /* Reply */ + zMimetype = PD("mimetype","text/x-fossil-wiki"); + zContent = PDT("content",""); + style_header("Reply"); + @

    Replying To:

    + forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit"); + if( P("preview") ){ + @

    Preview:

    + forum_render(0, zMimetype,zContent, "forumEdit"); + } + @

    Enter Reply:

    + @ + @ + @ + forum_from_line(); + forum_entry_widget(0, zMimetype, zContent); + } + if( !isDelete ){ + @ + } + @ + if( P("preview") || isDelete ){ + @ + } + if( g.perm.Debug ){ + /* For the test-forumnew page add these extra debugging controls */ + @
    + @ + @
    + @
    + @
    + } + @
    style_footer(); } /* -** Use content in CGI parameters "s" (subject), "b" (body), and -** "mimetype" (mimetype) to create a new forum entry. -** Return the id of the new forum entry. -** -** If any problems occur, return 0 and set *pzErr to a description of -** the problem. -** -** Cases: -** -** itemId==0 && parentId==0 Starting a new thread. -** itemId==0 && parentId>0 New reply to parentId -** itemId>0 && parentId==0 Edit existing post itemId -*/ -static int forum_post(int itemId, int parentId, char **pzErr){ - const char *zSubject = 0; - int threadId; - double rNow = db_double(0.0, "SELECT julianday('now')"); - const char *zMime = wiki_filter_mimetypes(P("mimetype")); - if( itemId==0 && parentId==0 ){ - /* Start a new thread. Subject required. */ - sqlite3_uint64 r1, r2; - zSubject = PT("s"); - if( zSubject==0 || zSubject[0]==0 ){ - *pzErr = "\"Subject\" required to start a new thread"; - return 0; - } - sqlite3_randomness(sizeof(r1), &r1); - sqlite3_randomness(sizeof(r2), &r2); - db_multi_exec( - "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)" - "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)", - zSubject, rNow - ); - threadId = db_last_insert_rowid(); - }else{ - threadId = db_int(0, "SELECT mthreadid FROM forumpost" - " WHERE mpostid=%d", itemId ? itemId : parentId); - } - if( itemId ){ - if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost" - " WHERE mpostid=%d", itemId) ){ - db_multi_exec( - "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d", - PT("s"), threadId - ); - } - db_multi_exec( - "UPDATE forumpost SET" - " mtime=%!.17g," - " mimetype=%Q," - " ipaddr=%Q," - " mbody=%Q" - " WHERE mpostid=%d", - rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId - ); - }else{ - db_multi_exec( - "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime," - " mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES" - " (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)", - threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b")); - itemId = db_last_insert_rowid(); - } - if( zSubject==0 ){ - db_multi_exec( - "UPDATE forumthread SET mtime=%!.17g, npost=npost+1" - " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)", - rNow, itemId - ); - } - return itemId; -} - -/* -** WEBPAGE: forumedit -** -** Query parameters: -** -** replyto=N Enter a reply to forum item N -** item=N Edit item N -** s=SUBJECT Subject. New thread only. Omitted for replies -** b=BODY Body of the post -** m=MIMETYPE Mimetype for the body of the post -** x Submit changes -** p Preview changes -*/ -void forum_edit_page(void){ - int itemId; - int parentId; - char *zErr = 0; - const char *zMime; - const char *zSub; - - login_check_credentials(); - if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } - forum_verify_schema(); - itemId = atoi(PD("item","0")); - parentId = atoi(PD("replyto","0")); - if( P("cancel")!=0 ){ - cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId); - return; - } - if( P("x")!=0 && cgi_csrf_safe(1) ){ - itemId = forum_post(itemId,parentId,&zErr); - if( itemId ){ - cgi_redirectf("%R/forum?item=%d",itemId); - return; - } - } - if( itemId && (P("mimetype")==0 || P("b")==0) ){ - Stmt q; - db_prepare(&q, "SELECT mimetype, mbody FROM forumpost" - " WHERE mpostid=%d", itemId); - if( db_step(&q)==SQLITE_ROW ){ - if( P("mimetype")==0 ){ - cgi_set_query_parameter("mimetype", db_column_text(&q, 0)); - } - if( P("b")==0 ){ - cgi_set_query_parameter("b", db_column_text(&q, 1)); - } - } - db_finalize(&q); - } - zMime = wiki_filter_mimetypes(P("mimetype")); - if( itemId>0 ){ - style_header("Edit Forum Post"); - }else if( parentId>0 ){ - style_header("Comment On Forum Post"); - }else{ - style_header("New Forum Thread"); - } - @
    - if( itemId ){ - @ - } - if( parentId ){ - @ - } - if( P("p") ){ - Blob x; - @
    - if( P("s") ){ - @

    %h(PT("s"))

    - } - @
    - blob_init(&x, PT("b"), -1); - wiki_render_by_mimetype(&x, PT("mimetype")); - blob_reset(&x); - @
    - @
    - @
    - } - @ - if( zErr ){ - @ - @ - @
    - @ %h(zErr) - } - if( (itemId==0 && parentId==0) - || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost" - " WHERE mpostid=%d", itemId)) - ){ - zSub = PT("s"); - if( zSub==0 && itemId ){ - zSub = db_text("", - "SELECT mtitle FROM forumthread" - " WHERE mthreadid=(SELECT mthreadid FROM forumpost" - " WHERE mpostid=%d)", itemId); - } - @
    Subject: - } - @
    Markup: - mimetype_option_menu(zMime); - @
    Comment: - @
    - @ - if( P("p")!=0 ){ - @ - } - @ - @
    - @
    +** WEBPAGE: forum +** +** The main page for the forum feature. Show a list of recent forum +** threads. Also show a search box at the top if search is enabled, +** and a button for creating a new thread, if enabled. +*/ +void forum_main_page(void){ + Stmt q; + int iLimit, iOfst; + login_check_credentials(); + if( !g.perm.RdForum ){ + login_needed(g.anon.RdForum); + return; + } + style_header("Forum"); + if( g.perm.WrForum ){ + style_submenu_element("New Message","%R/forumnew"); + } + if( g.perm.ModForum && moderation_needed() ){ + style_submenu_element("Moderation Requests", "%R/modreq"); + } + if( search_screen(SRCH_FORUM, 0) ){ + style_submenu_element("Recent Threads","%R/forum"); + style_footer(); + return; + } + iLimit = 50; + iOfst = 0; + @

    Recent Threads

    + @
    + db_prepare(&q, + "SELECT julianday('now') - max(fmtime)," + " (SELECT uuid FROM blob WHERE rid=fpid)," + " (SELECT substr(comment,instr(comment,':')+2)" + " FROM event WHERE objid=fpid)" + " FROM forumpost" + " GROUP BY froot ORDER BY 1 LIMIT %d OFFSET %d", + iLimit, iOfst + ); + while( db_step(&q)==SQLITE_ROW ){ + char *zAge = human_readable_age(db_column_double(&q,0)); + const char *zUuid = db_column_text(&q, 1); + const char *zTitle = db_column_text(&q, 2); + @ + @ + fossil_free(zAge); + } + @
    %h(zAge) ago%z(href("%R/forumpost/%S",zUuid))%h(zTitle) + @
    style_footer(); } ADDED src/forum.js Index: src/forum.js ================================================================== --- /dev/null +++ src/forum.js @@ -0,0 +1,19 @@ +(function(){ + function absoluteY(obj){ + var top = 0; + if( obj.offsetParent ){ + do{ + top += obj.offsetTop; + }while( obj = obj.offsetParent ); + } + return top; + } + var x = document.getElementsByClassName('forumSel'); + if(x[0]){ + var w = window.innerHeight; + var h = x[0].scrollHeight; + var y = absoluteY(x[0]); + if( w>h ) y = y + (h-w)/2; + if( y>0 ) window.scrollTo(0, y); + } +}()) Index: src/info.c ================================================================== --- src/info.c +++ src/info.c @@ -935,14 +935,11 @@ @ Artifact ID: @ %z(href("%R/artifact/%!S",zUuid))%s(zUuid) if( g.perm.Setup ){ @ (%d(rid)) } - modPending = moderation_pending(rid); - if( modPending ){ - @ *** Awaiting Moderator Approval *** - } + modPending = moderation_pending_www(rid); @ @ Page Name:%h(pWiki->zWikiTitle) @ Date: hyperlink_to_date(zDate, ""); @ Original User: @@ -978,25 +975,10 @@ @
    Content
    blob_init(&wiki, pWiki->zWiki, -1); wiki_render_by_mimetype(&wiki, pWiki->zMimetype); blob_reset(&wiki); manifest_destroy(pWiki); - style_footer(); -} - -/* -** Show a webpage error message -*/ -void webpage_error(const char *zFormat, ...){ - va_list ap; - const char *z; - va_start(ap, zFormat); - z = vmprintf(zFormat, ap); - va_end(ap); - style_header("URL Error"); - @

    Error

    - @

    %h(z)

    style_footer(); } /* ** Find an check-in based on query parameter zParam and parse its @@ -1246,10 +1228,11 @@ #define OBJTYPE_ATTACHMENT 0x0010 #define OBJTYPE_EVENT 0x0020 #define OBJTYPE_TAG 0x0040 #define OBJTYPE_SYMLINK 0x0080 #define OBJTYPE_EXE 0x0100 +#define OBJTYPE_FORUM 0x0200 /* ** Possible flags for the second parameter to ** object_description() */ @@ -1438,10 +1421,13 @@ objType |= OBJTYPE_EVENT; hyperlink_to_event_tagid(db_column_int(&q, 5)); }else{ @ Attachment to technote } + }else if( zType[0]=='f' ){ + objType |= OBJTYPE_FORUM; + @ Forum post }else{ @ Tag referencing } if( zType[0]!='e' || eventTagId == 0){ hyperlink_to_uuid(zUuid); @@ -2248,14 +2234,11 @@ @ Artifact ID: @ %z(href("%R/artifact/%!S",zUuid))%s(zUuid) if( g.perm.Setup ){ @ (%d(rid)) } - modPending = moderation_pending(rid); - if( modPending ){ - @ *** Awaiting Moderator Approval *** - } + modPending = moderation_pending_www(rid); @ Ticket: @ %z(href("%R/tktview/%s",zTktName))%s(zTktName) if( zTktTitle ){ @
    %h(zTktTitle) } @@ -2364,10 +2347,15 @@ if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){ ci_page(); }else if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){ ainfo_page(); + }else + if( db_table_exists("repository","forumpost") + && db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid) + ){ + forumthread_page(); }else { artifact_page(); } } Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -470,10 +470,28 @@ zPattern = mprintf("%s/login*", g.zBaseURL); rc = sqlite3_strglob(zPattern, zReferer)==0; fossil_free(zPattern); return rc; } + +/* +** Return TRUE if self-registration is available. If the zNeeded +** argument is not NULL, then only return true if self-registration is +** available and any of the capabilities named in zNeeded are available +** to self-registered users. +*/ +int login_self_register_available(const char *zNeeded){ + CapabilityString *pCap; + int rc; + if( !db_get_boolean("self-register",0) ) return 0; + if( zNeeded==0 ) return 1; + pCap = capability_add(0, db_get("default-perms","")); + capability_expand(pCap); + rc = capability_has_any(pCap, zNeeded); + capability_free(pCap); + return rc; +} /* ** There used to be a page named "my" that was designed to show information ** about a specific user. The "my" page was linked from the "Logged in as USER" ** line on the title bar. The "my" page was never completed so it is now @@ -498,10 +516,11 @@ char *zErrMsg = ""; int uid; /* User id logged in user */ char *zSha1Pw; const char *zIpAddr; /* IP address of requestor */ const char *zReferer; + int noAnon = P("noanon")!=0; login_check_credentials(); if( login_wants_https_redirect() ){ const char *zQS = P("QUERY_STRING"); if( P("redir")!=0 ){ @@ -536,10 +555,16 @@ if( P("out") ){ login_clear_login_data(); redirect_to_g(); return; } + + /* Redirect for create-new-account requests */ + if( P("self") ){ + cgi_redirectf("%R/register"); + return; + } /* Deal with password-change requests */ if( g.perm.Password && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ @@ -631,11 +656,11 @@ } } style_header("Login/Logout"); style_adunit_config(ADUNIT_OFF); @ %s(zErrMsg) - if( zGoto ){ + if( zGoto && !noAnon ){ char *zAbbrev = fossil_strdup(zGoto); int i; for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){} zAbbrev[i] = 0; if( g.zLogin ){ @@ -675,11 +700,11 @@ @ }else{ @ } if( P("HTTPS")==0 ){ - @ + @ @

    @ Warning: Your password will be sent in the clear over an @ unencrypted connection. if( g.sslNotAvailable ){ @ No encrypted connection is available on this server. @@ -690,28 +715,33 @@ @

    } @ @ @ Password: - @ + @ @ if( g.zLogin==0 && (anonFlag || zGoto==0) ){ zAnonPw = db_text(0, "SELECT pw FROM user" " WHERE login='anonymous'" " AND cap!=''"); } @ @ - @ + @ + @ ← Pressing this button grants\ + @ permission to store a cookie @ + if( !noAnon && login_self_register_available(0) ){ + @ + @ + @ + @ \ + @ ← Don't have a login? Click this button to create one. + @ + } @ - @

    Pressing the Login button grants permission to store a cookie.

    - if( db_get_boolean("self-register", 0) ){ - @

    If you do not have an account, you can - @ create one. - } - if( zAnonPw ){ + if( zAnonPw && !noAnon ){ unsigned int uSeed = captcha_seed(); const char *zDecoded = captcha_decode(uSeed); int bAutoCaptcha = db_get_boolean("auto-captcha", 0); char *zCaptcha = captcha_render(zDecoded); @@ -1459,82 +1489,81 @@ ** ** Page to allow users to self-register. The "self-register" setting ** must be enabled for this page to operate. */ void register_page(void){ - const char *zUsername, *zPasswd, *zConfirm, *zContact, *zCS, *zPw, *zCap; + const char *zUserID, *zPasswd, *zConfirm, *zEAddr; + const char *zDName; unsigned int uSeed; const char *zDecoded; char *zCaptcha; + int iErrLine = -1; + const char *zErr; if( !db_get_boolean("self-register", 0) ){ style_header("Registration not possible"); @

    This project does not allow user self-registration. Please contact the @ project administrator to obtain an account.

    style_footer(); return; } style_header("Register"); - zUsername = P("u"); - zPasswd = P("p"); - zConfirm = P("cp"); - zContact = P("c"); - zCap = P("cap"); - zCS = P("cs"); /* Captcha Secret */ + zUserID = PDT("u",""); + zPasswd = PDT("p",""); + zConfirm = PDT("cp",""); + zEAddr = PDT("ea",""); + zDName = PDT("dn",""); /* Try to make any sense from user input. */ - if( P("new") ){ - if( zCS==0 ) fossil_redirect_home(); /* Forged request */ - zPw = captcha_decode((unsigned int)atoi(zCS)); - if( !(zUsername && zPasswd && zConfirm && zContact) ){ - @

    - @ All fields are obligatory. - @

    - }else if( strlen(zPasswd) < 6){ - @

    - @ Password too weak. - @

    - }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ - @

    - @ The two copies of your new passwords do not match. - @

    - }else if( fossil_stricmp(zPw, zCap)!=0 ){ - @

    - @ Captcha text invalid. - @

    - }else{ - /* This almost is stupid copy-paste of code from user.c:user_cmd(). */ - Blob passwd, login, caps, contact; - - blob_init(&login, zUsername, -1); - blob_init(&contact, zContact, -1); - blob_init(&caps, db_get("default-perms", "u"), -1); - blob_init(&passwd, zPasswd, -1); - - if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){ - /* Here lies the reason I don't use zErrMsg - it would not substitute - * this %s(zUsername), or at least I don't know how to force it to.*/ - @

    - @ %h(zUsername) already exists. - @

    - }else{ - char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0); - int uid; - db_multi_exec( - "INSERT INTO user(login,pw,cap,info,mtime)" - "VALUES(%B,%Q,%B,%B,strftime('%%s','now'))", - &login, zPw, &caps, &contact - ); - free(zPw); - - /* The user is registered, now just log him in. */ - uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUsername); - login_set_user_cookie( zUsername, uid, NULL ); - redirect_to_g(); - - } - } + if( P("new")==0 || !cgi_csrf_safe(1) ){ + /* This is not a valid form submission. Fall through into + ** the form display */ + }else if( !captcha_is_correct(1) ){ + iErrLine = 6; + zErr = "Incorrect CAPTCHA"; + }else if( strlen(zUserID)<3 ){ + iErrLine = 1; + zErr = "User ID too short. Must be at least 3 characters."; + }else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){ + iErrLine = 1; + zErr = "User ID may not contain spaces or special characters."; + }else if( zDName[0]==0 ){ + iErrLine = 2; + zErr = "Required"; + }else if( zEAddr[0]==0 ){ + iErrLine = 3; + zErr = "Required"; + }else if( email_copy_addr(zEAddr,0)==0 ){ + iErrLine = 3; + zErr = "Not a valid email address"; + }else if( strlen(zPasswd)<6 ){ + iErrLine = 4; + zErr = "Password must be at least 6 characters long"; + }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ + iErrLine = 5; + zErr = "Passwords do not match"; + }else if( db_exists("SELECT 1 FROM user WHERE login=%Q", zUserID) ){ + iErrLine = 1; + zErr = "This User ID is already taken. Choose something different."; + }else if( db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr) ){ + iErrLine = 3; + zErr = "This address is already used."; + }else{ + Blob sql; + int uid; + char *zPass = sha1_shared_secret(zPasswd, zUserID, 0); + blob_init(&sql, 0, 0); + blob_append_sql(&sql, + "INSERT INTO user(login,pw,cap,info,mtime)\n" + "VALUES(%Q,%Q,%Q," + "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())", + zUserID, zPass, db_get("default-perms","u"), zDName, zEAddr, g.zIpAddr); + fossil_free(zPass); + db_multi_exec("%s", blob_sql_text(&sql)); + uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID); + login_set_user_cookie(zUserID, uid, NULL); + redirect_to_g(); } /* Prepare the captcha. */ uSeed = captcha_seed(); zDecoded = captcha_decode(uSeed); @@ -1543,31 +1572,53 @@ /* Print out the registration form. */ form_begin(0, "%R/register"); if( P("g") ){ @ } - @

    + @

    @ @ @ - @ + @ + if( iErrLine==1 ){ + @ + } + @ + @ + @ + @ + if( iErrLine==2 ){ + @ + } + @ + @ + @ + @ + if( iErrLine==3 ){ + @ + } @ @ @ - @ + @ + if( iErrLine==4 ){ + @ + } @ @ @ - @ - @ - @ - @ - @ + @ + if( iErrLine==5 ){ + @ + } @ @ @ - @ + @ + if( iErrLine==6 ){ + @ + } @ @ @ @ @

    @ @ @ Index: src/wiki.c ================================================================== --- src/wiki.c +++ src/wiki.c @@ -3,11 +3,11 @@ ** Copyright (c) 2008 Stephan Beal ** ** 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: @@ -167,11 +167,11 @@ Blob tail = BLOB_INITIALIZER; markdown_to_html(pWiki, 0, &tail); @ %s(blob_str(&tail)) blob_reset(&tail); }else{ - @
    +    @ 
         @ %h(blob_str(pWiki))
         @ 
    } } @@ -426,11 +426,11 @@ } /* ** Write a wiki artifact into the repository */ -static void wiki_put(Blob *pWiki, int parent, int needMod){ +int wiki_put(Blob *pWiki, int parent, int needMod){ int nrid; if( !needMod ){ nrid = content_put_ex(pWiki, 0, 0, 0, 0); if( parent) content_deltify(parent, &nrid, 1, 0); }else{ @@ -439,10 +439,11 @@ db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid); } db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); manifest_crosslink(nrid, pWiki, MC_NONE); + return nrid; } /* ** Output a selection box from which the user can select the ** wiki mimetype. Index: src/xfer.c ================================================================== --- src/xfer.c +++ src/xfer.c @@ -1306,11 +1306,11 @@ && blob_is_hname(&xfer.aToken[2]) ){ const char *zPCode; zPCode = db_get("project-code", 0); if( zPCode==0 ){ - fossil_panic("missing project code"); + fossil_fatal("missing project code"); } if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ cgi_reset_content(); @ error wrong\sproject nErr++; Index: win/Makefile.dmc ================================================================== --- win/Makefile.dmc +++ win/Makefile.dmc @@ -28,13 +28,13 @@ SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB SHELL_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -Dmain=sqlite3_shell -DSQLITE_SHELL_IS_UTF8=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen -SRC = add_.c allrepo_.c attach_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c +SRC = add_.c allrepo_.c attach_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c capabilities_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c -OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O +OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\capabilities$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O RC=$(DMDIR)\bin\rcc RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ @@ -49,11 +49,11 @@ $(OBJDIR)\fossil.res: $B\win\fossil.rc $(RC) $(RCFLAGS) -o$@ $** $(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res - +echo add allrepo attach backoffice bag bisect blob branch browse builtin bundle cache captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ + +echo add allrepo attach backoffice bag bisect blob branch browse builtin bundle cache capabilities captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ +echo fossil >> $@ +echo fossil >> $@ +echo $(LIBS) >> $@ +echo. >> $@ +echo fossil >> $@ @@ -200,10 +200,16 @@ $(OBJDIR)\cache$O : cache_.c cache.h $(TCC) -o$@ -c cache_.c cache_.c : $(SRCDIR)\cache.c +translate$E $** > $@ + +$(OBJDIR)\capabilities$O : capabilities_.c capabilities.h + $(TCC) -o$@ -c capabilities_.c + +capabilities_.c : $(SRCDIR)\capabilities.c + +translate$E $** > $@ $(OBJDIR)\captcha$O : captcha_.c captcha.h $(TCC) -o$@ -c captcha_.c captcha_.c : $(SRCDIR)\captcha.c @@ -928,7 +934,7 @@ zip_.c : $(SRCDIR)\zip.c +translate$E $** > $@ headers: makeheaders$E page_index.h builtin_data.h default_css.h VERSION.h - +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h + +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h capabilities_.c:capabilities.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h @copy /Y nul: headers Index: win/Makefile.mingw ================================================================== --- win/Makefile.mingw +++ win/Makefile.mingw @@ -448,10 +448,11 @@ $(SRCDIR)/branch.c \ $(SRCDIR)/browse.c \ $(SRCDIR)/builtin.c \ $(SRCDIR)/bundle.c \ $(SRCDIR)/cache.c \ + $(SRCDIR)/capabilities.c \ $(SRCDIR)/captcha.c \ $(SRCDIR)/cgi.c \ $(SRCDIR)/checkin.c \ $(SRCDIR)/checkout.c \ $(SRCDIR)/clearsign.c \ @@ -628,10 +629,11 @@ $(SRCDIR)/../skins/xekri/details.txt \ $(SRCDIR)/../skins/xekri/footer.txt \ $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/diff.tcl \ + $(SRCDIR)/forum.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ @@ -654,10 +656,11 @@ $(OBJDIR)/branch_.c \ $(OBJDIR)/browse_.c \ $(OBJDIR)/builtin_.c \ $(OBJDIR)/bundle_.c \ $(OBJDIR)/cache_.c \ + $(OBJDIR)/capabilities_.c \ $(OBJDIR)/captcha_.c \ $(OBJDIR)/cgi_.c \ $(OBJDIR)/checkin_.c \ $(OBJDIR)/checkout_.c \ $(OBJDIR)/clearsign_.c \ @@ -789,10 +792,11 @@ $(OBJDIR)/branch.o \ $(OBJDIR)/browse.o \ $(OBJDIR)/builtin.o \ $(OBJDIR)/bundle.o \ $(OBJDIR)/cache.o \ + $(OBJDIR)/capabilities.o \ $(OBJDIR)/captcha.o \ $(OBJDIR)/cgi.o \ $(OBJDIR)/checkin.o \ $(OBJDIR)/checkout.o \ $(OBJDIR)/clearsign.o \ @@ -1143,10 +1147,11 @@ $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ + $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \ $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ @@ -1368,10 +1373,18 @@ $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c $(OBJDIR)/cache.h: $(OBJDIR)/headers + +$(OBJDIR)/capabilities_.c: $(SRCDIR)/capabilities.c $(TRANSLATE) + $(TRANSLATE) $(SRCDIR)/capabilities.c >$@ + +$(OBJDIR)/capabilities.o: $(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h + $(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c + +$(OBJDIR)/capabilities.h: $(OBJDIR)/headers $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(TRANSLATE) $(TRANSLATE) $(SRCDIR)/captcha.c >$@ $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h Index: win/Makefile.msc ================================================================== --- win/Makefile.msc +++ win/Makefile.msc @@ -390,10 +390,11 @@ branch_.c \ browse_.c \ builtin_.c \ bundle_.c \ cache_.c \ + capabilities_.c \ captcha_.c \ cgi_.c \ checkin_.c \ checkout_.c \ clearsign_.c \ @@ -569,10 +570,11 @@ $(SRCDIR)\..\skins\xekri\details.txt \ $(SRCDIR)\..\skins\xekri\footer.txt \ $(SRCDIR)\..\skins\xekri\header.txt \ $(SRCDIR)\ci_edit.js \ $(SRCDIR)\diff.tcl \ + $(SRCDIR)\forum.js \ $(SRCDIR)\graph.js \ $(SRCDIR)\href.js \ $(SRCDIR)\login.js \ $(SRCDIR)\markdown.md \ $(SRCDIR)\menu.js \ @@ -594,10 +596,11 @@ $(OX)\branch$O \ $(OX)\browse$O \ $(OX)\builtin$O \ $(OX)\bundle$O \ $(OX)\cache$O \ + $(OX)\capabilities$O \ $(OX)\captcha$O \ $(OX)\cgi$O \ $(OX)\checkin$O \ $(OX)\checkout$O \ $(OX)\clearsign$O \ @@ -788,10 +791,11 @@ echo $(OX)\branch.obj >> $@ echo $(OX)\browse.obj >> $@ echo $(OX)\builtin.obj >> $@ echo $(OX)\bundle.obj >> $@ echo $(OX)\cache.obj >> $@ + echo $(OX)\capabilities.obj >> $@ echo $(OX)\captcha.obj >> $@ echo $(OX)\cgi.obj >> $@ echo $(OX)\checkin.obj >> $@ echo $(OX)\checkout.obj >> $@ echo $(OX)\clearsign.obj >> $@ @@ -1104,10 +1108,16 @@ $(OX)\cache$O : cache_.c cache.h $(TCC) /Fo$@ -c cache_.c cache_.c : $(SRCDIR)\cache.c translate$E $** > $@ + +$(OX)\capabilities$O : capabilities_.c capabilities.h + $(TCC) /Fo$@ -c capabilities_.c + +capabilities_.c : $(SRCDIR)\capabilities.c + translate$E $** > $@ $(OX)\captcha$O : captcha_.c captcha.h $(TCC) /Fo$@ -c captcha_.c captcha_.c : $(SRCDIR)\captcha.c @@ -1847,10 +1857,11 @@ branch_.c:branch.h \ browse_.c:browse.h \ builtin_.c:builtin.h \ bundle_.c:bundle.h \ cache_.c:cache.h \ + capabilities_.c:capabilities.h \ captcha_.c:captcha.h \ cgi_.c:cgi.h \ checkin_.c:checkin.h \ checkout_.c:checkout.h \ clearsign_.c:clearsign.h \ Index: www/fileformat.wiki ================================================================== --- www/fileformat.wiki +++ www/fileformat.wiki @@ -71,13 +71,14 @@
  • [#ctrl | Control Artifacts]
  • [#wikichng | Wiki Pages]
  • [#tktchng | Ticket Changes]
  • [#attachment | Attachments]
  • [#event | TechNotes]
  • +
  • [#forum | Forum Posts]
  • -These seven structural artifact types are described in subsections below. +These eight structural artifact types are described in subsections below. Structural artifacts are ASCII text. The artifact may be PGP clearsigned. After removal of the PGP clearsign header and suffix (if any) a structural artifact consists of one or more "cards" separated by a single newline (ASCII: 0x0a) character. Each card begins with a single @@ -525,10 +526,84 @@ technote. The format of the W card is exactly the same as for a [#wikichng | wiki artifact]. The Z card is the required checksum over the rest of the artifact. + +

    2.8 Forum Posts

    + +Forum posts are intended as a mechanism for users and developers to +discuss a project. Forum mosts are like messages on a mailing list. + +The following cards are allowed on an forum post artifact: + +
    +D time-and-date-stamp
    +G thread-root
    +H thread-title
    +I in-reply-to
    +N mimetype
    +P parent-artifact-id
    +U user-name
    +W size \n text \n
    +Z checksum +
    + +Every forum post must have either one I card and one G card +or one H card. +Forum posts are organized into topic threads. The initial +post for a thread (the root post) has an H card giving the title or +subject for that thread. The argument to the H card is a string +in the same format as a comment string in a C card. +All follow-up posts have an I card that +indicates which prior post in the same thread the current forum +post is replying to, and a G card specifying the root post for +the entire thread. The argument to G and I cards is the +artifact hash for the prior forum post to which the card refers. + +In theory, it is sufficient for follow-up posts to have only an +I card, since the G card value could be computed by following a +chain of I cards. However, the G card is required in order to +associate the artifact with a forum thread in the case where an +intermediate artifact in the I card chain is shunned or otherwise +becomes unreadable. + +A single D card is required to give the date and time when the +forum post was created. + +The optional N card specifies the mimetype of the text of the technote +that is contained in the W card. If the N card is omitted, then the +W card text mimetype is assumed to be text/x-fossil, which is the +Fossil wiki format. + +The optional P card specifies a prior forum post for which this +forum post is an edit. For display purposes, only the child post +is shown, though the historical post is retained as a record. +If P cards are used and there exist multiple versions of the same +forum post, then I cards for other artifacts refer to whichever +version of the post was current at the time the reply was made, +but G cards refer to the initial, unedited root post for the thread. +Thus, following the chain of I cards back to the root of the thread +may land on a different post than the one given in the G card. +However, following the chain of I cards back to the thread root, +then following P cards back to the initial version of the thread +root must give the same artifact as is provided by the G card, +otherwise the artifact containing the G card is considered invalid +and should be ignored. + +In general, P cards may contain multiple arguments, indicating a +merge. But since forum posts cannot be merged, the +P card of a forum post may only contain a single argument. + +The U card gives name of the user who entered the forum post. + +A single W card provides wiki text for the forum post. +The format of the W card is exactly the same as for a +[#wikichng | wiki artifact]. + +The Z card is the required checksum over the rest of the artifact. +

    3.0 Card Summary

    The following table summarizes the various kinds of cards that appear @@ -539,20 +614,21 @@ or more such cards are required.
    
    Index: src/main.c
    ==================================================================
    --- src/main.c
    +++ src/main.c
    @@ -90,11 +90,11 @@
       char WrUnver;          /* y: can push unversioned content */
       char RdForum;          /* 2: Read forum posts */
       char WrForum;          /* 3: Create new forum posts */
       char WrTForum;         /* 4: Post to forums not subject to moderation */
       char ModForum;         /* 5: Moderate (approve or reject) forum posts */
    -  char AdminForum;       /* 6: Edit forum posts by other users */
    +  char AdminForum;       /* 6: Set or remove capability 4 on other users */
       char EmailAlert;       /* 7: Sign up for email notifications */
       char Announce;         /* A: Send announcements */
       char Debug;            /* D: show extra Fossil debugging features */
     };
     
    @@ -640,11 +640,11 @@
       if( g.zVfsName ){
         sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName);
         if( pVfs ){
           sqlite3_vfs_register(pVfs, 1);
         }else{
    -      fossil_panic("no such VFS: \"%s\"", g.zVfsName);
    +      fossil_fatal("no such VFS: \"%s\"", g.zVfsName);
         }
       }
       if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){
         zCmdName = "cgi";
         g.isHTTP = 1;
    @@ -691,11 +691,11 @@
         g.zErrlog = find_option("errorlog", 0, 1);
         fossil_init_flags_from_options();
         if( find_option("utc",0,0) ) g.fTimeFormat = 1;
         if( find_option("localtime",0,0) ) g.fTimeFormat = 2;
         if( zChdir && file_chdir(zChdir, 0) ){
    -      fossil_panic("unable to change directories to %s", zChdir);
    +      fossil_fatal("unable to change directories to %s", zChdir);
         }
         if( find_option("help",0,0)!=0 ){
           /* If --help is found anywhere on the command line, translate the command
            * to "fossil help cmdname" where "cmdname" is the first argument that
            * does not begin with a "-" character.  If all arguments start with "-",
    @@ -756,11 +756,11 @@
           rc = TH_OK;
         }
         if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
           if( rc==TH_OK || rc==TH_RETURN ){
     #endif
    -        fossil_panic("%s: unknown command: %s\n"
    +        fossil_fatal("%s: unknown command: %s\n"
                          "%s: use \"help\" for more information",
                          g.argv[0], zCmdName, g.argv[0]);
     #ifdef FOSSIL_ENABLE_TH1_HOOKS
           }
           if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
    @@ -821,11 +821,11 @@
     
     /*
     ** Print a usage comment and quit
     */
     void usage(const char *zFormat){
    -  fossil_panic("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat);
    +  fossil_fatal("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat);
     }
     
     /*
     ** Remove n elements from g.argv beginning with the i-th element.
     */
    @@ -939,11 +939,11 @@
     */
     void verify_all_options(void){
       int i;
       for(i=1; iGenerate a message to the error log
         @ by clicking on one of the following cases:
       }else{
         @ 

    This is the test page for case=%d(iCase). All possible cases: } - for(i=1; i<=5; i++){ + for(i=1; i<=7; i++){ @ [%d(i)] } @

    @

      @
    1. Call fossil_warning() @@ -2852,9 +2854,18 @@ } @
    2. simulate segfault handling if( iCase==5 ){ sigsegv_handler(0); } + @
    3. call webpage_assert(0) + if( iCase==6 ){ + webpage_assert( 5==7 ); + } + @
    4. call webpage_error()" + if( iCase==7 ){ + cgi_reset_content(); + webpage_error("Case 7 from /test-warning"); + } @
    @

    End of test

    style_footer(); } Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -25,10 +25,11 @@ $(SRCDIR)/branch.c \ $(SRCDIR)/browse.c \ $(SRCDIR)/builtin.c \ $(SRCDIR)/bundle.c \ $(SRCDIR)/cache.c \ + $(SRCDIR)/capabilities.c \ $(SRCDIR)/captcha.c \ $(SRCDIR)/cgi.c \ $(SRCDIR)/checkin.c \ $(SRCDIR)/checkout.c \ $(SRCDIR)/clearsign.c \ @@ -205,10 +206,11 @@ $(SRCDIR)/../skins/xekri/details.txt \ $(SRCDIR)/../skins/xekri/footer.txt \ $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/diff.tcl \ + $(SRCDIR)/forum.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ @@ -231,10 +233,11 @@ $(OBJDIR)/branch_.c \ $(OBJDIR)/browse_.c \ $(OBJDIR)/builtin_.c \ $(OBJDIR)/bundle_.c \ $(OBJDIR)/cache_.c \ + $(OBJDIR)/capabilities_.c \ $(OBJDIR)/captcha_.c \ $(OBJDIR)/cgi_.c \ $(OBJDIR)/checkin_.c \ $(OBJDIR)/checkout_.c \ $(OBJDIR)/clearsign_.c \ @@ -366,10 +369,11 @@ $(OBJDIR)/branch.o \ $(OBJDIR)/browse.o \ $(OBJDIR)/builtin.o \ $(OBJDIR)/bundle.o \ $(OBJDIR)/cache.o \ + $(OBJDIR)/capabilities.o \ $(OBJDIR)/captcha.o \ $(OBJDIR)/cgi.o \ $(OBJDIR)/checkin.o \ $(OBJDIR)/checkout.o \ $(OBJDIR)/clearsign.o \ @@ -699,10 +703,11 @@ $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ + $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \ $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ @@ -922,10 +927,18 @@ $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c $(OBJDIR)/cache.h: $(OBJDIR)/headers + +$(OBJDIR)/capabilities_.c: $(SRCDIR)/capabilities.c $(OBJDIR)/translate + $(OBJDIR)/translate $(SRCDIR)/capabilities.c >$@ + +$(OBJDIR)/capabilities.o: $(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h + $(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c + +$(OBJDIR)/capabilities.h: $(OBJDIR)/headers $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(OBJDIR)/translate $(OBJDIR)/translate $(SRCDIR)/captcha.c >$@ $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h Index: src/makemake.tcl ================================================================== --- src/makemake.tcl +++ src/makemake.tcl @@ -37,10 +37,11 @@ branch browse builtin bundle cache + capabilities captcha cgi checkin checkout clearsign Index: src/manifest.c ================================================================== --- src/manifest.c +++ src/manifest.c @@ -34,10 +34,11 @@ #define CFTYPE_CONTROL 3 #define CFTYPE_WIKI 4 #define CFTYPE_TICKET 5 #define CFTYPE_ATTACHMENT 6 #define CFTYPE_EVENT 7 +#define CFTYPE_FORUM 8 /* ** File permissions used by Fossil internally. */ #define PERM_REG 0 /* regular file */ @@ -76,16 +77,19 @@ char *zUser; /* Name of the user from the U card. */ char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */ char *zWiki; /* Text of the wiki page. W card. */ char *zWikiTitle; /* Name of the wiki page. L card. */ char *zMimetype; /* Mime type of wiki or comment text. N card. */ + char *zThreadTitle; /* The forum thread title. H card */ double rEventDate; /* Date of an event. E card. */ char *zEventId; /* Artifact hash for an event. E card. */ char *zTicketUuid; /* UUID for a ticket. K card. */ char *zAttachName; /* Filename of an attachment. A card. */ char *zAttachSrc; /* Artifact hash for document being attached. A card. */ char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */ + char *zThreadRoot; /* Thread root artifact. G card */ + char *zInReplyTo; /* Forum in-reply-to artifact. I card */ int nFile; /* Number of F cards */ int nFileAlloc; /* Slots allocated in aFile[] */ int iFile; /* Index of current file in iterator */ ManifestFile *aFile; /* One entry for each F-card */ int nParent; /* Number of parents. */ @@ -112,10 +116,42 @@ char *zName; /* Key or field name */ char *zValue; /* Value of the field */ } *aField; /* One for each J card */ }; #endif + +/* +** Allowed and required card types in each style of artifact +*/ +static struct { + const char *zAllowed; /* Allowed cards. Human-readable */ + const char *zRequired; /* Required cards. Human-readable */ +} manifestCardTypes[] = { + /* Allowed Required */ + /* CFTYPE_MANIFEST 1 */ { "BCDFNPQRTUZ", "CDUZ" }, + /* CFTYPE_CLUSTER 2 */ { "MZ", "MZ" }, + /* CFTYPE_CONTROL 3 */ { "DTUZ", "DTUZ" }, + /* CFTYPE_WIKI 4 */ { "DLNPUWZ", "DLUWZ" }, + /* CFTYPE_TICKET 5 */ { "DJKUZ", "DJKUZ" }, + /* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ", "ADZ" }, + /* CFTYPE_EVENT 7 */ { "CDENPTUWZ", "DEWZ" }, + /* CFTYPE_FORUM 8 */ { "DGHINPUWZ", "DUWZ" }, +}; + +/* +** Names of manifest types +*/ +static const char *azNameOfMType[] = { + "manifest", + "cluster", + "tag", + "wiki", + "ticket", + "attachment", + "technote", + "forum post" +}; /* ** A cache of parsed manifests. This reduces the number of ** calls to manifest_parse() when doing a rebuild. */ @@ -147,10 +183,35 @@ if( p->pBaseline ) manifest_destroy(p->pBaseline); memset(p, 0, sizeof(*p)); fossil_free(p); } } + +/* +** Given a string of upper-case letters, compute a mask of the letters +** present. For example, "ABC" computes 0x0007. "DE" gives 0x0018". +*/ +static unsigned int manifest_card_mask(const char *z){ + unsigned int m = 0; + char c; + while( (c = *(z++))>='A' && c<='Z' ){ + m |= 1 << (c - 'A'); + } + return m; +} + +/* +** Given an integer mask representing letters A-Z, return the +** letter which is the first bit set in the mask. Example: +** 0x03520 gives 'F' since the F-bit is the lowest. +*/ +static char maskToType(unsigned int x){ + char c = 'A'; + if( x==0 ) return '?'; + while( (x&1)==0 ){ x >>= 1; c++; } + return c; +} /* ** Add an element to the manifest cache using LRU replacement. */ void manifest_cache_insert(Manifest *p){ @@ -352,22 +413,26 @@ ** The card type determines the other parameters to the card. ** Cards must occur in lexicographical order. */ Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){ Manifest *p; - int seenZ = 0; int i, lineNo=0; ManifestText x; char cPrevType = 0; char cType; char *z; int n; char *zUuid; int sz = 0; - int isRepeat, hasSelfRefTag = 0; + int isRepeat; + int nSelfTag = 0; /* Number of T cards referring to this manifest */ + int nSimpleTag = 0; /* Number of T cards with "+" prefix */ static Bag seen; const char *zErr = 0; + unsigned int m; + unsigned int seenCard = 0; /* Which card types have been seen */ + char zErrBuf[100]; /* Write error messages here */ if( rid==0 ){ isRepeat = 1; }else if( bag_find(&seen, rid) ){ isRepeat = 1; @@ -422,10 +487,12 @@ x.z = z; x.zEnd = &z[n]; x.atEol = 1; while( (cType = next_card(&x))!=0 && cType>=cPrevType ){ lineNo++; + if( cType<'A' || cType>'Z' ) SYNTAX("bad card type"); + seenCard |= 1 << (cType-'A'); switch( cType ){ /* ** A ?? ** ** Identifies an attachment to either a wiki page or a ticket. @@ -454,10 +521,11 @@ SYNTAX("invalid source on A-card"); } p->zAttachName = (char*)file_tail(zName); p->zAttachSrc = zSrc; p->zAttachTarget = zTarget; + p->type = CFTYPE_ATTACHMENT; break; } /* ** B @@ -469,10 +537,11 @@ p->zBaseline = next_token(&x, &sz); if( p->zBaseline==0 ) SYNTAX("missing hash on B-card"); if( !hname_validate(p->zBaseline,sz) ){ SYNTAX("invalid hash on B-card"); } + p->type = CFTYPE_MANIFEST; break; } /* @@ -520,10 +589,11 @@ if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card"); p->zEventId = next_token(&x, &sz); if( !hname_validate(p->zEventId, sz) ){ SYNTAX("malformed hash on E-card"); } + p->type = CFTYPE_EVENT; break; } /* ** F ?? ?? ?? @@ -565,10 +635,59 @@ p->aFile[i].zPerm = zPerm; p->aFile[i].zPrior = zPriorName; if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){ SYNTAX("incorrect F-card sort order"); } + p->type = CFTYPE_MANIFEST; + break; + } + + /* + ** G + ** + ** A G-card identifies the initial root forum post for the thread + ** of which this post is a part. Forum posts only. + */ + case 'G': { + if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card"); + p->zThreadRoot = next_token(&x, &sz); + if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card"); + if( !hname_validate(p->zThreadRoot,sz) ){ + SYNTAX("Invalid hash on G-card"); + } + p->type = CFTYPE_FORUM; + break; + } + + /* + ** H + ** + ** The title for a forum thread. + */ + case 'H': { + if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card"); + p->zThreadTitle = next_token(&x,0); + if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card"); + defossilize(p->zThreadTitle); + p->type = CFTYPE_FORUM; + break; + } + + /* + ** I + ** + ** A I-card identifies another forum post that the current forum post + ** is in reply to. + */ + case 'I': { + if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card"); + p->zInReplyTo = next_token(&x, &sz); + if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card"); + if( !hname_validate(p->zInReplyTo,sz) ){ + SYNTAX("Invalid hash on I-card"); + } + p->type = CFTYPE_FORUM; break; } /* ** J ?? @@ -594,10 +713,11 @@ p->aField[i].zName = zName; p->aField[i].zValue = zValue; if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){ SYNTAX("incorrect J-card sort order"); } + p->type = CFTYPE_TICKET; break; } /* @@ -611,10 +731,11 @@ p->zTicketUuid = next_token(&x, &sz); if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size"); if( !validate16(p->zTicketUuid, sz) ){ SYNTAX("invalid K-card UUID"); } + p->type = CFTYPE_TICKET; break; } /* ** L @@ -628,10 +749,11 @@ if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card"); defossilize(p->zWikiTitle); if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){ SYNTAX("L-card has malformed wiki name"); } + p->type = CFTYPE_WIKI; break; } /* ** M @@ -653,10 +775,11 @@ i = p->nCChild++; p->azCChild[i] = zUuid; if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){ SYNTAX("M-card in the wrong order"); } + p->type = CFTYPE_CLUSTER; break; } /* ** N @@ -717,10 +840,11 @@ p->aCherrypick[n].zCPTarget = zUuid; p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz); if( zUuid && !hname_validate(zUuid,sz) ){ SYNTAX("invalid second hash on Q-card"); } + p->type = CFTYPE_MANIFEST; break; } /* ** R @@ -731,10 +855,11 @@ case 'R': { if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card"); p->zRepoCksum = next_token(&x, &sz); if( sz!=32 ) SYNTAX("wrong size cksum on R-card"); if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum"); + p->type = CFTYPE_MANIFEST; break; } /* ** T (+|*|-) ?? @@ -759,24 +884,21 @@ if( zUuid==0 ) SYNTAX("missing artifact hash on T-card"); zValue = next_token(&x, 0); if( zValue ) defossilize(zValue); if( hname_validate(zUuid, sz) ){ /* A valid artifact hash */ - if( p->zEventId ) SYNTAX("non-self-referential T-card in event"); }else if( sz==1 && zUuid[0]=='*' ){ zUuid = 0; - hasSelfRefTag = 1; - if( p->zEventId && zName[0]!='+' ){ - SYNTAX("propagating T-card in event"); - } + nSelfTag++; }else{ SYNTAX("malformed artifact hash on T-card"); } defossilize(zName); if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){ SYNTAX("T-card name does not begin with '-', '+', or '*'"); } + if( zName[0]=='+' ) nSimpleTag++; if( validate16(&zName[1], strlen(&zName[1])) ){ /* Do not allow tags whose names look like a hash */ SYNTAX("T-card name looks like a hexadecimal hash"); } if( p->nTag>=p->nTagAlloc ){ @@ -858,104 +980,78 @@ */ case 'Z': { zUuid = next_token(&x, &sz); if( sz!=32 ) SYNTAX("wrong size for Z-card cksum"); if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum"); - seenZ = 1; break; } default: { SYNTAX("unrecognized card"); } } } if( x.znCChild>0 ){ - if( p->zAttachName - || p->zBaseline - || p->zComment - || p->rDate>0.0 - || p->zEventId - || p->nFile>0 - || p->nField>0 - || p->zTicketUuid - || p->zWikiTitle - || p->zMimetype - || p->nParent>0 - || p->nCherrypick>0 - || p->zRepoCksum - || p->nTag>0 - || p->zUser - || p->zWiki - ){ - SYNTAX("cluster contains a card other than M- or Z-"); - } - if( !seenZ ) SYNTAX("missing Z-card on cluster"); - p->type = CFTYPE_CLUSTER; - }else if( p->zEventId ){ - if( p->zAttachName ) SYNTAX("A-card in event"); - if( p->zBaseline ) SYNTAX("B-card in event"); - if( p->rDate<=0.0 ) SYNTAX("missing date on event"); - if( p->nFile>0 ) SYNTAX("F-card in event"); - if( p->nField>0 ) SYNTAX("J-card in event"); - if( p->zTicketUuid ) SYNTAX("K-card in event"); - if( p->zWikiTitle!=0 ) SYNTAX("L-card in event"); - if( p->zRepoCksum ) SYNTAX("R-card in event"); - if( p->zWiki==0 ) SYNTAX("missing W-card on event"); - if( !seenZ ) SYNTAX("missing Z-card on event"); - p->type = CFTYPE_EVENT; - }else if( p->zWiki!=0 || p->zWikiTitle!=0 ){ - if( p->zAttachName ) SYNTAX("A-card in wiki"); - if( p->zBaseline ) SYNTAX("B-card in wiki"); - if( p->rDate<=0.0 ) SYNTAX("missing date on wiki"); - if( p->nFile>0 ) SYNTAX("F-card in wiki"); - if( p->nField>0 ) SYNTAX("J-card in wiki"); - if( p->zTicketUuid ) SYNTAX("K-card in wiki"); - if( p->zWikiTitle==0 ) SYNTAX("missing L-card on wiki"); - if( p->zRepoCksum ) SYNTAX("R-card in wiki"); - if( p->nTag>0 ) SYNTAX("T-card in wiki"); - if( p->zWiki==0 ) SYNTAX("missing W-card on wiki"); - if( !seenZ ) SYNTAX("missing Z-card on wiki"); - p->type = CFTYPE_WIKI; - }else if( hasSelfRefTag || p->nFile>0 || p->zRepoCksum!=0 || p->zBaseline - || p->nParent>0 ){ - if( p->zAttachName ) SYNTAX("A-card in manifest"); - if( p->rDate<=0.0 ) SYNTAX("missing date on manifest"); - if( p->nField>0 ) SYNTAX("J-card in manifest"); - if( p->zTicketUuid ) SYNTAX("K-card in manifest"); - p->type = CFTYPE_MANIFEST; - }else if( p->nField>0 || p->zTicketUuid!=0 ){ - if( p->zAttachName ) SYNTAX("A-card in ticket"); - if( p->rDate<=0.0 ) SYNTAX("missing date on ticket"); - if( p->nField==0 ) SYNTAX("missing J-card on ticket"); - if( p->zTicketUuid==0 ) SYNTAX("missing K-card on ticket"); - if( p->zMimetype) SYNTAX("N-card in ticket"); - if( p->nTag>0 ) SYNTAX("T-card in ticket"); - if( p->zUser==0 ) SYNTAX("missing U-card on ticket"); - if( !seenZ ) SYNTAX("missing Z-card on ticket"); - p->type = CFTYPE_TICKET; - }else if( p->zAttachName ){ - if( p->rDate<=0.0 ) SYNTAX("missing date on attachment"); - if( p->nTag>0 ) SYNTAX("T-card in attachment"); - if( !seenZ ) SYNTAX("missing Z-card on attachment"); - p->type = CFTYPE_ATTACHMENT; - }else{ - if( p->rDate<=0.0 ) SYNTAX("missing date on control"); - if( p->zMimetype ) SYNTAX("N-card in control"); - if( !seenZ ) SYNTAX("missing Z-card on control"); - p->type = CFTYPE_CONTROL; - } + /* If the artifact type has not yet been determined, then compute + ** it now. */ + if( p->type==0 ){ + p->type = p->zComment!=0 ? CFTYPE_MANIFEST : CFTYPE_CONTROL; + } + + /* Verify that no disallowed cards are present for this artifact type */ + m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed); + if( seenCard & ~m ){ + sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s", + maskToType(seenCard & ~m), + azNameOfMType[p->type-1]); + zErr = zErrBuf; + goto manifest_syntax_error; + } + + /* Verify that all required cards are present for this artifact type */ + m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired); + if( ~seenCard & m ){ + sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s", + maskToType(~seenCard & m), + azNameOfMType[p->type-1]); + zErr = zErrBuf; + goto manifest_syntax_error; + } + + /* Additional checks based on artifact type */ + switch( p->type ){ + case CFTYPE_CONTROL: { + if( nSelfTag ) SYNTAX("self-referential T-card in control artifact"); + break; + } + case CFTYPE_EVENT: { + if( p->nTag!=nSelfTag ){ + SYNTAX("non-self-referential T-card in technote"); + } + if( p->nTag!=nSimpleTag ){ + SYNTAX("T-card with '*' or '-' in technote"); + } + break; + } + case CFTYPE_FORUM: { + if( p->zThreadTitle && p->zInReplyTo ){ + SYNTAX("cannot have I-card and H-card in a forum post"); + } + if( p->nParent>1 ) SYNTAX("too many arguments to P-card"); + break; + } + } + md5sum_init(); if( !isRepeat ) g.parseCnt[p->type]++; return p; manifest_syntax_error: { char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); if( zUuid ){ - blob_appendf(pErr, "manifest [%s] ", zUuid); + blob_appendf(pErr, "artifact [%s] ", zUuid); fossil_free(zUuid); } } if( zErr ){ blob_appendf(pErr, "line %d: %s", lineNo, zErr); @@ -1015,11 +1111,12 @@ /* ** COMMAND: test-parse-manifest ** ** Usage: %fossil test-parse-manifest FILENAME ?N? ** -** Parse the manifest and discarded. Use for testing only. +** Parse the manifest(s) given on the command-line and report any +** errors. If the N argument is given, run the parsing N times. */ void manifest_test_parse_cmd(void){ Manifest *p; Blob b; int i; @@ -1039,10 +1136,47 @@ if( p==0 ) fossil_print("ERROR: %s\n", blob_str(&err)); blob_reset(&err); manifest_destroy(p); } } + +/* +** COMMAND: test-parse-all-blobs +** +** Usage: %fossil test-parse-all-blobs +** +** Parse all entries in the BLOB table that are believed to be non-data +** artifacts and report any errors. Run this test command on historical +** repositories after making any changes to the manifest_parse() +** implementation to confirm that the changes did not break anything. +*/ +void manifest_test_parse_all_blobs_cmd(void){ + Manifest *p; + Blob err; + Stmt q; + int nTest = 0; + int nErr = 0; + db_find_and_open_repository(0, 0); + verify_all_options(); + db_prepare(&q, "SELECT DISTINCT objid FROM EVENT"); + while( db_step(&q)==SQLITE_ROW ){ + int id = db_column_int(&q,0); + fossil_print("Checking %d \r", id); + nTest++; + fflush(stdout); + blob_init(&err, 0, 0); + p = manifest_get(id, CFTYPE_ANY, &err); + if( p==0 ){ + fossil_print("%d ERROR: %s\n", id, blob_str(&err)); + nErr++; + } + blob_reset(&err); + manifest_destroy(p); + } + db_finalize(&q); + fossil_print("%d tests with %d errors\n", nTest, nErr); +} /* ** Fetch the baseline associated with the delta-manifest p. ** Return 0 on success. If unable to parse the baseline, ** throw an error. If the baseline is a manifest, throw an @@ -1059,11 +1193,11 @@ "INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)", p->rid, rid ); return 1; } - fossil_panic("cannot access baseline manifest %S", p->zBaseline); + fossil_fatal("cannot access baseline manifest %S", p->zBaseline); } } return 0; } @@ -2378,10 +2512,71 @@ "REPLACE INTO event(type,mtime,objid,user,comment)" "VALUES('g',%.17g,%d,%Q,%Q)", p->rDate, rid, p->zUser, blob_str(&comment)+1 ); blob_reset(&comment); + } + if( p->type==CFTYPE_FORUM ){ + int froot, fprev, firt; + char *zFType; + char *zTitle; + schema_forum(); + froot = p->zThreadRoot ? uuid_to_rid(p->zThreadRoot, 1) : rid; + fprev = p->nParent ? uuid_to_rid(p->azParent[0],1) : 0; + firt = p->zInReplyTo ? uuid_to_rid(p->zInReplyTo,1) : 0; + db_multi_exec( + "INSERT INTO forumpost(fpid,froot,fprev,firt,fmtime)" + "VALUES(%d,%d,nullif(%d,0),nullif(%d,0),%.17g)", + p->rid, froot, fprev, firt, p->rDate + ); + if( firt==0 ){ + /* This is the start of a new thread, either the initial entry + ** or an edit of the initial entry. */ + zTitle = p->zThreadTitle; + if( zTitle==0 || zTitle[0]==0 ){ + zTitle = "(Deleted)"; + } + zFType = fprev ? "Edit" : "Post"; + db_multi_exec( + "REPLACE INTO event(type,mtime,objid,user,comment)" + "VALUES('f',%.17g,%d,%Q,'%q: %q')", + p->rDate, rid, p->zUser, zFType, zTitle + ); + /* + ** If this edit is the most recent, then make it the title for + ** all other entries for the same thread + */ + if( !db_exists("SELECT 1 FROM forumpost WHERE froot=%d AND firt=0" + " AND fpid!=%d AND fmtime>%.17g", froot, rid, p->rDate) + ){ + /* This entry establishes a new title for all entries on the thread */ + db_multi_exec( + "UPDATE event" + " SET comment=substr(comment,1,instr(comment,':')) || ' %q'" + " WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=%d)", + zTitle, froot + ); + } + }else{ + /* This is a reply to a prior post. Take the title from the root. */ + zTitle = db_text(0, "SELECT substr(comment,instr(comment,':')+2)" + " FROM event WHERE objid=%d", froot); + if( zTitle==0 ) zTitle = fossil_strdup("Unknown"); + if( p->zWiki[0]==0 ){ + zFType = "Delete reply"; + }else if( fprev ){ + zFType = "Edit reply"; + }else{ + zFType = "Reply"; + } + db_multi_exec( + "REPLACE INTO event(type,mtime,objid,user,comment)" + "VALUES('f',%.17g,%d,%Q,'%q: %q')", + p->rDate, rid, p->zUser, zFType, zTitle + ); + fossil_free(zTitle); + } } db_end_transaction(0); if( permitHooks ){ rc = xfer_run_common_script(); if( rc==TH_OK ){ Index: src/moderate.c ================================================================== --- src/moderate.c +++ src/moderate.c @@ -54,10 +54,34 @@ db_bind_int(&q, ":objid", rid); rc = db_step(&q)==SQLITE_ROW; db_reset(&q); return rc; } + +/* +** If the rid object is being held for moderation, write out +** an "awaiting moderation" message and return true. +** +** If the object is not being held for moderation, simply return +** false without generating any output. +*/ +int moderation_pending_www(int rid){ + int pending = moderation_pending(rid); + if( pending ){ + @ (Awaiting Moderator Approval) + } + return pending; +} + + +/* +** Return TRUE if there any pending moderation requests. +*/ +int moderation_needed(void){ + if( !moderation_table_exists() ) return 0; + return db_exists("SELECT 1 FROM modreq"); +} /* ** Check to see if the object identified by RID is used for anything. */ static int object_used(int rid){ @@ -101,10 +125,13 @@ "DELETE FROM tagxref WHERE rid=%d;" "DELETE FROM private WHERE rid=%d;" "DELETE FROM attachment WHERE attachid=%d;", rid, rid, rid, rid, rid, rid ); + if( db_table_exists("repository","forumpost") ){ + db_multi_exec("DELETE FROM forumpost WHERE fpid=%d", rid); + } zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid); if( zTktid && zTktid[0] ){ ticket_rebuild_entry(zTktid); fossil_free(zTktid); } @@ -144,12 +171,12 @@ void modreq_page(void){ Blob sql; Stmt q; login_check_credentials(); - if( !g.perm.ModWiki && !g.perm.ModTkt ){ - login_needed(g.anon.ModWiki && g.anon.ModTkt); + if( !g.perm.ModWiki && !g.perm.ModTkt && !g.perm.ModForum ){ + login_needed(g.anon.ModWiki && g.anon.ModTkt && g.anon.ModForum); return; } style_header("Pending Moderation Requests"); @

    All Pending Moderation Requests

    if( moderation_table_exists() ){ Index: src/name.c ================================================================== --- src/name.c +++ src/name.c @@ -94,19 +94,19 @@ ** * "next" ** ** Return the RID of the matching artifact. Or return 0 if the name does not ** match any known object. Or return -1 if the name is ambiguous. ** -** The zType parameter specifies the type of artifact: ci, t, w, e, g. +** The zType parameter specifies the type of artifact: ci, t, w, e, g, f. ** If zType is NULL or "" or "*" then any type of artifact will serve. ** If zType is "br" then find the first check-in of the named branch ** rather than the last. ** zType is "ci" in most use cases since we are usually searching for ** a check-in. ** ** Note that the input zTag for types "t" and "e" is the artifact hash of -** the ticket-change or event-change artifact, not the randomly generated +** the ticket-change or technote-change artifact, not the randomly generated ** hexadecimal identifier assigned to tickets and events. Those identifiers ** live in a separate namespace. */ int symbolic_name_to_rid(const char *zTag, const char *zType){ int vid; @@ -603,11 +603,12 @@ if( db_step(&q)==SQLITE_ROW ){ const char *zType; switch( db_column_text(&q,0)[0] ){ case 'c': zType = "Check-in"; break; case 'w': zType = "Wiki-edit"; break; - case 'e': zType = "Event"; break; + case 'e': zType = "Technote"; break; + case 'f': zType = "Forum-post"; break; case 't': zType = "Ticket-change"; break; case 'g': zType = "Tag-change"; break; default: zType = "Unknown"; break; } fossil_print("type: %s by %s on %s\n", zType, db_column_text(&q,2), @@ -926,10 +927,25 @@ " WHERE (blob.rid %s)\n" " AND blob.rid NOT IN (SELECT rid FROM description)\n" " AND blob.uuid=attachment.src", zWhere /*safe-for-%s*/ ); + + /* Forum posts */ + if( db_table_exists("repository","forumpost") ){ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,ctime,type,summary)\n" + "SELECT postblob.rid, postblob.uuid, forumpost.fmtime, 'forumpost',\n" + " CASE WHEN fpid=froot THEN 'forum-post '\n" + " ELSE 'forum-reply-to ' END || substr(rootblob.uuid,1,14)\n" + " FROM forumpost, blob AS postblob, blob AS rootblob\n" + " WHERE (forumpost.fpid %s)\n" + " AND postblob.rid=forumpost.fpid" + " AND rootblob.rid=forumpost.froot", + zWhere /*safe-for-%s*/ + ); + } /* Everything else */ db_multi_exec( "INSERT OR IGNORE INTO description(rid,uuid,type,summary)\n" "SELECT blob.rid, blob.uuid," Index: src/popen.c ================================================================== --- src/popen.c +++ src/popen.c @@ -25,11 +25,11 @@ #include /* ** Print a fatal error and quit. */ static void win32_fatal_error(const char *zMsg){ - fossil_panic("%s", zMsg); + fossil_fatal("%s", zMsg); } #else #include #include #endif Index: src/printf.c ================================================================== --- src/printf.c +++ src/printf.c @@ -1085,12 +1085,16 @@ mainInFatalError = 1; db_force_rollback(); va_start(ap, zFormat); sqlite3_vsnprintf(sizeof(z),z,zFormat, ap); va_end(ap); + if( g.fAnyTrace ){ + fprintf(stderr, "/***** panic on %d *****/\n", getpid()); + } fossil_errorlog("panic: %s", z); rc = fossil_print_error(rc, z); + abort(); exit(rc); } NORETURN void fossil_fatal(const char *zFormat, ...){ char *z; int rc = 1; Index: src/schema.c ================================================================== --- src/schema.c +++ src/schema.c @@ -290,14 +290,20 @@ @ -- and so it makes sense to precompute the set of leaves. There is @ -- one entry in the following table for each leaf. @ -- @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY); @ -@ -- Events used to generate a timeline +@ -- Events used to generate a timeline. Type meanings: +@ -- ci Check-ins +@ -- e Technotes +@ -- f Forum posts +@ -- g Tags +@ -- t Ticket changes +@ -- w Wiki page edit @ -- @ CREATE TABLE event( -@ type TEXT, -- Type of event: 'ci', 'w', 'e', 't', 'g' +@ type TEXT, -- Type of event: ci, e, f, g, t, w @ mtime DATETIME, -- Time of occurrence. Julian day. @ objid INTEGER PRIMARY KEY, -- Associated record ID @ tagid INTEGER, -- Associated ticket or wiki name tag @ uid INTEGER REFERENCES user, -- User who caused the event @ bgcolor TEXT, -- Color set by 'bgcolor' property @@ -542,5 +548,28 @@ @ @ -- Identifier for this file type. @ -- The integer is the same as 'FSLC'. @ PRAGMA application_id=252006674; ; + +/* +** The following table holds information about forum posts. It +** is created on-demand whenever the manifest parser encounters +** a forum-post artifact. +*/ +static const char zForumSchema[] = +@ CREATE TABLE repository.forumpost( +@ fpid INTEGER PRIMARY KEY, -- BLOB.rid for the artifact +@ froot INT, -- fpid of the thread root +@ fprev INT, -- Previous version of this same post +@ firt INT, -- This post is in-reply-to +@ fmtime REAL -- When posted. Julian day +@ ); +@ CREATE INDEX repository.forumthread ON forumpost(froot); +; + +/* Create the forum-post schema if it does not already exist */ +void schema_forum(void){ + if( !db_table_exists("repository","forumpost") ){ + db_multi_exec("%s",zForumSchema/*safe-for-%s*/); + } +} Index: src/search.c ================================================================== --- src/search.c +++ src/search.c @@ -638,11 +638,12 @@ #define SRCH_CKIN 0x0001 /* Search over check-in comments */ #define SRCH_DOC 0x0002 /* Search over embedded documents */ #define SRCH_TKT 0x0004 /* Search over tickets */ #define SRCH_WIKI 0x0008 /* Search over wiki */ #define SRCH_TECHNOTE 0x0010 /* Search over tech notes */ -#define SRCH_ALL 0x001f /* Search over everything */ +#define SRCH_FORUM 0x0020 /* Search over forum messages */ +#define SRCH_ALL 0x003f /* Search over everything */ #endif /* ** Remove bits from srchFlags which are disallowed by either the ** current server configuration or by user permissions. @@ -654,15 +655,17 @@ { SRCH_CKIN, "search-ci" }, { SRCH_DOC, "search-doc" }, { SRCH_TKT, "search-tkt" }, { SRCH_WIKI, "search-wiki" }, { SRCH_TECHNOTE, "search-technote" }, + { SRCH_FORUM, "search-forum" }, }; int i; if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC|SRCH_TECHNOTE); if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT); if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI); + if( g.perm.RdForum==0) srchFlags &= ~(SRCH_FORUM); for(i=0; i }else{ @
    } @ - if( useYparam && (srchFlags & (srchFlags-1))!=0 && useYparam ){ + if( (mFlags & 0x01)!=0 && (srchFlags & (srchFlags-1))!=0 ){ static const struct { char *z; char *zNm; unsigned m; } aY[] = { { "all", "All", SRCH_ALL }, { "c", "Check-ins", SRCH_CKIN }, { "d", "Docs", SRCH_DOC }, { "t", "Tickets", SRCH_TKT }, { "w", "Wiki", SRCH_WIKI }, { "e", "Tech Notes", SRCH_TECHNOTE }, + { "f", "Forum", SRCH_FORUM }, }; const char *zY = PD("y","all"); unsigned newFlags = srchFlags; int i; @ + stats_for_email(); + @
    + }else{ + @
  • Email alerts are disabled + } @ style_footer(); } Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -296,91 +296,10 @@ 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
    DDebug: Enable debugging features
    -} - /* ** 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 @@ -417,11 +336,11 @@ @ anonymous, and @ nobody. @

  • @ @
  • The permission flags are as follows:

    - setup_usercap_table(); + capabilities_table(); @
  • @ style_footer(); } @@ -431,11 +350,11 @@ ** 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(); + capabilities_table(); style_footer(); } /* ** Return true if zPw is a valid password string. A valid @@ -1312,10 +1231,12 @@ "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.
    + @

    Recommended value: "u" for Reader. + @ Capability Key. @ (Property: "default-perms") @

    @
    onoff_attribute("Show javascript button to fill in CAPTCHA", @@ -2312,10 +2233,12 @@ 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); + @
    + onoff_attribute("Search Forum", "search-forum", "sf", 0, 0); @
    @

    @
    if( P("fts0") ){ search_drop_index(); Index: src/shell.c ================================================================== --- src/shell.c +++ src/shell.c @@ -95,16 +95,19 @@ # endif #endif #if (!defined(_WIN32) && !defined(WIN32)) || defined(__MINGW32__) # include # include +# define GETPID getpid # if defined(__MINGW32__) # define DIRENT dirent # ifndef S_ISLNK # define S_ISLNK(mode) (0) # endif # endif +#else +# define GETPID (int)GetCurrentProcessId #endif #include #include #if HAVE_READLINE @@ -15847,10 +15850,27 @@ setBinaryMode(stdin, 0); setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */ stdin_is_interactive = isatty(0); stdout_is_console = isatty(1); + +#if !defined(_WIN32_WCE) + if( getenv("SQLITE_DEBUG_BREAK") ){ + if( isatty(0) && isatty(2) ){ + fprintf(stderr, + "attach debugger to process %d and press any key to continue.\n", + GETPID()); + fgetc(stdin); + }else{ +#if defined(_WIN32) || defined(WIN32) + DebugBreak(); +#elif defined(SIGTRAP) + raise(SIGTRAP); +#endif + } + } +#endif #if USE_SYSTEM_SQLITE+0!=1 if( strncmp(sqlite3_sourceid(),SQLITE_SOURCE_ID,60)!=0 ){ utf8_printf(stderr, "SQLite header and source version mismatch\n%s\n%s\n", sqlite3_sourceid(), SQLITE_SOURCE_ID); Index: src/skins.c ================================================================== --- src/skins.c +++ src/skins.c @@ -254,11 +254,11 @@ */ const char *skin_detail(const char *zName){ struct SkinDetail *pDetail; skin_detail_initialize(); pDetail = skin_detail_find(zName); - if( pDetail==0 ) fossil_panic("no such skin detail: %s", zName); + if( pDetail==0 ) fossil_fatal("no such skin detail: %s", zName); return pDetail->zValue; } int skin_detail_boolean(const char *zName){ return !is_false(skin_detail(zName)); } Index: src/smtp.c ================================================================== --- src/smtp.c +++ src/smtp.c @@ -1271,11 +1271,11 @@ smtp_server_send(&x, "250 ok\r\n"); }else if( strncmp(z, "MAIL FROM:<", 11)==0 ){ smtp_server_route_incoming(&x, 0); smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); - x.zFrom = email_copy_addr(z+11); + x.zFrom = email_copy_addr(z+11,'>'); if( x.zFrom==0 ){ smtp_server_send(&x, "500 unacceptable email address\r\n"); }else{ smtp_server_send(&x, "250 ok\r\n"); } @@ -1284,11 +1284,11 @@ char *zAddr; if( x.zFrom==0 ){ smtp_server_send(&x, "500 missing MAIL FROM\r\n"); continue; } - zAddr = email_copy_addr(z+9); + zAddr = email_copy_addr(z+9, '>'); if( zAddr==0 ){ smtp_server_send(&x, "505 no such user\r\n"); continue; } smtp_append_to(&x, zAddr, 0); Index: src/sqlite3.c ================================================================== --- src/sqlite3.c +++ src/sqlite3.c @@ -53,10 +53,16 @@ /* These macros are provided to "stringify" the value of the define ** for those options in which the value is meaningful. */ #define CTIMEOPT_VAL_(opt) #opt #define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt) +/* Like CTIMEOPT_VAL, but especially for SQLITE_DEFAULT_LOOKASIDE. This +** option requires a separate macro because legal values contain a single +** comma. e.g. (-DSQLITE_DEFAULT_LOOKASIDE="100,100") */ +#define CTIMEOPT_VAL2_(opt1,opt2) #opt1 "," #opt2 +#define CTIMEOPT_VAL2(opt) CTIMEOPT_VAL2_(opt) + /* ** An array of names of all compile-time options. This array should ** be sorted A-Z. ** ** This array looks large, but in a typical installation actually uses @@ -136,11 +142,11 @@ #endif #ifdef SQLITE_DEFAULT_LOCKING_MODE "DEFAULT_LOCKING_MODE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOCKING_MODE), #endif #ifdef SQLITE_DEFAULT_LOOKASIDE - "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOOKASIDE), + "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL2(SQLITE_DEFAULT_LOOKASIDE), #endif #if SQLITE_DEFAULT_MEMSTATUS "DEFAULT_MEMSTATUS", #endif #ifdef SQLITE_DEFAULT_MMAP_SIZE @@ -1150,11 +1156,11 @@ ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ #define SQLITE_VERSION "3.25.0" #define SQLITE_VERSION_NUMBER 3025000 -#define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f" +#define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c" /* ** CAPI3REF: Run-Time Library Version Numbers ** KEYWORDS: sqlite3_version sqlite3_sourceid ** @@ -1912,11 +1918,12 @@ ** interrogated. The zDbName parameter is ignored. ** **
  • [[SQLITE_FCNTL_PERSIST_WAL]] ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary -** write ahead log and shared memory files used for transaction control +** write ahead log ([WAL file]) and shared memory +** files used for transaction control ** are automatically deleted when the latest connection to the database ** closes. Setting persistent WAL mode causes those files to persist after ** close. Persisting the files is useful when other processes that do not ** have write permission on the directory containing the database file want ** to read the database file, as the WAL and shared memory files must exist @@ -9985,11 +9992,10 @@ SQLITE_API int sqlite3_system_errno(sqlite3*); /* ** CAPI3REF: Database Snapshot ** KEYWORDS: {snapshot} {sqlite3_snapshot} -** EXPERIMENTAL ** ** An instance of the snapshot object records the state of a [WAL mode] ** database for some specific point in history. ** ** In [WAL mode], multiple [database connections] that are open on the @@ -10002,23 +10008,18 @@ ** ** The sqlite3_snapshot object records state information about an historical ** version of the database file so that it is possible to later open a new read ** transaction that sees that historical version of the database rather than ** the most recent version. -** -** The constructor for this object is [sqlite3_snapshot_get()]. The -** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer -** to an historical snapshot (if possible). The destructor for -** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. */ typedef struct sqlite3_snapshot { unsigned char hidden[48]; } sqlite3_snapshot; /* ** CAPI3REF: Record A Database Snapshot -** EXPERIMENTAL +** CONSTRUCTOR: sqlite3_snapshot ** ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a ** new [sqlite3_snapshot] object that records the current state of ** schema S in database connection D. ^On success, the ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly @@ -10053,21 +10054,21 @@ ** The [sqlite3_snapshot] object returned from a successful call to ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] ** to avoid a memory leak. ** ** The [sqlite3_snapshot_get()] interface is only available when the -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( sqlite3 *db, const char *zSchema, sqlite3_snapshot **ppSnapshot ); /* ** CAPI3REF: Start a read transaction on an historical snapshot -** EXPERIMENTAL +** METHOD: sqlite3_snapshot ** ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a ** read transaction for schema S of ** [database connection] D such that the read transaction ** refers to historical [snapshot] P, rather than the most @@ -10091,34 +10092,34 @@ ** after the most recent I/O on the database connection.)^ ** (Hint: Run "[PRAGMA application_id]" against a newly opened ** database connection in order to make it ready to use snapshots.) ** ** The [sqlite3_snapshot_open()] interface is only available when the -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( sqlite3 *db, const char *zSchema, sqlite3_snapshot *pSnapshot ); /* ** CAPI3REF: Destroy a snapshot -** EXPERIMENTAL +** DESTRUCTOR: sqlite3_snapshot ** ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. ** The application must eventually free every [sqlite3_snapshot] object ** using this routine to avoid a memory leak. ** ** The [sqlite3_snapshot_free()] interface is only available when the -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. */ SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); /* ** CAPI3REF: Compare the ages of two snapshot handles. -** EXPERIMENTAL +** METHOD: sqlite3_snapshot ** ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages ** of two valid snapshot handles. ** ** If the two snapshot handles are not associated with the same database @@ -10133,35 +10134,41 @@ ** is undefined. ** ** Otherwise, this API returns a negative value if P1 refers to an older ** snapshot than P2, zero if the two handles refer to the same database ** snapshot, and a positive value if P1 is a newer snapshot than P2. +** +** This interface is only available if SQLite is compiled with the +** [SQLITE_ENABLE_SNAPSHOT] option. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( sqlite3_snapshot *p1, sqlite3_snapshot *p2 ); /* ** CAPI3REF: Recover snapshots from a wal file -** EXPERIMENTAL -** -** If all connections disconnect from a database file but do not perform -** a checkpoint, the existing wal file is opened along with the database -** file the next time the database is opened. At this point it is only -** possible to successfully call sqlite3_snapshot_open() to open the most -** recent snapshot of the database (the one at the head of the wal file), -** even though the wal file may contain other valid snapshots for which -** clients have sqlite3_snapshot handles. -** -** This function attempts to scan the wal file associated with database zDb +** METHOD: sqlite3_snapshot +** +** If a [WAL file] remains on disk after all database connections close +** (either through the use of the [SQLITE_FCNTL_PERSIST_WAL] [file control] +** or because the last process to have the database opened exited without +** calling [sqlite3_close()]) and a new connection is subsequently opened +** on that database and [WAL file], the [sqlite3_snapshot_open()] interface +** will only be able to open the last transaction added to the WAL file +** even though the WAL file contains other valid transactions. +** +** This function attempts to scan the WAL file associated with database zDb ** of database handle db and make all valid snapshots available to ** sqlite3_snapshot_open(). It is an error if there is already a read -** transaction open on the database, or if the database is not a wal mode +** transaction open on the database, or if the database is not a WAL mode ** database. ** ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. +** +** This interface is only available if SQLite is compiled with the +** [SQLITE_ENABLE_SNAPSHOT] option. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); /* ** CAPI3REF: Serialize a database @@ -18937,11 +18944,11 @@ SQLITE_PRIVATE void sqlite3Reindex(Parse*, Token*, Token*); SQLITE_PRIVATE void sqlite3AlterFunctions(void); SQLITE_PRIVATE void sqlite3AlterRenameTable(Parse*, SrcList*, Token*); SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *, int *); SQLITE_PRIVATE void sqlite3NestedParse(Parse*, const char*, ...); -SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*); +SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*, int); SQLITE_PRIVATE int sqlite3CodeSubselect(Parse*, Expr *, int, int); SQLITE_PRIVATE void sqlite3SelectPrep(Parse*, Select*, NameContext*); SQLITE_PRIVATE void sqlite3SelectWrongNumTermsError(Parse *pParse, Select *p); SQLITE_PRIVATE int sqlite3MatchSpanName(const char*, const char*, const char*, const char*); SQLITE_PRIVATE int sqlite3ResolveExprNames(NameContext*, Expr*); @@ -20025,13 +20032,13 @@ #endif u16 nResColumn; /* Number of columns in one row of the result set */ u8 errorAction; /* Recovery action to do in case of an error */ u8 minWriteFileFormat; /* Minimum file format for writable database files */ u8 prepFlags; /* SQLITE_PREPARE_* flags */ - bft expired:1; /* True if the VM needs to be recompiled */ - bft doingRerun:1; /* True if rerunning after an auto-reprepare */ + bft expired:2; /* 1: recompile VM immediately 2: when convenient */ bft explain:2; /* True if EXPLAIN present on SQL command */ + bft doingRerun:1; /* True if rerunning after an auto-reprepare */ bft changeCntOn:1; /* True to update the change-counter */ bft runOnlyOnce:1; /* Automatically expire on reset */ bft usesStmtJournal:1; /* True if uses a statement journal */ bft readOnly:1; /* True for statements that do not write */ bft bIsReader:1; /* True for statements that read */ @@ -28640,11 +28647,11 @@ sqlite3TreeViewLine(pView, "FUNCTION %Q", pExpr->u.zToken); } if( pFarg ){ sqlite3TreeViewExprList(pView, pFarg, pWin!=0, 0); } -#ifndef SQLITe_OMIT_WINDOWFUNC +#ifndef SQLITE_OMIT_WINDOWFUNC if( pWin ){ sqlite3TreeViewWindow(pView, pWin, 0); } #endif break; @@ -42948,10 +42955,13 @@ */ static int winTruncate(sqlite3_file *id, sqlite3_int64 nByte){ winFile *pFile = (winFile*)id; /* File handle object */ int rc = SQLITE_OK; /* Return code for this function */ DWORD lastErrno; +#if SQLITE_MAX_MMAP_SIZE>0 + sqlite3_int64 oldMmapSize; +#endif assert( pFile ); SimulateIOError(return SQLITE_IOERR_TRUNCATE); OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, size=%lld, lock=%d\n", osGetCurrentProcessId(), pFile, pFile->h, nByte, pFile->locktype)); @@ -42962,10 +42972,19 @@ ** size). */ if( pFile->szChunk>0 ){ nByte = ((nByte + pFile->szChunk - 1)/pFile->szChunk) * pFile->szChunk; } + +#if SQLITE_MAX_MMAP_SIZE>0 + if( pFile->pMapRegion ){ + oldMmapSize = pFile->mmapSize; + }else{ + oldMmapSize = 0; + } + winUnmapfile(pFile); +#endif /* SetEndOfFile() returns non-zero when successful, or zero when it fails. */ if( winSeekFile(pFile, nByte) ){ rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, "winTruncate1", pFile->zPath); @@ -42975,16 +42994,16 @@ rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, "winTruncate2", pFile->zPath); } #if SQLITE_MAX_MMAP_SIZE>0 - /* If the file was truncated to a size smaller than the currently - ** mapped region, reduce the effective mapping size as well. SQLite will - ** use read() and write() to access data beyond this point from now on. - */ - if( pFile->pMapRegion && nBytemmapSize ){ - pFile->mmapSize = nByte; + if( rc==SQLITE_OK && oldMmapSize>0 ){ + if( oldMmapSize>nByte ){ + winMapfile(pFile, -1); + }else{ + winMapfile(pFile, oldMmapSize); + } } #endif OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, rc=%s\n", osGetCurrentProcessId(), pFile, pFile->h, sqlite3ErrName(rc))); @@ -56461,22 +56480,22 @@ if( (rc&0xFF)==SQLITE_IOERR && rc!=SQLITE_IOERR_NOMEM ){ rc = sqlite3JournalCreate(pPager->jfd); if( rc!=SQLITE_OK ){ sqlite3OsClose(pPager->jfd); + goto commit_phase_one_exit; } bBatch = 0; }else{ sqlite3OsClose(pPager->jfd); } } #endif /* SQLITE_ENABLE_BATCH_ATOMIC_WRITE */ - if( bBatch==0 && rc==SQLITE_OK ){ + if( bBatch==0 ){ rc = pager_write_pagelist(pPager, pList); } - if( rc!=SQLITE_OK ){ assert( rc!=SQLITE_IOERR_BLOCKED ); goto commit_phase_one_exit; } sqlite3PcacheCleanAll(pPager->pPCache); @@ -65751,10 +65770,16 @@ */ if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){ goto trans_begun; } assert( pBt->inTransaction==TRANS_WRITE || IfNotOmitAV(pBt->bDoTruncate)==0 ); + + if( (p->db->flags & SQLITE_ResetDatabase) + && sqlite3PagerIsreadonly(pBt->pPager)==0 + ){ + pBt->btsFlags &= ~BTS_READ_ONLY; + } /* Write transactions are not possible on a read-only database */ if( (pBt->btsFlags & BTS_READ_ONLY)!=0 && wrflag ){ rc = SQLITE_READONLY; goto trans_begun; @@ -71767,12 +71792,11 @@ ** if this is the first reference to the page. ** ** Also check that the page number is in bounds. */ static int checkRef(IntegrityCk *pCheck, Pgno iPage){ - if( iPage==0 ) return 1; - if( iPage>pCheck->nPage ){ + if( iPage>pCheck->nPage || iPage==0 ){ checkAppendMsg(pCheck, "invalid page number %d", iPage); return 1; } if( getPageReferenced(pCheck, iPage) ){ checkAppendMsg(pCheck, "2nd reference to page %d", iPage); @@ -71823,21 +71847,16 @@ int iPage, /* Page number for first page in the list */ int N /* Expected number of pages in the list */ ){ int i; int expected = N; - int iFirst = iPage; - while( N-- > 0 && pCheck->mxErr ){ + int nErrAtStart = pCheck->nErr; + while( iPage!=0 && pCheck->mxErr ){ DbPage *pOvflPage; unsigned char *pOvflData; - if( iPage<1 ){ - checkAppendMsg(pCheck, - "%d of %d pages missing from overflow list starting at %d", - N+1, expected, iFirst); - break; - } if( checkRef(pCheck, iPage) ) break; + N--; if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage, 0) ){ checkAppendMsg(pCheck, "failed to get page %d", iPage); break; } pOvflData = (unsigned char *)sqlite3PagerGetData(pOvflPage); @@ -71877,14 +71896,16 @@ } } #endif iPage = get4byte(pOvflData); sqlite3PagerUnref(pOvflPage); - - if( isFreeList && N<(iPage!=0) ){ - checkAppendMsg(pCheck, "free-page count in header is too small"); - } + } + if( N && nErrAtStart==pCheck->nErr ){ + checkAppendMsg(pCheck, + "%s is %d but should be %d", + isFreeList ? "size" : "overflow list length", + expected-N, expected); } } #endif /* SQLITE_OMIT_INTEGRITY_CHECK */ /* @@ -72274,10 +72295,28 @@ get4byte(&pBt->pPage1->aData[36])); sCheck.zPfx = 0; /* Check all the tables. */ +#ifndef SQLITE_OMIT_AUTOVACUUM + if( pBt->autoVacuum ){ + int mx = 0; + int mxInHdr; + for(i=0; (int)ipPage1->aData[52]); + if( mx!=mxInHdr ){ + checkAppendMsg(&sCheck, + "max rootpage (%d) disagrees with header (%d)", + mx, mxInHdr + ); + } + }else if( get4byte(&pBt->pPage1->aData[64])!=0 ){ + checkAppendMsg(&sCheck, + "incremental_vacuum enabled with a max rootpage of zero" + ); + } +#endif testcase( pBt->db->flags & SQLITE_CellSizeCk ); pBt->db->flags &= ~SQLITE_CellSizeCk; for(i=0; (int)ipVdbe; p; p=p->pNext){ - p->expired = 1; + p->expired = iCode+1; } } /* ** Return the database associated with the Vdbe. @@ -85520,11 +85567,11 @@ if( rc!=SQLITE_OK ){ goto abort_due_to_error; } } if( isSchemaChange ){ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); sqlite3ResetAllSchemasOfConnection(db); db->mDbFlags |= DBFLAG_SchemaChange; } } @@ -85809,11 +85856,11 @@ pDb->pSchema->file_format = pOp->p3; } if( pOp->p1==1 ){ /* Invalidate all prepared statements whenever the TEMP database ** schema is changed. Ticket #1644 */ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); p->expired = 0; } if( rc ) goto abort_due_to_error; break; } @@ -85927,11 +85974,11 @@ assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ ); assert( p->bIsReader ); assert( pOp->opcode==OP_OpenRead || pOp->opcode==OP_ReopenIdx || p->readOnly==0 ); - if( p->expired ){ + if( p->expired==1 ){ rc = SQLITE_ABORT_ROLLBACK; goto abort_due_to_error; } nField = 0; @@ -89108,25 +89155,32 @@ } break; } #endif -/* Opcode: Expire P1 * * * * +/* Opcode: Expire P1 P2 * * * ** ** Cause precompiled statements to expire. When an expired statement ** is executed using sqlite3_step() it will either automatically ** reprepare itself (if it was originally created using sqlite3_prepare_v2()) ** or it will fail with SQLITE_SCHEMA. ** ** If P1 is 0, then all SQL statements become expired. If P1 is non-zero, ** then only the currently executing statement is expired. +** +** If P2 is 0, then SQL statements are expired immediately. If P2 is 1, +** then running SQL statements are allowed to continue to run to completion. +** The P2==1 case occurs when a CREATE INDEX or similar schema change happens +** that might help the statement run faster but which does not affect the +** correctness of operation. */ case OP_Expire: { + assert( pOp->p2==0 || pOp->p2==1 ); if( !pOp->p1 ){ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, pOp->p2); }else{ - p->expired = 1; + p->expired = pOp->p2+1; } break; } #ifndef SQLITE_OMIT_SHARED_CACHE @@ -103813,11 +103867,11 @@ if( !pIdx->hasStat1 ) sqlite3DefaultRowEst(pIdx); } /* Load the statistics from the sqlite_stat4 table. */ #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 - if( rc==SQLITE_OK && OptimizationEnabled(db, SQLITE_Stat34) ){ + if( rc==SQLITE_OK ){ db->lookaside.bDisable++; rc = loadStat4(db, sInfo.zDatabase); db->lookaside.bDisable--; } for(i=sqliteHashFirst(&pSchema->idxHash); i; i=sqliteHashNext(i)){ @@ -104545,11 +104599,11 @@ if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; #endif sqlite3_mutex_enter(db->mutex); db->xAuth = (sqlite3_xauth)xAuth; db->pAuthArg = pArg; - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); sqlite3_mutex_leave(db->mutex); return SQLITE_OK; } /* @@ -108209,11 +108263,11 @@ if( pTblName ){ sqlite3RefillIndex(pParse, pIndex, iMem); sqlite3ChangeCookie(pParse, iDb); sqlite3VdbeAddParseSchemaOp(v, iDb, sqlite3MPrintf(db, "name='%q' AND type='index'", pIndex->zName)); - sqlite3VdbeAddOp0(v, OP_Expire); + sqlite3VdbeAddOp2(v, OP_Expire, 0, 1); } sqlite3VdbeJumpHere(v, pIndex->tnum); } @@ -131894,11 +131948,11 @@ assert( sqlite3BtreeHoldsAllMutexes(db) ); assert( sqlite3_mutex_held(db->mutex) ); if( p ){ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); do { VTable *pNext = p->pNext; sqlite3VtabUnlock(p); p = pNext; }while( p ); @@ -138680,11 +138734,13 @@ #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 Index *p = pLoop->u.btree.pIndex; int nEq = pLoop->u.btree.nEq; - if( p->nSample>0 && nEqnSampleCol ){ + if( p->nSample>0 && nEqnSampleCol + && OptimizationEnabled(pParse->db, SQLITE_Stat34) + ){ if( nEq==pBuilder->nRecValid ){ UnpackedRecord *pRec = pBuilder->pRec; tRowcnt a[2]; int nBtm = pLoop->u.btree.nBtm; int nTop = pLoop->u.btree.nTop; @@ -139828,10 +139884,11 @@ tRowcnt nOut = 0; if( nInMul==0 && pProbe->nSample && pNew->u.btree.nEq<=pProbe->nSampleCol && ((eOp & WO_IN)==0 || !ExprHasProperty(pTerm->pExpr, EP_xIsSelect)) + && OptimizationEnabled(db, SQLITE_Stat34) ){ Expr *pExpr = pTerm->pExpr; if( (eOp & (WO_EQ|WO_ISNULL|WO_IS))!=0 ){ testcase( eOp & WO_EQ ); testcase( eOp & WO_IS ); @@ -146916,11 +146973,11 @@ /* ** Find the appropriate action for a parser given the non-terminal ** look-ahead token iLookAhead. */ -static int yy_find_reduce_action( +static YYACTIONTYPE yy_find_reduce_action( YYACTIONTYPE stateno, /* Current state number */ YYCODETYPE iLookAhead /* The look-ahead token */ ){ int i; #ifdef YYERRORSYMBOL @@ -147421,11 +147478,11 @@ int yyLookahead, /* Lookahead token, or YYNOCODE if none */ sqlite3ParserTOKENTYPE yyLookaheadToken /* Value of the lookahead token */ sqlite3ParserCTX_PDECL /* %extra_context */ ){ int yygoto; /* The next state */ - int yyact; /* The next action */ + YYACTIONTYPE yyact; /* The next action */ yyStackEntry *yymsp; /* The top of the parser's stack */ int yysize; /* Amount to pop the stack */ sqlite3ParserARG_FETCH (void)yyLookahead; (void)yyLookaheadToken; @@ -148980,16 +149037,16 @@ } #endif do{ assert( yyact==yypParser->yytos->stateno ); - yyact = yy_find_shift_action(yymajor,yyact); + yyact = yy_find_shift_action((YYCODETYPE)yymajor,yyact); if( yyact >= YY_MIN_REDUCE ){ yyact = yy_reduce(yypParser,yyact-YY_MIN_REDUCE,yymajor, yyminor sqlite3ParserCTX_PARAM); }else if( yyact <= YY_MAX_SHIFTREDUCE ){ - yy_shift(yypParser,yyact,yymajor,yyminor); + yy_shift(yypParser,yyact,(YYCODETYPE)yymajor,yyminor); #ifndef YYNOERRORRECOVERY yypParser->yyerrcnt--; #endif break; }else if( yyact==YY_ACCEPT_ACTION ){ @@ -151421,11 +151478,11 @@ db->flags |= aFlagOp[i].mask; }else if( onoff==0 ){ db->flags &= ~aFlagOp[i].mask; } if( oldFlags!=db->flags ){ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); } if( pRes ){ *pRes = (db->flags & aFlagOp[i].mask)!=0; } rc = SQLITE_OK; @@ -151864,11 +151921,11 @@ } sqlite3VtabRollback(db); sqlite3EndBenignMalloc(); if( schemaChange ){ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); sqlite3ResetAllSchemasOfConnection(db); } sqlite3BtreeLeaveAll(db); /* Any deferred constraint violations have now been resolved. */ @@ -152319,11 +152376,11 @@ sqlite3ErrorWithMsg(db, SQLITE_BUSY, "unable to delete/modify user-function due to active statements"); assert( !db->mallocFailed ); return SQLITE_BUSY; }else{ - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); } } p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 1); assert(p || db->mallocFailed); @@ -153094,11 +153151,11 @@ if( db->nVdbeActive ){ sqlite3ErrorWithMsg(db, SQLITE_BUSY, "unable to delete/modify collation sequence due to active statements"); return SQLITE_BUSY; } - sqlite3ExpirePreparedStatements(db); + sqlite3ExpirePreparedStatements(db, 0); /* If collation sequence pColl was created directly by a call to ** sqlite3_create_collation, and not generated by synthCollSeq(), ** then any copies made by synthCollSeq() need to be invalidated. ** Also, collation destructor - CollSeq.xDel() - function may need @@ -194031,16 +194088,18 @@ rc = sqlite3_create_function(db, aFunc[i].zName, aFunc[i].nArg, SQLITE_UTF8 | SQLITE_DETERMINISTIC, (void*)&aFunc[i].flag, aFunc[i].xFunc, 0, 0); } +#ifndef SQLITE_OMIT_WINDOWFUNC for(i=0; ifts5yytos->stateno ); - fts5yyact = fts5yy_find_shift_action(fts5yymajor,fts5yyact); + fts5yyact = fts5yy_find_shift_action((fts5YYCODETYPE)fts5yymajor,fts5yyact); if( fts5yyact >= fts5YY_MIN_REDUCE ){ fts5yyact = fts5yy_reduce(fts5yypParser,fts5yyact-fts5YY_MIN_REDUCE,fts5yymajor, fts5yyminor sqlite3Fts5ParserCTX_PARAM); }else if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){ - fts5yy_shift(fts5yypParser,fts5yyact,fts5yymajor,fts5yyminor); + fts5yy_shift(fts5yypParser,fts5yyact,(fts5YYCODETYPE)fts5yymajor,fts5yyminor); #ifndef fts5YYNOERRORRECOVERY fts5yypParser->fts5yyerrcnt--; #endif break; }else if( fts5yyact==fts5YY_ACCEPT_ACTION ){ @@ -211513,11 +211572,11 @@ int nArg, /* Number of args */ sqlite3_value **apUnused /* Function arguments */ ){ assert( nArg==0 ); UNUSED_PARAM2(nArg, apUnused); - sqlite3_result_text(pCtx, "fts5: 2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f", -1, SQLITE_TRANSIENT); + sqlite3_result_text(pCtx, "fts5: 2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c", -1, SQLITE_TRANSIENT); } static int fts5Init(sqlite3 *db){ static const sqlite3_module fts5Mod = { /* iVersion */ 2, @@ -214799,11 +214858,11 @@ int iTbl = 0; while( i<128 ){ int bToken = aArray[ aFts5UnicodeData[iTbl] & 0x1F ]; int n = (aFts5UnicodeData[iTbl] >> 5) + i; for(; i<128 && i[[SQLITE_FCNTL_PERSIST_WAL]] ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary -** write ahead log and shared memory files used for transaction control +** write ahead log ([WAL file]) and shared memory +** files used for transaction control ** are automatically deleted when the latest connection to the database ** closes. Setting persistent WAL mode causes those files to persist after ** close. Persisting the files is useful when other processes that do not ** have write permission on the directory containing the database file want ** to read the database file, as the WAL and shared memory files must exist @@ -8958,11 +8959,10 @@ SQLITE_API int sqlite3_system_errno(sqlite3*); /* ** CAPI3REF: Database Snapshot ** KEYWORDS: {snapshot} {sqlite3_snapshot} -** EXPERIMENTAL ** ** An instance of the snapshot object records the state of a [WAL mode] ** database for some specific point in history. ** ** In [WAL mode], multiple [database connections] that are open on the @@ -8975,23 +8975,18 @@ ** ** The sqlite3_snapshot object records state information about an historical ** version of the database file so that it is possible to later open a new read ** transaction that sees that historical version of the database rather than ** the most recent version. -** -** The constructor for this object is [sqlite3_snapshot_get()]. The -** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer -** to an historical snapshot (if possible). The destructor for -** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. */ typedef struct sqlite3_snapshot { unsigned char hidden[48]; } sqlite3_snapshot; /* ** CAPI3REF: Record A Database Snapshot -** EXPERIMENTAL +** CONSTRUCTOR: sqlite3_snapshot ** ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a ** new [sqlite3_snapshot] object that records the current state of ** schema S in database connection D. ^On success, the ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly @@ -9026,21 +9021,21 @@ ** The [sqlite3_snapshot] object returned from a successful call to ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] ** to avoid a memory leak. ** ** The [sqlite3_snapshot_get()] interface is only available when the -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( sqlite3 *db, const char *zSchema, sqlite3_snapshot **ppSnapshot ); /* ** CAPI3REF: Start a read transaction on an historical snapshot -** EXPERIMENTAL +** METHOD: sqlite3_snapshot ** ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a ** read transaction for schema S of ** [database connection] D such that the read transaction ** refers to historical [snapshot] P, rather than the most @@ -9064,34 +9059,34 @@ ** after the most recent I/O on the database connection.)^ ** (Hint: Run "[PRAGMA application_id]" against a newly opened ** database connection in order to make it ready to use snapshots.) ** ** The [sqlite3_snapshot_open()] interface is only available when the -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( sqlite3 *db, const char *zSchema, sqlite3_snapshot *pSnapshot ); /* ** CAPI3REF: Destroy a snapshot -** EXPERIMENTAL +** DESTRUCTOR: sqlite3_snapshot ** ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. ** The application must eventually free every [sqlite3_snapshot] object ** using this routine to avoid a memory leak. ** ** The [sqlite3_snapshot_free()] interface is only available when the -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. */ SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); /* ** CAPI3REF: Compare the ages of two snapshot handles. -** EXPERIMENTAL +** METHOD: sqlite3_snapshot ** ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages ** of two valid snapshot handles. ** ** If the two snapshot handles are not associated with the same database @@ -9106,35 +9101,41 @@ ** is undefined. ** ** Otherwise, this API returns a negative value if P1 refers to an older ** snapshot than P2, zero if the two handles refer to the same database ** snapshot, and a positive value if P1 is a newer snapshot than P2. +** +** This interface is only available if SQLite is compiled with the +** [SQLITE_ENABLE_SNAPSHOT] option. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( sqlite3_snapshot *p1, sqlite3_snapshot *p2 ); /* ** CAPI3REF: Recover snapshots from a wal file -** EXPERIMENTAL -** -** If all connections disconnect from a database file but do not perform -** a checkpoint, the existing wal file is opened along with the database -** file the next time the database is opened. At this point it is only -** possible to successfully call sqlite3_snapshot_open() to open the most -** recent snapshot of the database (the one at the head of the wal file), -** even though the wal file may contain other valid snapshots for which -** clients have sqlite3_snapshot handles. -** -** This function attempts to scan the wal file associated with database zDb +** METHOD: sqlite3_snapshot +** +** If a [WAL file] remains on disk after all database connections close +** (either through the use of the [SQLITE_FCNTL_PERSIST_WAL] [file control] +** or because the last process to have the database opened exited without +** calling [sqlite3_close()]) and a new connection is subsequently opened +** on that database and [WAL file], the [sqlite3_snapshot_open()] interface +** will only be able to open the last transaction added to the WAL file +** even though the WAL file contains other valid transactions. +** +** This function attempts to scan the WAL file associated with database zDb ** of database handle db and make all valid snapshots available to ** sqlite3_snapshot_open(). It is an error if there is already a read -** transaction open on the database, or if the database is not a wal mode +** transaction open on the database, or if the database is not a WAL mode ** database. ** ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. +** +** This interface is only available if SQLite is compiled with the +** [SQLITE_ENABLE_SNAPSHOT] option. */ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); /* ** CAPI3REF: Serialize a database Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -433,10 +433,15 @@ Th_Unstore("title"); /* Avoid collisions with ticket field names */ cgi_destination(CGI_BODY); g.cgiOutput = 1; headerHasBeenGenerated = 1; sideboxUsed = 0; + if( g.perm.Debug && P("showqp") ){ + @
    + cgi_print_all(0, 0); + @
    + } } #if INTERFACE /* Allowed parameters for style_adunit() */ #define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */ @@ -502,15 +507,36 @@ ** Generate code to load a single javascript file */ void style_load_one_js_file(const char *zFile){ @ } + +/* +** All extra JS files to load. +*/ +static const char *azJsToLoad[4]; +static int nJsToLoad = 0; + +/* +** Register a new JS file to load at the end of the document. +*/ +void style_load_js(const char *zName){ + int i; + for(i=0; i=sizeof(azJsToLoad)/sizeof(azJsToLoad[0]) ){ + fossil_panic("too man JS files"); + } + azJsToLoad[nJsToLoad++] = zName; +} /* ** Generate code to load all required javascript files. */ static void style_load_all_js_files(void){ + int i; if( needHrefJs ){ int nDelay = db_get_int("auto-hyperlink-delay",0); int bMouseover; /* Load up the page data */ bMouseover = (!g.isHuman || db_get_boolean("auto-hyperlink-ishuman",0)) @@ -523,10 +549,13 @@ style_load_one_js_file("sorttable.js"); } if( needGraphJs ){ style_load_one_js_file("graph.js"); } + for(i=0; iPlease enable javascript or log in to see this content

    +} + +/* +** Webpages that encounter an error due to missing or incorrect +** query parameters can jump to this routine to render an error +** message screen. +** +** For administators, or if the test_env_enable setting is true, then +** details of the request environment are displayed. Otherwise, just +** the error message is shown. +** +** If zFormat is an empty string, then this is the /test_env page. +*/ +void webpage_error(const char *zFormat, ...){ int i; int showAll; - char zCap[30]; + char *zErr = 0; + int isAuth = 0; + char zCap[100]; static const char *const azCgiVars[] = { "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE", "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENICATION", "HTTP_CONNECTION", "HTTP_HOST", @@ -895,72 +982,88 @@ "FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS", "TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST" }; login_check_credentials(); - if( !g.perm.Admin && !g.perm.Setup && !db_get_boolean("test_env_enable",0) ){ - login_needed(0); - return; + if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ + isAuth = 1; } for(i=0; i -#endif - @ g.zBaseURL = %h(g.zBaseURL)
    - @ g.zHttpsURL = %h(g.zHttpsURL)
    - @ g.zTop = %h(g.zTop)
    - @ g.zPath = %h(g.zPath)
    - for(i=0, c='a'; c<='z'; c++){ - if( login_has_capability(&c, 1, 0) ) zCap[i++] = c; - } - zCap[i] = 0; - @ g.userUid = %d(g.userUid)
    - @ g.zLogin = %h(g.zLogin)
    - @ g.isHuman = %d(g.isHuman)
    - if( g.nRequest ){ - @ g.nRequest = %d(g.nRequest)
    - } - if( g.nPendingRequest>1 ){ - @ g.nPendingRequest = %d(g.nPendingRequest)
    - } - @ capabilities = %s(zCap)
    - for(i=0, c='a'; c<='z'; c++){ - if( login_has_capability(&c, 1, LOGIN_ANON) - && !login_has_capability(&c, 1, 0) ) zCap[i++] = c; - } - zCap[i] = 0; - if( i>0 ){ - @ anonymous-adds = %s(zCap)
    - } - @ g.zRepositoryName = %h(g.zRepositoryName)
    - @ load_average() = %f(load_average())
    - @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))
    - @
    - P("HTTP_USER_AGENT"); - cgi_print_all(showAll, 0); - if( showAll && blob_size(&g.httpHeader)>0 ){ - @
    - @
    -    @ %h(blob_str(&g.httpHeader))
    -    @ 
    - } - if( g.perm.Setup ){ - const char *zRedir = P("redirect"); - if( zRedir ) cgi_redirect(zRedir); + if( zFormat[0] ){ + va_list ap; + va_start(ap, zFormat); + zErr = vmprintf(zFormat, ap); + va_end(ap); + style_header("Bad Request"); + @

    /%h(g.zPath): %h(zErr)

    + showAll = 0; + cgi_set_status(500, "Bad Request"); + }else if( !isAuth ){ + login_needed(0); + return; + }else{ + style_header("Environment Test"); + showAll = PB("showall"); + style_submenu_checkbox("showall", "Cookies", 0, 0); + style_submenu_element("Stats", "%R/stat"); + } + + if( isAuth ){ + #if !defined(_WIN32) + @ uid=%d(getuid()), gid=%d(getgid())
    + #endif + @ g.zBaseURL = %h(g.zBaseURL)
    + @ g.zHttpsURL = %h(g.zHttpsURL)
    + @ g.zTop = %h(g.zTop)
    + @ g.zPath = %h(g.zPath)
    + @ g.userUid = %d(g.userUid)
    + @ g.zLogin = %h(g.zLogin)
    + @ g.isHuman = %d(g.isHuman)
    + if( g.nRequest ){ + @ g.nRequest = %d(g.nRequest)
    + } + if( g.nPendingRequest>1 ){ + @ g.nPendingRequest = %d(g.nPendingRequest)
    + } + @ capabilities = %s(find_capabilities(zCap))
    + if( zCap[0] ){ + @ anonymous-adds = %s(find_anon_capabilities(zCap))
    + } + @ g.zRepositoryName = %h(g.zRepositoryName)
    + @ load_average() = %f(load_average())
    + @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))
    + @
    + P("HTTP_USER_AGENT"); + cgi_print_all(showAll, 0); + if( showAll && blob_size(&g.httpHeader)>0 ){ + @
    + @
    +      @ %h(blob_str(&g.httpHeader))
    +      @ 
    + } } style_footer(); - if( g.perm.Admin && P("err") ) fossil_fatal("%s", P("err")); + if( zErr ){ + cgi_reply(); + fossil_exit(1); + } +} + +/* +** Generate a Not Yet Implemented error page. +*/ +void webpage_not_yet_implemented(void){ + webpage_error("Not yet implemented"); } /* -** WEBPAGE: honeypot -** This page is a honeypot for spiders and bots. +** Generate a webpage for a webpage_assert(). */ -void honeypot_page(void){ - cgi_set_status(403, "Forbidden"); - @

    Please enable javascript or log in to see this content

    +void webpage_assert_page(const char *zFile, int iLine, const char *zExpr){ + fossil_warning("assertion fault at %s:%d - %s", zFile, iLine, zExpr); + cgi_reset_content(); + webpage_error("assertion fault at %s:%d - %s", zFile, iLine, zExpr); } + +#if INTERFACE +# define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);} +#endif Index: src/timeline.c ================================================================== --- src/timeline.c +++ src/timeline.c @@ -574,11 +574,11 @@ cgi_printf("technote: "); hyperlink_to_event_tagid(tagid<0?-tagid:tagid); }else{ cgi_printf("artifact: %z%S ",href("%R/info/%!S",zUuid),zUuid); } - }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' ){ + }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' || zType[0]=='f'){ cgi_printf("artifact: %z%S ",href("%R/info/%!S",zUuid),zUuid); } if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){ char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate); @@ -1031,11 +1031,11 @@ ** set the y= parameter that determines which elements to display ** on the timeline. */ static void timeline_y_submenu(int isDisabled){ static int i = 0; - static const char *az[12]; + static const char *az[14]; if( i==0 ){ az[0] = "all"; az[1] = "Any Type"; i = 2; if( g.perm.Read ){ @@ -1053,10 +1053,14 @@ az[i++] = "Tickets"; } if( g.perm.RdWiki ){ az[i++] = "w"; az[i++] = "Wiki"; + } + if( g.perm.RdForum ){ + az[i++] = "f"; + az[i++] = "Forum"; } assert( i<=count(az) ); } if( i>2 ){ style_submenu_multichoice("y", i/2, az, isDisabled); @@ -1357,11 +1361,11 @@ ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel ** rel Show related check-ins as well as those matching t=TAG ** mionly Limit rel to show ancestors but not descendants ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP ** u=USER Only show items associated with USER -** y=TYPE 'ci', 'w', 't', 'e', or 'all'. +** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'. ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar" ** advm Use the "Advanced" or "Busy" menu design. ** ng No Graph. ** nd Do not highlight the focus check-in ** v Show details of files changed @@ -1477,11 +1481,11 @@ pd_rid = name_to_typed_rid(P("dp"),"ci"); if( pd_rid ){ p_rid = d_rid = pd_rid; } login_check_credentials(); - if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki) + if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum) || (bisectOnly && !g.perm.Setup) ){ login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); return; } @@ -1854,10 +1858,11 @@ if( (zType[0]=='w' && !g.perm.RdWiki) || (zType[0]=='t' && !g.perm.RdTkt) || (zType[0]=='e' && !g.perm.RdWiki) || (zType[0]=='c' && !g.perm.Read) || (zType[0]=='g' && !g.perm.Read) + || (zType[0]=='f' && !g.perm.RdForum) ){ zType = "all"; } if( zType[0]=='a' ){ if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){ @@ -1872,10 +1877,14 @@ cSep = ','; } if( g.perm.RdTkt ){ blob_append_sql(&cond, "%c't'", cSep); cSep = ','; + } + if( g.perm.RdForum ){ + blob_append_sql(&cond, "%c'f'", cSep); + cSep = ','; } blob_append_sql(&cond, ")"); } }else{ /* zType!="all" */ blob_append_sql(&cond, " AND event.type=%Q", zType); @@ -1887,10 +1896,12 @@ zEType = "ticket change"; }else if( zType[0]=='e' ){ zEType = "technical note"; }else if( zType[0]=='g' ){ zEType = "tag"; + }else if( zType[0]=='f' ){ + zEType = "forum post"; } } if( zUser ){ int n = db_int(0,"SELECT count(*) FROM event" " WHERE user=%Q OR euser=%Q", zUser, zUser); Index: src/webmail.c ================================================================== --- src/webmail.c +++ src/webmail.c @@ -708,11 +708,11 @@ @
  • if( d==2 ){ @ @ }else{ - @ + @ if( d!=1 ){ @ } @ } @@ -732,10 +732,11 @@ while( db_step(&q)==SQLITE_ROW ){ const char *zId = db_column_text(&q,0); const char *zFrom = db_column_text(&q, 1); const char *zDate = db_column_text(&q, 2); const char *zSubject = db_column_text(&q, 4); + if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)"; @
    %h(zFrom)%h(zSubject) \ @ %s(zDate)
    - + + @@ -559,36 +635,39 @@ + - + + - + + @@ -600,10 +679,11 @@ + @@ -610,18 +690,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -630,10 +745,11 @@ + @@ -640,15 +756,17 @@ + + @@ -660,10 +778,11 @@ + @@ -670,14 +789,16 @@ + + @@ -690,19 +811,21 @@ + + @@ -709,10 +832,11 @@ + @@ -719,13 +843,15 @@ + +
    Card FormatUsed ByUsed By
    Manifest Cluster Control Wiki Ticket Attachment TechnoteForum
    A filename target ?source?          1   
    B baseline0-1*0-1             
     * = Required for delta manifests
    C comment-text 1         0-1 0-1 
    D date-time-stamp 1  1 1 1 1 1 1        1 
    F filename ?uuid? ?permissions? ?oldname? 0+            
    G thread-root       0-1
    H thread-title       0-1
    I in-reply-to       0-1
    J name ?value?         1+     
    K ticket-uuid    1     
    L wiki-title      1       
    M uuid   1+             0-1   0-1 0-10-1
    P uuid ... 0-1    0-1     0-10-1
    Q (+|-)uuid ?uuid? 0+                     
    T (+|*|-)tagname uuid ?value? 0+   1+       0+ 
    U username 1  1 1 1 0-1 0-11
    W size \n text \n      1     11
    Z md5sum1 1 1 1 1 1