| | @@ -510,10 +510,19 @@ |
| 510 | 510 | zPattern = mprintf("%s/login*", g.zBaseURL); |
| 511 | 511 | rc = sqlite3_strglob(zPattern, zReferer)==0; |
| 512 | 512 | fossil_free(zPattern); |
| 513 | 513 | return rc; |
| 514 | 514 | } |
| 515 | + |
| 516 | +/* |
| 517 | +** Return true if users are allowed to reset their own passwords. |
| 518 | +*/ |
| 519 | +int login_self_password_reset_available(void){ |
| 520 | + if( !db_get_boolean("self-pw-reset",0) ) return 0; |
| 521 | + if( !alert_tables_exist() ) return 0; |
| 522 | + return 1; |
| 523 | +} |
| 515 | 524 | |
| 516 | 525 | /* |
| 517 | 526 | ** Return TRUE if self-registration is available. If the zNeeded |
| 518 | 527 | ** argument is not NULL, then only return true if self-registration is |
| 519 | 528 | ** available and any of the capabilities named in zNeeded are available |
| | @@ -560,10 +569,16 @@ |
| 560 | 569 | const int noAnon = P("noanon")!=0; |
| 561 | 570 | int rememberMe; /* If true, use persistent cookie, else |
| 562 | 571 | session cookie. Toggled per |
| 563 | 572 | checkbox. */ |
| 564 | 573 | |
| 574 | + if( P("pwreset")!=0 && login_self_password_reset_available() ){ |
| 575 | + /* If the "Reset Password" button in the form was pressed, render |
| 576 | + ** the Request Password Reset page in place of this one. */ |
| 577 | + login_reqpwreset_page(); |
| 578 | + return; |
| 579 | + } |
| 565 | 580 | login_check_credentials(); |
| 566 | 581 | fossil_redirect_to_https_if_needed(1); |
| 567 | 582 | sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0, |
| 568 | 583 | constant_time_cmp_function, 0, 0); |
| 569 | 584 | zUsername = P("u"); |
| | @@ -613,10 +628,14 @@ |
| 613 | 628 | char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); |
| 614 | 629 | char *zChngPw; |
| 615 | 630 | char *zErr; |
| 616 | 631 | int rc; |
| 617 | 632 | |
| 633 | + /* vvvvvvv--- tag-20230106-1 ----vvvvvv |
| 634 | + ** |
| 635 | + ** Replicate changes made below to tag-20230106-2 |
| 636 | + */ |
| 618 | 637 | db_unprotect(PROTECT_USER); |
| 619 | 638 | db_multi_exec( |
| 620 | 639 | "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 621 | 640 | ); |
| 622 | 641 | zChngPw = mprintf( |
| | @@ -627,10 +646,16 @@ |
| 627 | 646 | zNew1, g.zLogin, g.zLogin |
| 628 | 647 | ); |
| 629 | 648 | fossil_free(zNewPw); |
| 630 | 649 | rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr); |
| 631 | 650 | db_protect_pop(); |
| 651 | + /* |
| 652 | + ** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^ |
| 653 | + ** |
| 654 | + ** Replicate changes above to tag-20230106-2 |
| 655 | + */ |
| 656 | + |
| 632 | 657 | if( rc ){ |
| 633 | 658 | zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr); |
| 634 | 659 | fossil_free(zErr); |
| 635 | 660 | }else{ |
| 636 | 661 | redirect_to_g(); |
| | @@ -770,10 +795,16 @@ |
| 770 | 795 | if( !noAnon && login_self_register_available(0) ){ |
| 771 | 796 | @ <tr> |
| 772 | 797 | @ <td></td> |
| 773 | 798 | @ <td><input type="submit" name="self" value="Create A New Account"> |
| 774 | 799 | @ </tr> |
| 800 | + } |
| 801 | + if( login_self_password_reset_available() ){ |
| 802 | + @ <tr> |
| 803 | + @ <td></td> |
| 804 | + @ <td><input type="submit" name="pwreset" value="Reset My Password"> |
| 805 | + @ </tr> |
| 775 | 806 | } |
| 776 | 807 | @ </table> |
| 777 | 808 | if( zAnonPw && !noAnon ){ |
| 778 | 809 | const char *zDecoded = captcha_decode(uSeed); |
| 779 | 810 | int bAutoCaptcha = db_get_boolean("auto-captcha", 0); |
| | @@ -828,10 +859,243 @@ |
| 828 | 859 | @ </form> |
| 829 | 860 | } |
| 830 | 861 | } |
| 831 | 862 | style_finish_page(); |
| 832 | 863 | } |
| 864 | + |
| 865 | +/* |
| 866 | +** Construct an appropriate URL suffix for the /resetpw page. The |
| 867 | +** suffix will be of the form: |
| 868 | +** |
| 869 | +** UID-TIMESTAMP-HASH |
| 870 | +** |
| 871 | +** Where UID and TIMESTAMP are the parameters to this function, and HASH |
| 872 | +** is constructed from information that is unique to the user in question |
| 873 | +** and which is not publicly available. In particular, the HASH includes |
| 874 | +** the existing user password. Thus, in order to construct a URL that can |
| 875 | +** change a password, an attacker must know the current password, in which |
| 876 | +** case the attacker does not need to construct the URL in order to take |
| 877 | +** over the account. |
| 878 | +** |
| 879 | +** Return a pointer to the resulting string in memory obtained |
| 880 | +** from fossil_malloc(). |
| 881 | +*/ |
| 882 | +char *login_resetpw_suffix(int uid, i64 timestamp){ |
| 883 | + char *zHash; |
| 884 | + char *zInnerSql; |
| 885 | + char *zResult; |
| 886 | + extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*); |
| 887 | + if( timestamp<=0 ){ timestamp = time(0); } |
| 888 | + sqlite3_shathree_init(g.db, 0, 0); |
| 889 | + if( db_table_exists("repository","subscriber") ){ |
| 890 | + zInnerSql = mprintf( |
| 891 | + "SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode" |
| 892 | + " FROM user LEFT JOIN subscriber ON suname=login" |
| 893 | + " WHERE uid=%d", timestamp, uid); |
| 894 | + }else{ |
| 895 | + zInnerSql = mprintf( |
| 896 | + "SELECT %lld, login, pw, cookie, user.mtime, user.info" |
| 897 | + " FROM user WHERE uid=%d", timestamp, uid); |
| 898 | + } |
| 899 | + zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql); |
| 900 | + fossil_free(zInnerSql); |
| 901 | + zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash); |
| 902 | + if( strlen(zHash)<64 || strlen(zResult)<70 ){ |
| 903 | + /* This should never happen, but if it does, we don't want it to lead |
| 904 | + ** to a security breach. */ |
| 905 | + fossil_panic("insecure password reset hash generated\n"); |
| 906 | + } |
| 907 | + fossil_free(zHash); |
| 908 | + return zResult; |
| 909 | +} |
| 910 | + |
| 911 | +/* |
| 912 | +** Check to see if the "name" query parameter is a valid resetpw suffix |
| 913 | +** for a user whose password we are allowed to reset. If it is, then return |
| 914 | +** the positive integer UID for that user. If the query parameter is not |
| 915 | +** valid, return 0. |
| 916 | +*/ |
| 917 | +static int login_resetpw_suffix_is_valid(const char *zName){ |
| 918 | + int i, j; |
| 919 | + int uid; |
| 920 | + i64 timestamp; |
| 921 | + i64 now; |
| 922 | + char *zHash; |
| 923 | + if( zName==0 || strlen(zName)<70 ) goto not_valid_suffix; |
| 924 | + for(i=0; fossil_isxdigit(zName[i]); i++){} |
| 925 | + if( i<1 || zName[i]!='-' ) goto not_valid_suffix; |
| 926 | + for(j=i+1; fossil_isxdigit(zName[j]); j++){} |
| 927 | + if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix; |
| 928 | + uid = strtol(zName, 0, 16); |
| 929 | + if( uid<=0 ) goto not_valid_suffix; |
| 930 | + if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){ |
| 931 | + goto not_valid_suffix; |
| 932 | + } |
| 933 | + timestamp = strtoll(&zName[i+1], 0, 16); |
| 934 | + now = time(0); |
| 935 | + if( timestamp+3600 <= now ) goto not_valid_suffix; |
| 936 | + zHash = login_resetpw_suffix(uid,timestamp); |
| 937 | + if( fossil_strcmp(zHash, zName)!=0 ){ |
| 938 | + fossil_free(zHash); |
| 939 | + goto not_valid_suffix; |
| 940 | + } |
| 941 | + fossil_free(zHash); |
| 942 | + return uid; |
| 943 | + |
| 944 | +not_valid_suffix: |
| 945 | + return 0; |
| 946 | +} |
| 947 | + |
| 948 | +/* |
| 949 | +** COMMAND: test-resetpw-url |
| 950 | +** Usage: fossil test-resetpw-url UID |
| 951 | +** |
| 952 | +** Generate and verify a /resetpw URL for user UID. |
| 953 | +** |
| 954 | +** This command is intended for unit testing the login_resetpw_suffix() |
| 955 | +** and login_resetpw_suffix_is_valid() functions. |
| 956 | +*/ |
| 957 | +void test_resetpw_url(void){ |
| 958 | + char *zSuffix; |
| 959 | + int uid; |
| 960 | + int xuid; |
| 961 | + char *zLogin; |
| 962 | + int i; |
| 963 | + db_find_and_open_repository(0, 0); |
| 964 | + verify_all_options(); |
| 965 | + if( g.argc<3 ){ |
| 966 | + usage("UID ..."); |
| 967 | + } |
| 968 | + for(i=2; i<g.argc; i++){ |
| 969 | + uid = atoi(g.argv[i]); |
| 970 | + zSuffix = login_resetpw_suffix(uid, 0); |
| 971 | + xuid = login_resetpw_suffix_is_valid(zSuffix); |
| 972 | + if( xuid>0 ){ |
| 973 | + zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", xuid); |
| 974 | + }else{ |
| 975 | + zLogin = 0; |
| 976 | + } |
| 977 | + fossil_print("/resetpw/%s %d (%s)\n", |
| 978 | + zSuffix, xuid, zLogin ? zLogin : "???"); |
| 979 | + fossil_free(zSuffix); |
| 980 | + fossil_free(zLogin); |
| 981 | + } |
| 982 | +} |
| 983 | + |
| 984 | +/* |
| 985 | +** WEBPAGE: resetpw |
| 986 | +** |
| 987 | +** The URL format must be like this: |
| 988 | +** |
| 989 | +** /resetpw/UID-TIMESTAMP-HASH |
| 990 | +** |
| 991 | +** Where UID is the uid of the user whose password is to be reset, |
| 992 | +** TIMESTAMP is the unix timestamp when the request was made, and |
| 993 | +** HASH is a hash based on UID, TIMESTAMP, and other information that |
| 994 | +** is unavailable to an attacher. |
| 995 | +** |
| 996 | +** With no other arguments, a form is present which allows the user to |
| 997 | +** enter a new password. When the SUBMIT button is pressed, a POST request |
| 998 | +** back to the same URL that will change the password. |
| 999 | +*/ |
| 1000 | +void login_resetpw(void){ |
| 1001 | + const char *zName; |
| 1002 | + int uid; |
| 1003 | + char *zRPW; |
| 1004 | + const char *zNew1, *zNew2; |
| 1005 | + |
| 1006 | + style_set_current_feature("resetpw"); |
| 1007 | + style_header("Reset Password"); |
| 1008 | + style_adunit_config(ADUNIT_OFF); |
| 1009 | + zName = PD("name",""); |
| 1010 | + uid = login_resetpw_suffix_is_valid(zName); |
| 1011 | + if( uid==0 ){ |
| 1012 | + @ <p><span class="loginError"> |
| 1013 | + @ This password-reset URL is invalid, probably because it has expired. |
| 1014 | + @ Password-reset URLs have a short lifespan. |
| 1015 | + @ </span></p> |
| 1016 | + style_finish_page(); |
| 1017 | + sleep(1); /* Introduce a small delay on an invalid suffix as an |
| 1018 | + ** extra defense against search attacks */ |
| 1019 | + return; |
| 1020 | + } |
| 1021 | + login_set_uid(uid, 0); |
| 1022 | + if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){ |
| 1023 | + @ <p><span class="loginError"> |
| 1024 | + @ Cannot change the password for user <b>%h(g.zLogin)</b>. |
| 1025 | + @ </span></p> |
| 1026 | + style_finish_page(); |
| 1027 | + return; |
| 1028 | + } |
| 1029 | + if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ |
| 1030 | + if( fossil_strcmp(zNew1,zNew2)!=0 ){ |
| 1031 | + @ <p><span class="loginError"> |
| 1032 | + @ The two copies of your new passwords do not match. |
| 1033 | + @ Try again. |
| 1034 | + @ </span></p> |
| 1035 | + }else{ |
| 1036 | + char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); |
| 1037 | + char *zChngPw; |
| 1038 | + char *zErr; |
| 1039 | + int rc; |
| 1040 | + |
| 1041 | + /* vvvvvvv--- tag-20230106-2 ----vvvvvv |
| 1042 | + ** |
| 1043 | + ** Replicate changes made below to tag-20230106-1 |
| 1044 | + */ |
| 1045 | + db_unprotect(PROTECT_USER); |
| 1046 | + db_multi_exec( |
| 1047 | + "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 1048 | + ); |
| 1049 | + zChngPw = mprintf( |
| 1050 | + "UPDATE user" |
| 1051 | + " SET pw=shared_secret(%Q,%Q," |
| 1052 | + " (SELECT value FROM config WHERE name='project-code'))" |
| 1053 | + " WHERE login=%Q", |
| 1054 | + zNew1, g.zLogin, g.zLogin |
| 1055 | + ); |
| 1056 | + fossil_free(zNewPw); |
| 1057 | + rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr); |
| 1058 | + db_protect_pop(); |
| 1059 | + /* |
| 1060 | + ** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^ |
| 1061 | + ** |
| 1062 | + ** Replicate changes above to tag-20230106-1 |
| 1063 | + */ |
| 1064 | + |
| 1065 | + if( rc ){ |
| 1066 | + @ <p><span class='loginError'> |
| 1067 | + @ %s(zErr); |
| 1068 | + @ </span></p> |
| 1069 | + fossil_free(zErr); |
| 1070 | + }else{ |
| 1071 | + @ <p>Password changed successfully. Go to the |
| 1072 | + @ <a href="%R/login?u=%t(g.zLogin)">Login</a> page and log in |
| 1073 | + @ using the new password to continue. |
| 1074 | + @ </p> |
| 1075 | + style_finish_page(); |
| 1076 | + return; |
| 1077 | + } |
| 1078 | + } |
| 1079 | + } |
| 1080 | + zRPW = fossil_random_password(12); |
| 1081 | + @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p> |
| 1082 | + form_begin(0, "%R/resetpw"); |
| 1083 | + @ <input type='hidden' name='name' value='%h(zName)'> |
| 1084 | + @ <table> |
| 1085 | + @ <tr><td class="form_label" id="newpw">New Password:</td> |
| 1086 | + @ <td><input aria-labelledby="newpw" type="password" name="n1" \ |
| 1087 | + @ size="30" /> Suggestion: %z(zRPW)</td></tr> |
| 1088 | + @ <tr><td class="form_label" id="reppw">Repeat New Password:</td> |
| 1089 | + @ <td><input aria-labledby="reppw" type="password" name="n2" \ |
| 1090 | + @ size="30" /></td></tr> |
| 1091 | + @ <tr><td></td> |
| 1092 | + @ <td><input type="submit" value="Change Password" /></td></tr> |
| 1093 | + @ </table> |
| 1094 | + @ </form> |
| 1095 | + style_finish_page(); |
| 1096 | +} |
| 833 | 1097 | |
| 834 | 1098 | /* |
| 835 | 1099 | ** Attempt to find login credentials for user zLogin on a peer repository |
| 836 | 1100 | ** with project code zCode. Transfer those credentials to the local |
| 837 | 1101 | ** repository. |
| | @@ -999,11 +1263,10 @@ |
| 999 | 1263 | void login_check_credentials(void){ |
| 1000 | 1264 | int uid = 0; /* User id */ |
| 1001 | 1265 | const char *zCookie; /* Text of the login cookie */ |
| 1002 | 1266 | const char *zIpAddr; /* Raw IP address of the requestor */ |
| 1003 | 1267 | const char *zCap = 0; /* Capability string */ |
| 1004 | | - const char *zPublicPages = 0; /* GLOB patterns of public pages */ |
| 1005 | 1268 | const char *zLogin = 0; /* Login user for credentials */ |
| 1006 | 1269 | |
| 1007 | 1270 | /* Only run this check once. */ |
| 1008 | 1271 | if( g.userUid!=0 ) return; |
| 1009 | 1272 | |
| | @@ -1139,10 +1402,21 @@ |
| 1139 | 1402 | zCap = ""; |
| 1140 | 1403 | } |
| 1141 | 1404 | sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none"); |
| 1142 | 1405 | } |
| 1143 | 1406 | |
| 1407 | + login_set_uid(uid, zCap); |
| 1408 | +} |
| 1409 | + |
| 1410 | +/* |
| 1411 | +** Set the current logged in user to be uid. zCap is precomputed |
| 1412 | +** (override) capabilities. If zCap==0, then look up the capabilities |
| 1413 | +** in the USER table. |
| 1414 | +*/ |
| 1415 | +int login_set_uid(int uid, const char *zCap){ |
| 1416 | + const char *zPublicPages = 0; /* GLOB patterns of public pages */ |
| 1417 | + |
| 1144 | 1418 | /* At this point, we know that uid!=0. Find the privileges associated |
| 1145 | 1419 | ** with user uid. |
| 1146 | 1420 | */ |
| 1147 | 1421 | assert( uid!=0 ); |
| 1148 | 1422 | if( zCap==0 ){ |
| | @@ -1219,10 +1493,11 @@ |
| 1219 | 1493 | if( glob_match(pGlob, zUri) ){ |
| 1220 | 1494 | login_set_capabilities(db_get("default-perms", "u"), 0); |
| 1221 | 1495 | } |
| 1222 | 1496 | glob_free(pGlob); |
| 1223 | 1497 | } |
| 1498 | + return g.zLogin!=0; |
| 1224 | 1499 | } |
| 1225 | 1500 | |
| 1226 | 1501 | /* |
| 1227 | 1502 | ** Memory of settings |
| 1228 | 1503 | */ |
| | @@ -1557,10 +1832,75 @@ |
| 1557 | 1832 | zUserID, zUserID, zUserID |
| 1558 | 1833 | ); |
| 1559 | 1834 | return rc; |
| 1560 | 1835 | } |
| 1561 | 1836 | |
| 1837 | +/* |
| 1838 | +** zEMail is an email address. (Example: "[email protected]".) This routine |
| 1839 | +** searches for a user or subscriber that has that email address. If the |
| 1840 | +** email address is used no-where in the system, return 0. If the email |
| 1841 | +** address is assigned to a particular user return the UID for that user. |
| 1842 | +** If the email address is used, but not by a particular user, return -1. |
| 1843 | +*/ |
| 1844 | +static int email_address_in_use(const char *zEMail){ |
| 1845 | + int uid; |
| 1846 | + uid = db_int(0, |
| 1847 | + "SELECT uid FROM user" |
| 1848 | + " WHERE info LIKE '%%<%q>%%'", zEMail); |
| 1849 | + if( uid>0 ){ |
| 1850 | + if( db_exists("SELECT 1 FROM user WHERE uid=%d AND (" |
| 1851 | + " cap GLOB '*[as]*' OR" |
| 1852 | + " find_emailaddr(info)<>%Q COLLATE nocase)", |
| 1853 | + uid, zEMail) ){ |
| 1854 | + uid = -1; |
| 1855 | + } |
| 1856 | + } |
| 1857 | + if( uid==0 && alert_tables_exist() ){ |
| 1858 | + uid = db_int(0, |
| 1859 | + "SELECT user.uid FROM subscriber JOIN user ON login=suname" |
| 1860 | + " WHERE semail=%Q AND sverified", zEMail); |
| 1861 | + if( uid ){ |
| 1862 | + if( db_exists("SELECT 1 FROM user WHERE uid=%d AND " |
| 1863 | + " cap GLOB '*[as]*'", |
| 1864 | + uid) ){ |
| 1865 | + uid = -1; |
| 1866 | + } |
| 1867 | + } |
| 1868 | + } |
| 1869 | + return uid; |
| 1870 | +} |
| 1871 | + |
| 1872 | +/* |
| 1873 | +** COMMAND: test-email-used |
| 1874 | +** Usage: fossil test-email-used EMAIL ... |
| 1875 | +** |
| 1876 | +** Given a list of email addresses, show the UID and LOGIN associated |
| 1877 | +** with each one. |
| 1878 | +*/ |
| 1879 | +void test_email_used(void){ |
| 1880 | + int i; |
| 1881 | + db_find_and_open_repository(0, 0); |
| 1882 | + verify_all_options(); |
| 1883 | + if( g.argc<3 ){ |
| 1884 | + usage("EMAIL ..."); |
| 1885 | + } |
| 1886 | + for(i=2; i<g.argc; i++){ |
| 1887 | + const char *zEMail = g.argv[i]; |
| 1888 | + int uid = email_address_in_use(zEMail); |
| 1889 | + if( uid==0 ){ |
| 1890 | + fossil_print("%s: not used\n", zEMail); |
| 1891 | + }else if( uid<0 ){ |
| 1892 | + fossil_print("%s: used but no password reset is available\n", zEMail); |
| 1893 | + }else{ |
| 1894 | + char *zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid); |
| 1895 | + fossil_print("%s: UID %d (%s)\n", zEMail, uid, zLogin); |
| 1896 | + fossil_free(zLogin); |
| 1897 | + } |
| 1898 | + } |
| 1899 | +} |
| 1900 | + |
| 1901 | + |
| 1562 | 1902 | /* |
| 1563 | 1903 | ** Check an email address and confirm that it is valid for self-registration. |
| 1564 | 1904 | ** The email address is known already to be well-formed. Return true |
| 1565 | 1905 | ** if the email address is on the allowed list. |
| 1566 | 1906 | ** |
| | @@ -1598,21 +1938,29 @@ |
| 1598 | 1938 | const char *zDName; |
| 1599 | 1939 | unsigned int uSeed; |
| 1600 | 1940 | const char *zDecoded; |
| 1601 | 1941 | int iErrLine = -1; |
| 1602 | 1942 | const char *zErr = 0; |
| 1943 | + int uid = 0; /* User id with the same email */ |
| 1603 | 1944 | int captchaIsCorrect = 0; /* True on a correct captcha */ |
| 1604 | 1945 | char *zCaptcha = ""; /* Value of the captcha text */ |
| 1605 | 1946 | char *zPerms; /* Permissions for the default user */ |
| 1606 | 1947 | int canDoAlerts = 0; /* True if receiving email alerts is possible */ |
| 1607 | 1948 | int doAlerts = 0; /* True if subscription is wanted too */ |
| 1949 | + |
| 1608 | 1950 | if( !db_get_boolean("self-register", 0) ){ |
| 1609 | 1951 | style_header("Registration not possible"); |
| 1610 | 1952 | @ <p>This project does not allow user self-registration. Please contact the |
| 1611 | 1953 | @ project administrator to obtain an account.</p> |
| 1612 | 1954 | style_finish_page(); |
| 1613 | 1955 | return; |
| 1956 | + } |
| 1957 | + if( P("pwreset")!=0 && login_self_password_reset_available() ){ |
| 1958 | + /* The "Request Password Reset" button was pressed, so render the |
| 1959 | + ** "Request Password Reset" page instead of this one. */ |
| 1960 | + login_reqpwreset_page(); |
| 1961 | + return; |
| 1614 | 1962 | } |
| 1615 | 1963 | zPerms = db_get("default-perms", "u"); |
| 1616 | 1964 | |
| 1617 | 1965 | /* Prompt the user for email alerts if this repository is configured for |
| 1618 | 1966 | ** email alerts and if the default permissions include "7" */ |
| | @@ -1656,26 +2004,16 @@ |
| 1656 | 2004 | iErrLine = 4; |
| 1657 | 2005 | zErr = "Password must be at least 6 characters long"; |
| 1658 | 2006 | }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ |
| 1659 | 2007 | iErrLine = 5; |
| 1660 | 2008 | zErr = "Passwords do not match"; |
| 2009 | + }else if( (uid = email_address_in_use(zEAddr))!=0 ){ |
| 2010 | + iErrLine = 3; |
| 2011 | + zErr = "This email address is already associated with a user"; |
| 1661 | 2012 | }else if( login_self_choosen_userid_already_exists(zUserID) ){ |
| 1662 | 2013 | iErrLine = 1; |
| 1663 | 2014 | zErr = "This User ID is already taken. Choose something different."; |
| 1664 | | - }else if( |
| 1665 | | - /* If the email is found anywhere in USER.INFO... */ |
| 1666 | | - db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr) |
| 1667 | | - || |
| 1668 | | - /* Or if the email is a verify subscriber email with an associated |
| 1669 | | - ** user... */ |
| 1670 | | - (alert_tables_exist() && |
| 1671 | | - db_exists( |
| 1672 | | - "SELECT 1 FROM subscriber WHERE semail=%Q AND suname IS NOT NULL" |
| 1673 | | - " AND sverified",zEAddr)) |
| 1674 | | - ){ |
| 1675 | | - iErrLine = 3; |
| 1676 | | - zErr = "This email address is already claimed by another user"; |
| 1677 | 2015 | }else{ |
| 1678 | 2016 | /* If all of the tests above have passed, that means that the submitted |
| 1679 | 2017 | ** form contains valid data and we can proceed to create the new login */ |
| 1680 | 2018 | Blob sql; |
| 1681 | 2019 | int uid; |
| | @@ -1809,11 +2147,17 @@ |
| 1809 | 2147 | @ <td class="form_label" align="right" id="emaddr">Email Address:</td> |
| 1810 | 2148 | @ <td><input aria-labelledby="emaddr" type="text" name="ea" \ |
| 1811 | 2149 | @ value="%h(zEAddr)" size="30"></td> |
| 1812 | 2150 | @ </tr> |
| 1813 | 2151 | if( iErrLine==3 ){ |
| 1814 | | - @ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
| 2152 | + @ <tr><td><td><span class='loginError'>↑ %h(zErr)</span> |
| 2153 | + if( uid>0 && login_self_password_reset_available() ){ |
| 2154 | + @ <br /> |
| 2155 | + @ <input type="submit" name="pwreset" \ |
| 2156 | + @ value="Request Password Reset For %h(zEAddr)"> |
| 2157 | + } |
| 2158 | + @ </td></tr> |
| 1815 | 2159 | } |
| 1816 | 2160 | if( canDoAlerts ){ |
| 1817 | 2161 | int a = atoi(PD("alerts","1")); |
| 1818 | 2162 | @ <tr> |
| 1819 | 2163 | @ <td class="form_label" align="right" id="emalrt">Email Alerts?</td> |
| | @@ -1865,10 +2209,151 @@ |
| 1865 | 2209 | @ </form> |
| 1866 | 2210 | style_finish_page(); |
| 1867 | 2211 | |
| 1868 | 2212 | free(zCaptcha); |
| 1869 | 2213 | } |
| 2214 | + |
| 2215 | +/* |
| 2216 | +** WEBPAGE: reqpwreset |
| 2217 | +** |
| 2218 | +** A web page to request a password reset. |
| 2219 | +*/ |
| 2220 | +void login_reqpwreset_page(void){ |
| 2221 | + const char *zEAddr; |
| 2222 | + const char *zDecoded; |
| 2223 | + unsigned int uSeed; |
| 2224 | + int iErrLine = -1; |
| 2225 | + const char *zErr = 0; |
| 2226 | + int uid = 0; /* User id with the email zEAddr */ |
| 2227 | + int captchaIsCorrect = 0; /* True on a correct captcha */ |
| 2228 | + char *zCaptcha = ""; /* Value of the captcha text */ |
| 2229 | + |
| 2230 | + if( !login_self_password_reset_available() ){ |
| 2231 | + style_header("Password reset not possible"); |
| 2232 | + @ <p>This project does not allow users to reset their own passwords. |
| 2233 | + @ If you need a password reset, you will have to negotiate that directly |
| 2234 | + @ with the project administrator. |
| 2235 | + style_finish_page(); |
| 2236 | + return; |
| 2237 | + } |
| 2238 | + zEAddr = PDT("ea",""); |
| 2239 | + |
| 2240 | + /* Verify user imputs */ |
| 2241 | + if( !cgi_csrf_safe(1) || P("reqpwreset")==0 ){ |
| 2242 | + /* This is the initial display of the form. No processing or error |
| 2243 | + ** checking is to be done. Fall through into the form display |
| 2244 | + */ |
| 2245 | + }else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){ |
| 2246 | + iErrLine = 2; |
| 2247 | + zErr = "Incorrect CAPTCHA"; |
| 2248 | + }else if( zEAddr[0]==0 ){ |
| 2249 | + iErrLine = 1; |
| 2250 | + zErr = "Required"; |
| 2251 | + }else if( email_address_is_valid(zEAddr,0)==0 ){ |
| 2252 | + iErrLine = 1; |
| 2253 | + zErr = "Not a valid email address"; |
| 2254 | + }else if( authorized_subscription_email(zEAddr)==0 ){ |
| 2255 | + iErrLine = 1; |
| 2256 | + zErr = "Not an authorized email address"; |
| 2257 | + }else if( (uid = email_address_in_use(zEAddr))<=0 ){ |
| 2258 | + iErrLine = 1; |
| 2259 | + zErr = "This email address is not associated with a user who has " |
| 2260 | + "password reset privileges."; |
| 2261 | + }else if( login_set_uid(uid,0)==0 || g.perm.Admin || g.perm.Setup |
| 2262 | + || !g.perm.Password ){ |
| 2263 | + iErrLine = 1; |
| 2264 | + zErr = "This email address is not associated with a user who has " |
| 2265 | + "password reset privileges."; |
| 2266 | + }else{ |
| 2267 | + |
| 2268 | + /* If all of the tests above have passed, that means that the submitted |
| 2269 | + ** form contains valid data and we can proceed to issue the password |
| 2270 | + ** reset email. */ |
| 2271 | + Blob hdr, body; |
| 2272 | + AlertSender *pSender; |
| 2273 | + char *zUrl = login_resetpw_suffix(uid, 0); |
| 2274 | + pSender = alert_sender_new(0,0); |
| 2275 | + blob_init(&hdr,0,0); |
| 2276 | + blob_init(&body,0,0); |
| 2277 | + blob_appendf(&hdr, "To: <%s>\n", zEAddr); |
| 2278 | + blob_appendf(&hdr, "Subject: Password reset for %s\n", g.zBaseURL); |
| 2279 | + blob_appendf(&body, |
| 2280 | + "Someone has requested to reset the password for user \"%s\"\n", |
| 2281 | + g.zLogin); |
| 2282 | + blob_appendf(&body, "at %s.\n\n", g.zBaseURL); |
| 2283 | + blob_appendf(&body, |
| 2284 | + "If you did not request this password reset, ignore\n" |
| 2285 | + "this email\n\n"); |
| 2286 | + blob_appendf(&body, |
| 2287 | + "To reset the password, visit the following link:\n\n" |
| 2288 | + " %s/resetpw/%s\n\n", g.zBaseURL, zUrl); |
| 2289 | + fossil_free(zUrl); |
| 2290 | + alert_send(pSender, &hdr, &body, 0); |
| 2291 | + style_header("Email Verification"); |
| 2292 | + if( pSender->zErr ){ |
| 2293 | + @ <h1>Internal Error</h1> |
| 2294 | + @ <p>The following internal error was encountered while trying |
| 2295 | + @ to send the confirmation email: |
| 2296 | + @ <blockquote><pre> |
| 2297 | + @ %h(pSender->zErr) |
| 2298 | + @ </pre></blockquote> |
| 2299 | + }else{ |
| 2300 | + @ <p>An email containing a hyperlink that can be used to reset |
| 2301 | + @ your password has been sent to "%h(zEAddr)".</p> |
| 2302 | + } |
| 2303 | + alert_sender_free(pSender); |
| 2304 | + style_finish_page(); |
| 2305 | + return; |
| 2306 | + } |
| 2307 | + |
| 2308 | + /* Prepare the captcha. */ |
| 2309 | + if( captchaIsCorrect ){ |
| 2310 | + uSeed = strtoul(P("captchaseed"),0,10); |
| 2311 | + }else{ |
| 2312 | + uSeed = captcha_seed(); |
| 2313 | + } |
| 2314 | + zDecoded = captcha_decode(uSeed); |
| 2315 | + zCaptcha = captcha_render(zDecoded); |
| 2316 | + |
| 2317 | + style_header("Request Password Reset"); |
| 2318 | + /* Print out the registration form. */ |
| 2319 | + g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */ |
| 2320 | + form_begin(0, "%R/reqpwreset"); |
| 2321 | + @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" /> |
| 2322 | + @ <p><input type="hidden" name="reqpwreset" value="1" /> |
| 2323 | + @ <table class="login_out"> |
| 2324 | + @ <tr> |
| 2325 | + @ <td class="form_label" align="right" id="emaddr">Email Address:</td> |
| 2326 | + @ <td><input aria-labelledby="emaddr" type="text" name="ea" \ |
| 2327 | + @ value="%h(zEAddr)" size="30"></td> |
| 2328 | + @ </tr> |
| 2329 | + if( iErrLine==1 ){ |
| 2330 | + @ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
| 2331 | + } |
| 2332 | + @ <tr> |
| 2333 | + @ <td class="form_label" align="right" id="cptcha">Captcha:</td> |
| 2334 | + @ <td><input type="text" name="captcha" aria-labelledby="cptcha" \ |
| 2335 | + @ value="%h(captchaIsCorrect?zDecoded:"")" size="30"> |
| 2336 | + captcha_speakit_button(uSeed, "Speak the captcha text"); |
| 2337 | + @ </td> |
| 2338 | + @ </tr> |
| 2339 | + if( iErrLine==2 ){ |
| 2340 | + @ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
| 2341 | + } |
| 2342 | + @ <tr><td></td> |
| 2343 | + @ <td><input type="submit" name="new" value="Request Password Reset"/>\ |
| 2344 | + @ </td></tr> |
| 2345 | + @ </table> |
| 2346 | + @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
| 2347 | + @ %h(zCaptcha) |
| 2348 | + @ </pre> |
| 2349 | + @ Enter this 8-letter code in the "Captcha" box above. |
| 2350 | + @ </td></tr></table></div> |
| 2351 | + @ </form> |
| 2352 | + style_finish_page(); |
| 2353 | + free(zCaptcha); |
| 2354 | +} |
| 1870 | 2355 | |
| 1871 | 2356 | /* |
| 1872 | 2357 | ** Run SQL on the repository database for every repository in our |
| 1873 | 2358 | ** login group. The SQL is run in a separate database connection. |
| 1874 | 2359 | ** |
| 1875 | 2360 | |