Index: H:\Java\eclipse\src\test\resources\config\SystemGlobalsTest.properties =================================================================== --- H:\Java\eclipse\src\test\resources\config\SystemGlobalsTest.properties (revision 195) +++ H:\Java\eclipse\src\test\resources\config\SystemGlobalsTest.properties (revision 199) @@ -145,6 +145,12 @@ captcha.min.font.size = 25 captcha.max.font.size = 35 +# ############### +# Post Rate Limit +# ############### +rate.limit.autolock.forum.ids = 2 +rate.limit.topic.interval.days = 7 + # Allowed HTML tags to be used when posting a message html.tags.welcome = u, a, img, i, u, li, ul, font, br, p, b, hr html.attributes.welcome = src, href, size, face, color, target, rel Index: H:\Java\eclipse\src\main\config\SystemGlobals.properties =================================================================== --- H:\Java\eclipse\src\main\config\SystemGlobals.properties (revision 195) +++ H:\Java\eclipse\src\main\config\SystemGlobals.properties (revision 199) @@ -146,6 +146,11 @@ captcha.min.font.size = 25 captcha.max.font.size = 35 +# ############### +# Post Rate Limit +# ############### +rate.limit.autolock.forum.ids = 2 +rate.limit.topic.interval.days = 7 # Allowed HTML tags to be used when posting a message html.tags.welcome = a, img, font, b, i, u, li, ul, br, hr, p html.attributes.welcome = href, target, rel, src, width, height, size, face, color Index: H:\Java\eclipse\src\main\config\database\generic\generic_queries.sql =================================================================== --- H:\Java\eclipse\src\main\config\database\generic\generic_queries.sql (revision 195) +++ H:\Java\eclipse\src\main\config\database\generic\generic_queries.sql (revision 199) @@ -281,6 +281,12 @@ AND p.user_id = u.user_id \ AND p.need_moderate = 0 +ForumModel.lastTopicTimestampByUser = SELECT MAX(t.topic_time) as latest_topic_time \ + FROM jforum_topics t \ + WHERE t.forum_id = ? \ + AND t.user_id = ? \ + AND t.moderated = 0 + ForumModel.getModeratorList = SELECT u.user_id AS id, u.username AS name \ FROM jforum_groups g, jforum_roles r, jforum_role_values rv, jforum_roles r2, jforum_users u, jforum_user_groups ug \ WHERE g.group_id = r.group_id \ Index: H:\Java\eclipse\src\main\config\languages\en_US.properties =================================================================== --- H:\Java\eclipse\src\main\config\languages\en_US.properties (revision 195) +++ H:\Java\eclipse\src\main\config\languages\en_US.properties (revision 199) @@ -298,6 +298,9 @@ Config.Form.postsCacheEnabled = Cache most recent read topics in memory Config.Form.postsCacheSize = Number of most recent read topics to keep in memory (LRU). Only used if the previous option is set to "true" Config.Form.postsNewDelay = Delay (in ms) between each post from the user. Set it to 0 (zero) to disable the config +Config.Form.rateLimit = Post Rate Limiting +Config.Form.rateLimitAutolockForumIds = Forum IDs where topics will be autolocked ( separated by comma ) +Config.Form.rateLimitTopicIntervalDays = Minimum days between new topic for standard users # Category listing Delete = Delete @@ -751,6 +754,7 @@ PostForm.textEmpty = Empty message, please enter a message or quit. PostForm.title = New Topic PostForm.tooSoon = You cannot post a new message so soon. Please wait some time and try again. +PostForm.tooSoonRateLimited = You have already created a new topic in this forum in the last %s days. PostShow.PostNotFound = The post you are trying to see does not exist. PostShow.TopicNotFound = The topic you are trying to see does not exist. Index: H:\Java\eclipse\src\main\java\net\jforum\util\preferences\ConfigKeys.java =================================================================== --- H:\Java\eclipse\src\main\java\net\jforum\util\preferences\ConfigKeys.java (revision 195) +++ H:\Java\eclipse\src\main\java\net\jforum\util\preferences\ConfigKeys.java (revision 199) @@ -170,6 +170,9 @@ public static final String CAPTCHA_MIN_WORDS = "captcha.min.words"; public static final String CAPTCHA_MAX_WORDS = "captcha.max.words"; + public static final String RATE_LIMIT_AUTOLOCK_FORUM_IDS = "rate.limit.autolock.forum.ids"; + public static final String RATE_LIMIT_TOPIC_INTERVAL_DAYS = "rate.limit.topic.interval.days"; + public static final String I18N_DEFAULT = "i18n.board.default"; public static final String I18N_DEFAULT_ADMIN = "i18n.internal"; public static final String I18N_IMAGES_DIR = "i18n.images.dir"; Index: H:\Java\eclipse\src\main\java\net\jforum\dao\ForumDAO.java =================================================================== --- H:\Java\eclipse\src\main\java\net\jforum\dao\ForumDAO.java (revision 195) +++ H:\Java\eclipse\src\main\java\net\jforum\dao\ForumDAO.java (revision 199) @@ -275,4 +275,12 @@ * @return the forum id of the given email, or 0 if not found */ int discoverForumId(String listEmail) ; -} \ No newline at end of file + + /** + * Returns the timestamp of the last topic created by the user in the given + * forum id. + * @param forumId forum id where to check + * @param userId user id to check + * @return creation time of the last topic, or -1 if the user has not created a topic yet. + */ + public long getLastTopicTimestampByUser(int forumId, int userId);} \ No newline at end of file Index: H:\Java\eclipse\src\main\java\net\jforum\dao\generic\GenericForumDAO.java =================================================================== --- H:\Java\eclipse\src\main\java\net\jforum\dao\generic\GenericForumDAO.java (revision 195) +++ H:\Java\eclipse\src\main\java\net\jforum\dao\generic\GenericForumDAO.java (revision 199) @@ -975,4 +975,34 @@ return forumId; } + + public long getLastTopicTimestampByUser(int forumId, int userId) { + + PreparedStatement pstmt = null; + ResultSet resultSet = null; + try { + pstmt = JForumExecutionContext.getConnection() + .prepareStatement(SystemGlobals.getSql("ForumModel.lastTopicTimestampByUser")); + pstmt.setInt(1, forumId); + pstmt.setInt(2, userId); + + resultSet = pstmt.executeQuery(); + + if (resultSet.next()) { + // We're asking for MAX but in case of no values, the MAX returns a result set with NULL as value. + Date lastTopicTime = resultSet.getDate("latest_topic_time"); + if (lastTopicTime != null) { + return lastTopicTime.getTime(); + } + } + + return -1; + } + catch (SQLException e) { + throw new DatabaseException(e); + } + finally { + DbUtils.close(resultSet, pstmt); + } + } } Index: H:\Java\eclipse\src\main\java\net\jforum\view\forum\PostAction.java =================================================================== --- H:\Java\eclipse\src\main\java\net\jforum\view\forum\PostAction.java (revision 195) +++ H:\Java\eclipse\src\main\java\net\jforum\view\forum\PostAction.java (revision 199) @@ -101,6 +101,8 @@ import org.apache.commons.lang3.StringUtils; import freemarker.template.SimpleHash; +import java.util.StringTokenizer; +import org.apache.log4j.Logger; /** * @author Rafael Steil @@ -108,6 +110,8 @@ */ public class PostAction extends Command { + private static final Logger LOGGER = Logger.getLogger(PostAction.class); + public PostAction() { } @@ -938,6 +942,8 @@ this.context.put("message", I18n.getMessage("PostShow.notModeratedYet")); } + // Gets called when inserting new post. Incidentally, it also creates the topic if it is the first post + // on a new topic ID. public void insertSave() { int forumId = this.request.getIntParameter("forum_id"); @@ -1033,6 +1039,40 @@ } } + // Check that preconditions for rate limiting are met before we start + // creating objects in the DB. + boolean isRateLimitedPost = false; + if (newTopic && topic.getType() == Topic.TYPE_NORMAL) { + // First, we don't want to lock anything created by super users + boolean isSuperUser = SecurityRepository.canAccess(SecurityConstants.PERM_ADMINISTRATION) + || SecurityRepository.canAccess(SecurityConstants.PERM_MODERATION); + + if (!isSuperUser && isPostedOnAutolockRateLimitForumIds(forumId)) { + isRateLimitedPost = true; + } + } + + // If the post is rate limited and there's less than the amount of given days + // just bail out. + if (isRateLimitedPost) { + int rateLimitIntervalDays = SystemGlobals.getIntValue(ConfigKeys.RATE_LIMIT_TOPIC_INTERVAL_DAYS); + + long lastTopicTimestamp = forumDao.getLastTopicTimestampByUser(forumId, us.getUserId()); + + if (lastTopicTimestamp != -1) { + long daysSinceLastTopic = ((new Date()).getTime() - lastTopicTimestamp) / 86400000; + + if (daysSinceLastTopic < rateLimitIntervalDays) { + this.context.put("post", post); + this.context.put("start", this.request.getParameter("start")); + String error = String.format(I18n.getMessage("PostForm.tooSoonRateLimited"), rateLimitIntervalDays); + this.context.put("error", error); + this.insert(); + return; + } + } + } + post.setForumId(forumId); if (StringUtils.isBlank(post.getSubject())) { @@ -1138,6 +1178,15 @@ topic.setLastPostTime(post.getFormattedTime()); } + // Autolock for the specified forum IDs and for normal user + if (isRateLimitedPost) { + // For the cache, but since the topicDao.update doesn't set the status flag, we also need the lockUnlock below. + topic.setStatus(Topic.STATUS_LOCKED); + + // The status in the database. + topicDao.lockUnlock(new int[] {topic.getId()}, Topic.STATUS_LOCKED); + } + topicDao.update(topic); attachments.insertAttachments(post); @@ -1511,4 +1560,34 @@ return true; } + + /** + * Returns true if the given forumId is part of the list of forum ids where + * topics should be locked automatically. + * + * @param forumId - the forum id to check for + * @return - true for autolock, false otherwise. + */ + private boolean isPostedOnAutolockRateLimitForumIds(int forumId) { + String autolockForumIds = SystemGlobals.getValue(ConfigKeys.RATE_LIMIT_AUTOLOCK_FORUM_IDS); + StringTokenizer st = new StringTokenizer(autolockForumIds, ","); + + // TODO -> we could convert the String to an array of ints when the properties are + // loaded, but! posting new topics is not that often, and second, if the + // author does not apply the patch, keeping this here makes porting easier in the + // future. + while(st.hasMoreTokens()) { + String autolockForumId = st.nextToken().trim(); + + try { + if (forumId == Integer.parseInt(autolockForumId)) { + return true; + } + } catch (NumberFormatException nfe) { + LOGGER.error("forumId " + st + " specified for rate limiting is not a number.", nfe); + } + } + + return false; + } } Index: H:\Java\eclipse\src\main\resources\templates\default\admin\config_list.htm =================================================================== --- H:\Java\eclipse\src\main\resources\templates\default\admin\config_list.htm (revision 195) +++ H:\Java\eclipse\src\main\resources\templates\default\admin\config_list.htm (revision 199) @@ -197,6 +197,20 @@ + + + ${I18n.getMessage("Config.Form.rateLimit")} + + + + ${I18n.getMessage("Config.Form.rateLimitAutolockForumIds")} + + + + + ${I18n.getMessage("Config.Form.rateLimitTopicIntervalDays")} + + ${I18n.getMessage("Config.Form.cacheSettings")}