| | @@ -64,11 +64,12 @@ |
| 64 | 64 | @ sdonotcall BOOLEAN, -- true for Do Not Call |
| 65 | 65 | @ sdigest BOOLEAN, -- true for daily digests only |
| 66 | 66 | @ ssub TEXT, -- baseline subscriptions |
| 67 | 67 | @ sctime INTDATE, -- When this entry was created. unixtime |
| 68 | 68 | @ mtime INTDATE, -- Last change. unixtime |
| 69 | | -@ smip TEXT -- IP address of last change |
| 69 | +@ smip TEXT, -- IP address of last change |
| 70 | +@ lastContact INT -- Last contact. days since 1970 |
| 70 | 71 | @ ); |
| 71 | 72 | @ CREATE INDEX repository.subscriberUname |
| 72 | 73 | @ ON subscriber(suname) WHERE suname IS NOT NULL; |
| 73 | 74 | @ |
| 74 | 75 | @ DROP TABLE IF EXISTS repository.pending_alert; |
| | @@ -83,28 +84,33 @@ |
| 83 | 84 | @ sentSep BOOLEAN DEFAULT false, -- individual alert sent |
| 84 | 85 | @ sentDigest BOOLEAN DEFAULT false, -- digest alert sent |
| 85 | 86 | @ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent |
| 86 | 87 | @ ) WITHOUT ROWID; |
| 87 | 88 | @ |
| 89 | +@ -- Obsolete table. No longer used. |
| 88 | 90 | @ DROP TABLE IF EXISTS repository.alert_bounce; |
| 89 | | -@ -- Record bounced emails. If too many bounces are received within |
| 90 | | -@ -- some defined time range, then cancel the subscription. Older |
| 91 | | -@ -- entries are periodically purged. |
| 92 | | -@ -- |
| 93 | | -@ CREATE TABLE repository.alert_bounce( |
| 94 | | -@ subscriberId INTEGER, -- to whom the email was sent. |
| 95 | | -@ sendTime INTEGER, -- seconds since 1970 when email was sent |
| 96 | | -@ rcvdTime INTEGER -- seconds since 1970 when bounce was received |
| 97 | | -@ ); |
| 98 | 91 | ; |
| 99 | 92 | |
| 100 | 93 | /* |
| 101 | 94 | ** Return true if the email notification tables exist. |
| 102 | 95 | */ |
| 103 | 96 | int alert_tables_exist(void){ |
| 104 | 97 | return db_table_exists("repository", "subscriber"); |
| 105 | 98 | } |
| 99 | + |
| 100 | +/* |
| 101 | +** Record the fact that user zUser has made contact with the repository. |
| 102 | +** This resets the subscription timeout on that user. |
| 103 | +*/ |
| 104 | +void alert_user_contact(const char *zUser){ |
| 105 | + if( db_table_has_column("repository","subscriber","lastContact") ){ |
| 106 | + db_multi_exec( |
| 107 | + "UPDATE subscriber SET lastContact=now()/86400 WHERE suname=%Q", |
| 108 | + zUser |
| 109 | + ); |
| 110 | + } |
| 111 | +} |
| 106 | 112 | |
| 107 | 113 | /* |
| 108 | 114 | ** Make sure the table needed for email notification exist in the repository. |
| 109 | 115 | ** |
| 110 | 116 | ** If the bOnlyIfEnabled option is true, then tables are only created |
| | @@ -116,16 +122,27 @@ |
| 116 | 122 | && fossil_strcmp(db_get("email-send-method",0),"off")==0 |
| 117 | 123 | ){ |
| 118 | 124 | return; /* Don't create table for disabled email */ |
| 119 | 125 | } |
| 120 | 126 | db_exec_sql(zAlertInit); |
| 121 | | - }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ |
| 122 | | - db_multi_exec( |
| 123 | | - "ALTER TABLE repository.pending_alert" |
| 124 | | - " ADD COLUMN sentMod BOOLEAN DEFAULT false;" |
| 125 | | - ); |
| 127 | + return; |
| 126 | 128 | } |
| 129 | + if( db_table_has_column("repository","subscriber","lastContact") ){ |
| 130 | + return; |
| 131 | + } |
| 132 | + db_multi_exec( |
| 133 | + "DROP TABLE IF EXISTS repository.alert_bounde;\n" |
| 134 | + "ALTER TABLE repository.subscriber ADD COLUMN lastContact INT;\n" |
| 135 | + "UPDATE subscriber SET lastContact=mtime/86400;" |
| 136 | + ); |
| 137 | + if( db_table_has_column("repository","pending_alert","sentMod") ){ |
| 138 | + return; |
| 139 | + } |
| 140 | + db_multi_exec( |
| 141 | + "ALTER TABLE repository.pending_alert" |
| 142 | + " ADD COLUMN sentMod BOOLEAN DEFAULT false;" |
| 143 | + ); |
| 127 | 144 | } |
| 128 | 145 | |
| 129 | 146 | /* |
| 130 | 147 | ** Enable triggers that automatically populate the pending_alert |
| 131 | 148 | ** table. |
| | @@ -270,10 +287,23 @@ |
| 270 | 287 | @ This is short name used to identifies the repository in the |
| 271 | 288 | @ Subject: line of email alerts. Traditionally this name is |
| 272 | 289 | @ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]". |
| 273 | 290 | @ (Property: "email-subname")</p> |
| 274 | 291 | @ <hr> |
| 292 | + |
| 293 | + entry_attribute("Subscription Renewal Interval In Days", 8, |
| 294 | + "email-renew-interval", "eri", "", 0); |
| 295 | + @ <p> |
| 296 | + @ If this value is a positive integer N, then email notification |
| 297 | + @ subscriptions will be suspended N days after the last known |
| 298 | + @ interaction with the user. This prevents sending notifications |
| 299 | + @ to abandoned accounts. If a subscription gets close to expiring |
| 300 | + @ a separate email goes out with the daily digest that prompts the |
| 301 | + @ subscriber to click on a link to the "/renew" webpage in order to |
| 302 | + @ extend their subscription. |
| 303 | + @ (Property: "email-renew-interval")</p> |
| 304 | + @ <hr> |
| 275 | 305 | |
| 276 | 306 | multiple_choice_attribute("Email Send Method", "email-send-method", "esm", |
| 277 | 307 | "off", count(azSendMethods)/2, azSendMethods); |
| 278 | 308 | @ <p>How to send email. Requires auxiliary information from the fields |
| 279 | 309 | @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page |
| | @@ -1391,12 +1421,12 @@ |
| 1391 | 1421 | if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w'; |
| 1392 | 1422 | if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x'; |
| 1393 | 1423 | ssub[nsub] = 0; |
| 1394 | 1424 | zCode = db_text(0, |
| 1395 | 1425 | "INSERT INTO subscriber(semail,suname," |
| 1396 | | - " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)" |
| 1397 | | - "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)" |
| 1426 | + " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip,lastContact)" |
| 1427 | + "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q,now()/86400)" |
| 1398 | 1428 | "RETURNING hex(subscriberCode);", |
| 1399 | 1429 | /* semail */ zEAddr, |
| 1400 | 1430 | /* suname */ suname, |
| 1401 | 1431 | /* sverified */ needCaptcha==0, |
| 1402 | 1432 | /* sdigest */ PB("di"), |
| | @@ -1641,10 +1671,11 @@ |
| 1641 | 1671 | int eErr = 0; /* Type of error */ |
| 1642 | 1672 | char *zErr = 0; /* Error message text */ |
| 1643 | 1673 | int sid = 0; /* Subscriber ID */ |
| 1644 | 1674 | int nName; /* Length of zName in bytes */ |
| 1645 | 1675 | char *zHalfCode; /* prefix of subscriberCode */ |
| 1676 | + int keepAlive = 0; /* True to update the last contact time */ |
| 1646 | 1677 | |
| 1647 | 1678 | db_begin_transaction(); |
| 1648 | 1679 | if( alert_webpages_disabled() ){ |
| 1649 | 1680 | db_commit_transaction(); |
| 1650 | 1681 | return; |
| | @@ -1665,10 +1696,11 @@ |
| 1665 | 1696 | sid = db_int(0, |
| 1666 | 1697 | "SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')" |
| 1667 | 1698 | " THEN subscriberId ELSE 0 END" |
| 1668 | 1699 | " FROM subscriber WHERE subscriberCode>=hextoblob(%Q)" |
| 1669 | 1700 | " LIMIT 1", zName, zName); |
| 1701 | + if( sid ) keepAlive = 1; |
| 1670 | 1702 | } |
| 1671 | 1703 | if( sid==0 && isLogin ){ |
| 1672 | 1704 | sid = db_int(0, "SELECT subscriberId FROM subscriber" |
| 1673 | 1705 | " WHERE suname=%Q", g.zLogin); |
| 1674 | 1706 | } |
| | @@ -1697,11 +1729,12 @@ |
| 1697 | 1729 | blob_init(&update, "UPDATE subscriber SET", -1); |
| 1698 | 1730 | blob_append_sql(&update, |
| 1699 | 1731 | " sdonotcall=%d," |
| 1700 | 1732 | " sdigest=%d," |
| 1701 | 1733 | " ssub=%Q," |
| 1702 | | - " mtime=strftime('%%s','now')," |
| 1734 | + " mtime=now()," |
| 1735 | + " lastContact=now()/86400," |
| 1703 | 1736 | " smip=%Q", |
| 1704 | 1737 | sdonotcall, |
| 1705 | 1738 | sdigest, |
| 1706 | 1739 | ssub, |
| 1707 | 1740 | g.zIpAddr |
| | @@ -1727,10 +1760,15 @@ |
| 1727 | 1760 | if( eErr==0 ){ |
| 1728 | 1761 | db_exec_sql(blob_str(&update)); |
| 1729 | 1762 | ssub = 0; |
| 1730 | 1763 | } |
| 1731 | 1764 | blob_reset(&update); |
| 1765 | + }else if( keepAlive ){ |
| 1766 | + db_multi_exec( |
| 1767 | + "UPDATE subscriber SET lastContact=now()/86400" |
| 1768 | + " WHERE subscriberId=%d", sid |
| 1769 | + ); |
| 1732 | 1770 | } |
| 1733 | 1771 | if( P("delete")!=0 && cgi_csrf_safe(1) ){ |
| 1734 | 1772 | if( !PB("dodelete") ){ |
| 1735 | 1773 | eErr = 9; |
| 1736 | 1774 | zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to" |
| | @@ -1921,10 +1959,59 @@ |
| 1921 | 1959 | db_finalize(&q); |
| 1922 | 1960 | style_finish_page(); |
| 1923 | 1961 | db_commit_transaction(); |
| 1924 | 1962 | return; |
| 1925 | 1963 | } |
| 1964 | + |
| 1965 | +/* |
| 1966 | +** WEBPAGE: renew |
| 1967 | +** |
| 1968 | +** Users visit this page to update the lastContact date on their |
| 1969 | +** subscription. This prevents their subscriptions from expiring. |
| 1970 | +** |
| 1971 | +** A valid subscriber code is supplied in the name= query parameter. |
| 1972 | +*/ |
| 1973 | +void renewal_page(void){ |
| 1974 | + const char *zName = P("name"); |
| 1975 | + int iInterval = db_get_int("email-renew-interval", 0); |
| 1976 | + Stmt s; |
| 1977 | + int rc; |
| 1978 | + |
| 1979 | + style_header("Subscription Renewal"); |
| 1980 | + if( zName==0 || strlen(zName)<4 ){ |
| 1981 | + @ <p>No subscription specified</p> |
| 1982 | + style_finish_page(); |
| 1983 | + return; |
| 1984 | + } |
| 1985 | + |
| 1986 | + if( !db_table_has_column("repository","subscriber","lastContact") |
| 1987 | + || iInterval<1 |
| 1988 | + ){ |
| 1989 | + @ <p>This repository does not expire email notification subscriptions. |
| 1990 | + @ No renewals are necessary.</p> |
| 1991 | + style_finish_page(); |
| 1992 | + return; |
| 1993 | + } |
| 1994 | + |
| 1995 | + db_prepare(&s, |
| 1996 | + "UPDATE subscriber" |
| 1997 | + " SET lastContact=now()/86400" |
| 1998 | + " WHERE subscriberCode=hextoblob(%Q)" |
| 1999 | + " RETURNING semail, date('now','+%d days');", |
| 2000 | + zName, iInterval+1 |
| 2001 | + ); |
| 2002 | + rc = db_step(&s); |
| 2003 | + if( rc==SQLITE_ROW ){ |
| 2004 | + @ <p>The email notification subscription for %h(db_column_text(&s,0)) |
| 2005 | + @ has been extended until %h(db_column_text(&s,1)) UTC. |
| 2006 | + }else{ |
| 2007 | + @ <p>No such subscriber-id: %h(zName)</p> |
| 2008 | + } |
| 2009 | + db_finalize(&s); |
| 2010 | + style_finish_page(); |
| 2011 | +} |
| 2012 | + |
| 1926 | 2013 | |
| 1927 | 2014 | /* This is the message that gets sent to describe how to change |
| 1928 | 2015 | ** or modify a subscription |
| 1929 | 2016 | */ |
| 1930 | 2017 | static const char zUnsubMsg[] = |
| | @@ -2111,11 +2198,11 @@ |
| 2111 | 2198 | nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified"); |
| 2112 | 2199 | if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){ |
| 2113 | 2200 | int nNewPending; |
| 2114 | 2201 | db_multi_exec( |
| 2115 | 2202 | "DELETE FROM subscriber" |
| 2116 | | - " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')" |
| 2203 | + " WHERE NOT sverified AND mtime<now()-86400" |
| 2117 | 2204 | ); |
| 2118 | 2205 | nNewPending = db_int(0, "SELECT count(*) FROM subscriber" |
| 2119 | 2206 | " WHERE NOT sverified"); |
| 2120 | 2207 | nDel = nPending - nNewPending; |
| 2121 | 2208 | nPending = nNewPending; |
| | @@ -2122,11 +2209,11 @@ |
| 2122 | 2209 | nTotal -= nDel; |
| 2123 | 2210 | } |
| 2124 | 2211 | if( nPending>0 ){ |
| 2125 | 2212 | @ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1> |
| 2126 | 2213 | if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber" |
| 2127 | | - " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')") |
| 2214 | + " WHERE NOT sverified AND mtime<now()-86400") |
| 2128 | 2215 | ){ |
| 2129 | 2216 | style_submenu_element("Purge Pending","subscribers?purge"); |
| 2130 | 2217 | } |
| 2131 | 2218 | }else{ |
| 2132 | 2219 | @ <h1>%,d(nTotal) Subscribers</h1> |
| | @@ -2142,11 +2229,12 @@ |
| 2142 | 2229 | " suname," /* 3 */ |
| 2143 | 2230 | " sverified," /* 4 */ |
| 2144 | 2231 | " sdigest," /* 5 */ |
| 2145 | 2232 | " mtime," /* 6 */ |
| 2146 | 2233 | " date(sctime,'unixepoch')," /* 7 */ |
| 2147 | | - " (SELECT uid FROM user WHERE login=subscriber.suname)" /* 8 */ |
| 2234 | + " (SELECT uid FROM user WHERE login=subscriber.suname)," /* 8 */ |
| 2235 | + " coalesce(lastContact,mtime/86400)" /* 9 */ |
| 2148 | 2236 | " FROM subscriber" |
| 2149 | 2237 | ); |
| 2150 | 2238 | if( P("only")!=0 ){ |
| 2151 | 2239 | blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only")); |
| 2152 | 2240 | style_submenu_element("Show All","%R/subscribers"); |
| | @@ -2153,27 +2241,30 @@ |
| 2153 | 2241 | } |
| 2154 | 2242 | blob_append_sql(&sql," ORDER BY mtime DESC"); |
| 2155 | 2243 | db_prepare_blob(&q, &sql); |
| 2156 | 2244 | iNow = time(0); |
| 2157 | 2245 | @ <table border='1' class='sortable' \ |
| 2158 | | - @ data-init-sort='6' data-column-types='tttttKt'> |
| 2246 | + @ data-init-sort='6' data-column-types='tttttKKt'> |
| 2159 | 2247 | @ <thead> |
| 2160 | 2248 | @ <tr> |
| 2161 | 2249 | @ <th>Email |
| 2162 | 2250 | @ <th>Events |
| 2163 | 2251 | @ <th>Digest-Only? |
| 2164 | 2252 | @ <th>User |
| 2165 | 2253 | @ <th>Verified? |
| 2166 | 2254 | @ <th>Last change |
| 2255 | + @ <th>Last contact |
| 2167 | 2256 | @ <th>Created |
| 2168 | 2257 | @ </tr> |
| 2169 | 2258 | @ </thead><tbody> |
| 2170 | 2259 | while( db_step(&q)==SQLITE_ROW ){ |
| 2171 | 2260 | sqlite3_int64 iMtime = db_column_int64(&q, 6); |
| 2172 | 2261 | double rAge = (iNow - iMtime)/86400.0; |
| 2173 | 2262 | int uid = db_column_int(&q, 8); |
| 2174 | 2263 | const char *zUname = db_column_text(&q, 3); |
| 2264 | + sqlite3_int64 iContact = db_column_int64(&q, 9); |
| 2265 | + double rContact = (iNow/86400) - iContact; |
| 2175 | 2266 | @ <tr> |
| 2176 | 2267 | @ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\ |
| 2177 | 2268 | @ %h(db_column_text(&q,1))</a></td> |
| 2178 | 2269 | @ <td>%h(db_column_text(&q,2))</td> |
| 2179 | 2270 | @ <td>%s(db_column_int(&q,5)?"digest":"")</td> |
| | @@ -2182,10 +2273,11 @@ |
| 2182 | 2273 | }else{ |
| 2183 | 2274 | @ <td>%h(zUname)</td> |
| 2184 | 2275 | } |
| 2185 | 2276 | @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td> |
| 2186 | 2277 | @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td> |
| 2278 | + @ <td data-sortkey='%010llx(iContact)'>%z(human_readable_age(rContact))</td> |
| 2187 | 2279 | @ <td>%h(db_column_text(&q,7))</td> |
| 2188 | 2280 | @ </tr> |
| 2189 | 2281 | } |
| 2190 | 2282 | @ </tbody></table> |
| 2191 | 2283 | db_finalize(&q); |
| 2192 | 2284 | |