diff --git a/pom.xml b/pom.xml index 74d9804..dc44257 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.0.16 + 0.0.17 diff --git a/src/main/java/de/srsoftware/widerhall/Util.java b/src/main/java/de/srsoftware/widerhall/Util.java index b182ef4..ce5b489 100644 --- a/src/main/java/de/srsoftware/widerhall/Util.java +++ b/src/main/java/de/srsoftware/widerhall/Util.java @@ -1,9 +1,12 @@ package de.srsoftware.widerhall; import de.srsoftware.tools.translations.Translation; +import de.srsoftware.widerhall.data.MailingList; +import de.srsoftware.widerhall.data.User; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; +import javax.servlet.http.HttpServletRequest; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -11,6 +14,8 @@ import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.stream.Collectors; +import static de.srsoftware.widerhall.Constants.*; + public class Util { private static final MessageDigest SHA256 = getSha256(); @@ -92,4 +97,21 @@ public class Util { } return value; } + + public static User getUser(HttpServletRequest req) { + var o = req.getSession().getAttribute(USER); + return o instanceof User ? (User) o : null; + } + + public static String getPath(HttpServletRequest req) { + var path = req.getPathInfo(); + return path == null ? INDEX : path.substring(1); + + } + + public static MailingList getMailingList(HttpServletRequest req) { + var listEmail = req.getParameter(LIST); + if (listEmail == null || listEmail.isBlank()) return null; + return MailingList.load(listEmail); + } } diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index 33dcdf9..1aa4897 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import java.util.*; import static de.srsoftware.widerhall.Constants.*; +import static de.srsoftware.widerhall.Constants.STATE; /** * @author Stephan Richter @@ -26,19 +27,21 @@ public class ListMember { private static final String USER_EMAIL = "user_email"; private static final String STATE = "state"; - private final String listEmail,token,userEmail; + private MailingList list; + private User user; + private final String token; private final int state; /** * create a new list member object - * @param listEmail - * @param userEmail + * @param list + * @param user * @param state * @param token */ - public ListMember(String listEmail, String userEmail, int state, String token){ - this.listEmail = listEmail; - this.userEmail = userEmail; + public ListMember(MailingList list, User user, int state, String token){ + this.list = list; + this.user = user; this.state = state; this.token = token; } @@ -59,19 +62,18 @@ public class ListMember { if (rs.next()){ var lm = ListMember.from(rs); rs.close(); - User user = User.loadAll(List.of(lm.userEmail)).stream().findAny().orElse(null); - if (user != null){ + if (lm.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) + .where(LIST_EMAIL,lm.list.email()) + .where(USER_EMAIL,lm.user.email()) .compile() .run(); } - return user; + return lm.user; } return null; } @@ -91,7 +93,7 @@ public class ListMember { if ((state & STATE_AWAITING_CONFIRMATION) > 0){ token = Util.sha256(String.join("/",list.email(),user.email(),user.salt())); } - return new ListMember(list.email(),user.email(),state,token).save(); + return new ListMember(list,user,state,token).save(); } /** @@ -118,8 +120,8 @@ public class ListMember { */ public static ListMember from(ResultSet rs) throws SQLException { return new ListMember( - rs.getString(LIST_EMAIL), - rs.getString(USER_EMAIL), + MailingList.load(rs.getString(LIST_EMAIL)), + User.load(rs.getString(USER_EMAIL)), rs.getInt(STATE), rs.getString(TOKEN)); } @@ -175,28 +177,24 @@ public class ListMember { return null; } - /** - * return a map of User → State for a given MailingList - * @param listEmail - * @return - * @throws SQLException - */ - public static Map of(String listEmail) throws SQLException { - // Step 1: create mal USER_EMAIL → STATE for MailingList - var rs = Database.open() - .select(TABLE_NAME) - .where(LIST_EMAIL,listEmail) - .compile() - .exec(); - var temp = new HashMap(); - while (rs.next()) temp.put(rs.getString(USER_EMAIL),rs.getInt(STATE)); - rs.close(); - // Step 2: map user emails to users - var result = new HashMap(); - User.loadAll(temp.keySet()) - .stream() - .forEach(user -> result.put(user,temp.get(user.email()))); - return result; + + public static Set of(MailingList list) throws SQLException { + var rs = Database.open().select(TABLE_NAME).where(LIST_EMAIL,list.email()).compile().exec(); + var set = new HashSet(); + try { + while (rs.next()) set.add(ListMember.from(rs)); + } finally { + rs.close(); + } + return set; + } + + public Map safeMap(){ + return Map.of( + EMAIL,user.email(), + NAME,user.name(), + STATE,ListMember.stateText(state) + ); } /** @@ -207,8 +205,8 @@ public class ListMember { private ListMember save() throws SQLException { var req = Database.open() .insertInto(TABLE_NAME) - .set(LIST_EMAIL,listEmail) - .set(USER_EMAIL,userEmail) + .set(LIST_EMAIL,list.email()) + .set(USER_EMAIL,user.email()) .set(STATE,state); if (token != null) req.set(TOKEN,token); req.compile().run(); @@ -255,4 +253,8 @@ public class ListMember { req.where(LIST_EMAIL,list.email()).where(USER_EMAIL,user.email()).compile().run(); } } + + public User user(){ + return user; + } } diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index cdcda3d..de3cbe3 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -7,12 +7,10 @@ import de.srsoftware.widerhall.mail.SmtpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.mail.Address; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; import java.io.UnsupportedEncodingException; import java.sql.ResultSet; import java.sql.SQLException; @@ -49,7 +47,7 @@ public class MailingList implements MessageHandler { private final SmtpClient smtp; private final ImapClient imap; - private static final HashMap lists = new HashMap<>(); + private static final HashMap cache = new HashMap<>(); /** * create a new ML object @@ -145,7 +143,7 @@ public class MailingList implements MessageHandler { private void forward(Message message) throws MessagingException { try { - var emails = members().stream().map(User::email).toList(); + var emails = members().stream().map(ListMember::user).map(User::email).toList(); String sender = (state & STATE_FORWARD_FROM) > 0 ? message.getFrom()[0].toString() : email(); smtp.bccForward(sender,message,emails); } catch (SQLException e) { @@ -163,8 +161,8 @@ public class MailingList implements MessageHandler { */ private static MailingList from(ResultSet rs) throws SQLException { String email = rs.getString(EMAIL); - var ml = lists.get(email); - if (ml == null) lists.put(email,ml = new MailingList(rs.getString(EMAIL), + var ml = cache.get(email); + if (ml == null) cache.put(email,ml = new MailingList(rs.getString(EMAIL), rs.getString(NAME), rs.getString(IMAP_HOST), rs.getInt(IMAP_PORT), @@ -213,7 +211,7 @@ public class MailingList implements MessageHandler { */ public static MailingList load(String listEmail) { if (listEmail == null) return null; - var ml = lists.get(listEmail); + var ml = cache.get(listEmail); if (ml == null) try { var rs = Database.open() .select(TABLE_NAME) @@ -226,10 +224,41 @@ public class MailingList implements MessageHandler { return ml; } - private Set members() throws SQLException { - return ListMember.of(email()).keySet(); + public boolean mayBeAlteredBy(User user) { + if (user.hashPermission(PERMISSION_ADMIN)) return true; + try { + if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + } catch (SQLException e) { + LOG.debug("Error loading list member for ({}, {})",user.email(),email()); + } + return false; } + public boolean mayBeTestedBy(User user) { + if (user.hashPermission(PERMISSION_ADMIN)) return true; + try { + if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + } catch (SQLException e) { + LOG.debug("Error loading list member for ({}, {})",user.email(),email()); + } + return false; + } + + public Set members() throws SQLException { + return ListMember.of(this); + } + + public boolean membersMayBeListedBy(User user) { + if (user.hashPermission(PERMISSION_ADMIN)) return true; + try { + if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + } catch (SQLException e) { + LOG.debug("Error loading list member for ({}, {})",user.email(),email()); + } + return false; + } + + /** * creates a map from the current ML object containing only email and name of the ML * @return @@ -424,5 +453,4 @@ public class MailingList implements MessageHandler { return new ArrayList().stream(); } } - } diff --git a/src/main/java/de/srsoftware/widerhall/data/User.java b/src/main/java/de/srsoftware/widerhall/data/User.java index 1513738..7473bc1 100644 --- a/src/main/java/de/srsoftware/widerhall/data/User.java +++ b/src/main/java/de/srsoftware/widerhall/data/User.java @@ -138,6 +138,17 @@ public class User { return (permissions & permission) > 0; } + public static User load(String email) throws SQLException { + var rs = Database.open().select(TABLE_NAME).where(EMAIL,email).compile().exec(); + try { + if (rs.next()) { + return User.from(rs); + } + return null; + } finally { + rs.close(); + } + } /** * Load the list of all users. Internally calls loadAll(null) diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index 2d6d9ad..ebf53d3 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -1,5 +1,6 @@ package de.srsoftware.widerhall.web; +import de.srsoftware.widerhall.Util; import de.srsoftware.widerhall.data.ListMember; import de.srsoftware.widerhall.data.MailingList; import de.srsoftware.widerhall.data.User; @@ -85,12 +86,25 @@ public class Rest extends HttpServlet { return Map.of(SUCCESS,"Updated user permissions"); } + private Map enableList(MailingList list, User user, boolean enable) { + if (list == null) return Map.of(ERROR,"no list email provided!"); + if (!list.mayBeAlteredBy(user)) Map.of(ERROR,t("You are not allowed to edit '{}'",list.email())); + try { + list.enable(enable); + return Map.of(SUCCESS,t("Mailing list '{}' was {}!",list.email(),enable ? "enabled" : "disabled")); + } catch (SQLException e) { + LOG.error("Failed to enable/disable mailing list: ",e); + return Map.of(ERROR,t("Failed to update list '{}'",list.email())); + } + } + public String handleGet(HttpServletRequest req, HttpServletResponse resp){ - Object o = req.getSession().getAttribute(USER); + var user = Util.getUser(req); + var path = Util.getPath(req); + JSONObject json = new JSONObject(); - var path = req.getPathInfo(); - path = path == null ? INDEX : path.substring(1); - if (o instanceof User user){ + + if (user != null){ json.put(USER,user.safeMap()); switch (path) { case USER_LIST: @@ -130,38 +144,38 @@ public class Rest extends HttpServlet { } public String handlePost(HttpServletRequest req, HttpServletResponse resp){ - Object o = req.getSession().getAttribute(USER); - JSONObject json = new JSONObject(); - var path = req.getPathInfo(); - path = path == null ? INDEX : path.substring(1); - if (o instanceof User user){ + var user = Util.getUser(req); + var path = Util.getPath(req); + + JSONObject json = new JSONObject(); + if (user != null){ json.put(USER,user.safeMap()); - var listEmail = req.getParameter(LIST); + var list = Util.getMailingList(req); var userEmail = req.getParameter(EMAIL); var permissions = req.getParameter(PERMISSIONS); switch (path) { case LIST_DETAIL: - json.putAll(listDetail(listEmail,user)); + json.putAll(listDetail(list,user)); break; case LIST_DISABLE: - json.putAll(enableList(listEmail,user,false)); + json.putAll(enableList(list,user,false)); break; case LIST_ENABLE: - json.putAll(enableList(listEmail,user,true)); + json.putAll(enableList(list,user,true)); break; case LIST_HIDE: - json.putAll(hideList(listEmail,user,true)); + json.putAll(hideList(list,user,true)); break; case LIST_MEMBERS: - json.putAll(listMembers(listEmail,user)); + json.putAll(listMembers(list,user)); break; case LIST_SHOW: - json.putAll(hideList(listEmail,user,false)); + json.putAll(hideList(list,user,false)); break; case LIST_TEST: - json.putAll(testList(listEmail,user)); + json.putAll(testList(list,user)); break; case USER_ADD_PERMISSION: if (user.hashPermission(User.PERMISSION_ADMIN)){ @@ -189,69 +203,45 @@ public class Rest extends HttpServlet { } } - private Map listDetail(String listEmail, User user) { - var ml = MailingList.load(listEmail); - if (ml == null) return Map.of(ERROR,t("Mailinglist {} unknown",listEmail)); - var map = new HashMap<>(); - if (ml.hasState(MailingList.STATE_FORWARD_FROM)) map.put("forward_from",true); - return map; - } - - private Map listMembers(String listEmail, User user) { - if (listEmail == null || listEmail.isBlank()) return Map.of(ERROR,"no list email provided!"); - if (user.hashPermission(User.PERMISSION_ADMIN) || ListMember.listsOwnedBy(user).contains(listEmail)) { - try { - var members = ListMember.of(listEmail) - .entrySet() - .stream() - .map(entry -> Map.of( - EMAIL,entry.getKey().email(), - NAME,entry.getKey().name(), - STATE,ListMember.stateText(entry.getValue()) - )) - .toList(); - return Map.of(MEMBERS,members); - } catch (SQLException e) { - LOG.error("Failed to load member list: ",e); - return Map.of("error",t("Failed to load member list '{}'",listEmail)); - } + private Map hideList(MailingList list, User user, boolean hide) { + if (list == null) return Map.of(ERROR,"no list email provided!"); + if (!list.mayBeAlteredBy(user)) Map.of(ERROR,t("You are not allowed to edit '{}'",list.email())); + try { + list.hide(hide); + return Map.of(SUCCESS,t("Mailing list '{}' was {}!",list.email(),hide ? "hidden" : "made public")); + } catch (SQLException e) { + LOG.error("Failed to (un)hide mailing list: ",e); + return Map.of(ERROR,t("Failed to update list '{}'",list.email())); } - return Map.of("error",t("You are not allowed to list members '{}'",listEmail)); } - private Map enableList(String listEmail, User user, boolean enable) { - if (listEmail == null || listEmail.isBlank()) return Map.of(ERROR,"no list email provided!"); - if (user.hashPermission(User.PERMISSION_ADMIN) || ListMember.listsOwnedBy(user).contains(listEmail)){ - try { - MailingList.load(listEmail).enable(enable); - return Map.of(SUCCESS,t("Mailing list '{}' was {}!",listEmail,enable ? "enabled" : "disabled")); - } catch (SQLException e) { - LOG.error("Failed to enable/disable mailing list: ",e); - return Map.of(ERROR,t("Failed to update list '{}'",listEmail)); - } - } - return Map.of(ERROR,t("You are not allowed to edit '{}'",listEmail)); + private Map listDetail(MailingList list, User user) { + if (list == null) return Map.of(ERROR,t("Mailinglist {} unknown",list.email())); + var map = new HashMap<>(); + if (list.hasState(MailingList.STATE_FORWARD_FROM)) map.put("forward_from",true); + return map; } - private Map hideList(String listEmail, User user, boolean hide) { - if (listEmail == null || listEmail.isBlank()) return Map.of(ERROR,"no list email provided!"); - if (user.hashPermission(User.PERMISSION_ADMIN) || ListMember.listsOwnedBy(user).contains(listEmail)){ - try { - MailingList.load(listEmail).hide(hide); - return Map.of(SUCCESS,t("Mailing list '{}' was {}!",listEmail,hide ? "hidden" : "made public")); - } catch (SQLException e) { - LOG.error("Failed to (un)hide mailing list: ",e); - return Map.of("error",t("Failed to update list '{}'",listEmail)); - } - + private Map listMembers(MailingList list, User user) { + if (list == null) return Map.of(ERROR,"no list email provided!"); + if (!list.membersMayBeListedBy(user)) Map.of(ERROR,t("You are not allowed to list members of '{}'",list.email())); + try { + var members = list.members() + .stream() + .map(ListMember::safeMap) + .toList(); + return Map.of(MEMBERS,members); + } catch (SQLException e) { + LOG.error("Failed to load member list: ",e); + return Map.of("error",t("Failed to load member list '{}'",list.email())); } - return Map.of(ERROR,t("You are not allowed to edit '{}'",listEmail)); } - private Map testList(String listEmail, User user) { - if (listEmail == null || listEmail.isBlank()) return Map.of(ERROR,"no list email provided!"); + private Map testList(MailingList list, User user) { + if (list == null) return Map.of(ERROR,"no list email provided!"); + if (!list.mayBeTestedBy(user)) Map.of(ERROR,t("You are not allowed to test '{}'",list.email())); try { - MailingList.load(listEmail).test(user); + list.test(user); return Map.of(SUCCESS,t("Sent test email to {}",user.email())); } catch (Exception e) { LOG.warn("Failed to send test email",e); diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 85c13f2..f203674 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -25,6 +25,7 @@ public class Web extends TemplateServlet { private static final String CONFIRM = "confirm"; private static final Logger LOG = LoggerFactory.getLogger(Web.class); private static final String ADMIN = "admin"; + private static final String INSPECT = "inspect"; private static final String LOGIN = "login"; private static final String LOGOUT = "logout"; private static final String REGISTER = "register"; @@ -245,11 +246,15 @@ public class Web extends TemplateServlet { } private String handlePost(HttpServletRequest req, HttpServletResponse resp) { - var path = req.getPathInfo(); - path = path == null ? INDEX : path.substring(1); + final var user = Util.getUser(req); + final var path = Util.getPath(req); + final var list = Util.getMailingList(req); + switch (path){ case ADD_LIST: return addList(req,resp); + case INSPECT: + return inspect(req,resp); case LOGIN: return handleLogin(req,resp); case REGISTER: @@ -263,8 +268,21 @@ public class Web extends TemplateServlet { return t("No handler for path {}!",path); } + private String inspect(HttpServletRequest req, HttpServletResponse resp) { + var o = req.getSession().getAttribute(USER); + if (!(o instanceof User user)) { + return redirectTo(LOGIN,resp); + } + var post = req.getParameterMap(); + var listEmail = req.getParameter(LIST); + var list = MailingList.load(listEmail); + if (list == null) return t("{} is does not denote a valid list",listEmail); + if (!list.mayBeAlteredBy(user)) { - + } + LOG.debug("POST: {}",post); + return null; + } private String redirectTo(String page, HttpServletResponse resp) { diff --git a/static/templates/login.st b/static/templates/login.st index 8f29a67..9b293d7 100644 --- a/static/templates/login.st +++ b/static/templates/login.st @@ -6,7 +6,7 @@ - + «navigation()» «messages()»

Widerhall login