}
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)]
}
@