Fossil SCM

Merge latest change from trunk.

mgagnon 2025-04-09 01:47 fix-timeline-cli-after merge
Commit 3144c3bea491c00ec0590f44cb1547302380b7cc58891cf1a4f49fe43e0a4051
+107 -44
--- src/alerts.c
+++ src/alerts.c
@@ -51,11 +51,11 @@
5151
@ -- f - Forum posts
5252
@ -- k - ** Special: Unsubscribed using /oneclickunsub
5353
@ -- n - New forum threads
5454
@ -- r - Replies to my own forum posts
5555
@ -- t - Ticket changes
56
-@ -- u - Elevation of users' permissions (admins only)
56
+@ -- u - Changes of users' permissions (admins only)
5757
@ -- w - Wiki changes
5858
@ -- x - Edits to forum posts
5959
@ -- Probably different codes will be added in the future. In the future
6060
@ -- we might also add a separate table that allows subscribing to email
6161
@ -- notifications for specific branches or tags or tickets.
@@ -282,10 +282,13 @@
282282
style_submenu_element("Subscribers","%R/subscribers");
283283
}
284284
if( fossil_strcmp(g.zPath,"subscribe") ){
285285
style_submenu_element("Add New Subscriber","%R/subscribe");
286286
}
287
+ if( fossil_strcmp(g.zPath,"setup_notification") ){
288
+ style_submenu_element("Notification Setup","%R/setup_notification");
289
+ }
287290
}
288291
}
289292
290293
291294
/*
@@ -295,14 +298,14 @@
295298
** Normally accessible via the /Admin/Notification menu.
296299
*/
297300
void setup_notification(void){
298301
static const char *const azSendMethods[] = {
299302
"off", "Disabled",
300
- "pipe", "Pipe to a command",
303
+ "relay", "SMTP relay",
301304
"db", "Store in a database",
302305
"dir", "Store in a directory",
303
- "relay", "SMTP relay"
306
+ "pipe", "Pipe to a command",
304307
};
305308
login_check_credentials();
306309
if( !g.perm.Setup ){
307310
login_needed(0);
308311
return;
@@ -311,22 +314,25 @@
311314
312315
alert_submenu_common();
313316
style_submenu_element("Send Announcement","%R/announce");
314317
style_set_current_feature("alerts");
315318
style_header("Email Notification Setup");
316
- @ <h1>Status</h1>
319
+ @ <form action="%R/setup_notification" method="post"><div>
320
+ @ <h1>Status &ensp; <input type="submit" name="submit" value="Refresh"></h1>
321
+ @ </form>
317322
@ <table class="label-value">
318323
if( alert_enabled() ){
319324
stats_for_email();
320325
}else{
321326
@ <th>Disabled</th>
322327
}
323328
@ </table>
324329
@ <hr>
325
- @ <h1> Configuration </h1>
326330
@ <form action="%R/setup_notification" method="post"><div>
327
- @ <input type="submit" name="submit" value="Apply Changes"><hr>
331
+ @ <h1> Configuration </h1>
332
+ @ <p><input type="submit" name="submit" value="Apply Changes"></p>
333
+ @ <hr>
328334
login_insert_csrf_secret();
329335
330336
entry_attribute("Canonical Server URL", 40, "email-url",
331337
"eurl", "", 0);
332338
@ <p><b>Required.</b>
@@ -391,38 +397,40 @@
391397
@ <p>How to send email. Requires auxiliary information from the fields
392398
@ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
393399
@ to send test message to debug this setting.
394400
@ (Property: "email-send-method")</p>
395401
alert_schema(1);
402
+ entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
403
+ "esrh", "localhost", 0);
404
+ @ <p>When the send method is "SMTP relay", each email message is
405
+ @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
406
+ @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
407
+ @ append a colon and TCP port number (ex: smtp.example.com:587).
408
+ @ The default TCP port number is 25.
409
+ @ Usage Hint: If Fossil is running inside of a chroot jail, then it might
410
+ @ not be able to resolve hostnames. Work around this by using a raw IP
411
+ @ address or create a "/etc/hosts" file inside the chroot jail.
412
+ @ (Property: "email-send-relayhost")</p>
413
+ @
414
+ entry_attribute("Store Emails In This Database", 60, "email-send-db",
415
+ "esdb", "", 0);
416
+ @ <p>When the send method is "store in a database", each email message is
417
+ @ stored in an SQLite database file with the name given here.
418
+ @ (Property: "email-send-db")</p>
396419
entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
397420
"ecmd", "sendmail -ti", 0);
398421
@ <p>When the send method is "pipe to a command", this is the command
399422
@ that is run. Email messages are piped into the standard input of this
400423
@ command. The command is expected to extract the sender address,
401424
@ recipient addresses, and subject from the header of the piped email
402425
@ text. (Property: "email-send-command")</p>
403
-
404
- entry_attribute("Store Emails In This Database", 60, "email-send-db",
405
- "esdb", "", 0);
406
- @ <p>When the send method is "store in a database", each email message is
407
- @ stored in an SQLite database file with the name given here.
408
- @ (Property: "email-send-db")</p>
409
-
410426
entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
411427
"esdir", "", 0);
412428
@ <p>When the send method is "store in a directory", each email message is
413429
@ stored as a separate file in the directory shown here.
414430
@ (Property: "email-send-dir")</p>
415431
416
- entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
417
- "esrh", "", 0);
418
- @ <p>When the send method is "SMTP relay", each email message is
419
- @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
420
- @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
421
- @ append a colon and TCP port number (ex: smtp.example.com:587).
422
- @ The default TCP port number is 25.
423
- @ (Property: "email-send-relayhost")</p>
424432
@ <hr>
425433
426434
@ <p><input type="submit" name="submit" value="Apply Changes"></p>
427435
@ </div></form>
428436
db_end_transaction(0);
@@ -630,18 +638,27 @@
630638
emailerGetSetting(p, &p->zCmd, "email-send-command");
631639
}else if( fossil_strcmp(p->zDest, "dir")==0 ){
632640
emailerGetSetting(p, &p->zDir, "email-send-dir");
633641
}else if( fossil_strcmp(p->zDest, "blob")==0 ){
634642
blob_init(&p->out, 0, 0);
635
- }else if( fossil_strcmp(p->zDest, "relay")==0 ){
643
+ }else if( fossil_strcmp(p->zDest, "relay")==0
644
+ || fossil_strcmp(p->zDest, "debug-relay")==0
645
+ ){
636646
const char *zRelay = 0;
637647
emailerGetSetting(p, &zRelay, "email-send-relayhost");
638648
if( zRelay ){
639649
u32 smtpFlags = SMTP_DIRECT;
640650
if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
651
+ blob_init(&p->out, 0, 0);
641652
p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
642
- smtpFlags);
653
+ smtpFlags, 0);
654
+ if( p->pSmtp==0 || p->pSmtp->zErr ){
655
+ emailerError(p, "Could not start SMTP session: %s",
656
+ p->pSmtp ? p->pSmtp->zErr : "reason unknown");
657
+ }else if( p->zDest[0]=='d' ){
658
+ smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
659
+ }
643660
smtp_client_startup(p->pSmtp);
644661
}
645662
}
646663
return p;
647664
}
@@ -1125,11 +1142,11 @@
11251142
** SETTING: email-listid width=40
11261143
** If this setting is not an empty string, then it becomes the argument to
11271144
** a "List-ID:" header that is added to all out-bound notification emails.
11281145
*/
11291146
/*
1130
-** SETTING: email-send-relayhost width=40 sensitive
1147
+** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
11311148
** This is the hostname and TCP port to which output email messages
11321149
** are sent when email-send-method is "relay". There should be an
11331150
** SMTP server configured as a Mail Submission Agent listening on the
11341151
** designated host and port and all times.
11351152
*/
@@ -1704,11 +1721,11 @@
17041721
@ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
17051722
@ Wiki</label><br>
17061723
}
17071724
if( g.perm.Admin ){
17081725
@ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1709
- @ User permission elevation</label>
1726
+ @ User permission changes</label>
17101727
}
17111728
di = PB("di");
17121729
@ </td></tr>
17131730
@ <tr>
17141731
@ <td class="form_label">Delivery:</td>
@@ -2114,11 +2131,11 @@
21142131
/* Corner-case bug: if an admin assigns 'u' to a non-admin, that
21152132
** subscription will get removed if the user later edits their
21162133
** subscriptions, as non-admins are not permitted to add that
21172134
** subscription. */
21182135
@ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2119
- @ User permission elevation</label>
2136
+ @ User permission changes</label>
21202137
}
21212138
@ </td></tr>
21222139
if( strchr(ssub,'k')!=0 ){
21232140
@ <tr><td></td><td>&nbsp;&uarr;&nbsp;
21242141
@ Note: User did a one-click unsubscribe</td></tr>
@@ -3436,16 +3453,28 @@
34363453
char *zSubject = PT("subject");
34373454
int bAll = PB("all");
34383455
int bAA = PB("aa");
34393456
int bMods = PB("mods");
34403457
const char *zSub = db_get("email-subname", "[Fossil Repo]");
3441
- int bTest2 = fossil_strcmp(P("name"),"test2")==0;
3458
+ const char *zName = P("name"); /* Debugging options */
3459
+ const char *zDest = 0; /* How to send the announcement */
3460
+ int bTest = 0;
34423461
Blob hdr, body;
3462
+
3463
+ if( fossil_strcmp(zName, "test2")==0 ){
3464
+ bTest = 2;
3465
+ zDest = "blob";
3466
+ }else if( fossil_strcmp(zName, "test3")==0 ){
3467
+ bTest = 3;
3468
+ if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
3469
+ zDest = "debug-relay";
3470
+ }
3471
+ }
34433472
blob_init(&body, 0, 0);
34443473
blob_init(&hdr, 0, 0);
34453474
blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3446
- pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
3475
+ pSender = alert_sender_new(zDest, 0);
34473476
if( zTo[0] ){
34483477
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
34493478
alert_send(pSender, &hdr, &body, 0);
34503479
}
34513480
if( bAll || bAA || bMods ){
@@ -3479,17 +3508,24 @@
34793508
}
34803509
alert_send(pSender, &hdr, &body, 0);
34813510
}
34823511
db_finalize(&q);
34833512
}
3484
- if( bTest2 ){
3485
- /* If the URL is /announce/test2 instead of just /announce, then no
3486
- ** email is actually sent. Instead, the text of the email that would
3487
- ** have been sent is displayed in the result window. */
3488
- @ <pre style='border: 2px solid blue; padding: 1ex'>
3513
+ if( bTest && blob_size(&pSender->out) ){
3514
+ /* If the URL is "/announce/test2" then no email is actually sent.
3515
+ ** Instead, the text of the email that would have been sent is
3516
+ ** displayed in the result window.
3517
+ **
3518
+ ** If the URL is "/announce/test3" and the email-send-method is "relay"
3519
+ ** then the announcement is sent as it normally would be, but a
3520
+ ** transcript of the SMTP conversation with the MTA is shown here.
3521
+ */
3522
+ blob_trim(&pSender->out);
3523
+ @ <pre style='border: 2px solid blue; padding: 1ex;'>
34893524
@ %h(blob_str(&pSender->out))
34903525
@ </pre>
3526
+ blob_reset(&pSender->out);
34913527
}
34923528
zErr = pSender->zErr;
34933529
pSender->zErr = 0;
34943530
alert_sender_free(pSender);
34953531
return zErr;
@@ -3505,35 +3541,43 @@
35053541
** also send a message to an arbitrary email address and/or to all
35063542
** subscribers regardless of whether or not they have elected to
35073543
** receive announcements.
35083544
*/
35093545
void announce_page(void){
3510
- const char *zAction = "announce"
3511
- /* Maintenance reminder: we need an explicit action=THIS_PAGE on the
3512
- ** form element to avoid that a URL arg of to=... passed to this
3513
- ** page ends up overwriting the form-posted "to" value. This
3514
- ** action value differs for the test1 request path.
3515
- */;
3516
-
3546
+ const char *zAction = "announce";
3547
+ const char *zName = PD("name","");
3548
+ /*
3549
+ ** Debugging Notes:
3550
+ **
3551
+ ** /announce/test1 -> Shows query parameter values
3552
+ ** /announce/test2 -> Shows the formatted message but does
3553
+ ** not send it.
3554
+ ** /announce/test3 -> Sends the message, but also shows
3555
+ ** the SMTP transcript.
3556
+ */
35173557
login_check_credentials();
35183558
if( !g.perm.Announce ){
35193559
login_needed(0);
35203560
return;
35213561
}
3562
+ if( !g.perm.Setup ){
3563
+ zName = 0; /* Disable debugging feature for non-admin users */
3564
+ }
35223565
style_set_current_feature("alerts");
3523
- if( fossil_strcmp(P("name"),"test1")==0 ){
3566
+ if( fossil_strcmp(zName,"test1")==0 ){
35243567
/* Visit the /announce/test1 page to see the CGI variables */
35253568
zAction = "announce/test1";
35263569
@ <p style='border: 1px solid black; padding: 1ex;'>
35273570
cgi_print_all(0, 0, 0);
35283571
@ </p>
35293572
}else if( P("submit")!=0 && cgi_csrf_safe(2) ){
35303573
char *zErr = alert_send_announcement();
35313574
style_header("Announcement Sent");
35323575
if( zErr ){
3533
- @ <h1>Internal Error</h1>
3534
- @ <p>The following error was reported by the system:
3576
+ @ <h1>Error</h1>
3577
+ @ <p>The following error was reported by the
3578
+ @ announcement-sending subsystem:
35353579
@ <blockquote><pre>
35363580
@ %h(zErr)
35373581
@ </pre></blockquote>
35383582
}else{
35393583
@ <p>The announcement has been sent.
@@ -3548,10 +3592,16 @@
35483592
@ for this repository.</p>
35493593
return;
35503594
}
35513595
35523596
style_header("Send Announcement");
3597
+ alert_submenu_common();
3598
+ if( fossil_strcmp(zName,"test2")==0 ){
3599
+ zAction = "announce/test2";
3600
+ }else if( fossil_strcmp(zName,"test3")==0 ){
3601
+ zAction = "announce/test3";
3602
+ }
35533603
@ <form method="POST" action="%R/%s(zAction)">
35543604
login_insert_csrf_secret();
35553605
@ <table class="subscribe">
35563606
if( g.perm.Admin ){
35573607
int aa = PB("aa");
@@ -3584,15 +3634,28 @@
35843634
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
35853635
@ %h(PT("msg"))</textarea>
35863636
@ </tr>
35873637
@ <tr>
35883638
@ <td></td>
3589
- if( fossil_strcmp(P("name"),"test2")==0 ){
3639
+ if( fossil_strcmp(zName,"test2")==0 ){
35903640
@ <td><input type="submit" name="submit" value="Dry Run">
35913641
}else{
35923642
@ <td><input type="submit" name="submit" value="Send Message">
35933643
}
35943644
@ </tr>
35953645
@ </table>
35963646
@ </form>
3647
+ if( g.perm.Setup ){
3648
+ @ <hr>
3649
+ @ <p>Trouble-shooting Options:</p>
3650
+ @ <ol>
3651
+ @ <li> <a href="%R/announce">Normal Processing</a>
3652
+ @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
3653
+ @ - Do not send the announcement.
3654
+ @ <li> <a href="%R/announce/test2">Show the email text</a> but do
3655
+ @ not actually send it.
3656
+ @ <li> Send the message and also <a href="%R/announce/test3">show the
3657
+ @ SMTP traffic</a> when using "relay" mode.
3658
+ @ </ol>
3659
+ }
35973660
style_finish_page();
35983661
}
35993662
--- src/alerts.c
+++ src/alerts.c
@@ -51,11 +51,11 @@
51 @ -- f - Forum posts
52 @ -- k - ** Special: Unsubscribed using /oneclickunsub
53 @ -- n - New forum threads
54 @ -- r - Replies to my own forum posts
55 @ -- t - Ticket changes
56 @ -- u - Elevation of users' permissions (admins only)
57 @ -- w - Wiki changes
58 @ -- x - Edits to forum posts
59 @ -- Probably different codes will be added in the future. In the future
60 @ -- we might also add a separate table that allows subscribing to email
61 @ -- notifications for specific branches or tags or tickets.
@@ -282,10 +282,13 @@
282 style_submenu_element("Subscribers","%R/subscribers");
283 }
284 if( fossil_strcmp(g.zPath,"subscribe") ){
285 style_submenu_element("Add New Subscriber","%R/subscribe");
286 }
 
 
 
287 }
288 }
289
290
291 /*
@@ -295,14 +298,14 @@
295 ** Normally accessible via the /Admin/Notification menu.
296 */
297 void setup_notification(void){
298 static const char *const azSendMethods[] = {
299 "off", "Disabled",
300 "pipe", "Pipe to a command",
301 "db", "Store in a database",
302 "dir", "Store in a directory",
303 "relay", "SMTP relay"
304 };
305 login_check_credentials();
306 if( !g.perm.Setup ){
307 login_needed(0);
308 return;
@@ -311,22 +314,25 @@
311
312 alert_submenu_common();
313 style_submenu_element("Send Announcement","%R/announce");
314 style_set_current_feature("alerts");
315 style_header("Email Notification Setup");
316 @ <h1>Status</h1>
 
 
317 @ <table class="label-value">
318 if( alert_enabled() ){
319 stats_for_email();
320 }else{
321 @ <th>Disabled</th>
322 }
323 @ </table>
324 @ <hr>
325 @ <h1> Configuration </h1>
326 @ <form action="%R/setup_notification" method="post"><div>
327 @ <input type="submit" name="submit" value="Apply Changes"><hr>
 
 
328 login_insert_csrf_secret();
329
330 entry_attribute("Canonical Server URL", 40, "email-url",
331 "eurl", "", 0);
332 @ <p><b>Required.</b>
@@ -391,38 +397,40 @@
391 @ <p>How to send email. Requires auxiliary information from the fields
392 @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
393 @ to send test message to debug this setting.
394 @ (Property: "email-send-method")</p>
395 alert_schema(1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396 entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
397 "ecmd", "sendmail -ti", 0);
398 @ <p>When the send method is "pipe to a command", this is the command
399 @ that is run. Email messages are piped into the standard input of this
400 @ command. The command is expected to extract the sender address,
401 @ recipient addresses, and subject from the header of the piped email
402 @ text. (Property: "email-send-command")</p>
403
404 entry_attribute("Store Emails In This Database", 60, "email-send-db",
405 "esdb", "", 0);
406 @ <p>When the send method is "store in a database", each email message is
407 @ stored in an SQLite database file with the name given here.
408 @ (Property: "email-send-db")</p>
409
410 entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
411 "esdir", "", 0);
412 @ <p>When the send method is "store in a directory", each email message is
413 @ stored as a separate file in the directory shown here.
414 @ (Property: "email-send-dir")</p>
415
416 entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
417 "esrh", "", 0);
418 @ <p>When the send method is "SMTP relay", each email message is
419 @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
420 @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
421 @ append a colon and TCP port number (ex: smtp.example.com:587).
422 @ The default TCP port number is 25.
423 @ (Property: "email-send-relayhost")</p>
424 @ <hr>
425
426 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
427 @ </div></form>
428 db_end_transaction(0);
@@ -630,18 +638,27 @@
630 emailerGetSetting(p, &p->zCmd, "email-send-command");
631 }else if( fossil_strcmp(p->zDest, "dir")==0 ){
632 emailerGetSetting(p, &p->zDir, "email-send-dir");
633 }else if( fossil_strcmp(p->zDest, "blob")==0 ){
634 blob_init(&p->out, 0, 0);
635 }else if( fossil_strcmp(p->zDest, "relay")==0 ){
 
 
636 const char *zRelay = 0;
637 emailerGetSetting(p, &zRelay, "email-send-relayhost");
638 if( zRelay ){
639 u32 smtpFlags = SMTP_DIRECT;
640 if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
 
641 p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
642 smtpFlags);
 
 
 
 
 
 
643 smtp_client_startup(p->pSmtp);
644 }
645 }
646 return p;
647 }
@@ -1125,11 +1142,11 @@
1125 ** SETTING: email-listid width=40
1126 ** If this setting is not an empty string, then it becomes the argument to
1127 ** a "List-ID:" header that is added to all out-bound notification emails.
1128 */
1129 /*
1130 ** SETTING: email-send-relayhost width=40 sensitive
1131 ** This is the hostname and TCP port to which output email messages
1132 ** are sent when email-send-method is "relay". There should be an
1133 ** SMTP server configured as a Mail Submission Agent listening on the
1134 ** designated host and port and all times.
1135 */
@@ -1704,11 +1721,11 @@
1704 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
1705 @ Wiki</label><br>
1706 }
1707 if( g.perm.Admin ){
1708 @ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1709 @ User permission elevation</label>
1710 }
1711 di = PB("di");
1712 @ </td></tr>
1713 @ <tr>
1714 @ <td class="form_label">Delivery:</td>
@@ -2114,11 +2131,11 @@
2114 /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
2115 ** subscription will get removed if the user later edits their
2116 ** subscriptions, as non-admins are not permitted to add that
2117 ** subscription. */
2118 @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2119 @ User permission elevation</label>
2120 }
2121 @ </td></tr>
2122 if( strchr(ssub,'k')!=0 ){
2123 @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
2124 @ Note: User did a one-click unsubscribe</td></tr>
@@ -3436,16 +3453,28 @@
3436 char *zSubject = PT("subject");
3437 int bAll = PB("all");
3438 int bAA = PB("aa");
3439 int bMods = PB("mods");
3440 const char *zSub = db_get("email-subname", "[Fossil Repo]");
3441 int bTest2 = fossil_strcmp(P("name"),"test2")==0;
 
 
3442 Blob hdr, body;
 
 
 
 
 
 
 
 
 
 
3443 blob_init(&body, 0, 0);
3444 blob_init(&hdr, 0, 0);
3445 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3446 pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
3447 if( zTo[0] ){
3448 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
3449 alert_send(pSender, &hdr, &body, 0);
3450 }
3451 if( bAll || bAA || bMods ){
@@ -3479,17 +3508,24 @@
3479 }
3480 alert_send(pSender, &hdr, &body, 0);
3481 }
3482 db_finalize(&q);
3483 }
3484 if( bTest2 ){
3485 /* If the URL is /announce/test2 instead of just /announce, then no
3486 ** email is actually sent. Instead, the text of the email that would
3487 ** have been sent is displayed in the result window. */
3488 @ <pre style='border: 2px solid blue; padding: 1ex'>
 
 
 
 
 
 
3489 @ %h(blob_str(&pSender->out))
3490 @ </pre>
 
3491 }
3492 zErr = pSender->zErr;
3493 pSender->zErr = 0;
3494 alert_sender_free(pSender);
3495 return zErr;
@@ -3505,35 +3541,43 @@
3505 ** also send a message to an arbitrary email address and/or to all
3506 ** subscribers regardless of whether or not they have elected to
3507 ** receive announcements.
3508 */
3509 void announce_page(void){
3510 const char *zAction = "announce"
3511 /* Maintenance reminder: we need an explicit action=THIS_PAGE on the
3512 ** form element to avoid that a URL arg of to=... passed to this
3513 ** page ends up overwriting the form-posted "to" value. This
3514 ** action value differs for the test1 request path.
3515 */;
3516
 
 
 
 
3517 login_check_credentials();
3518 if( !g.perm.Announce ){
3519 login_needed(0);
3520 return;
3521 }
 
 
 
3522 style_set_current_feature("alerts");
3523 if( fossil_strcmp(P("name"),"test1")==0 ){
3524 /* Visit the /announce/test1 page to see the CGI variables */
3525 zAction = "announce/test1";
3526 @ <p style='border: 1px solid black; padding: 1ex;'>
3527 cgi_print_all(0, 0, 0);
3528 @ </p>
3529 }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
3530 char *zErr = alert_send_announcement();
3531 style_header("Announcement Sent");
3532 if( zErr ){
3533 @ <h1>Internal Error</h1>
3534 @ <p>The following error was reported by the system:
 
3535 @ <blockquote><pre>
3536 @ %h(zErr)
3537 @ </pre></blockquote>
3538 }else{
3539 @ <p>The announcement has been sent.
@@ -3548,10 +3592,16 @@
3548 @ for this repository.</p>
3549 return;
3550 }
3551
3552 style_header("Send Announcement");
 
 
 
 
 
 
3553 @ <form method="POST" action="%R/%s(zAction)">
3554 login_insert_csrf_secret();
3555 @ <table class="subscribe">
3556 if( g.perm.Admin ){
3557 int aa = PB("aa");
@@ -3584,15 +3634,28 @@
3584 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
3585 @ %h(PT("msg"))</textarea>
3586 @ </tr>
3587 @ <tr>
3588 @ <td></td>
3589 if( fossil_strcmp(P("name"),"test2")==0 ){
3590 @ <td><input type="submit" name="submit" value="Dry Run">
3591 }else{
3592 @ <td><input type="submit" name="submit" value="Send Message">
3593 }
3594 @ </tr>
3595 @ </table>
3596 @ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
3597 style_finish_page();
3598 }
3599
--- src/alerts.c
+++ src/alerts.c
@@ -51,11 +51,11 @@
51 @ -- f - Forum posts
52 @ -- k - ** Special: Unsubscribed using /oneclickunsub
53 @ -- n - New forum threads
54 @ -- r - Replies to my own forum posts
55 @ -- t - Ticket changes
56 @ -- u - Changes of users' permissions (admins only)
57 @ -- w - Wiki changes
58 @ -- x - Edits to forum posts
59 @ -- Probably different codes will be added in the future. In the future
60 @ -- we might also add a separate table that allows subscribing to email
61 @ -- notifications for specific branches or tags or tickets.
@@ -282,10 +282,13 @@
282 style_submenu_element("Subscribers","%R/subscribers");
283 }
284 if( fossil_strcmp(g.zPath,"subscribe") ){
285 style_submenu_element("Add New Subscriber","%R/subscribe");
286 }
287 if( fossil_strcmp(g.zPath,"setup_notification") ){
288 style_submenu_element("Notification Setup","%R/setup_notification");
289 }
290 }
291 }
292
293
294 /*
@@ -295,14 +298,14 @@
298 ** Normally accessible via the /Admin/Notification menu.
299 */
300 void setup_notification(void){
301 static const char *const azSendMethods[] = {
302 "off", "Disabled",
303 "relay", "SMTP relay",
304 "db", "Store in a database",
305 "dir", "Store in a directory",
306 "pipe", "Pipe to a command",
307 };
308 login_check_credentials();
309 if( !g.perm.Setup ){
310 login_needed(0);
311 return;
@@ -311,22 +314,25 @@
314
315 alert_submenu_common();
316 style_submenu_element("Send Announcement","%R/announce");
317 style_set_current_feature("alerts");
318 style_header("Email Notification Setup");
319 @ <form action="%R/setup_notification" method="post"><div>
320 @ <h1>Status &ensp; <input type="submit" name="submit" value="Refresh"></h1>
321 @ </form>
322 @ <table class="label-value">
323 if( alert_enabled() ){
324 stats_for_email();
325 }else{
326 @ <th>Disabled</th>
327 }
328 @ </table>
329 @ <hr>
 
330 @ <form action="%R/setup_notification" method="post"><div>
331 @ <h1> Configuration </h1>
332 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
333 @ <hr>
334 login_insert_csrf_secret();
335
336 entry_attribute("Canonical Server URL", 40, "email-url",
337 "eurl", "", 0);
338 @ <p><b>Required.</b>
@@ -391,38 +397,40 @@
397 @ <p>How to send email. Requires auxiliary information from the fields
398 @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
399 @ to send test message to debug this setting.
400 @ (Property: "email-send-method")</p>
401 alert_schema(1);
402 entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
403 "esrh", "localhost", 0);
404 @ <p>When the send method is "SMTP relay", each email message is
405 @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
406 @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
407 @ append a colon and TCP port number (ex: smtp.example.com:587).
408 @ The default TCP port number is 25.
409 @ Usage Hint: If Fossil is running inside of a chroot jail, then it might
410 @ not be able to resolve hostnames. Work around this by using a raw IP
411 @ address or create a "/etc/hosts" file inside the chroot jail.
412 @ (Property: "email-send-relayhost")</p>
413 @
414 entry_attribute("Store Emails In This Database", 60, "email-send-db",
415 "esdb", "", 0);
416 @ <p>When the send method is "store in a database", each email message is
417 @ stored in an SQLite database file with the name given here.
418 @ (Property: "email-send-db")</p>
419 entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
420 "ecmd", "sendmail -ti", 0);
421 @ <p>When the send method is "pipe to a command", this is the command
422 @ that is run. Email messages are piped into the standard input of this
423 @ command. The command is expected to extract the sender address,
424 @ recipient addresses, and subject from the header of the piped email
425 @ text. (Property: "email-send-command")</p>
 
 
 
 
 
 
 
426 entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
427 "esdir", "", 0);
428 @ <p>When the send method is "store in a directory", each email message is
429 @ stored as a separate file in the directory shown here.
430 @ (Property: "email-send-dir")</p>
431
 
 
 
 
 
 
 
 
432 @ <hr>
433
434 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
435 @ </div></form>
436 db_end_transaction(0);
@@ -630,18 +638,27 @@
638 emailerGetSetting(p, &p->zCmd, "email-send-command");
639 }else if( fossil_strcmp(p->zDest, "dir")==0 ){
640 emailerGetSetting(p, &p->zDir, "email-send-dir");
641 }else if( fossil_strcmp(p->zDest, "blob")==0 ){
642 blob_init(&p->out, 0, 0);
643 }else if( fossil_strcmp(p->zDest, "relay")==0
644 || fossil_strcmp(p->zDest, "debug-relay")==0
645 ){
646 const char *zRelay = 0;
647 emailerGetSetting(p, &zRelay, "email-send-relayhost");
648 if( zRelay ){
649 u32 smtpFlags = SMTP_DIRECT;
650 if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
651 blob_init(&p->out, 0, 0);
652 p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
653 smtpFlags, 0);
654 if( p->pSmtp==0 || p->pSmtp->zErr ){
655 emailerError(p, "Could not start SMTP session: %s",
656 p->pSmtp ? p->pSmtp->zErr : "reason unknown");
657 }else if( p->zDest[0]=='d' ){
658 smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
659 }
660 smtp_client_startup(p->pSmtp);
661 }
662 }
663 return p;
664 }
@@ -1125,11 +1142,11 @@
1142 ** SETTING: email-listid width=40
1143 ** If this setting is not an empty string, then it becomes the argument to
1144 ** a "List-ID:" header that is added to all out-bound notification emails.
1145 */
1146 /*
1147 ** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
1148 ** This is the hostname and TCP port to which output email messages
1149 ** are sent when email-send-method is "relay". There should be an
1150 ** SMTP server configured as a Mail Submission Agent listening on the
1151 ** designated host and port and all times.
1152 */
@@ -1704,11 +1721,11 @@
1721 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
1722 @ Wiki</label><br>
1723 }
1724 if( g.perm.Admin ){
1725 @ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1726 @ User permission changes</label>
1727 }
1728 di = PB("di");
1729 @ </td></tr>
1730 @ <tr>
1731 @ <td class="form_label">Delivery:</td>
@@ -2114,11 +2131,11 @@
2131 /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
2132 ** subscription will get removed if the user later edits their
2133 ** subscriptions, as non-admins are not permitted to add that
2134 ** subscription. */
2135 @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2136 @ User permission changes</label>
2137 }
2138 @ </td></tr>
2139 if( strchr(ssub,'k')!=0 ){
2140 @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
2141 @ Note: User did a one-click unsubscribe</td></tr>
@@ -3436,16 +3453,28 @@
3453 char *zSubject = PT("subject");
3454 int bAll = PB("all");
3455 int bAA = PB("aa");
3456 int bMods = PB("mods");
3457 const char *zSub = db_get("email-subname", "[Fossil Repo]");
3458 const char *zName = P("name"); /* Debugging options */
3459 const char *zDest = 0; /* How to send the announcement */
3460 int bTest = 0;
3461 Blob hdr, body;
3462
3463 if( fossil_strcmp(zName, "test2")==0 ){
3464 bTest = 2;
3465 zDest = "blob";
3466 }else if( fossil_strcmp(zName, "test3")==0 ){
3467 bTest = 3;
3468 if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
3469 zDest = "debug-relay";
3470 }
3471 }
3472 blob_init(&body, 0, 0);
3473 blob_init(&hdr, 0, 0);
3474 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3475 pSender = alert_sender_new(zDest, 0);
3476 if( zTo[0] ){
3477 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
3478 alert_send(pSender, &hdr, &body, 0);
3479 }
3480 if( bAll || bAA || bMods ){
@@ -3479,17 +3508,24 @@
3508 }
3509 alert_send(pSender, &hdr, &body, 0);
3510 }
3511 db_finalize(&q);
3512 }
3513 if( bTest && blob_size(&pSender->out) ){
3514 /* If the URL is "/announce/test2" then no email is actually sent.
3515 ** Instead, the text of the email that would have been sent is
3516 ** displayed in the result window.
3517 **
3518 ** If the URL is "/announce/test3" and the email-send-method is "relay"
3519 ** then the announcement is sent as it normally would be, but a
3520 ** transcript of the SMTP conversation with the MTA is shown here.
3521 */
3522 blob_trim(&pSender->out);
3523 @ <pre style='border: 2px solid blue; padding: 1ex;'>
3524 @ %h(blob_str(&pSender->out))
3525 @ </pre>
3526 blob_reset(&pSender->out);
3527 }
3528 zErr = pSender->zErr;
3529 pSender->zErr = 0;
3530 alert_sender_free(pSender);
3531 return zErr;
@@ -3505,35 +3541,43 @@
3541 ** also send a message to an arbitrary email address and/or to all
3542 ** subscribers regardless of whether or not they have elected to
3543 ** receive announcements.
3544 */
3545 void announce_page(void){
3546 const char *zAction = "announce";
3547 const char *zName = PD("name","");
3548 /*
3549 ** Debugging Notes:
3550 **
3551 ** /announce/test1 -> Shows query parameter values
3552 ** /announce/test2 -> Shows the formatted message but does
3553 ** not send it.
3554 ** /announce/test3 -> Sends the message, but also shows
3555 ** the SMTP transcript.
3556 */
3557 login_check_credentials();
3558 if( !g.perm.Announce ){
3559 login_needed(0);
3560 return;
3561 }
3562 if( !g.perm.Setup ){
3563 zName = 0; /* Disable debugging feature for non-admin users */
3564 }
3565 style_set_current_feature("alerts");
3566 if( fossil_strcmp(zName,"test1")==0 ){
3567 /* Visit the /announce/test1 page to see the CGI variables */
3568 zAction = "announce/test1";
3569 @ <p style='border: 1px solid black; padding: 1ex;'>
3570 cgi_print_all(0, 0, 0);
3571 @ </p>
3572 }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
3573 char *zErr = alert_send_announcement();
3574 style_header("Announcement Sent");
3575 if( zErr ){
3576 @ <h1>Error</h1>
3577 @ <p>The following error was reported by the
3578 @ announcement-sending subsystem:
3579 @ <blockquote><pre>
3580 @ %h(zErr)
3581 @ </pre></blockquote>
3582 }else{
3583 @ <p>The announcement has been sent.
@@ -3548,10 +3592,16 @@
3592 @ for this repository.</p>
3593 return;
3594 }
3595
3596 style_header("Send Announcement");
3597 alert_submenu_common();
3598 if( fossil_strcmp(zName,"test2")==0 ){
3599 zAction = "announce/test2";
3600 }else if( fossil_strcmp(zName,"test3")==0 ){
3601 zAction = "announce/test3";
3602 }
3603 @ <form method="POST" action="%R/%s(zAction)">
3604 login_insert_csrf_secret();
3605 @ <table class="subscribe">
3606 if( g.perm.Admin ){
3607 int aa = PB("aa");
@@ -3584,15 +3634,28 @@
3634 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
3635 @ %h(PT("msg"))</textarea>
3636 @ </tr>
3637 @ <tr>
3638 @ <td></td>
3639 if( fossil_strcmp(zName,"test2")==0 ){
3640 @ <td><input type="submit" name="submit" value="Dry Run">
3641 }else{
3642 @ <td><input type="submit" name="submit" value="Send Message">
3643 }
3644 @ </tr>
3645 @ </table>
3646 @ </form>
3647 if( g.perm.Setup ){
3648 @ <hr>
3649 @ <p>Trouble-shooting Options:</p>
3650 @ <ol>
3651 @ <li> <a href="%R/announce">Normal Processing</a>
3652 @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
3653 @ - Do not send the announcement.
3654 @ <li> <a href="%R/announce/test2">Show the email text</a> but do
3655 @ not actually send it.
3656 @ <li> Send the message and also <a href="%R/announce/test3">show the
3657 @ SMTP traffic</a> when using "relay" mode.
3658 @ </ol>
3659 }
3660 style_finish_page();
3661 }
3662
--- src/backoffice.c
+++ src/backoffice.c
@@ -545,19 +545,14 @@
545545
break;
546546
}
547547
}else{
548548
if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
549549
sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
550
- if( warningDelay<=240 && runningFor>1800 ){
551
- /* On a busy system with 15-bit process-id numbers, we can sometimes
552
- ** wrap-around the process-id space causing backofficeProcessDone()
553
- ** to return a false negative. Try to prevent this from causing a
554
- ** false-positive hung-backoffice warning. */
555
- }else{
550
+ if( warningDelay>=240 && runningFor<1800 ){
556551
fossil_warning(
557552
"backoffice process %lld still running after %d seconds",
558
- x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));
553
+ x.idCurrent, runningFor);
559554
}
560555
lastWarning = tmNow;
561556
warningDelay *= 2;
562557
}
563558
if( backofficeSleep(1000) ){
564559
--- src/backoffice.c
+++ src/backoffice.c
@@ -545,19 +545,14 @@
545 break;
546 }
547 }else{
548 if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
549 sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
550 if( warningDelay<=240 && runningFor>1800 ){
551 /* On a busy system with 15-bit process-id numbers, we can sometimes
552 ** wrap-around the process-id space causing backofficeProcessDone()
553 ** to return a false negative. Try to prevent this from causing a
554 ** false-positive hung-backoffice warning. */
555 }else{
556 fossil_warning(
557 "backoffice process %lld still running after %d seconds",
558 x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));
559 }
560 lastWarning = tmNow;
561 warningDelay *= 2;
562 }
563 if( backofficeSleep(1000) ){
564
--- src/backoffice.c
+++ src/backoffice.c
@@ -545,19 +545,14 @@
545 break;
546 }
547 }else{
548 if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
549 sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
550 if( warningDelay>=240 && runningFor<1800 ){
 
 
 
 
 
551 fossil_warning(
552 "backoffice process %lld still running after %d seconds",
553 x.idCurrent, runningFor);
554 }
555 lastWarning = tmNow;
556 warningDelay *= 2;
557 }
558 if( backofficeSleep(1000) ){
559
+3
--- src/cgi.c
+++ src/cgi.c
@@ -637,10 +637,13 @@
637637
cgi_set_status(iStat, zStat);
638638
free(zLocation);
639639
cgi_reply();
640640
fossil_exit(0);
641641
}
642
+NORETURN void cgi_redirect_perm(const char *zURL){
643
+ cgi_redirect_with_status(zURL, 301, "Moved Permanently");
644
+}
642645
NORETURN void cgi_redirect(const char *zURL){
643646
cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
644647
}
645648
NORETURN void cgi_redirect_with_method(const char *zURL){
646649
cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
647650
--- src/cgi.c
+++ src/cgi.c
@@ -637,10 +637,13 @@
637 cgi_set_status(iStat, zStat);
638 free(zLocation);
639 cgi_reply();
640 fossil_exit(0);
641 }
 
 
 
642 NORETURN void cgi_redirect(const char *zURL){
643 cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
644 }
645 NORETURN void cgi_redirect_with_method(const char *zURL){
646 cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
647
--- src/cgi.c
+++ src/cgi.c
@@ -637,10 +637,13 @@
637 cgi_set_status(iStat, zStat);
638 free(zLocation);
639 cgi_reply();
640 fossil_exit(0);
641 }
642 NORETURN void cgi_redirect_perm(const char *zURL){
643 cgi_redirect_with_status(zURL, 301, "Moved Permanently");
644 }
645 NORETURN void cgi_redirect(const char *zURL){
646 cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
647 }
648 NORETURN void cgi_redirect_with_method(const char *zURL){
649 cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
650
+23 -5
--- src/main.c
+++ src/main.c
@@ -2053,11 +2053,12 @@
20532053
*/
20542054
set_base_url(0);
20552055
if( fossil_redirect_to_https_if_needed(2) ) return;
20562056
if( zPathInfo==0 || zPathInfo[0]==0
20572057
|| (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
2058
- /* Second special case: If the PATH_INFO is blank, issue a redirect:
2058
+ /* Second special case: If the PATH_INFO is blank, issue a
2059
+ ** temporary 302 redirect:
20592060
** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
20602061
** (2) to the home page identified by the "index-page" setting
20612062
** in the repository CONFIG table
20622063
** (3) to "/index" if there no "index-page" setting in CONFIG
20632064
*/
@@ -2267,10 +2268,21 @@
22672268
**
22682269
** #!/usr/bin/fossil
22692270
** redirect: * https://fossil-scm.org/home
22702271
**
22712272
** Thus requests to the .com website redirect to the .org website.
2273
+** This form uses a 301 Permanent redirect.
2274
+**
2275
+** On a "*" redirect, the PATH_INFO and QUERY_STRING of the query
2276
+** that provoked the redirect are appended to the target. So, for
2277
+** example, if the input URL for the redirect above were
2278
+** "http://www.fossil.com/index.html/timeline?c=20250404", then
2279
+** the redirect would be to:
2280
+**
2281
+** https://fossil-scm.org/home/timeline?c=20250404
2282
+** ^^^^^^^^^^^^^^^^^^^^
2283
+** Copied from input URL
22722284
*/
22732285
static void redirect_web_page(int nRedirect, char **azRedirect){
22742286
int i; /* Loop counter */
22752287
const char *zNotFound = 0; /* Not found URL */
22762288
const char *zName = P("name");
@@ -2297,21 +2309,22 @@
22972309
}
22982310
if( zNotFound ){
22992311
Blob to;
23002312
const char *z;
23012313
if( strstr(zNotFound, "%s") ){
2302
- cgi_redirectf(zNotFound /*works-like:"%s"*/, zName);
2314
+ char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName);
2315
+ cgi_redirect_perm(zTarget);
23032316
}
23042317
if( strchr(zNotFound, '?') ){
2305
- cgi_redirect(zNotFound);
2318
+ cgi_redirect_perm(zNotFound);
23062319
}
23072320
blob_init(&to, zNotFound, -1);
23082321
z = P("PATH_INFO");
23092322
if( z && z[0]=='/' ) blob_append(&to, z, -1);
23102323
z = P("QUERY_STRING");
23112324
if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
2312
- cgi_redirect(blob_str(&to));
2325
+ cgi_redirect_perm(blob_str(&to));
23132326
}else{
23142327
@ <html>
23152328
@ <head><title>No Such Object</title></head>
23162329
@ <body>
23172330
@ <p>No such object: <b>%h(zName)</b></p>
@@ -2394,10 +2407,13 @@
23942407
** REPO for a check-in or ticket that matches the
23952408
** value of "name", then redirect to URL. There
23962409
** can be multiple "redirect:" lines that are
23972410
** processed in order. If the REPO is "*", then
23982411
** an unconditional redirect to URL is taken.
2412
+** When "*" is used a 301 permanent redirect is
2413
+** issued and the tail and query string from the
2414
+** original query are appeneded onto URL.
23992415
**
24002416
** jsmode: VALUE Specifies the delivery mode for JavaScript
24012417
** files. See the help text for the --jsmode
24022418
** flag of the http command.
24032419
**
@@ -3052,23 +3068,25 @@
30523068
** using this command interactively over SSH. A better solution would be
30533069
** to use a different command for "ssh" sync, but we cannot do that without
30543070
** breaking legacy.
30553071
**
30563072
** Options:
3073
+** --csrf-safe N Set cgi_csrf_safe() to to return N
30573074
** --nobody Pretend to be user "nobody"
30583075
** --test Do not do special "sync" processing when operating
30593076
** over an SSH link
30603077
** --th-trace Trace TH1 execution (for debugging purposes)
30613078
** --usercap CAP User capability string (Default: "sxy")
3062
-**
30633079
*/
30643080
void cmd_test_http(void){
30653081
const char *zIpAddr; /* IP address of remote client */
30663082
const char *zUserCap;
30673083
int bTest = 0;
3084
+ const char *zCsrfSafe = find_option("csrf-safe",0,1);
30683085
30693086
Th_InitTraceLog();
3087
+ if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
30703088
zUserCap = find_option("usercap",0,1);
30713089
if( !find_option("nobody",0,0) ){
30723090
if( zUserCap==0 ){
30733091
g.useLocalauth = 1;
30743092
zUserCap = "sxy";
30753093
--- src/main.c
+++ src/main.c
@@ -2053,11 +2053,12 @@
2053 */
2054 set_base_url(0);
2055 if( fossil_redirect_to_https_if_needed(2) ) return;
2056 if( zPathInfo==0 || zPathInfo[0]==0
2057 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
2058 /* Second special case: If the PATH_INFO is blank, issue a redirect:
 
2059 ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
2060 ** (2) to the home page identified by the "index-page" setting
2061 ** in the repository CONFIG table
2062 ** (3) to "/index" if there no "index-page" setting in CONFIG
2063 */
@@ -2267,10 +2268,21 @@
2267 **
2268 ** #!/usr/bin/fossil
2269 ** redirect: * https://fossil-scm.org/home
2270 **
2271 ** Thus requests to the .com website redirect to the .org website.
 
 
 
 
 
 
 
 
 
 
 
2272 */
2273 static void redirect_web_page(int nRedirect, char **azRedirect){
2274 int i; /* Loop counter */
2275 const char *zNotFound = 0; /* Not found URL */
2276 const char *zName = P("name");
@@ -2297,21 +2309,22 @@
2297 }
2298 if( zNotFound ){
2299 Blob to;
2300 const char *z;
2301 if( strstr(zNotFound, "%s") ){
2302 cgi_redirectf(zNotFound /*works-like:"%s"*/, zName);
 
2303 }
2304 if( strchr(zNotFound, '?') ){
2305 cgi_redirect(zNotFound);
2306 }
2307 blob_init(&to, zNotFound, -1);
2308 z = P("PATH_INFO");
2309 if( z && z[0]=='/' ) blob_append(&to, z, -1);
2310 z = P("QUERY_STRING");
2311 if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
2312 cgi_redirect(blob_str(&to));
2313 }else{
2314 @ <html>
2315 @ <head><title>No Such Object</title></head>
2316 @ <body>
2317 @ <p>No such object: <b>%h(zName)</b></p>
@@ -2394,10 +2407,13 @@
2394 ** REPO for a check-in or ticket that matches the
2395 ** value of "name", then redirect to URL. There
2396 ** can be multiple "redirect:" lines that are
2397 ** processed in order. If the REPO is "*", then
2398 ** an unconditional redirect to URL is taken.
 
 
 
2399 **
2400 ** jsmode: VALUE Specifies the delivery mode for JavaScript
2401 ** files. See the help text for the --jsmode
2402 ** flag of the http command.
2403 **
@@ -3052,23 +3068,25 @@
3052 ** using this command interactively over SSH. A better solution would be
3053 ** to use a different command for "ssh" sync, but we cannot do that without
3054 ** breaking legacy.
3055 **
3056 ** Options:
 
3057 ** --nobody Pretend to be user "nobody"
3058 ** --test Do not do special "sync" processing when operating
3059 ** over an SSH link
3060 ** --th-trace Trace TH1 execution (for debugging purposes)
3061 ** --usercap CAP User capability string (Default: "sxy")
3062 **
3063 */
3064 void cmd_test_http(void){
3065 const char *zIpAddr; /* IP address of remote client */
3066 const char *zUserCap;
3067 int bTest = 0;
 
3068
3069 Th_InitTraceLog();
 
3070 zUserCap = find_option("usercap",0,1);
3071 if( !find_option("nobody",0,0) ){
3072 if( zUserCap==0 ){
3073 g.useLocalauth = 1;
3074 zUserCap = "sxy";
3075
--- src/main.c
+++ src/main.c
@@ -2053,11 +2053,12 @@
2053 */
2054 set_base_url(0);
2055 if( fossil_redirect_to_https_if_needed(2) ) return;
2056 if( zPathInfo==0 || zPathInfo[0]==0
2057 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
2058 /* Second special case: If the PATH_INFO is blank, issue a
2059 ** temporary 302 redirect:
2060 ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
2061 ** (2) to the home page identified by the "index-page" setting
2062 ** in the repository CONFIG table
2063 ** (3) to "/index" if there no "index-page" setting in CONFIG
2064 */
@@ -2267,10 +2268,21 @@
2268 **
2269 ** #!/usr/bin/fossil
2270 ** redirect: * https://fossil-scm.org/home
2271 **
2272 ** Thus requests to the .com website redirect to the .org website.
2273 ** This form uses a 301 Permanent redirect.
2274 **
2275 ** On a "*" redirect, the PATH_INFO and QUERY_STRING of the query
2276 ** that provoked the redirect are appended to the target. So, for
2277 ** example, if the input URL for the redirect above were
2278 ** "http://www.fossil.com/index.html/timeline?c=20250404", then
2279 ** the redirect would be to:
2280 **
2281 ** https://fossil-scm.org/home/timeline?c=20250404
2282 ** ^^^^^^^^^^^^^^^^^^^^
2283 ** Copied from input URL
2284 */
2285 static void redirect_web_page(int nRedirect, char **azRedirect){
2286 int i; /* Loop counter */
2287 const char *zNotFound = 0; /* Not found URL */
2288 const char *zName = P("name");
@@ -2297,21 +2309,22 @@
2309 }
2310 if( zNotFound ){
2311 Blob to;
2312 const char *z;
2313 if( strstr(zNotFound, "%s") ){
2314 char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName);
2315 cgi_redirect_perm(zTarget);
2316 }
2317 if( strchr(zNotFound, '?') ){
2318 cgi_redirect_perm(zNotFound);
2319 }
2320 blob_init(&to, zNotFound, -1);
2321 z = P("PATH_INFO");
2322 if( z && z[0]=='/' ) blob_append(&to, z, -1);
2323 z = P("QUERY_STRING");
2324 if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
2325 cgi_redirect_perm(blob_str(&to));
2326 }else{
2327 @ <html>
2328 @ <head><title>No Such Object</title></head>
2329 @ <body>
2330 @ <p>No such object: <b>%h(zName)</b></p>
@@ -2394,10 +2407,13 @@
2407 ** REPO for a check-in or ticket that matches the
2408 ** value of "name", then redirect to URL. There
2409 ** can be multiple "redirect:" lines that are
2410 ** processed in order. If the REPO is "*", then
2411 ** an unconditional redirect to URL is taken.
2412 ** When "*" is used a 301 permanent redirect is
2413 ** issued and the tail and query string from the
2414 ** original query are appeneded onto URL.
2415 **
2416 ** jsmode: VALUE Specifies the delivery mode for JavaScript
2417 ** files. See the help text for the --jsmode
2418 ** flag of the http command.
2419 **
@@ -3052,23 +3068,25 @@
3068 ** using this command interactively over SSH. A better solution would be
3069 ** to use a different command for "ssh" sync, but we cannot do that without
3070 ** breaking legacy.
3071 **
3072 ** Options:
3073 ** --csrf-safe N Set cgi_csrf_safe() to to return N
3074 ** --nobody Pretend to be user "nobody"
3075 ** --test Do not do special "sync" processing when operating
3076 ** over an SSH link
3077 ** --th-trace Trace TH1 execution (for debugging purposes)
3078 ** --usercap CAP User capability string (Default: "sxy")
 
3079 */
3080 void cmd_test_http(void){
3081 const char *zIpAddr; /* IP address of remote client */
3082 const char *zUserCap;
3083 int bTest = 0;
3084 const char *zCsrfSafe = find_option("csrf-safe",0,1);
3085
3086 Th_InitTraceLog();
3087 if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
3088 zUserCap = find_option("usercap",0,1);
3089 if( !find_option("nobody",0,0) ){
3090 if( zUserCap==0 ){
3091 g.useLocalauth = 1;
3092 zUserCap = "sxy";
3093
+1 -1
--- src/printf.c
+++ src/printf.c
@@ -1100,11 +1100,11 @@
11001100
if( zFormat[0]=='X' ){
11011101
bDetail = 1;
11021102
zFormat++;
11031103
}
11041104
vfprintf(out, zFormat, ap);
1105
- fprintf(out, "\n");
1105
+ fprintf(out, " (pid %d)\n", (int)getpid());
11061106
va_end(ap);
11071107
if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
11081108
if( bDetail ){
11091109
cgi_print_all(1,3,out);
11101110
}else{
11111111
--- src/printf.c
+++ src/printf.c
@@ -1100,11 +1100,11 @@
1100 if( zFormat[0]=='X' ){
1101 bDetail = 1;
1102 zFormat++;
1103 }
1104 vfprintf(out, zFormat, ap);
1105 fprintf(out, "\n");
1106 va_end(ap);
1107 if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
1108 if( bDetail ){
1109 cgi_print_all(1,3,out);
1110 }else{
1111
--- src/printf.c
+++ src/printf.c
@@ -1100,11 +1100,11 @@
1100 if( zFormat[0]=='X' ){
1101 bDetail = 1;
1102 zFormat++;
1103 }
1104 vfprintf(out, zFormat, ap);
1105 fprintf(out, " (pid %d)\n", (int)getpid());
1106 va_end(ap);
1107 if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
1108 if( bDetail ){
1109 cgi_print_all(1,3,out);
1110 }else{
1111
+19 -1
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
3131
int isValid; /* True if zRepoName is a valid Fossil repository */
3232
int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
3333
** for the repository list. 2 means do use this
3434
** repository but do not display it in the list. */
3535
char *zProjName; /* Project Name. Memory from fossil_malloc() */
36
+ char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
3637
char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
3738
double rMTime; /* Last update. Julian day number */
3839
};
3940
#endif
4041
@@ -49,10 +50,11 @@
4950
int rc;
5051
5152
pRepo->isRepolistSkin = 0;
5253
pRepo->isValid = 0;
5354
pRepo->zProjName = 0;
55
+ pRepo->zProjDesc = 0;
5456
pRepo->zLoginGroup = 0;
5557
pRepo->rMTime = 0.0;
5658
5759
g.dbIgnoreErrors++;
5860
rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
7173
-1, &pStmt, 0);
7274
if( rc ) goto finish_repo_list;
7375
if( sqlite3_step(pStmt)==SQLITE_ROW ){
7476
pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
7577
}
78
+ sqlite3_finalize(pStmt);
79
+ if( rc ) goto finish_repo_list;
80
+ rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81
+ " WHERE name='project-description'",
82
+ -1, &pStmt, 0);
83
+ if( rc ) goto finish_repo_list;
84
+ if( sqlite3_step(pStmt)==SQLITE_ROW ){
85
+ pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86
+ }
7687
sqlite3_finalize(pStmt);
7788
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
7889
" WHERE name='login-group-name'",
7990
-1, &pStmt, 0);
8091
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -162,13 +173,14 @@
162173
}else{
163174
Stmt q;
164175
double rNow;
165176
blob_append_sql(&html,
166177
"<table border='0' class='sortable' data-init-sort='1'"
167
- " data-column-types='txtxkxt'><thead>\n"
178
+ " data-column-types='txtxtxkxt'><thead>\n"
168179
"<tr><th>Filename<th width='20'>"
169180
"<th>Project Name<th width='20'>"
181
+ "<th>Project Description<th width='20'>"
170182
"<th>Last Modified<th width='20'>"
171183
"<th>Login Group</tr>\n"
172184
"</thead><tbody>\n");
173185
db_prepare(&q, "SELECT pathname"
174186
" FROM sfile ORDER BY pathname COLLATE nocase;");
@@ -278,10 +290,16 @@
278290
zUrl, zName);
279291
}
280292
if( x.zProjName ){
281293
blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
282294
fossil_free(x.zProjName);
295
+ }else{
296
+ blob_append_sql(&html, "<td></td><td></td>\n");
297
+ }
298
+ if( x.zProjDesc ){
299
+ blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjDesc);
300
+ fossil_free(x.zProjDesc);
283301
}else{
284302
blob_append_sql(&html, "<td></td><td></td>\n");
285303
}
286304
blob_append_sql(&html,
287305
"<td></td><td data-sortkey='%08x'>%h</td>\n",
288306
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
31 int isValid; /* True if zRepoName is a valid Fossil repository */
32 int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33 ** for the repository list. 2 means do use this
34 ** repository but do not display it in the list. */
35 char *zProjName; /* Project Name. Memory from fossil_malloc() */
 
36 char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
37 double rMTime; /* Last update. Julian day number */
38 };
39 #endif
40
@@ -49,10 +50,11 @@
49 int rc;
50
51 pRepo->isRepolistSkin = 0;
52 pRepo->isValid = 0;
53 pRepo->zProjName = 0;
 
54 pRepo->zLoginGroup = 0;
55 pRepo->rMTime = 0.0;
56
57 g.dbIgnoreErrors++;
58 rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
71 -1, &pStmt, 0);
72 if( rc ) goto finish_repo_list;
73 if( sqlite3_step(pStmt)==SQLITE_ROW ){
74 pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
75 }
 
 
 
 
 
 
 
 
 
76 sqlite3_finalize(pStmt);
77 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
78 " WHERE name='login-group-name'",
79 -1, &pStmt, 0);
80 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -162,13 +173,14 @@
162 }else{
163 Stmt q;
164 double rNow;
165 blob_append_sql(&html,
166 "<table border='0' class='sortable' data-init-sort='1'"
167 " data-column-types='txtxkxt'><thead>\n"
168 "<tr><th>Filename<th width='20'>"
169 "<th>Project Name<th width='20'>"
 
170 "<th>Last Modified<th width='20'>"
171 "<th>Login Group</tr>\n"
172 "</thead><tbody>\n");
173 db_prepare(&q, "SELECT pathname"
174 " FROM sfile ORDER BY pathname COLLATE nocase;");
@@ -278,10 +290,16 @@
278 zUrl, zName);
279 }
280 if( x.zProjName ){
281 blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
282 fossil_free(x.zProjName);
 
 
 
 
 
 
283 }else{
284 blob_append_sql(&html, "<td></td><td></td>\n");
285 }
286 blob_append_sql(&html,
287 "<td></td><td data-sortkey='%08x'>%h</td>\n",
288
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
31 int isValid; /* True if zRepoName is a valid Fossil repository */
32 int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33 ** for the repository list. 2 means do use this
34 ** repository but do not display it in the list. */
35 char *zProjName; /* Project Name. Memory from fossil_malloc() */
36 char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
37 char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
38 double rMTime; /* Last update. Julian day number */
39 };
40 #endif
41
@@ -49,10 +50,11 @@
50 int rc;
51
52 pRepo->isRepolistSkin = 0;
53 pRepo->isValid = 0;
54 pRepo->zProjName = 0;
55 pRepo->zProjDesc = 0;
56 pRepo->zLoginGroup = 0;
57 pRepo->rMTime = 0.0;
58
59 g.dbIgnoreErrors++;
60 rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
73 -1, &pStmt, 0);
74 if( rc ) goto finish_repo_list;
75 if( sqlite3_step(pStmt)==SQLITE_ROW ){
76 pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
77 }
78 sqlite3_finalize(pStmt);
79 if( rc ) goto finish_repo_list;
80 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81 " WHERE name='project-description'",
82 -1, &pStmt, 0);
83 if( rc ) goto finish_repo_list;
84 if( sqlite3_step(pStmt)==SQLITE_ROW ){
85 pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86 }
87 sqlite3_finalize(pStmt);
88 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
89 " WHERE name='login-group-name'",
90 -1, &pStmt, 0);
91 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -162,13 +173,14 @@
173 }else{
174 Stmt q;
175 double rNow;
176 blob_append_sql(&html,
177 "<table border='0' class='sortable' data-init-sort='1'"
178 " data-column-types='txtxtxkxt'><thead>\n"
179 "<tr><th>Filename<th width='20'>"
180 "<th>Project Name<th width='20'>"
181 "<th>Project Description<th width='20'>"
182 "<th>Last Modified<th width='20'>"
183 "<th>Login Group</tr>\n"
184 "</thead><tbody>\n");
185 db_prepare(&q, "SELECT pathname"
186 " FROM sfile ORDER BY pathname COLLATE nocase;");
@@ -278,10 +290,16 @@
290 zUrl, zName);
291 }
292 if( x.zProjName ){
293 blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
294 fossil_free(x.zProjName);
295 }else{
296 blob_append_sql(&html, "<td></td><td></td>\n");
297 }
298 if( x.zProjDesc ){
299 blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjDesc);
300 fossil_free(x.zProjDesc);
301 }else{
302 blob_append_sql(&html, "<td></td><td></td>\n");
303 }
304 blob_append_sql(&html,
305 "<td></td><td data-sortkey='%08x'>%h</td>\n",
306
+3 -2
--- src/search.c
+++ src/search.c
@@ -618,10 +618,11 @@
618618
int bDebug = find_option("debug",0,0)!=0; /* Undocumented */
619619
int nLimit = zLimit ? atoi(zLimit) : -1000;
620620
int width;
621621
int nTty = 0; /* VT100 highlight color for matching text */
622622
const char *zHighlight = 0;
623
+ int bFlags = 0; /* DB open flags */
623624
624625
nTty = terminal_is_vt100();
625626
626627
/* Undocumented option to change highlight color */
627628
zHighlight = find_option("highlight",0,1);
@@ -666,12 +667,12 @@
666667
if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; }
667668
668669
/* If no search objects are specified, default to "check-in comments" */
669670
if( srchFlags==0 ) srchFlags = SRCH_CKIN;
670671
671
-
672
- db_find_and_open_repository(0, 0);
672
+ if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE;
673
+ db_find_and_open_repository(bFlags, 0);
673674
verify_all_options();
674675
if( g.argc<3 ) return;
675676
login_set_capabilities("s", 0);
676677
if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
677678
const char *zC1 = 0, *zPlural = "s";
678679
--- src/search.c
+++ src/search.c
@@ -618,10 +618,11 @@
618 int bDebug = find_option("debug",0,0)!=0; /* Undocumented */
619 int nLimit = zLimit ? atoi(zLimit) : -1000;
620 int width;
621 int nTty = 0; /* VT100 highlight color for matching text */
622 const char *zHighlight = 0;
 
623
624 nTty = terminal_is_vt100();
625
626 /* Undocumented option to change highlight color */
627 zHighlight = find_option("highlight",0,1);
@@ -666,12 +667,12 @@
666 if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; }
667
668 /* If no search objects are specified, default to "check-in comments" */
669 if( srchFlags==0 ) srchFlags = SRCH_CKIN;
670
671
672 db_find_and_open_repository(0, 0);
673 verify_all_options();
674 if( g.argc<3 ) return;
675 login_set_capabilities("s", 0);
676 if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
677 const char *zC1 = 0, *zPlural = "s";
678
--- src/search.c
+++ src/search.c
@@ -618,10 +618,11 @@
618 int bDebug = find_option("debug",0,0)!=0; /* Undocumented */
619 int nLimit = zLimit ? atoi(zLimit) : -1000;
620 int width;
621 int nTty = 0; /* VT100 highlight color for matching text */
622 const char *zHighlight = 0;
623 int bFlags = 0; /* DB open flags */
624
625 nTty = terminal_is_vt100();
626
627 /* Undocumented option to change highlight color */
628 zHighlight = find_option("highlight",0,1);
@@ -666,12 +667,12 @@
667 if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; }
668
669 /* If no search objects are specified, default to "check-in comments" */
670 if( srchFlags==0 ) srchFlags = SRCH_CKIN;
671
672 if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE;
673 db_find_and_open_repository(bFlags, 0);
674 verify_all_options();
675 if( g.argc<3 ) return;
676 login_set_capabilities("s", 0);
677 if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
678 const char *zC1 = 0, *zPlural = "s";
679
+2 -2
--- src/setup.c
+++ src/setup.c
@@ -201,11 +201,11 @@
201201
login_needed(0);
202202
return;
203203
}
204204
style_header("Log Menu");
205205
@ <table border="0" cellspacing="3">
206
-
206
+
207207
if( db_get_boolean("admin-log",1)==0 ){
208208
blob_appendf(&desc,
209209
"The admin log records configuration changes to the repository.\n"
210210
"<b>Disabled</b>: Turn on the "
211211
" <a href='%R/setup_settings'>admin-log setting</a> to enable."
@@ -1296,11 +1296,11 @@
12961296
@ </p>
12971297
@ <hr>
12981298
textarea_attribute("Project Description", 3, 80,
12991299
"project-description", "pd", "", 0);
13001300
@ <p>Describe your project. This will be used in page headers for search
1301
- @ engines as well as a short RSS description.
1301
+ @ engines, the repository listing and a short RSS description.
13021302
@ (Property: "project-description")</p>
13031303
@ <hr>
13041304
entry_attribute("Canonical Server URL", 40, "email-url",
13051305
"eurl", "", 0);
13061306
@ <p>This is the URL used to access this repository as a server.
13071307
--- src/setup.c
+++ src/setup.c
@@ -201,11 +201,11 @@
201 login_needed(0);
202 return;
203 }
204 style_header("Log Menu");
205 @ <table border="0" cellspacing="3">
206
207 if( db_get_boolean("admin-log",1)==0 ){
208 blob_appendf(&desc,
209 "The admin log records configuration changes to the repository.\n"
210 "<b>Disabled</b>: Turn on the "
211 " <a href='%R/setup_settings'>admin-log setting</a> to enable."
@@ -1296,11 +1296,11 @@
1296 @ </p>
1297 @ <hr>
1298 textarea_attribute("Project Description", 3, 80,
1299 "project-description", "pd", "", 0);
1300 @ <p>Describe your project. This will be used in page headers for search
1301 @ engines as well as a short RSS description.
1302 @ (Property: "project-description")</p>
1303 @ <hr>
1304 entry_attribute("Canonical Server URL", 40, "email-url",
1305 "eurl", "", 0);
1306 @ <p>This is the URL used to access this repository as a server.
1307
--- src/setup.c
+++ src/setup.c
@@ -201,11 +201,11 @@
201 login_needed(0);
202 return;
203 }
204 style_header("Log Menu");
205 @ <table border="0" cellspacing="3">
206
207 if( db_get_boolean("admin-log",1)==0 ){
208 blob_appendf(&desc,
209 "The admin log records configuration changes to the repository.\n"
210 "<b>Disabled</b>: Turn on the "
211 " <a href='%R/setup_settings'>admin-log setting</a> to enable."
@@ -1296,11 +1296,11 @@
1296 @ </p>
1297 @ <hr>
1298 textarea_attribute("Project Description", 3, 80,
1299 "project-description", "pd", "", 0);
1300 @ <p>Describe your project. This will be used in page headers for search
1301 @ engines, the repository listing and a short RSS description.
1302 @ (Property: "project-description")</p>
1303 @ <hr>
1304 entry_attribute("Canonical Server URL", 40, "email-url",
1305 "eurl", "", 0);
1306 @ <p>This is the URL used to access this repository as a server.
1307
+70 -28
--- src/setupuser.c
+++ src/setupuser.c
@@ -155,18 +155,19 @@
155155
zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
156156
}else{
157157
zWith = "";
158158
}
159159
db_prepare(&s,
160
- "SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
161
- " lower(login) AS sortkey, "
160
+ "SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," /* 0..4 */
161
+ " lower(login) AS sortkey, " /* 5 */
162162
" CASE WHEN info LIKE '%%expires 20%%'"
163163
" THEN substr(info,instr(lower(info),'expires')+8,10)"
164
- " END AS exp,"
165
- "atime,"
166
- " subscriber.ssub, subscriber.subscriberId,"
167
- " user.mtime AS sorttime"
164
+ " END AS exp," /* 6 */
165
+ "atime," /* 7 */
166
+ " subscriber.ssub, subscriber.subscriberId," /* 8, 9 */
167
+ " user.mtime AS sorttime," /* 10 */
168
+ " subscriber.semail" /* 11 */
168169
" FROM user LEFT JOIN lastAccess ON login=uname"
169170
" LEFT JOIN subscriber ON login=suname"
170171
" WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
171172
" ORDER BY sorttime DESC", zWith/*safe-for-%s*/
172173
);
@@ -202,11 +203,13 @@
202203
if( db_column_type(&s,8)==SQLITE_NULL ){
203204
@ <td>
204205
}else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
205206
@ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
206207
}else{
207
- @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>
208
+ const char *zEmail = db_column_text(&s, 11);
209
+ char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
210
+ @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
208211
}
209212
210213
@ </tr>
211214
fossil_free(zAge);
212215
}
@@ -304,22 +307,59 @@
304307
while( zPw[0]=='*' ){ zPw++; }
305308
return zPw[0]!=0;
306309
}
307310
308311
/*
309
-** Return true if user capability string zNew contains any capability
310
-** letter which is not in user capability string zOrig, else 0. This
311
-** does not take inherited permissions into account. Either argument
312
-** may be NULL.
312
+** Return true if user capability strings zOrig and zNew materially
313
+** differ, taking into account that they may be sorted in an arbitary
314
+** order. This does not take inherited permissions into
315
+** account. Either argument may be NULL. A NULL and an empty string
316
+** are considered equivalent here. e.g. "abc" and "cab" are equivalent
317
+** for this purpose, but "aCb" and "acb" are not.
318
+*/
319
+static int userCapsChanged(const char *zOrig, const char *zNew){
320
+ if( !zOrig ){
321
+ return zNew ? (0!=*zNew) : 0;
322
+ }else if( !zNew ){
323
+ return 0!=*zOrig;
324
+ }else if( 0==fossil_strcmp(zOrig, zNew) ){
325
+ return 0;
326
+ }else{
327
+ /* We don't know that zOrig and zNew are sorted equivalently. The
328
+ ** following steps will compare strings which contain all the same
329
+ ** capabilities letters as equivalent, regardless of the letters'
330
+ ** order in their strings. */
331
+ char aOrig[128]; /* table of zOrig bytes */
332
+ int nOrig = 0, nNew = 0;
333
+
334
+ memset( &aOrig[0], 0, sizeof(aOrig) );
335
+ for( ; *zOrig; ++zOrig, ++nOrig ){
336
+ if( 0==(*zOrig & 0x80) ){
337
+ aOrig[(int)*zOrig] = 1;
338
+ }
339
+ }
340
+ for( ; *zNew; ++zNew, ++nNew ){
341
+ if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
342
+ return 1;
343
+ }
344
+ }
345
+ return nOrig!=nNew;
346
+ }
347
+}
348
+
349
+/*
350
+** COMMAND: test-user-caps-changed
351
+**
352
+** Usage: %fossil test-user-caps-changed caps1 caps2
353
+**
313354
*/
314
-static int userHasNewCaps(const char *zOrig, const char *zNew){
315
- for( ; zNew && *zNew; ++zNew ){
316
- if( !zOrig || strchr(zOrig,*zNew)==0 ){
317
- return *zNew;
318
- }
319
- }
320
- return 0;
355
+void test_user_caps_changed(void){
356
+
357
+ char const * zOld = g.argc>2 ? g.argv[2] : NULL;
358
+ char const * zNew = g.argc>3 ? g.argv[3] : NULL;
359
+ fossil_print("Has changes? = %d\n",
360
+ userCapsChanged( zOld, zNew ));
321361
}
322362
323363
/*
324364
** Sends notification of user permission elevation changes to all
325365
** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +373,11 @@
333373
** edits their subscriptions after an admin assigns them this one,
334374
** this particular one will be lost. "Feature or bug?" is unclear,
335375
** but it would be odd for a non-admin to be assigned this
336376
** capability.
337377
*/
338
-static void alert_user_elevation(const char *zLogin, /*Affected user*/
378
+static void alert_user_cap_change(const char *zLogin, /*Affected user*/
339379
int uid, /*[user].uid*/
340380
int bIsNew, /*true if new user*/
341381
const char *zOrigCaps,/*Old caps*/
342382
const char *zNewCaps /*New caps*/){
343383
Blob hdr, body;
@@ -349,21 +389,21 @@
349389
char * zSubject;
350390
351391
if( !alert_enabled() ) return;
352392
zSubject = bIsNew
353393
? mprintf("New user created: [%q]", zLogin)
354
- : mprintf("User [%q] permissions elevated", zLogin);
394
+ : mprintf("User [%q] capabilities changed", zLogin);
355395
zURL = db_get("email-url",0);
356396
zSubname = db_get("email-subname", "[Fossil Repo]");
357397
blob_init(&body, 0, 0);
358398
blob_init(&hdr, 0, 0);
359399
if( bIsNew ){
360
- blob_appendf(&body, "User [%q] was created by with "
400
+ blob_appendf(&body, "User [%q] was created with "
361401
"permissions [%q] by user [%q].\n",
362402
zLogin, zNewCaps, g.zLogin);
363403
} else {
364
- blob_appendf(&body, "Permissions for user [%q] where elevated "
404
+ blob_appendf(&body, "Permissions for user [%q] where changed "
365405
"from [%q] to [%q] by user [%q].\n",
366406
zLogin, zOrigCaps, zNewCaps, g.zLogin);
367407
}
368408
if( zURL ){
369409
blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +526,11 @@
486526
}else if( !cgi_csrf_safe(2) ){
487527
/* This might be a cross-site request forgery, so ignore it */
488528
}else{
489529
/* We have all the information we need to make the change to the user */
490530
char c;
491
- int bHasNewCaps = 0 /* 1 if user's permissions are increased */;
531
+ int bCapsChanged = 0 /* 1 if user's permissions changed */;
492532
const int bIsNew = uid<=0;
493533
char aCap[70], zNm[4];
494534
zNm[0] = 'a';
495535
zNm[2] = 0;
496536
for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +548,11 @@
508548
a[c&0x7f] = P(zNm)!=0;
509549
if( a[c&0x7f] ) aCap[i++] = c;
510550
}
511551
512552
aCap[i] = 0;
513
- bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]);
553
+ bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
514554
zPw = P("pw");
515555
zLogin = P("login");
516556
if( strlen(zLogin)==0 ){
517557
const char *zRef = cgi_referer("setup_ulist");
518558
style_header("User Creation Error");
@@ -613,18 +653,20 @@
613653
@ <span class="loginError">%h(zErr)</span>
614654
@
615655
@ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
616656
@ [Bummer]</a></p>
617657
style_finish_page();
618
- if( bHasNewCaps ){
619
- alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
658
+ if( bCapsChanged ){
659
+ /* It's possible that caps were updated locally even if
660
+ ** login group updates failed. */
661
+ alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
620662
}
621663
return;
622664
}
623665
}
624
- if( bHasNewCaps ){
625
- alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
666
+ if( bCapsChanged ){
667
+ alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
626668
}
627669
cgi_redirect(cgi_referer("setup_ulist"));
628670
return;
629671
}
630672
631673
--- src/setupuser.c
+++ src/setupuser.c
@@ -155,18 +155,19 @@
155 zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
156 }else{
157 zWith = "";
158 }
159 db_prepare(&s,
160 "SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
161 " lower(login) AS sortkey, "
162 " CASE WHEN info LIKE '%%expires 20%%'"
163 " THEN substr(info,instr(lower(info),'expires')+8,10)"
164 " END AS exp,"
165 "atime,"
166 " subscriber.ssub, subscriber.subscriberId,"
167 " user.mtime AS sorttime"
 
168 " FROM user LEFT JOIN lastAccess ON login=uname"
169 " LEFT JOIN subscriber ON login=suname"
170 " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
171 " ORDER BY sorttime DESC", zWith/*safe-for-%s*/
172 );
@@ -202,11 +203,13 @@
202 if( db_column_type(&s,8)==SQLITE_NULL ){
203 @ <td>
204 }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
205 @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
206 }else{
207 @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>
 
 
208 }
209
210 @ </tr>
211 fossil_free(zAge);
212 }
@@ -304,22 +307,59 @@
304 while( zPw[0]=='*' ){ zPw++; }
305 return zPw[0]!=0;
306 }
307
308 /*
309 ** Return true if user capability string zNew contains any capability
310 ** letter which is not in user capability string zOrig, else 0. This
311 ** does not take inherited permissions into account. Either argument
312 ** may be NULL.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313 */
314 static int userHasNewCaps(const char *zOrig, const char *zNew){
315 for( ; zNew && *zNew; ++zNew ){
316 if( !zOrig || strchr(zOrig,*zNew)==0 ){
317 return *zNew;
318 }
319 }
320 return 0;
321 }
322
323 /*
324 ** Sends notification of user permission elevation changes to all
325 ** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +373,11 @@
333 ** edits their subscriptions after an admin assigns them this one,
334 ** this particular one will be lost. "Feature or bug?" is unclear,
335 ** but it would be odd for a non-admin to be assigned this
336 ** capability.
337 */
338 static void alert_user_elevation(const char *zLogin, /*Affected user*/
339 int uid, /*[user].uid*/
340 int bIsNew, /*true if new user*/
341 const char *zOrigCaps,/*Old caps*/
342 const char *zNewCaps /*New caps*/){
343 Blob hdr, body;
@@ -349,21 +389,21 @@
349 char * zSubject;
350
351 if( !alert_enabled() ) return;
352 zSubject = bIsNew
353 ? mprintf("New user created: [%q]", zLogin)
354 : mprintf("User [%q] permissions elevated", zLogin);
355 zURL = db_get("email-url",0);
356 zSubname = db_get("email-subname", "[Fossil Repo]");
357 blob_init(&body, 0, 0);
358 blob_init(&hdr, 0, 0);
359 if( bIsNew ){
360 blob_appendf(&body, "User [%q] was created by with "
361 "permissions [%q] by user [%q].\n",
362 zLogin, zNewCaps, g.zLogin);
363 } else {
364 blob_appendf(&body, "Permissions for user [%q] where elevated "
365 "from [%q] to [%q] by user [%q].\n",
366 zLogin, zOrigCaps, zNewCaps, g.zLogin);
367 }
368 if( zURL ){
369 blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +526,11 @@
486 }else if( !cgi_csrf_safe(2) ){
487 /* This might be a cross-site request forgery, so ignore it */
488 }else{
489 /* We have all the information we need to make the change to the user */
490 char c;
491 int bHasNewCaps = 0 /* 1 if user's permissions are increased */;
492 const int bIsNew = uid<=0;
493 char aCap[70], zNm[4];
494 zNm[0] = 'a';
495 zNm[2] = 0;
496 for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +548,11 @@
508 a[c&0x7f] = P(zNm)!=0;
509 if( a[c&0x7f] ) aCap[i++] = c;
510 }
511
512 aCap[i] = 0;
513 bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]);
514 zPw = P("pw");
515 zLogin = P("login");
516 if( strlen(zLogin)==0 ){
517 const char *zRef = cgi_referer("setup_ulist");
518 style_header("User Creation Error");
@@ -613,18 +653,20 @@
613 @ <span class="loginError">%h(zErr)</span>
614 @
615 @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
616 @ [Bummer]</a></p>
617 style_finish_page();
618 if( bHasNewCaps ){
619 alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
 
 
620 }
621 return;
622 }
623 }
624 if( bHasNewCaps ){
625 alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
626 }
627 cgi_redirect(cgi_referer("setup_ulist"));
628 return;
629 }
630
631
--- src/setupuser.c
+++ src/setupuser.c
@@ -155,18 +155,19 @@
155 zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
156 }else{
157 zWith = "";
158 }
159 db_prepare(&s,
160 "SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," /* 0..4 */
161 " lower(login) AS sortkey, " /* 5 */
162 " CASE WHEN info LIKE '%%expires 20%%'"
163 " THEN substr(info,instr(lower(info),'expires')+8,10)"
164 " END AS exp," /* 6 */
165 "atime," /* 7 */
166 " subscriber.ssub, subscriber.subscriberId," /* 8, 9 */
167 " user.mtime AS sorttime," /* 10 */
168 " subscriber.semail" /* 11 */
169 " FROM user LEFT JOIN lastAccess ON login=uname"
170 " LEFT JOIN subscriber ON login=suname"
171 " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
172 " ORDER BY sorttime DESC", zWith/*safe-for-%s*/
173 );
@@ -202,11 +203,13 @@
203 if( db_column_type(&s,8)==SQLITE_NULL ){
204 @ <td>
205 }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
206 @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
207 }else{
208 const char *zEmail = db_column_text(&s, 11);
209 char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
210 @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
211 }
212
213 @ </tr>
214 fossil_free(zAge);
215 }
@@ -304,22 +307,59 @@
307 while( zPw[0]=='*' ){ zPw++; }
308 return zPw[0]!=0;
309 }
310
311 /*
312 ** Return true if user capability strings zOrig and zNew materially
313 ** differ, taking into account that they may be sorted in an arbitary
314 ** order. This does not take inherited permissions into
315 ** account. Either argument may be NULL. A NULL and an empty string
316 ** are considered equivalent here. e.g. "abc" and "cab" are equivalent
317 ** for this purpose, but "aCb" and "acb" are not.
318 */
319 static int userCapsChanged(const char *zOrig, const char *zNew){
320 if( !zOrig ){
321 return zNew ? (0!=*zNew) : 0;
322 }else if( !zNew ){
323 return 0!=*zOrig;
324 }else if( 0==fossil_strcmp(zOrig, zNew) ){
325 return 0;
326 }else{
327 /* We don't know that zOrig and zNew are sorted equivalently. The
328 ** following steps will compare strings which contain all the same
329 ** capabilities letters as equivalent, regardless of the letters'
330 ** order in their strings. */
331 char aOrig[128]; /* table of zOrig bytes */
332 int nOrig = 0, nNew = 0;
333
334 memset( &aOrig[0], 0, sizeof(aOrig) );
335 for( ; *zOrig; ++zOrig, ++nOrig ){
336 if( 0==(*zOrig & 0x80) ){
337 aOrig[(int)*zOrig] = 1;
338 }
339 }
340 for( ; *zNew; ++zNew, ++nNew ){
341 if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
342 return 1;
343 }
344 }
345 return nOrig!=nNew;
346 }
347 }
348
349 /*
350 ** COMMAND: test-user-caps-changed
351 **
352 ** Usage: %fossil test-user-caps-changed caps1 caps2
353 **
354 */
355 void test_user_caps_changed(void){
356
357 char const * zOld = g.argc>2 ? g.argv[2] : NULL;
358 char const * zNew = g.argc>3 ? g.argv[3] : NULL;
359 fossil_print("Has changes? = %d\n",
360 userCapsChanged( zOld, zNew ));
 
361 }
362
363 /*
364 ** Sends notification of user permission elevation changes to all
365 ** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +373,11 @@
373 ** edits their subscriptions after an admin assigns them this one,
374 ** this particular one will be lost. "Feature or bug?" is unclear,
375 ** but it would be odd for a non-admin to be assigned this
376 ** capability.
377 */
378 static void alert_user_cap_change(const char *zLogin, /*Affected user*/
379 int uid, /*[user].uid*/
380 int bIsNew, /*true if new user*/
381 const char *zOrigCaps,/*Old caps*/
382 const char *zNewCaps /*New caps*/){
383 Blob hdr, body;
@@ -349,21 +389,21 @@
389 char * zSubject;
390
391 if( !alert_enabled() ) return;
392 zSubject = bIsNew
393 ? mprintf("New user created: [%q]", zLogin)
394 : mprintf("User [%q] capabilities changed", zLogin);
395 zURL = db_get("email-url",0);
396 zSubname = db_get("email-subname", "[Fossil Repo]");
397 blob_init(&body, 0, 0);
398 blob_init(&hdr, 0, 0);
399 if( bIsNew ){
400 blob_appendf(&body, "User [%q] was created with "
401 "permissions [%q] by user [%q].\n",
402 zLogin, zNewCaps, g.zLogin);
403 } else {
404 blob_appendf(&body, "Permissions for user [%q] where changed "
405 "from [%q] to [%q] by user [%q].\n",
406 zLogin, zOrigCaps, zNewCaps, g.zLogin);
407 }
408 if( zURL ){
409 blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +526,11 @@
526 }else if( !cgi_csrf_safe(2) ){
527 /* This might be a cross-site request forgery, so ignore it */
528 }else{
529 /* We have all the information we need to make the change to the user */
530 char c;
531 int bCapsChanged = 0 /* 1 if user's permissions changed */;
532 const int bIsNew = uid<=0;
533 char aCap[70], zNm[4];
534 zNm[0] = 'a';
535 zNm[2] = 0;
536 for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +548,11 @@
548 a[c&0x7f] = P(zNm)!=0;
549 if( a[c&0x7f] ) aCap[i++] = c;
550 }
551
552 aCap[i] = 0;
553 bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
554 zPw = P("pw");
555 zLogin = P("login");
556 if( strlen(zLogin)==0 ){
557 const char *zRef = cgi_referer("setup_ulist");
558 style_header("User Creation Error");
@@ -613,18 +653,20 @@
653 @ <span class="loginError">%h(zErr)</span>
654 @
655 @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
656 @ [Bummer]</a></p>
657 style_finish_page();
658 if( bCapsChanged ){
659 /* It's possible that caps were updated locally even if
660 ** login group updates failed. */
661 alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
662 }
663 return;
664 }
665 }
666 if( bCapsChanged ){
667 alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
668 }
669 cgi_redirect(cgi_referer("setup_ulist"));
670 return;
671 }
672
673
+32 -23
--- src/smtp.c
+++ src/smtp.c
@@ -184,26 +184,23 @@
184184
}
185185
186186
/*
187187
** Allocate a new SmtpSession object.
188188
**
189
-** Both zFrom and zDest must be specified.
190
-**
191
-** The ... arguments are in this order:
189
+** Both zFrom and zDest must be specified. smtpFlags may not contain
190
+** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
191
+** added by a subsequent call to smtp_session_config().
192192
**
193
-** SMTP_PORT: int
194
-** SMTP_TRACE_FILE: FILE*
195
-** SMTP_TRACE_BLOB: Blob*
193
+** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
196194
*/
197195
SmtpSession *smtp_session_new(
198196
const char *zFrom, /* Domain for the client */
199197
const char *zDest, /* Domain of the server */
200198
u32 smtpFlags, /* Flags */
201
- ... /* Arguments depending on the flags */
199
+ int iPort /* TCP port if the SMTP_PORT flags is present */
202200
){
203201
SmtpSession *p;
204
- va_list ap;
205202
UrlData url;
206203
207204
p = fossil_malloc( sizeof(*p) );
208205
memset(p, 0, sizeof(*p));
209206
p->zFrom = zFrom;
@@ -210,21 +207,13 @@
210207
p->zDest = zDest;
211208
p->smtpFlags = smtpFlags;
212209
memset(&url, 0, sizeof(url));
213210
url.port = 25;
214211
blob_init(&p->inbuf, 0, 0);
215
- va_start(ap, smtpFlags);
216212
if( smtpFlags & SMTP_PORT ){
217
- url.port = va_arg(ap, int);
218
- }
219
- if( smtpFlags & SMTP_TRACE_FILE ){
220
- p->logFile = va_arg(ap, FILE*);
221
- }
222
- if( smtpFlags & SMTP_TRACE_BLOB ){
223
- p->pTranscript = va_arg(ap, Blob*);
224
- }
225
- va_end(ap);
213
+ url.port = iPort;
214
+ }
226215
if( (smtpFlags & SMTP_DIRECT)!=0 ){
227216
int i;
228217
p->zHostname = fossil_strdup(zDest);
229218
for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
230219
if( p->zHostname[i]==':' ){
@@ -246,10 +235,27 @@
246235
p->zErr = socket_errmsg();
247236
socket_close();
248237
}
249238
return p;
250239
}
240
+
241
+/*
242
+** Configure debugging options on SmtpSession. Add all bits in
243
+** smtpFlags to the settings. The following bits can be added:
244
+**
245
+** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use
246
+**
247
+** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use.
248
+*/
249
+void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
250
+ p->smtpFlags = smtpFlags;
251
+ if( smtpFlags & SMTP_TRACE_FILE ){
252
+ p->logFile = (FILE*)pArg;
253
+ }else if( smtpFlags & SMTP_TRACE_BLOB ){
254
+ p->pTranscript = (Blob*)pArg;
255
+ }
256
+}
251257
252258
/*
253259
** Send a single line of output the SMTP client to the server.
254260
*/
255261
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
@@ -375,15 +381,17 @@
375381
int smtp_client_quit(SmtpSession *p){
376382
Blob in = BLOB_INITIALIZER;
377383
int iCode = 0;
378384
int bMore = 0;
379385
char *zArg = 0;
380
- smtp_send_line(p, "QUIT\r\n");
381
- do{
382
- smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
383
- }while( bMore );
384
- p->atEof = 1;
386
+ if( !p->atEof ){
387
+ smtp_send_line(p, "QUIT\r\n");
388
+ do{
389
+ smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
390
+ }while( bMore );
391
+ p->atEof = 1;
392
+ }
385393
socket_close();
386394
return 0;
387395
}
388396
389397
/*
@@ -395,10 +403,11 @@
395403
int smtp_client_startup(SmtpSession *p){
396404
Blob in = BLOB_INITIALIZER;
397405
int iCode = 0;
398406
int bMore = 0;
399407
char *zArg = 0;
408
+ if( p==0 || p->atEof ) return 1;
400409
do{
401410
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
402411
}while( bMore );
403412
if( iCode!=220 ){
404413
smtp_client_quit(p);
405414
--- src/smtp.c
+++ src/smtp.c
@@ -184,26 +184,23 @@
184 }
185
186 /*
187 ** Allocate a new SmtpSession object.
188 **
189 ** Both zFrom and zDest must be specified.
190 **
191 ** The ... arguments are in this order:
192 **
193 ** SMTP_PORT: int
194 ** SMTP_TRACE_FILE: FILE*
195 ** SMTP_TRACE_BLOB: Blob*
196 */
197 SmtpSession *smtp_session_new(
198 const char *zFrom, /* Domain for the client */
199 const char *zDest, /* Domain of the server */
200 u32 smtpFlags, /* Flags */
201 ... /* Arguments depending on the flags */
202 ){
203 SmtpSession *p;
204 va_list ap;
205 UrlData url;
206
207 p = fossil_malloc( sizeof(*p) );
208 memset(p, 0, sizeof(*p));
209 p->zFrom = zFrom;
@@ -210,21 +207,13 @@
210 p->zDest = zDest;
211 p->smtpFlags = smtpFlags;
212 memset(&url, 0, sizeof(url));
213 url.port = 25;
214 blob_init(&p->inbuf, 0, 0);
215 va_start(ap, smtpFlags);
216 if( smtpFlags & SMTP_PORT ){
217 url.port = va_arg(ap, int);
218 }
219 if( smtpFlags & SMTP_TRACE_FILE ){
220 p->logFile = va_arg(ap, FILE*);
221 }
222 if( smtpFlags & SMTP_TRACE_BLOB ){
223 p->pTranscript = va_arg(ap, Blob*);
224 }
225 va_end(ap);
226 if( (smtpFlags & SMTP_DIRECT)!=0 ){
227 int i;
228 p->zHostname = fossil_strdup(zDest);
229 for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
230 if( p->zHostname[i]==':' ){
@@ -246,10 +235,27 @@
246 p->zErr = socket_errmsg();
247 socket_close();
248 }
249 return p;
250 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
252 /*
253 ** Send a single line of output the SMTP client to the server.
254 */
255 static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
@@ -375,15 +381,17 @@
375 int smtp_client_quit(SmtpSession *p){
376 Blob in = BLOB_INITIALIZER;
377 int iCode = 0;
378 int bMore = 0;
379 char *zArg = 0;
380 smtp_send_line(p, "QUIT\r\n");
381 do{
382 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
383 }while( bMore );
384 p->atEof = 1;
 
 
385 socket_close();
386 return 0;
387 }
388
389 /*
@@ -395,10 +403,11 @@
395 int smtp_client_startup(SmtpSession *p){
396 Blob in = BLOB_INITIALIZER;
397 int iCode = 0;
398 int bMore = 0;
399 char *zArg = 0;
 
400 do{
401 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
402 }while( bMore );
403 if( iCode!=220 ){
404 smtp_client_quit(p);
405
--- src/smtp.c
+++ src/smtp.c
@@ -184,26 +184,23 @@
184 }
185
186 /*
187 ** Allocate a new SmtpSession object.
188 **
189 ** Both zFrom and zDest must be specified. smtpFlags may not contain
190 ** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
191 ** added by a subsequent call to smtp_session_config().
192 **
193 ** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
 
 
194 */
195 SmtpSession *smtp_session_new(
196 const char *zFrom, /* Domain for the client */
197 const char *zDest, /* Domain of the server */
198 u32 smtpFlags, /* Flags */
199 int iPort /* TCP port if the SMTP_PORT flags is present */
200 ){
201 SmtpSession *p;
 
202 UrlData url;
203
204 p = fossil_malloc( sizeof(*p) );
205 memset(p, 0, sizeof(*p));
206 p->zFrom = zFrom;
@@ -210,21 +207,13 @@
207 p->zDest = zDest;
208 p->smtpFlags = smtpFlags;
209 memset(&url, 0, sizeof(url));
210 url.port = 25;
211 blob_init(&p->inbuf, 0, 0);
 
212 if( smtpFlags & SMTP_PORT ){
213 url.port = iPort;
214 }
 
 
 
 
 
 
 
215 if( (smtpFlags & SMTP_DIRECT)!=0 ){
216 int i;
217 p->zHostname = fossil_strdup(zDest);
218 for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
219 if( p->zHostname[i]==':' ){
@@ -246,10 +235,27 @@
235 p->zErr = socket_errmsg();
236 socket_close();
237 }
238 return p;
239 }
240
241 /*
242 ** Configure debugging options on SmtpSession. Add all bits in
243 ** smtpFlags to the settings. The following bits can be added:
244 **
245 ** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use
246 **
247 ** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use.
248 */
249 void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
250 p->smtpFlags = smtpFlags;
251 if( smtpFlags & SMTP_TRACE_FILE ){
252 p->logFile = (FILE*)pArg;
253 }else if( smtpFlags & SMTP_TRACE_BLOB ){
254 p->pTranscript = (Blob*)pArg;
255 }
256 }
257
258 /*
259 ** Send a single line of output the SMTP client to the server.
260 */
261 static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
@@ -375,15 +381,17 @@
381 int smtp_client_quit(SmtpSession *p){
382 Blob in = BLOB_INITIALIZER;
383 int iCode = 0;
384 int bMore = 0;
385 char *zArg = 0;
386 if( !p->atEof ){
387 smtp_send_line(p, "QUIT\r\n");
388 do{
389 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
390 }while( bMore );
391 p->atEof = 1;
392 }
393 socket_close();
394 return 0;
395 }
396
397 /*
@@ -395,10 +403,11 @@
403 int smtp_client_startup(SmtpSession *p){
404 Blob in = BLOB_INITIALIZER;
405 int iCode = 0;
406 int bMore = 0;
407 char *zArg = 0;
408 if( p==0 || p->atEof ) return 1;
409 do{
410 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
411 }while( bMore );
412 if( iCode!=220 ){
413 smtp_client_quit(p);
414
--- src/sorttable.js
+++ src/sorttable.js
@@ -9,11 +9,11 @@
99
** function. Example:
1010
**
1111
** <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
1212
**
1313
** Column data types are determined by the data-column-types attribute of
14
-** the table. The value of data-column-types is a string where each
14
+** the table. The value of data-column-types is a string where each
1515
** character of the string represents a datatype for one column in the
1616
** table.
1717
**
1818
** t Sort by text
1919
** n Sort numerically
@@ -86,18 +86,22 @@
8686
hdrCell.className = clsName;
8787
}
8888
}
8989
this.sortText = function(a,b) {
9090
var i = thisObject.sortIndex;
91
+ if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */
92
+ if (b.cells.length<=i) return 1;
9193
aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
9294
bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
9395
if(aa<bb) return -1;
9496
if(aa==bb) return a.rowIndex-b.rowIndex;
9597
return 1;
9698
}
9799
this.sortReverseText = function(a,b) {
98100
var i = thisObject.sortIndex;
101
+ if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */
102
+ if (b.cells.length<=i) return -1;
99103
aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
100104
bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
101105
if(aa<bb) return +1;
102106
if(aa==bb) return a.rowIndex-b.rowIndex;
103107
return -1;
104108
--- src/sorttable.js
+++ src/sorttable.js
@@ -9,11 +9,11 @@
9 ** function. Example:
10 **
11 ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
12 **
13 ** Column data types are determined by the data-column-types attribute of
14 ** the table. The value of data-column-types is a string where each
15 ** character of the string represents a datatype for one column in the
16 ** table.
17 **
18 ** t Sort by text
19 ** n Sort numerically
@@ -86,18 +86,22 @@
86 hdrCell.className = clsName;
87 }
88 }
89 this.sortText = function(a,b) {
90 var i = thisObject.sortIndex;
 
 
91 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
92 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
93 if(aa<bb) return -1;
94 if(aa==bb) return a.rowIndex-b.rowIndex;
95 return 1;
96 }
97 this.sortReverseText = function(a,b) {
98 var i = thisObject.sortIndex;
 
 
99 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
100 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
101 if(aa<bb) return +1;
102 if(aa==bb) return a.rowIndex-b.rowIndex;
103 return -1;
104
--- src/sorttable.js
+++ src/sorttable.js
@@ -9,11 +9,11 @@
9 ** function. Example:
10 **
11 ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
12 **
13 ** Column data types are determined by the data-column-types attribute of
14 ** the table. The value of data-column-types is a string where each
15 ** character of the string represents a datatype for one column in the
16 ** table.
17 **
18 ** t Sort by text
19 ** n Sort numerically
@@ -86,18 +86,22 @@
86 hdrCell.className = clsName;
87 }
88 }
89 this.sortText = function(a,b) {
90 var i = thisObject.sortIndex;
91 if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */
92 if (b.cells.length<=i) return 1;
93 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
94 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
95 if(aa<bb) return -1;
96 if(aa==bb) return a.rowIndex-b.rowIndex;
97 return 1;
98 }
99 this.sortReverseText = function(a,b) {
100 var i = thisObject.sortIndex;
101 if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */
102 if (b.cells.length<=i) return -1;
103 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
104 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
105 if(aa<bb) return +1;
106 if(aa==bb) return a.rowIndex-b.rowIndex;
107 return -1;
108
--- www/changes.wiki
+++ www/changes.wiki
@@ -127,13 +127,14 @@
127127
<li> Added button 'Submit and New' to create multiple tickets
128128
in a row.
129129
</ol>
130130
* Added the "hash" query parameter to the
131131
[/help?cmd=/whatis|/whatis webpage].
132
- * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
132
+ * Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
133133
which alerts subscribers when an admin creates a new user or
134
- adds new permissions to one.
134
+ when a user's permissions change.
135
+ * Show project description on repository list.
135136
* Diverse minor fixes and additions.
136137
137138
138139
<h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
139140
140141
--- www/changes.wiki
+++ www/changes.wiki
@@ -127,13 +127,14 @@
127 <li> Added button 'Submit and New' to create multiple tickets
128 in a row.
129 </ol>
130 * Added the "hash" query parameter to the
131 [/help?cmd=/whatis|/whatis webpage].
132 * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
133 which alerts subscribers when an admin creates a new user or
134 adds new permissions to one.
 
135 * Diverse minor fixes and additions.
136
137
138 <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
139
140
--- www/changes.wiki
+++ www/changes.wiki
@@ -127,13 +127,14 @@
127 <li> Added button 'Submit and New' to create multiple tickets
128 in a row.
129 </ol>
130 * Added the "hash" query parameter to the
131 [/help?cmd=/whatis|/whatis webpage].
132 * Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
133 which alerts subscribers when an admin creates a new user or
134 when a user's permissions change.
135 * Show project description on repository list.
136 * Diverse minor fixes and additions.
137
138
139 <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
140
141

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button