diff --git a/src/main/java/de/srsoftware/widerhall/Application.java b/src/main/java/de/srsoftware/widerhall/Application.java index 318d9bb..40d6009 100644 --- a/src/main/java/de/srsoftware/widerhall/Application.java +++ b/src/main/java/de/srsoftware/widerhall/Application.java @@ -1,21 +1,15 @@ package de.srsoftware.widerhall; -import de.srsoftware.widerhall.mail.Forwarder; -import de.srsoftware.widerhall.mail.ImapClient; -import de.srsoftware.widerhall.mail.MessageHandler; -import de.srsoftware.widerhall.web.Web; import de.srsoftware.widerhall.web.Rest; +import de.srsoftware.widerhall.web.Web; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.json.simple.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Path; - public class Application { private static final Logger LOG = LoggerFactory.getLogger(Application.class); diff --git a/src/main/java/de/srsoftware/widerhall/Constants.java b/src/main/java/de/srsoftware/widerhall/Constants.java index b460f6f..477e637 100644 --- a/src/main/java/de/srsoftware/widerhall/Constants.java +++ b/src/main/java/de/srsoftware/widerhall/Constants.java @@ -21,6 +21,7 @@ public class Constants { public static final String PREFIX = "prefix"; public static final String PROTOCOL = "mail.store.protocol"; public static final String STATE = "state"; + public static final String TOKEN = "token"; public static final String USER = "user"; public static final String VARCHAR = "VARCHAR(255)"; diff --git a/src/main/java/de/srsoftware/widerhall/Util.java b/src/main/java/de/srsoftware/widerhall/Util.java index 6eb5a81..829b278 100644 --- a/src/main/java/de/srsoftware/widerhall/Util.java +++ b/src/main/java/de/srsoftware/widerhall/Util.java @@ -83,4 +83,11 @@ public class Util { } return false; } + + public static int unset(int value, int...flags) { + for (int flag : flags){ + if ((value & flag) > 0) value ^= flag; + } + return value; + } } diff --git a/src/main/java/de/srsoftware/widerhall/data/Database.java b/src/main/java/de/srsoftware/widerhall/data/Database.java index 8b50a0b..c8e4288 100644 --- a/src/main/java/de/srsoftware/widerhall/data/Database.java +++ b/src/main/java/de/srsoftware/widerhall/data/Database.java @@ -70,10 +70,10 @@ public class Database { var keys = new ArrayList(); var expressions = new ArrayList(); for (var entry : setValues.entrySet()) { - expressions.add(" SET "+entry.getKey()+" = ?"); + expressions.add(entry.getKey()+" = ?"); args.add(entry.getValue()); } - sql.append(String.join(", ",expressions)); + sql.append(" SET ").append(String.join(", ",expressions)); } if (!values.isEmpty()){ @@ -226,11 +226,12 @@ public class Database { } } - public Request update(String tableName,String ...expressions) { - var sql = new StringBuilder("UPDATE ").append(tableName); - if (expressions != null && expressions.length > 0) { - sql.append(" SET ").append(String.join(", ",expressions)); - } - return new Request(sql); + public Request update(String tableName) { + return new Request(new StringBuilder("UPDATE ").append(tableName)); + } + + public static String xor(Object a, Object b){ + // https://stackoverflow.com/a/16443025/1285585 + return "(~("+a+"&"+b+"))&("+a+"|"+b+")"; } } diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index cf793b3..e990437 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -5,16 +5,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.stringtemplate.v4.ST; -import javax.xml.crypto.Data; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.*; import static de.srsoftware.widerhall.Constants.*; -import static de.srsoftware.widerhall.Constants.STATE; public class ListMember { private static final Logger LOG = LoggerFactory.getLogger(ListMember.class); @@ -25,7 +19,6 @@ public class ListMember { private static final String LIST_EMAIL = "list_email"; private static final String USER_EMAIL = "user_email"; private static final String STATE = "state"; - private static final String TOKEN = "token"; private final String listEmail,token,userEmail; private final int state; @@ -36,6 +29,27 @@ public class ListMember { this.token = token; } + public static User confirm(String token) throws SQLException { + var rs = Database.open().select(TABLE_NAME).where(TOKEN,token).exec(); + while (rs.next()){ + var lm = new ListMember(rs.getString(LIST_EMAIL),rs.getString(USER_EMAIL),rs.getInt(STATE),rs.getString(TOKEN)); + User user = User.loadAll(List.of(lm.userEmail)).stream().findAny().orElse(null); + if (user != null){ + int newState = lm.state ^ STATE_AWAITING_CONFIRMATION | STATE_SUBSCRIBER; + Database.open() + .update(TABLE_NAME) + .set(TOKEN,"NULL") + .set(STATE, newState) //drop confirmation state, set subscriber state + .where(LIST_EMAIL,lm.listEmail) + .where(USER_EMAIL,lm.userEmail) + .run(); + } + return user; + } + return null; + } + + public static ListMember create(MailingList list, User user, int state) throws SQLException { String token = null; if ((state & STATE_AWAITING_CONFIRMATION) > 0){ @@ -56,8 +70,8 @@ public class ListMember { Database.open().query(sql).run(); } - public static List listsOwnedBy(User user) { - var list = new ArrayList(); + public static Set listsOwnedBy(User user) { + var list = new HashSet(); try { var rs = Database.open().select(TABLE_NAME,LIST_EMAIL) .where(USER_EMAIL,user.email()) @@ -113,7 +127,7 @@ public class ListMember { .where(USER_EMAIL,user.email()) .exec(); while (rs.next()){ - int state = rs.getInt(STATE) ^ STATE_SUBSCRIBER; + int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_AWAITING_CONFIRMATION); if (state < 1) { // drop entry db.deleteFrom(TABLE_NAME) .where(LIST_EMAIL,list.email()) diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index a3bfe6a..811be90 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -8,10 +8,7 @@ import org.slf4j.LoggerFactory; import javax.mail.MessagingException; import java.io.UnsupportedEncodingException; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Util.t; @@ -78,48 +75,28 @@ public class MailingList { public static void enable(String listEmail, boolean enable) throws SQLException { - // https://stackoverflow.com/questions/16440831/bitwise-xor-in-sqlite-bitwise-not-not-working-as-i-expect - String expression = enable ? "state = state | "+ STATE_ENABLED : "state = (~(state & "+ STATE_ENABLED +"))&(state|"+ STATE_ENABLED +")"; - Database.open().update(TABLE_NAME,expression).where(EMAIL, listEmail).run(); + Database.open() + .update(TABLE_NAME) + .set(STATE,enable ? STATE+" | "+ STATE_ENABLED : Database.xor(STATE,STATE_ENABLED)) + .where(EMAIL, listEmail).run(); } public static void hide(String listEmail, boolean hide) throws SQLException { - // https://stackoverflow.com/questions/16440831/bitwise-xor-in-sqlite-bitwise-not-not-working-as-i-expect - String expression = hide ? "state = (~(state & "+ STATE_PUBLIC +"))&(state|"+ STATE_PUBLIC +")" : ("state = state | "+ STATE_PUBLIC); - Database.open().update(TABLE_NAME,expression).where(EMAIL, listEmail).run(); + Database.open() + .update(TABLE_NAME) + .set(STATE,hide ? STATE+" | "+ STATE_PUBLIC : Database.xor(STATE,STATE_PUBLIC)) + .where(EMAIL, listEmail).run(); } - public static boolean isOpen(String list) { return openLists().stream().filter(ml -> ml.email.equals(list)).count() > 0; } - public static List listsOf(User user) { - - List keys = (user.is(ADMIN)) ? null : ListMember.listsOwnedBy(user); - var list = new ArrayList(); - if (keys != null && keys.isEmpty()) return list; - try { - Database.Request query = Database.open().select(TABLE_NAME); - if (keys != null) query.where(EMAIL, keys); - var rs = query.exec(); - while (rs.next()) { - var email = rs.getString(EMAIL); - var name = rs.getString(NAME); - var imapHost = rs.getString(IMAP_HOST); - var imapPort = rs.getInt(IMAP_PORT); - var imapUser = rs.getString(IMAP_USER); - var imapPass = rs.getString(IMAP_PASS); - var smtpHost = rs.getString(SMTP_HOST); - var smtpPort = rs.getInt(SMTP_PORT); - var smtpUser = rs.getString(SMTP_USER); - var smtpPass = rs.getString(SMTP_PASS); - var state = rs.getInt(STATE); - list.add(new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, smtpHost, smtpPort, smtpUser, smtpPass, state)); - } - } catch (SQLException e) { - LOG.warn("Listing mailing lists failed: ", e); - } + public static Set listsOf(User user) { + var list = openLists(); + Set keys = (user.is(ADMIN)) ? null : ListMember.listsOwnedBy(user); + if (keys == null || keys.isEmpty()) return list; + for (String key : keys) list.add(load(key)); return list; } @@ -155,19 +132,17 @@ public class MailingList { return name; } - public static List openLists() { - var list = new ArrayList(); + public static Set openLists() { + var list = new HashSet(); try { var rs = Database.open() - .select(TABLE_NAME,"*", "(" + STATE + " & " + STATE_PUBLIC + ") as test") + .select(TABLE_NAME,EMAIL, "(" + STATE + " & " + STATE_PUBLIC + ") as test") .where("test", STATE_PUBLIC) .exec(); - while (rs.next()) { - var email = rs.getString(EMAIL); - var name = rs.getString(NAME); - var state = rs.getInt(STATE); - list.add(new MailingList(email, name, null, 0, null, null, null, 0, null, null, state)); - } + var emails = new ArrayList(); + while (rs.next()) emails.add(rs.getString(EMAIL)); + rs.close(); + for (String email : emails) list.add(load(email)); } catch (SQLException e) { LOG.warn("Listing mailing lists failed: ", e); } diff --git a/src/main/java/de/srsoftware/widerhall/data/User.java b/src/main/java/de/srsoftware/widerhall/data/User.java index cc20610..c35e906 100644 --- a/src/main/java/de/srsoftware/widerhall/data/User.java +++ b/src/main/java/de/srsoftware/widerhall/data/User.java @@ -117,7 +117,7 @@ public class User { private boolean matching(String password) { - if (hashedPass == null && password == null) return true; + if (hashedPass == null) return password == null; return hashedPass.equals(Util.sha256(password+salt)); } diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 47d24c5..1dddb2c 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -28,13 +28,14 @@ import static de.srsoftware.widerhall.Util.t; public class Web extends HttpServlet { private static final String ADD_LIST = "add_list"; + private static final String CONFIRM = "confirm"; private static final Logger LOG = LoggerFactory.getLogger(Web.class); private static final String LOGIN = "login"; private static final String LOGOUT = "logout"; private static final String REGISTER = "register"; + private static final String RELOAD = "reload"; private static final String SUBSCRIBE = "subscribe"; private static final String UNSUBSCRIBE = "unsubscribe"; - private static final String RELOAD = "reload"; private static final String IMAP_HOST = "imap_host"; private static final String IMAP_PORT = "imap_port"; private static final String IMAP_USER = "imap_user"; @@ -128,6 +129,19 @@ public class Web extends HttpServlet { } + private String confirm(HttpServletRequest req, HttpServletResponse resp) { + try { + var token = req.getParameter(TOKEN); + if (token== null || token.isBlank()) return t("Invalid or missing token!"); + var user = ListMember.confirm(token); + if (user != null) return loadTemplate(INDEX,Map.of(USER,user.safeMap(),NOTES,"Confirmed list subscription!"),resp); + return t("Unknown user"); + } catch (SQLException e) { + LOG.debug("Failed to confirm list membership:",e); + return t("Confirmation of list membership failed!"); + } + } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { @@ -150,6 +164,10 @@ public class Web extends HttpServlet { return sqle; } + private User getSessionUser(HttpServletRequest req) { + return req.getSession().getAttribute(USER) instanceof User user ? user : null; + } + private String handleGet(HttpServletRequest req, HttpServletResponse resp) { var o = req.getSession().getAttribute("user"); User user = o instanceof User ? (User) o : null; @@ -161,6 +179,8 @@ public class Web extends HttpServlet { var list = req.getParameter(LIST); if (list != null && !list.isBlank()) data.put(LIST,list); switch (path){ + case CONFIRM: + return confirm(req,resp); case RELOAD: loadTemplates(); data.put(NOTES,t("Templates have been reloaded")); @@ -203,6 +223,7 @@ public class Web extends HttpServlet { } + private String handleLogin(HttpServletRequest req, HttpServletResponse resp) { var email = req.getParameter("email"); var pass = req.getParameter("pass"); @@ -218,7 +239,7 @@ public class Web extends HttpServlet { LOG.warn("Static.handleLogin failed:",e); Thread.sleep(10000); } finally { - return loadTemplate("login", Map.of("error",t("Invalid username/password")), resp); + return loadTemplate("login", Map.of(ERROR,t("Invalid username/password"),EMAIL,email), resp); } } return null; @@ -429,7 +450,5 @@ public class Web extends HttpServlet { } } - private User getSessionUser(HttpServletRequest req) { - return req.getSession().getAttribute(USER) instanceof User user ? user : null; - } + } diff --git a/src/test/java/de/srsoftware/widerhall/UtilTest.java b/src/test/java/de/srsoftware/widerhall/UtilTest.java index faa4ace..412e2c4 100644 --- a/src/test/java/de/srsoftware/widerhall/UtilTest.java +++ b/src/test/java/de/srsoftware/widerhall/UtilTest.java @@ -52,4 +52,13 @@ public class UtilTest extends TestCase { assertFalse(Util.simplePassword("8986546054")); // digits only, but long enough assertFalse(Util.simplePassword("salgksdjbw")); // chars only, but long enough } + + public void testUnsetFlags(){ + assertEquals(0,Util.unset(31,1,2,4,8,16)); + assertEquals(1,Util.unset(31,2,4,8,16)); + assertEquals(2,Util.unset(31,1,4,8,16)); + assertEquals(4,Util.unset(31,1,2,8,16)); + assertEquals(8,Util.unset(31,1,2,4,16)); + assertEquals(16,Util.unset(31,1,2,4,8)); + } } \ No newline at end of file diff --git a/src/test/java/de/srsoftware/widerhall/data/DatabaseTest.java b/src/test/java/de/srsoftware/widerhall/data/DatabaseTest.java index 8d22e1d..cdd9288 100644 --- a/src/test/java/de/srsoftware/widerhall/data/DatabaseTest.java +++ b/src/test/java/de/srsoftware/widerhall/data/DatabaseTest.java @@ -51,7 +51,11 @@ public class DatabaseTest extends TestCase { public void testUpdate(){ assertEquals("UPDATE Test",Database.open().update("Test").sql()); - assertEquals("UPDATE Test SET x = 5",Database.open().update("Test","x = 5").sql()); - assertEquals("UPDATE Test SET x = 5, y = 6",Database.open().update("Test","x = 5","y = 6").sql()); + assertEquals("UPDATE Test SET x = 5",Database.open().update("Test").set("x",5).sql()); + assertEquals("UPDATE Test SET x = 5, y = 6",Database.open().update("Test").set("x",5).set("y",6).sql()); + } + + public void testXor(){ + assertEquals("(~(a&b))&(a|b)",Database.xor("a","b")); } } \ No newline at end of file diff --git a/static/templates/login.st b/static/templates/login.st index d69542f..2049ba8 100644 --- a/static/templates/login.st +++ b/static/templates/login.st @@ -13,7 +13,7 @@
Login-Daten