}
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){
+ @
+ @
a
+ @
Admin: Create and delete users
+ @
b
+ @
Attach: Add attachments to wiki or tickets
+ @
c
+ @
Append-Tkt: Append to tickets
+ @
d
+ @
Delete: Delete wiki and tickets
+ @
e
+ @
View-PII: \
+ @ View sensitive data such as email addresses
+ @
f
+ @
New-Wiki: Create new wiki pages
+ @
g
+ @
Clone: Clone the repository
+ @
h
+ @
Hyperlinks: Show hyperlinks to detailed
+ @ repository history
+ @
i
+ @
Check-In: Commit new versions in the repository
+ @
j
+ @
Read-Wiki: View wiki pages
+ @
k
+ @
Write-Wiki: Edit wiki pages
+ @
l
+ @
Mod-Wiki: Moderator for wiki pages
+ @
m
+ @
Append-Wiki: Append to wiki pages
+ @
n
+ @
New-Tkt: Create new tickets
+ @
o
+ @
Check-Out: Check out versions
+ @
p
+ @
Password: Change your own password
+ @
q
+ @
Mod-Tkt: Moderator for tickets
+ @
r
+ @
Read-Tkt: View tickets
+ @
s
+ @
Setup/Super-user: Setup and configure this website
+ @
t
+ @
Tkt-Report: Create new bug summary reports
+ @
u
+ @
Reader: Inherit privileges of
+ @ user reader
+ @
v
+ @
Developer: Inherit privileges of
+ @ user developer
+ @
w
+ @
Write-Tkt: Edit tickets
+ @
x
+ @
Private: Push and/or pull private branches
+ @
y
+ @
Write-Unver: Push unversioned files
+ @
z
+ @
Zip download: Download a ZIP archive or tarball
+ @
2
+ @
Forum-Read: Read forum posts by others
+ @
3
+ @
Forum-Append: Add new forum posts
+ @
4
+ @
Forum-Trusted: Add pre-approved forum posts
+ @
5
+ @
Forum-Moderator: Approve or disapprove forum posts
+ @
6
+ @
Forum-Supervisor: \
+ @ Forum administrator: Set or remove capability "4" for other users
+ @
7
+ @
Email-Alerts: Sign up for email nofications
+ @
A
+ @
Announce: Send announcements
+ @
D
+ @
Debug: 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","")
+ );
+ @
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;
- @
- @
- }
- }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 ){
- @
- }
- @
+ }
+ 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
+ }
+ @
%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
@
- @ 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") ){
@
}
- @
+ @
@
@
@
User ID:
- @
+ @
+ if( iErrLine==1 ){
+ @
← %h(zErr)
+ }
+ @
+ @
+ @
Display Name:
+ @
+ if( iErrLine==2 ){
+ @
← %h(zErr)
+ }
+ @
+ @
+ @
Email Address:
+ @
+ if( iErrLine==3 ){
+ @
← %h(zErr)
+ }
@
@
@
Password:
- @
+ @
+ if( iErrLine==4 ){
+ @
← %h(zErr)
+ }
@
@
@
Confirm password:
- @
- @
- @
- @
Contact info:
- @
+ @
+ if( iErrLine==5 ){
+ @
← %h(zErr)
+ }
@
@
@
Captcha text (below):
- @
+ @
+ if( iErrLine==6 ){
+ @
← %h(zErr)
+ }
@
@
@
@
@
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)]
}
@
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{
@
+ haveResult = 1;
}
+ return haveResult;
}
/*
** WEBPAGE: search
**
@@ -1143,10 +1172,11 @@
** c -> check-ins
** d -> documentation
** t -> tickets
** w -> wiki
** e -> tech notes
+** f -> forum
** all -> everything
*/
void search_page(void){
login_check_credentials();
style_header("Search");
@@ -1250,10 +1280,11 @@
** cType: d Embedded documentation
** w Wiki page
** c Check-in comment
** t Ticket text
** e Tech note
+** f Forum
**
** rid The RID of an artifact that defines the object
** being searched.
**
** zName Name of the object being searched. This is used
@@ -1275,17 +1306,27 @@
blob_to_utf8_no_bom(&doc, 0);
get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut);
blob_reset(&doc);
break;
}
+ case 'f': /* Forum messages */
case 'e': /* Tech Notes */
case 'w': { /* Wiki */
Manifest *pWiki = manifest_get(rid,
- cType == 'e' ? CFTYPE_EVENT : CFTYPE_WIKI, 0);
+ cType == 'e' ? CFTYPE_EVENT :
+ cType == 'f' ? CFTYPE_FORUM : CFTYPE_WIKI, 0);
Blob wiki;
if( pWiki==0 ) break;
- blob_init(&wiki, pWiki->zWiki, -1);
+ if( cType=='f' ){
+ blob_init(&wiki, 0, 0);
+ if( pWiki->zThreadTitle ){
+ blob_appendf(&wiki, "
%h
\n", pWiki->zThreadTitle);
+ }
+ blob_appendf(&wiki, "From %s:\n\n%s", pWiki->zUser, pWiki->zWiki);
+ }else{
+ blob_init(&wiki, pWiki->zWiki, -1);
+ }
get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype),
pOut);
blob_reset(&wiki);
manifest_destroy(pWiki);
break;
@@ -1516,11 +1557,11 @@
"INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
" SELECT 't', tkt_id, 0 FROM ticket;"
);
db_multi_exec(
"INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)"
- " SELECT 'e', objid, comment, 0 FROM event WHERE type='e';"
+ " SELECT type, objid, comment, 0 FROM event WHERE type IN ('e','f');"
);
}
/*
** The document described by cType,rid,zName is about to be added or
@@ -1553,10 +1594,11 @@
db_multi_exec(
"DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d",
cType, zName, rid
);
}
+ /* All forum posts are always indexed */
}
}
/*
** If the doc-glob and doc-br settings are valid for document search
@@ -1681,10 +1723,33 @@
" tagxref.mtime"
" FROM tagxref WHERE tagxref.rid=ftsdocs.rid)"
" WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed"
);
}
+
+/*
+** Deal with all of the unindexed 'f' terms in FTSDOCS
+*/
+static void search_update_forum_index(void){
+ db_multi_exec(
+ "INSERT INTO ftsidx(docid,title,body)"
+ " SELECT rowid, title('f',rid,NULL),body('f',rid,NULL) FROM ftsdocs"
+ " WHERE type='f' AND NOT idxed;"
+ );
+ if( db_changes()==0 ) return;
+ db_multi_exec(
+ "UPDATE ftsdocs SET idxed=1, name=NULL,"
+ " (label,url,mtime) = "
+ " (SELECT 'Forum '||event.comment,"
+ " '/forumpost/'||blob.uuid,"
+ " event.mtime"
+ " FROM event, blob"
+ " WHERE event.objid=ftsdocs.rid"
+ " AND blob.rid=ftsdocs.rid)"
+ "WHERE ftsdocs.type='f' AND NOT ftsdocs.idxed"
+ );
+}
/*
** Deal with all of the unindexed 'e' terms in FTSDOCS
*/
static void search_update_technote_index(void){
@@ -1728,10 +1793,13 @@
search_update_wiki_index();
}
if( srchFlags & SRCH_TECHNOTE ){
search_update_technote_index();
}
+ if( srchFlags & SRCH_FORUM ){
+ search_update_forum_index();
+ }
}
/*
** Construct, prepopulate, and then update the full-text index.
*/
@@ -1780,10 +1848,11 @@
{ "search-ci", "check-in search:", "c" },
{ "search-doc", "document search:", "d" },
{ "search-tkt", "ticket search:", "t" },
{ "search-wiki", "wiki search:", "w" },
{ "search-technote", "tech note search:", "e" },
+ { "search-forum", "forum search:", "f" },
};
char *zSubCmd = 0;
int i, j, n;
int iCmd = 0;
int iAction = 0;
Index: src/security_audit.c
==================================================================
--- src/security_audit.c
+++ src/security_audit.c
@@ -58,11 +58,11 @@
/* Step 1: Determine if the repository is public or private. "Public"
** means that any anonymous user on the internet can access all content.
** "Private" repos require (non-anonymous) login to access all content,
** though some content may be accessible anonymously.
*/
- zAnonCap = db_text("", "SELECT group_concat(coalesce(cap,'')) FROM user"
+ zAnonCap = db_text("", "SELECT capunion(cap) FROM user"
" WHERE login IN ('anonymous','nobody')");
zPubPages = db_get("public-pages",0);
if( hasAnyCap(zAnonCap,"as") ){
@
This repository is Wildly INSECURE because
@ it grants administrator privileges to anonymous users. You
@@ -78,11 +78,12 @@
@ "nobody" on the User Configuration page.
}else if( hasAnyCap(zAnonCap,"goz") ){
@
This repository is PUBLIC. All
@ checked-in content can be accessed by anonymous users.
@ Take it private.
WARNING:
@ Anonymous users can push new check-ins into the repository.
- @
Fix this by removing the "Check-in" privilege from users
+ @
Fix this by removing the "Check-in" privilege
+ @ (capability "i") from users
@ "anonymous" and "nobody" on the
@ User Configuration page.
}
/* Anonymous users probably should not be allowed act as moderators
** for wiki or tickets.
*/
- if( hasAnyCap(zAnonCap, "lq") ){
+ if( hasAnyCap(zAnonCap, "lq5") ){
@
WARNING:
- @ Anonymous users can act as moderators for wiki and/or tickets.
- @ This defeats the whole purpose of moderation.
- @
Fix this by removing the "Mod-Wiki" and "Mod-Tkt"
- @ privilege from users "anonymous" and "nobody" on the
- @ User Configuration page.
+ @ Anonymous users can act as moderators for wiki, tickets, or
+ @ forum posts. This defeats the whole purpose of moderation.
+ @
Fix this by removing the "Mod-Wiki", "Mod-Tkt", and "Mod-Forum"
+ @ privileges (capabilities "fq5")
+ @ from users "anonymous" and "nobody"
+ @ on the User Configuration page.
}
/* Anonymous users probably should not be allowed to delete
** wiki or tickets.
*/
@@ -174,11 +181,11 @@
if( db_get_boolean("modreq-wiki",0)==0 ){
@
WARNING:
@ Anonymous users can create or edit wiki without moderation.
@ This can result in robots inserting lots of wiki spam into
@ repository.
- @
Fix this by removing the "New-Wiki" and "Write-Wiki"
+ @ Fix this by removing the "New-Wiki" and "Write-Wiki"
@ privileges from users "anonymous" and "nobody" on the
@ User Configuration page or
@ by enabling wiki moderation on the
@ Moderation Setup page.
}else{
@@ -185,10 +192,36 @@
@
@ Anonymous users can create or edit wiki, but moderator
@ approval is required before the edits become permanent.
}
}
+
+ /* Anonymous users should not be able to create trusted forum
+ ** posts.
+ */
+ if( hasAnyCap(zAnonCap, "456") ){
+ @
WARNING:
+ @ Anonymous users can create forum posts that are
+ @ accepted into the permanent record without moderation.
+ @ This can result in robots generating spam on forum posts.
+ @ Fix this by removing the "WriteTrusted-Forum" privilege
+ @ (capabilities "456") from
+ @ users "anonymous" and "nobody" on the
+ @ User Configuration page or
+ }
+
+ /* Anonymous users should not be able to send announcements.
+ */
+ if( hasAnyCap(zAnonCap, "A") ){
+ @
WARNING:
+ @ Anonymous users can send announcements to anybody who is signed
+ @ up to receive announcements. This can result in spam.
+ @ Fix this by removing the "Announce" privilege
+ @ (capability "A") from
+ @ users "anonymous" and "nobody" on the
+ @ User Configuration page or
+ }
/* Administrative privilege should only be provided to
** specific individuals, not to entire classes of people.
** And not too many people should have administrator privilege.
*/
@@ -349,10 +382,22 @@
@
@ The error log at "%h(g.zErrlog)" that is
@ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size.
}
}
+
+ @
View-PII: \
- @ View sensitive data such as email addresses
- @
f
- @
New-Wiki: Create new wiki pages
- @
g
- @
Clone: Clone the repository
- @
h
- @
Hyperlinks: Show hyperlinks to detailed
- @ repository history
- @
i
- @
Check-In: Commit new versions in the repository
- @
j
- @
Read-Wiki: View wiki pages
- @
k
- @
Write-Wiki: Edit wiki pages
- @
l
- @
Mod-Wiki: Moderator for wiki pages
- @
m
- @
Append-Wiki: Append to wiki pages
- @
n
- @
New-Tkt: Create new tickets
- @
o
- @
Check-Out: Check out versions
- @
p
- @
Password: Change your own password
- @
q
- @
Mod-Tkt: Moderator for tickets
- @
r
- @
Read-Tkt: View tickets
- @
s
- @
Setup/Super-user: Setup and configure this website
- @
t
- @
Tkt-Report: Create new bug summary reports
- @
u
- @
Reader: Inherit privileges of
- @ user reader
- @
v
- @
Developer: Inherit privileges of
- @ user developer
- @
w
- @
Write-Tkt: Edit tickets
- @
x
- @
Private: Push and/or pull private branches
- @
y
- @
Write-Unver: Push unversioned files
- @
z
- @
Zip download: Download a ZIP archive or tarball
- @
2
- @
Forum-Read: Read forum posts by others
- @
3
- @
Forum-Append: Add new forum posts
- @
4
- @
Forum-Trusted: Add pre-approved forum posts
- @
5
- @
Forum-Moderator: Approve or disapprove forum posts
- @
6
- @
Forum-Supervisor: \
- @ Forum administrator
- @
7
- @
Email-Alerts: Sign up for email nofications
- @
A
- @
Announce: Send announcements
- @
D
- @
Debug: 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.
@
@
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
@
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 ){
- @
- @
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{
- @
-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:
+
+
+
+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.