| | @@ -48,11 +48,11 @@ |
| 48 | 48 | @ -- notifications for specific branches or tags or tickets. |
| 49 | 49 | @ -- |
| 50 | 50 | @ CREATE TABLE repository.subscriber( |
| 51 | 51 | @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use |
| 52 | 52 | @ subscriberCode BLOB UNIQUE, -- UUID for subscriber. External use |
| 53 | | -@ semail TEXT, -- email address |
| 53 | +@ semail TEXT UNIQUE COLLATE nocase,-- email address |
| 54 | 54 | @ suname TEXT, -- corresponding USER entry |
| 55 | 55 | @ sverify BOOLEAN, -- email address verified |
| 56 | 56 | @ sdonotcall BOOLEAN, -- true for Do Not Call |
| 57 | 57 | @ sdigest BOOLEAN, -- true for daily digests only |
| 58 | 58 | @ ssub TEXT, -- baseline subscriptions |
| | @@ -449,10 +449,100 @@ |
| 449 | 449 | } |
| 450 | 450 | else{ |
| 451 | 451 | usage("reset|send|setting"); |
| 452 | 452 | } |
| 453 | 453 | } |
| 454 | + |
| 455 | +/* |
| 456 | +** Do error checking on a submitted subscription form. Return TRUE |
| 457 | +** if the submission is valid. Return false if any problems are seen. |
| 458 | +*/ |
| 459 | +static int subscribe_error_check( |
| 460 | + int *peErr, /* Type of error */ |
| 461 | + char **pzErr, /* Error message text */ |
| 462 | + int needCaptcha /* True if captcha check needed */ |
| 463 | +){ |
| 464 | + const char *zEAddr; |
| 465 | + int i, j, n; |
| 466 | + char c; |
| 467 | + |
| 468 | + *peErr = 0; |
| 469 | + *pzErr = 0; |
| 470 | + |
| 471 | + /* Check the validity of the email address. |
| 472 | + ** |
| 473 | + ** (1) Exactly one '@' character. |
| 474 | + ** (2) No other characters besides [a-zA-Z0-9._-] |
| 475 | + */ |
| 476 | + zEAddr = P("e"); |
| 477 | + if( zEAddr==0 ) return 0; |
| 478 | + for(i=j=0; (c = zEAddr[i])!=0; i++){ |
| 479 | + if( c=='@' ){ |
| 480 | + n = i; |
| 481 | + j++; |
| 482 | + continue; |
| 483 | + } |
| 484 | + if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){ |
| 485 | + *peErr = 1; |
| 486 | + *pzErr = mprintf("illegal character in email address: 0x%x '%c'", |
| 487 | + c, c); |
| 488 | + return 0; |
| 489 | + } |
| 490 | + } |
| 491 | + if( j!=1 ){ |
| 492 | + *peErr = 1; |
| 493 | + *pzErr = mprintf("email address should contain exactly one '@'"); |
| 494 | + return 0; |
| 495 | + } |
| 496 | + if( n<1 ){ |
| 497 | + *peErr = 1; |
| 498 | + *pzErr = mprintf("name missing before '@' in email address"); |
| 499 | + return 0; |
| 500 | + } |
| 501 | + if( n>i-5 ){ |
| 502 | + *peErr = 1; |
| 503 | + *pzErr = mprintf("email domain too short"); |
| 504 | + return 0; |
| 505 | + } |
| 506 | + |
| 507 | + /* Verify the captcha */ |
| 508 | + if( needCaptcha && !captcha_is_correct(1) ){ |
| 509 | + *peErr = 2; |
| 510 | + *pzErr = mprintf("incorrect security code"); |
| 511 | + return 0; |
| 512 | + } |
| 513 | + |
| 514 | + /* Check to make sure the email address is available for reuse */ |
| 515 | + if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){ |
| 516 | + *peErr = 1; |
| 517 | + *pzErr = mprintf("this email address is used by someone else"); |
| 518 | + return 0; |
| 519 | + } |
| 520 | + |
| 521 | + /* If we reach this point, all is well */ |
| 522 | + return 1; |
| 523 | +} |
| 524 | + |
| 525 | +/* |
| 526 | +** Text of email message sent in order to confirm a subscription. |
| 527 | +*/ |
| 528 | +static const char zConfirmMsg[] = |
| 529 | +@ Someone has signed you up for email alerts on the Fossil repository |
| 530 | +@ at %R. |
| 531 | +@ |
| 532 | +@ To confirm your subscription and begin receiving alerts, click on |
| 533 | +@ the following hyperlink: |
| 534 | +@ |
| 535 | +@ %R/alerts/%s |
| 536 | +@ |
| 537 | +@ Save the hyperlink above! You can reuse this same hyperlink to |
| 538 | +@ unsubscribe or to change the kinds of alerts you receive. |
| 539 | +@ |
| 540 | +@ If you do not want to subscribe, you can simply ignore this message. |
| 541 | +@ You will not be contacted again. |
| 542 | +@ |
| 543 | +; |
| 454 | 544 | |
| 455 | 545 | /* |
| 456 | 546 | ** WEBPAGE: subscribe |
| 457 | 547 | ** |
| 458 | 548 | ** Allow users to subscribe to email notifications, or to change or |
| | @@ -461,66 +551,140 @@ |
| 461 | 551 | void subscribe_page(void){ |
| 462 | 552 | int needCaptcha; |
| 463 | 553 | unsigned int uSeed; |
| 464 | 554 | const char *zDecoded; |
| 465 | 555 | char *zCaptcha; |
| 556 | + char *zErr = 0; |
| 557 | + int eErr = 0; |
| 466 | 558 | |
| 467 | 559 | login_check_credentials(); |
| 468 | 560 | if( !g.perm.EmailAlert ){ |
| 469 | 561 | login_needed(g.anon.EmailAlert); |
| 470 | 562 | return; |
| 471 | 563 | } |
| 472 | 564 | if( login_is_individual() |
| 473 | 565 | && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin) |
| 474 | 566 | ){ |
| 567 | + /* This person is already signed up for email alerts. Jump |
| 568 | + ** to the screen that lets them edit their alert preferences. |
| 569 | + */ |
| 475 | 570 | cgi_redirect("%R/alerts"); |
| 476 | 571 | return; |
| 477 | 572 | } |
| 478 | | - style_header("Email Subscription"); |
| 479 | | - needCaptcha = P("usecaptcha")!=0 || !login_is_individual(); |
| 573 | + needCaptcha = !login_is_individual(); |
| 574 | + if( P("submit") |
| 575 | + && cgi_csrf_safe(1) |
| 576 | + && subscribe_error_check(&eErr,&zErr,needCaptcha) |
| 577 | + ){ |
| 578 | + /* A validated request for a new subscription has been received. */ |
| 579 | + char ssub[20]; |
| 580 | + const char *zEAddr = P("e"); |
| 581 | + sqlite3_int64 id; /* New subscriber Id */ |
| 582 | + const char *zCode; /* New subscriber code (in hex) */ |
| 583 | + int nsub = 0; |
| 584 | + if( PB("sa") ) ssub[nsub++] = 'a'; |
| 585 | + if( PB("sc") ) ssub[nsub++] = 'c'; |
| 586 | + if( PB("st") ) ssub[nsub++] = 't'; |
| 587 | + if( PB("sw") ) ssub[nsub++] = 'w'; |
| 588 | + ssub[nsub] = 0; |
| 589 | + db_multi_exec( |
| 590 | + "INSERT INTO subscriber(subscriberCode,semail,suname," |
| 591 | + " sverify,sdonotcall,sdigest,ssub,sctime,smtime,smip)" |
| 592 | + "VALUES(randomblob(32),%Q,%Q,%d,0,%d,%Q," |
| 593 | + " julianday('now'),julianday('now'),%Q)", |
| 594 | + /* semail */ zEAddr, |
| 595 | + /* suname */ needCaptcha==0 ? g.zLogin : 0, |
| 596 | + /* sverify */ needCaptcha==0, |
| 597 | + /* sdigest */ PB("di"), |
| 598 | + /* ssub */ ssub, |
| 599 | + /* smip */ g.zIpAddr |
| 600 | + ); |
| 601 | + id = db_last_insert_rowid(); |
| 602 | + zCode = db_text(0, |
| 603 | + "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld", |
| 604 | + id); |
| 605 | + if( !needCaptcha ){ |
| 606 | + /* The new subscription has been added on behalf of a logged-in user. |
| 607 | + ** No verification is required. Jump immediately to /alerts page. |
| 608 | + */ |
| 609 | + cgi_redirectf("%R/alerts/%s", zCode); |
| 610 | + return; |
| 611 | + }else{ |
| 612 | + /* We need to send a verification email */ |
| 613 | + Blob hdr, body; |
| 614 | + blob_init(&hdr,0,0); |
| 615 | + blob_init(&body,0,0); |
| 616 | + blob_appendf(&hdr, "To: %s\n", zEAddr); |
| 617 | + blob_appendf(&hdr, "Subject: Subscription verification\n"); |
| 618 | + blob_appendf(&body, zConfirmMsg/*works-like:"%s"*/, zCode); |
| 619 | + email_send(&hdr, &body, 0, 0); |
| 620 | + style_header("Email Alert Verification"); |
| 621 | + @ <p>An email has been sent to "%h(zEAddr)". That email contains a |
| 622 | + @ hyperlink that you must click on in order to activate your |
| 623 | + @ subscription.</p> |
| 624 | + style_footer(); |
| 625 | + } |
| 626 | + return; |
| 627 | + } |
| 628 | + style_header("Signup For Email Alerts"); |
| 629 | + @ <p>To receive email notifications for changes to this |
| 630 | + @ repository, fill out the form below and press "Submit" button.</p> |
| 480 | 631 | form_begin(0, "%R/subscribe"); |
| 481 | 632 | @ <table class="subscribe"> |
| 482 | 633 | @ <tr> |
| 483 | 634 | @ <td class="form_label">Email Address:</td> |
| 484 | 635 | @ <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td> |
| 485 | | - @ <td></td> |
| 636 | + if( eErr==1 ){ |
| 637 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 638 | + } |
| 486 | 639 | @ </tr> |
| 487 | 640 | if( needCaptcha ){ |
| 488 | 641 | uSeed = captcha_seed(); |
| 489 | 642 | zDecoded = captcha_decode(uSeed); |
| 490 | 643 | zCaptcha = captcha_render(zDecoded); |
| 491 | 644 | @ <tr> |
| 492 | 645 | @ <td class="form_label">Security Code:</td> |
| 493 | 646 | @ <td><input type="text" name="captcha" value="" size="30"> |
| 494 | | - @ <input type="hidden" name="usecaptcha" value="1"></td> |
| 495 | 647 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 648 | + if( eErr==2 ){ |
| 649 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 650 | + } |
| 496 | 651 | @ </tr> |
| 497 | 652 | } |
| 498 | 653 | if( g.perm.Admin ){ |
| 499 | 654 | @ <tr> |
| 500 | 655 | @ <td class="form_label">User:</td> |
| 501 | 656 | @ <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \ |
| 502 | 657 | @ size="30"></td> |
| 503 | | - @ <td></td> |
| 658 | + if( eErr==3 ){ |
| 659 | + @ <td><span class="loginError">← %h(zErr)</span></td> |
| 660 | + } |
| 504 | 661 | @ </tr> |
| 505 | 662 | } |
| 506 | 663 | @ <tr> |
| 507 | 664 | @ <td class="form_label">Options:</td> |
| 508 | | - @ <td><label><input type="checkbox" name="sa" value="%d(P10("sa"))">\ |
| 665 | + @ <td><label><input type="checkbox" name="sa" %s(PCK("sa"))>\ |
| 509 | 666 | @ Announcements</label><br> |
| 510 | | - @ <label><input type="checkbox" name="sc" value="%d(P01("sc"))">\ |
| 667 | + @ <label><input type="checkbox" name="sc" %s(PCK("sc"))>\ |
| 511 | 668 | @ Check-ins</label><br> |
| 512 | | - @ <label><input type="checkbox" name="st" value="%d(P01("st"))">\ |
| 669 | + @ <label><input type="checkbox" name="st" %s(PCK("st"))>\ |
| 513 | 670 | @ Ticket changes</label><br> |
| 514 | | - @ <label><input type="checkbox" name="sw" value="%d(P01("sw"))">\ |
| 671 | + @ <label><input type="checkbox" name="sw" %s(PCK("sw"))>\ |
| 515 | 672 | @ Wiki</label><br> |
| 516 | | - @ <label><input type="checkbox" name="di" value="%d(P01("di"))">\ |
| 517 | | - @ Daily digest only</label><br></td> |
| 673 | + @ <label><input type="checkbox" name="di" %s(PCK("di"))>\ |
| 674 | + @ Daily digest only</label><br> |
| 675 | + if( g.perm.Admin ){ |
| 676 | + @ <label><input type="checkbox" name="vi" %s(PCK("vi"))>\ |
| 677 | + @ Verified</label><br> |
| 678 | + @ <label><input type="checkbox" name="dnc" %s(PCK("dnc"))>\ |
| 679 | + @ Do not call</label><br> |
| 680 | + } |
| 681 | + @ </td> |
| 518 | 682 | @ </tr> |
| 519 | 683 | @ <tr> |
| 520 | 684 | @ <td></td> |
| 521 | | - @ <td><input type="submit" value="Submit"></td> |
| 685 | + @ <td><input type="submit" name="submit" value="Submit"></td> |
| 522 | 686 | @ </tr> |
| 523 | 687 | @ </table> |
| 524 | 688 | if( needCaptcha ){ |
| 525 | 689 | @ <div class="captcha"><table class="captcha"><tr><td><pre> |
| 526 | 690 | @ %h(zCaptcha) |
| | @@ -527,10 +691,11 @@ |
| 527 | 691 | @ </pre> |
| 528 | 692 | @ Enter the 8 characters above in the "Security Code" box |
| 529 | 693 | @ </td></tr></table></div> |
| 530 | 694 | } |
| 531 | 695 | @ </form> |
| 696 | + fossil_free(zErr); |
| 532 | 697 | style_footer(); |
| 533 | 698 | } |
| 534 | 699 | |
| 535 | 700 | /* |
| 536 | 701 | ** WEBPAGE: alerts |
| 537 | 702 | |