From b9b3196ae65e03f9820aaeda5f209275234962b1 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 22 Apr 2022 12:35:16 +0200 Subject: [PATCH] new permissions: * list members may be nominated as moderators by admin * admin may allow moderators to nominate more moderators * admin may set allowed senders to one of the following: * owners and mods * all subscribers * everyone * moderators are now able to remove members from list --- pom.xml | 2 +- .../srsoftware/widerhall/data/ListMember.java | 149 +++++++++++++++++- .../widerhall/data/MailingList.java | 62 ++++++-- .../de/srsoftware/widerhall/web/Rest.java | 69 +++++++- .../java/de/srsoftware/widerhall/web/Web.java | 21 ++- static/templates/inspect.st | 6 +- static/templates/js.st | 29 +++- static/templates/listadminlist.st | 2 +- static/templates/listmembers.st | 1 + 9 files changed, 305 insertions(+), 36 deletions(-) diff --git a/pom.xml b/pom.xml index acbbd78..7023c6d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.2.14 + 0.2.15 diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index 78886b8..b0c5554 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -1,17 +1,15 @@ package de.srsoftware.widerhall.data; import de.srsoftware.widerhall.Util; -import org.antlr.runtime.MismatchedTokenException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.stringtemplate.v4.ST; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import static de.srsoftware.widerhall.Constants.*; -import static de.srsoftware.widerhall.Constants.STATE; +import static de.srsoftware.widerhall.Util.t; /** * @author Stephan Richter @@ -47,6 +45,37 @@ public class ListMember { this.token = token; } + public String addNewModerator(String userEmail) { + if (!isAllowedToEditMods()) return t("You are not allowed to nominate new mods for {}",list.email()); + User moderator = null; + try { + moderator = User.load(userEmail); + } catch (SQLException e) { + LOG.warn("Failed to load user for {}",userEmail,e); + return t("Failed to load user for {}",userEmail); + } + if (moderator == null) return t("No such user: {}",userEmail); + + ListMember member = null; + try { + member = ListMember.load(list,moderator); + } catch (SQLException e) { + LOG.warn("Failed to load list member for {}/{}",moderator.email(),list.email(),e); + return t("Failed to load list member for {}/{}",moderator.email(),list.email()); + } + try { + if (member == null) { + ListMember.create(list, moderator, ListMember.STATE_MODERATOR); + } else { + member.setState(ListMember.STATE_MODERATOR); + } + } catch (SQLException e) { + LOG.warn("Failed to make {} a moderator of {}",moderator.email(),list.email(),e); + return t("Failed to make {} a moderator of {}",moderator.email(),list.email()); + } + return null; + } + /** * tries to confirm the token: * This method loads the list member, that is assigned with the token. @@ -113,6 +142,68 @@ public class ListMember { Database.open().query(sql).compile().run(); } + public String dropMember(String userEmail) { + if (!isModerator()) return t("You are not allowed to remove members of {}",list.email()); + User user = null; + try { + user = User.load(userEmail); + } catch (SQLException e) { + LOG.warn("Failed to load user for {}",userEmail,e); + return t("Failed to load user for {}",userEmail); + } + if (user == null) return t("No such user: {}",userEmail); + + ListMember member = null; + try { + member = ListMember.load(list,user); + } catch (SQLException e) { + LOG.warn("Failed to load list member for {}/{}",user.email(),list.email(),e); + return t("Failed to load list member for {}/{}",user.email(),list.email()); + } + + if (member == null) return t("{} is no member of {}",user.email(),list.email()); + if (member.isOwner()) return t("You are not allowed to remvoe the list owner!"); + + try { + member.unsubscribe(); + } catch (SQLException e) { + LOG.warn("Failed to un-subscribe {} from {}",user.email(),list.email(),e); + return t("Failed to un-subscribe {} from {}",user.email(),list.email()); + } + return null; + } + + public String dropModerator(String userEmail) { + if (!isAllowedToEditMods()) return t("You are not allowed to edit mods of {}",list.email()); + User moderator = null; + try { + moderator = User.load(userEmail); + } catch (SQLException e) { + LOG.warn("Failed to load user for {}",userEmail,e); + return t("Failed to load user for {}",userEmail); + } + if (moderator == null) return t("No such user: {}",userEmail); + + ListMember member = null; + try { + member = ListMember.load(list,moderator); + } catch (SQLException e) { + LOG.warn("Failed to load list member for {}/{}",moderator.email(),list.email(),e); + return t("Failed to load list member for {}/{}",moderator.email(),list.email()); + } + try { + if (member == null) { + ListMember.create(list, moderator, ListMember.STATE_SUBSCRIBER); + } else { + member.setState(ListMember.STATE_SUBSCRIBER); + } + } catch (SQLException e) { + LOG.warn("Failed to make {} a subscriber of {}",moderator.email(),list.email(),e); + return t("Failed to make {} a subscriber of {}",moderator.email(),list.email()); + } + return null; + } + /** * create a new ListMember object from a ResultSet * @param rs @@ -136,6 +227,12 @@ public class ListMember { return (state & testState) > 0; } + public boolean isAllowedToEditMods(){ + if (isOwner()) return true; + if (isModerator()) return list.modsMayEditMods(); + return false; + } + public boolean isAwaiting(){ return hasState(STATE_AWAITING_CONFIRMATION); } @@ -152,6 +249,35 @@ public class ListMember { return hasState(STATE_SUBSCRIBER|STATE_MODERATOR|STATE_OWNER); } + public MailingList list(){ + return list; + } + /** + * return a set of list emails of MailingLists the given user attends + * @param user + * @return + */ + public static Set listsOf(User user) { + var listEmails = new HashSet(); + try { + var request = Database.open() + .select(TABLE_NAME); + if (!user.hashPermission(User.PERMISSION_ADMIN)) request = request.where(USER_EMAIL, user.email()); + var rs = request.compile().exec(); + while (rs.next()) listEmails.add(rs.getString(LIST_EMAIL)); + } catch (SQLException e) { + LOG.warn("Collecting lists of {} failed: ",user.email(),e); + } + var lists = MailingList.loadAll(listEmails); + var result = new HashSet(); + try { + for (var ml : lists) result.add(ListMember.load(ml,user)); + } catch (SQLException e) { + e.printStackTrace(); + } + + return result; + } /** * return a set of list emails of MailingLists owned by the given user @@ -230,6 +356,17 @@ public class ListMember { return this; } + public ListMember setState(int newState) throws SQLException { + Database.open() + .update(TABLE_NAME) + .set(STATE,newState) + .where(USER_EMAIL,user.email()) + .where(LIST_EMAIL,list.email()) + .compile() + .run(); + return this; + } + /** * convert state flag to readable text * @return @@ -253,11 +390,9 @@ public class ListMember { /** * unsubscribe a list member - * @param list - * @param user * @throws SQLException */ - public static void unsubscribe(MailingList list, User user) throws SQLException { + public void unsubscribe() throws SQLException { var db = Database.open(); var rs = db.select(TABLE_NAME) .where(LIST_EMAIL,list.email()) @@ -265,7 +400,7 @@ public class ListMember { .compile() .exec(); while (rs.next()){ - int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_AWAITING_CONFIRMATION); // drop subscription and awaiting flags + int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_MODERATOR,STATE_AWAITING_CONFIRMATION); // drop subscription and awaiting flags var req = state < 1 ? db.deleteFrom(TABLE_NAME) : db.update(TABLE_NAME).set(STATE,state).set(TOKEN,null); req.where(LIST_EMAIL,list.email()).where(USER_EMAIL,user.email()).compile().run(); } diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index a8cd967..921d13e 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -35,6 +35,7 @@ public class MailingList implements MessageHandler { public static final String KEY_OPEN_FOR_GUESTS = "open_for_guests"; public static final String KEY_OPEN_FOR_SUBSCRIBERS = "open_for_subscribers"; public static final String KEY_ARCHIVE = "archive"; + public static final String KEY_MODS_CAN_EDIT_MODS = "edit_mods"; private static final Logger LOG = LoggerFactory.getLogger(MailingList.class); private static final String IMAP_HOST = "imap_host"; private static final String IMAP_PORT = "imap_port"; @@ -55,7 +56,7 @@ public class MailingList implements MessageHandler { public static final int STATE_OPEN_FOR_GUESTS = 64; // allow anyone to send via this list? public static final int STATE_PUBLIC_ARCHIVE = 128; // save received messages in archive? public static final int STATE_OPEN_FOR_SUBSCRIBERS = 256; // allow mods to send via this list? - public static final int STATE_MODS_CAN_CREATE_MODS = 512; // allow mods to make subscribers to mods? + public static final int STATE_MODS_CAN_EDIT_MODS = 512; // allow mods to make subscribers to mods? private static final int VISIBLE = 1; private static final int HIDDEN = 0; private static final int DEFAULT_STATE = STATE_PENDING|STATE_HIDE_RECEIVERS|STATE_PUBLIC_ARCHIVE; @@ -137,17 +138,6 @@ public class MailingList implements MessageHandler { Database.open().query(sql).compile().run(); } - /** - * load the set of mailing lists a given user is allowed to edit - * @param user - * @return - */ - public static Set editableBy(User user) { - var list = new HashSet(); - for (String key : ListMember.listsOwnedBy(user)) list.add(load(key)); - return list; - } - public String email() { return email; } @@ -295,10 +285,32 @@ public class MailingList implements MessageHandler { return ml; } + /** + * Load a ML object by it's identifying email address. + * This is a cached method: if the ML has been loaded before, the already-loaded object will be returned. + * @param listEmails + * @return + */ + public static Set loadAll(Collection listEmails) { + if (listEmails == null) return null; + if (listEmails.isEmpty()) return Set.of(); + var list = new HashSet(); + try { + var rs = Database.open() + .select(TABLE_NAME) + .where(EMAIL,listEmails) + .compile().exec(); + while (rs.next()) list.add(MailingList.from(rs)); + } catch (SQLException e) { + LOG.debug("Failed to load MailingLists: ",e); + } + return list; + } + public boolean mayBeAlteredBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).isOwner()) return true; + if (ListMember.load(this,user).isModerator()) return true; } catch (SQLException e) { LOG.debug("Error loading list member for ({}, {})",user.email(),email()); } @@ -343,6 +355,28 @@ public class MailingList implements MessageHandler { return map; } + /** + * load the set of mailing lists a given user is allowed to edit + * @param user + * @return + */ + public static List moderatedBy(User user) { + return ListMember.listsOf(user) + .stream() + .filter(listMember -> listMember.isModerator()) + .map(ListMember::list) + .toList(); + + } + + public boolean modsMayEditMods(){ + return hasState(STATE_MODS_CAN_EDIT_MODS); + } + + public MailingList modsMayNominateMods(boolean allowed) throws SQLException { + return setFlag(STATE_MODS_CAN_EDIT_MODS,allowed); + } + public String name(){ return name; } @@ -482,7 +516,7 @@ public class MailingList implements MessageHandler { private void sendConfirmationRequest(User user, String token) throws MessagingException, UnsupportedEncodingException { var subject = t("Please confirm your list subscription"); var config = Configuration.instance(); - var url = new StringBuilder(config.baseUrl()).append("/confirm?token=").append(token); + var url = new StringBuilder(config.baseUrl()).append("/web/confirm?token=").append(token); var text = t("Please go to {} in order to complete your list subscription!",url); smtp.send(email(),name(),user.email(),subject,text); } diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index 311c83e..ad70b5b 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -25,13 +25,16 @@ import static de.srsoftware.widerhall.data.MailingList.*; public class Rest extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(Rest.class); + private static final String LIST_ADD_MOD = "list/add_mod"; private static final String LIST_ARCHIVE = "list/archive"; private static final String LIST_DISABLE = "list/disable"; - private static final String LIST_EDITABLE = "list/editable"; + private static final String LIST_DROP_MEMBER = "list/drop_member"; + private static final String LIST_DROP_MOD = "list/drop_mod"; private static final String LIST_DETAIL = "list/detail"; private static final String LIST_ENABLE = "list/enable"; private static final String LIST_HIDE = "list/hide"; private static final String LIST_MEMBERS = "list/members"; + private static final String LIST_MODERATED = "list/moderated"; private static final String LIST_SHOW = "list/show"; private static final String LIST_TEST = "list/test"; private static final String LIST_SUBSCRIBABLE = "list/subscribable"; @@ -119,6 +122,9 @@ public class Rest extends HttpServlet { if (user != null){ json.put(USER,user.safeMap()); switch (path) { + case LIST_ARCHIVE: + json.put("archive",archive(req)); + break; case USER_LIST: try { json.put("users", (user.hashPermission(User.PERMISSION_ADMIN) ? User.loadAll() : List.of(user)).stream().map(User::safeMap).toList()); @@ -127,8 +133,8 @@ public class Rest extends HttpServlet { json.put(ERROR,"failed to load user list"); } break; - case LIST_EDITABLE: - json.put("lists", MailingList.editableBy(user).stream().map(MailingList::safeMap).toList()); + case LIST_MODERATED: + json.put("lists", MailingList.moderatedBy(user).stream().map(MailingList::safeMap).toList()); break; case LIST_SUBSCRIBABLE: json.put("lists", MailingList.subscribable(user).stream().map(MailingList::minimalMap).toList()); @@ -173,12 +179,21 @@ public class Rest extends HttpServlet { var userEmail = req.getParameter(EMAIL); var permissions = req.getParameter(PERMISSIONS); switch (path) { + case LIST_ADD_MOD: + json.putAll(listAddMod(list,userEmail,user)); + break; case LIST_DETAIL: json.putAll(listDetail(list,user)); break; case LIST_DISABLE: json.putAll(enableList(list,user,false)); break; + case LIST_DROP_MEMBER: + json.putAll(listDropMember(list,userEmail,user)); + break; + case LIST_DROP_MOD: + json.putAll(listDropMod(list,userEmail,user)); + break; case LIST_ENABLE: json.putAll(enableList(list,user,true)); break; @@ -232,6 +247,21 @@ public class Rest extends HttpServlet { } } + private Map listAddMod(MailingList list, String userEmail, User user) { + ListMember moderator = null; + try { + moderator = ListMember.load(list,user); + } catch (SQLException e) { + LOG.warn("Failed to load list member for {}/{}",user.email(),list.email(),e); + return Map.of(ERROR,t("Failed to load list member for {}/{}",user.email(),list.email())); + } + if (moderator == null) return Map.of(ERROR,t("{} is not a member of {}",user.email(),list.email())); + + var error = moderator.addNewModerator(userEmail); + + return error == null ? Map.of(SUCCESS,t("{} is now a moderator of {}",userEmail,list.email())) : Map.of(ERROR,error); + } + private Map listDetail(MailingList list, User user) { if (list == null) return Map.of(ERROR,"no list email provided!"); var map = new HashMap<>(); @@ -242,9 +272,40 @@ public class Rest extends HttpServlet { if (list.isOpenForGuests()) map.put(KEY_OPEN_FOR_GUESTS,true); if (list.isOpenForSubscribers()) map.put(KEY_OPEN_FOR_SUBSCRIBERS,true); if (list.hasState(MailingList.STATE_PUBLIC_ARCHIVE)) map.put(KEY_ARCHIVE,true); + if (list.hasState(STATE_MODS_CAN_EDIT_MODS)) map.put(KEY_MODS_CAN_EDIT_MODS,true); return map; } + private Map listDropMember(MailingList list, String userEmail, User user) { + ListMember moderator = null; + try { + moderator = ListMember.load(list,user); + } catch (SQLException e) { + LOG.warn("Failed to load list member for {}/{}",user.email(),list.email(),e); + return Map.of(ERROR,t("Failed to load list member for {}/{}",user.email(),list.email())); + } + if (moderator == null) return Map.of(ERROR,t("{} is not a member of {}",user.email(),list.email())); + + var error = moderator.dropMember(userEmail); + + return error == null ? Map.of(SUCCESS,t("{} is now a moderator of {}",userEmail,list.email())) : Map.of(ERROR,error); + } + + private Map listDropMod(MailingList list, String userEmail, User user) { + ListMember moderator = null; + try { + moderator = ListMember.load(list,user); + } catch (SQLException e) { + LOG.warn("Failed to load list member for {}/{}",user.email(),list.email(),e); + return Map.of(ERROR,t("Failed to load list member for {}/{}",user.email(),list.email())); + } + if (moderator == null) return Map.of(ERROR,t("{} is not a member of {}",user.email(),list.email())); + + var error = moderator.dropModerator(userEmail); + + return error == null ? Map.of(SUCCESS,t("{} is now a moderator of {}",userEmail,list.email())) : Map.of(ERROR,error); + } + 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())); @@ -253,7 +314,7 @@ public class Rest extends HttpServlet { .stream() .map(ListMember::safeMap) .toList(); - return Map.of(MEMBERS,members); + return Map.of(MEMBERS,members,LIST,list.minimalMap()); } catch (SQLException e) { LOG.error("Failed to load member list: ",e); return Map.of(ERROR,t("Failed to load member list '{}'",list.email())); diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 721a4db..f2afac1 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -5,7 +5,6 @@ import de.srsoftware.widerhall.data.ListMember; import de.srsoftware.widerhall.data.MailingList; import de.srsoftware.widerhall.data.Post; import de.srsoftware.widerhall.data.User; -import org.json.simple.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -296,7 +295,7 @@ public class Web extends TemplateServlet { if (!error && !list.mayBeAlteredBy(user)) { error = true; - data.put(ERROR,t("You are not allowed to alter this list!")); + data.put(ERROR,t("You are not alter settings of this list!")); } if (!error){ @@ -305,6 +304,7 @@ public class Web extends TemplateServlet { .forwardAttached(Util.getCheckbox(req, KEY_FORWARD_ATTACHED)) .hideReceivers(Util.getCheckbox(req, KEY_HIDE_RECEIVERS)) .replyToList(Util.getCheckbox(req, KEY_REPLY_TO_LIST)) + .modsMayNominateMods(Util.getCheckbox(req, KEY_MODS_CAN_EDIT_MODS)) .openForGuests(Util.getCheckbox(req,KEY_OPEN_FOR_GUESTS)) .openForSubscribers(Util.getCheckbox(req,KEY_OPEN_FOR_SUBSCRIBERS)) .archive(Util.getCheckbox(req,KEY_ARCHIVE)); @@ -483,9 +483,22 @@ public class Web extends TemplateServlet { return loadTemplate(UNSUBSCRIBE,data,resp); } } - // if we get here, we should have a valid user + + ListMember member = null; + try { + member = ListMember.load(list,user); + } catch (SQLException e) { + LOG.debug("Failed to load list member for {}/{}",user.email(),list.email(),e); + data.put(ERROR, t("Failed to load list member for {}/{}",user.email(),list.email())); + return loadTemplate(UNSUBSCRIBE,data,resp); + } + if (member == null){ + data.put(ERROR, t("{} is no member of {}",user.email(),list.email())); + return loadTemplate(UNSUBSCRIBE,data,resp); + } + // if we get here, we should have a valid member object try { - ListMember.unsubscribe(list,user); + member.unsubscribe(); data.put(NOTES,t("Sucessfully un-subscribed from '{}'.",list.email())); return loadTemplate(INDEX,data,resp); } catch (SQLException e) { diff --git a/static/templates/inspect.st b/static/templates/inspect.st index 3650777..eba9b7a 100644 --- a/static/templates/inspect.st +++ b/static/templates/inspect.st @@ -49,11 +49,15 @@
- Archive options + Other options +
diff --git a/static/templates/js.st b/static/templates/js.st index fc29db7..7456975 100644 --- a/static/templates/js.st +++ b/static/templates/js.st @@ -1,3 +1,7 @@ +function addMod(userEmail,listEmail){ + $.post('/api/list/add_mod',{list:listEmail,email:userEmail},reload,'json'); +} + function addPermission(userEmail,permission){ if (confirm("Really give permission to "+userEmail+"?")){ $.post('/api/user/addpermission',{email:userEmail,permissions:permission},reload,'json'); @@ -12,6 +16,15 @@ function dropList(listEmail){ console.log('dopList('+listEmail+')'); } +function dropMember(userEmail,listEmail){ + $.post('/api/list/drop_member',{list:listEmail,email:userEmail},reload,'json'); +} + +function dropMod(userEmail,listEmail){ + $.post('/api/list/drop_mod',{list:listEmail,email:userEmail},reload,'json'); +} + + function dropPermission(userEmail,permission){ if (confirm("Really withdraw permission from "+userEmail+"?")){ $.post('/api/user/droppermission',{email:userEmail,permissions:permission},reload,'json'); @@ -36,8 +49,8 @@ function loadListDetail(listEmail){ $.post('/api/list/detail',{list:listEmail},showListDetail,'json'); } -function loadListOfEditableLists(){ - $.getJSON('/api/list/editable', showListOfEditableLists); +function loadListOfModeratedLists(){ + $.getJSON('/api/list/moderated', showListOfModeratedLists); } function loadListOfSubscribableLists(){ @@ -74,14 +87,14 @@ function showListArchive(data){ } function showListDetail(data){ - var options = ['forward_from','forward_attached','hide_receivers','reply_to_list','open_for_guests','open_for_subscribers','archive']; + var options = ['forward_from','forward_attached','hide_receivers','reply_to_list','open_for_guests','open_for_subscribers','archive','edit_mods']; options.forEach(function(option,index,array){ console.log(option,'→',data[option]); if (data[option]) $('input[name="'+option+'"]').prop('checked',true); }); } -function showListOfEditableLists(data){ +function showListOfModeratedLists(data){ for (let i in data.lists){ let list = data.lists[i]; let row = $(''); @@ -161,12 +174,20 @@ function showListResult(result){ } function showMembers(data){ + var list_mail = data.list.email.prefix+'@'+data.list.email.domain; for (let i in data.members){ let member = data.members[i]; let row = $(''); $('').text(member.name).appendTo(row); $('').text(member.email).appendTo(row); $('').text(member.state).appendTo(row); + let col = $(''); + console.log("data",data); + if (member.state.includes("moderator")) { + if (!member.state.includes("owner")) $('