merged with 1.1
[mir.git] / source / mircoders / global / Abuse.java
index f67524a..e4b29fa 100755 (executable)
@@ -1,5 +1,5 @@
 /*\r
- * Copyright (C) 2001, 2002  The Mir-coders group\r
+ * Copyright (C) 2001, 2002 The Mir-coders group\r
  *\r
  * This file is part of Mir.\r
  *\r
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
  *\r
  * In addition, as a special exception, The Mir-coders gives permission to link\r
- * the code of this program with the com.oreilly.servlet library, any library\r
- * licensed under the Apache Software License, The Sun (tm) Java Advanced\r
- * Imaging library (JAI), The Sun JIMI library (or with modified versions of\r
- * the above that use the same license as the above), and distribute linked\r
- * combinations including the two.  You must obey the GNU General Public\r
- * License in all respects for all of the code used other than the above\r
- * mentioned libraries.  If you modify this file, you may extend this exception\r
- * to your version of the file, but you are not obligated to do so.  If you do\r
- * not wish to do so, delete this exception statement from your version.\r
+ * the code of this program with  any library licensed under the Apache Software License,\r
+ * The Sun (tm) Java Advanced Imaging library (JAI), The Sun JIMI library\r
+ * (or with modified versions of the above that use the same license as the above),\r
+ * and distribute linked combinations including the two.  You must obey the\r
+ * GNU General Public License in all respects for all of the code used other than\r
+ * the above mentioned libraries.  If you modify this file, you may extend this\r
+ * exception to your version of the file, but you are not obligated to do so.\r
+ * If you do not wish to do so, delete this exception statement from your version.\r
  */\r
 \r
 package mircoders.global;\r
 \r
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Vector;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.collections.ExtendedProperties;
-import gnu.regexp.RE;
-
-import mir.entity.Entity;
-import mir.log.LoggerWrapper;
-import mir.session.HTTPAdapters;
-import mir.session.Request;
-import mir.util.DateToMapAdapter;
-import mir.util.InternetFunctions;
-import mir.util.StringRoutines;
-import mircoders.entity.EntityComment;
-import mircoders.entity.EntityContent;
+import java.io.File;\r
+import java.io.FileNotFoundException;\r
+import java.io.FileOutputStream;\r
+import java.util.Arrays;\r
+import java.util.Date;\r
+import java.util.GregorianCalendar;\r
+import java.util.HashMap;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.Random;\r
+import java.util.Vector;\r
+import javax.servlet.http.Cookie;\r
+import javax.servlet.http.HttpServletResponse;\r
+\r
+import org.apache.commons.collections.ExtendedProperties;\r
+import mir.config.MirPropertiesConfiguration;\r
+import mir.entity.Entity;\r
+import mir.log.LoggerWrapper;\r
+import mir.session.Request;\r
+import mir.util.GeneratorFormatAdapters;\r
+import mir.util.StringRoutines;\r
+import mircoders.entity.EntityComment;\r
+import mircoders.entity.EntityContent;\r
+import mircoders.entity.EntityUsers;\r
 import mircoders.localizer.MirAdminInterfaceLocalizer;\r
+import mircoders.localizer.MirAntiAbuseFilterType;\r
 \r
 \r
 public class Abuse {\r
-  private List filters;\r
+  private List filterRules;\r
+  private Map filterTypes;\r
+  private List filterTypeIds;\r
   private int maxIdentifier;\r
   private LoggerWrapper logger;\r
+  private LoggerWrapper adminUsageLogger;\r
   private int logSize;\r
   private boolean logEnabled;\r
   private boolean openPostingDisabled;\r
@@ -75,18 +76,25 @@ public class Abuse {
   private List log;\r
   private String configFile = MirGlobal.config().getStringWithHome("Abuse.Config");\r
 \r
+  private MirPropertiesConfiguration configuration;\r
 \r
-  private static final String IP_FILTER_TYPE="ip";\r
-  private static final String REGEXP_FILTER_TYPE="regexp";\r
   private static String cookieName=MirGlobal.config().getString("Abuse.CookieName");\r
   private static int cookieMaxAge = 60*60*MirGlobal.config().getInt("Abuse.CookieMaxAge");\r
 \r
   public Abuse() {\r
     logger = new LoggerWrapper("Global.Abuse");\r
-    filters = new Vector();\r
+    adminUsageLogger = new LoggerWrapper("AdminUsage");\r
+    filterRules = new Vector();\r
     maxIdentifier = 0;\r
     log = new Vector();\r
 \r
+    try {\r
+      configuration = MirPropertiesConfiguration.instance();\r
+    }\r
+    catch (Throwable e) {\r
+      throw new RuntimeException("Can't get configuration: " + e.getMessage());\r
+    }\r
+\r
     logSize = 100;\r
     logEnabled = false;\r
     articleBlockAction = "";\r
@@ -95,61 +103,23 @@ public class Abuse {
     openPostingDisabled = false;\r
     cookieOnBlock = false;\r
 \r
-    load();\r
-  }\r
+    try {\r
+      filterTypes = new HashMap();\r
+      filterTypeIds = new Vector();\r
 \r
-  public boolean checkIpFilter(String anIpAddress) {\r
-    synchronized (filters) {\r
-      Iterator i = filters.iterator();\r
+      Iterator i = MirGlobal.localizer().openPostings().getAntiAbuseFilterTypes().iterator();\r
 \r
       while (i.hasNext()) {\r
-        Filter filter = (Filter) i.next();\r
-\r
-        try {\r
-          if ( (filter.getType().equals(IP_FILTER_TYPE)) &&\r
-              InternetFunctions.isIpAddressInNetwork(anIpAddress, filter.getExpression())) {\r
-            logger.debug("ip match on " + filter.getExpression());\r
-            return true;\r
-          }\r
-        }\r
-        catch (Throwable t) {\r
-          logger.warn("error while checking ip address " + anIpAddress + " over network " + filter.expression + ": " + t.getMessage());\r
-        }\r
+        MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) i.next();\r
+        filterTypes.put(filterType.getName(), filterType);\r
+        filterTypeIds.add(filterType.getName());\r
       }\r
-\r
-      return false;\r
     }\r
-  }\r
-\r
-  private boolean checkRegExpFilter(Entity anEntity) {\r
-    synchronized (filters) {\r
-      Iterator i = filters.iterator();\r
-\r
-      while (i.hasNext()) {\r
-        Filter filter = (Filter) i.next();\r
-\r
-        if (filter.getType().equals(REGEXP_FILTER_TYPE)) {\r
-          try {\r
-            RE regularExpression = new RE(filter.getExpression());\r
-\r
-            Iterator j = anEntity.getFields().iterator();\r
-            while (j.hasNext()) {\r
-              String field = anEntity.getValue( (String) j.next());\r
-\r
-              if (field != null && regularExpression.isMatch(field.toLowerCase())) {\r
-                logger.debug("regexp match on " + filter.getExpression());\r
-                return true;\r
-              }\r
-            }\r
-          }\r
-          catch (Throwable t) {\r
-            logger.warn("error while checking entity with regexp " + filter.getExpression() + ": " + t.getMessage());\r
-          }\r
-        }\r
-      }\r
-\r
-      return false;\r
+    catch (Throwable t) {\r
+      throw new RuntimeException("Can't get filter types: " + t.getMessage());\r
     }\r
+\r
+    load();\r
   }\r
 \r
   private void setCookie(HttpServletResponse aResponse) {\r
@@ -180,59 +150,63 @@ public class Abuse {
     return false;\r
   }\r
 \r
-  public void checkComment(EntityComment aComment, Request aRequest, HttpServletResponse aResponse) {\r
-    try {\r
-      long time = System.currentTimeMillis();\r
-      String address = "0.0.0.0";\r
-      String browser = "unknown";\r
-      Cookie[] cookies = {};\r
+  FilterRule findMatchingFilter(Entity anEntity, Request aRequest) {\r
+    Iterator iterator = filterRules.iterator();\r
 \r
-      HttpServletRequest request = null;\r
+    while (iterator.hasNext()) {\r
+      FilterRule rule = (FilterRule) iterator.next();\r
 \r
-      if (aRequest instanceof HTTPAdapters.HTTPParsedRequestAdapter) {\r
-        request = ((HTTPAdapters.HTTPParsedRequestAdapter) aRequest).getRequest();\r
-      }\r
-      else if (aRequest instanceof HTTPAdapters.HTTPRequestAdapter) {\r
-        request = ((HTTPAdapters.HTTPRequestAdapter) aRequest).getRequest();\r
-      }\r
-      if (request!=null) {\r
-        browser = (String) request.getHeader("User-Agent");\r
-        address = request.getRemoteAddr();\r
-        cookies = request.getCookies();\r
-      }\r
+      if (rule.test(anEntity, aRequest))\r
+        return rule;\r
+    }\r
+\r
+    return null;\r
+  }\r
 \r
-      logComment(address, aComment.getId(), new Date(), browser);\r
+  public void checkComment(EntityComment aComment, Request aRequest, HttpServletResponse aResponse) {\r
+    logComment(aComment, aRequest);\r
+\r
+    try {\r
+      long time = System.currentTimeMillis();\r
 \r
-      MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation = MirGlobal.localizer().adminInterface().simpleCommentOperationForName(commentBlockAction);\r
+      FilterRule filterRule = findMatchingFilter(aComment, aRequest);\r
 \r
-      if (checkCookie(Arrays.asList(cookies)) || checkIpFilter(address) || checkRegExpFilter(aComment)) {\r
-        operation.perform(null, MirGlobal.localizer().dataModel().adapterModel().makeEntityAdapter("comment", aComment));\r
+      if (filterRule!=null) {\r
+        logger.debug("Match for " + filterRule.getType()+" rule '"+ filterRule.getExpression()+"'");\r
+        filterRule.setLastHit(new GregorianCalendar().getTime());\r
+        MirGlobal.performCommentOperation(null, aComment, filterRule.getCommentAction());\r
         setCookie(aResponse);\r
+        save();\r
       }\r
 \r
       logger.info("checkComment: " + (System.currentTimeMillis()-time) + "ms");\r
     }\r
     catch (Throwable t) {\r
+      t.printStackTrace(logger.asPrintWriter(logger.DEBUG_MESSAGE));\r
       logger.error("Abuse.checkComment: " + t.toString());\r
     }\r
   }\r
 \r
-  public void checkArticle(EntityContent anArticle, HttpServletRequest aRequest, HttpServletResponse aResponse) {\r
+  public void checkArticle(EntityContent anArticle, Request aRequest, HttpServletResponse aResponse) {\r
+    logArticle(anArticle, aRequest);\r
+\r
     try {\r
       long time = System.currentTimeMillis();\r
 \r
-      logArticle(aRequest.getRemoteAddr(), anArticle.getId(), new Date(), (String) aRequest.getHeader("User-Agent"));\r
-\r
-      MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation = MirGlobal.localizer().adminInterface().simpleCommentOperationForName(commentBlockAction);\r
+      FilterRule filterRule = findMatchingFilter(anArticle, aRequest);\r
 \r
-      if (checkCookie(Arrays.asList(aRequest.getCookies())) || checkIpFilter(aRequest.getRemoteAddr()) || checkRegExpFilter(anArticle)) {\r
-        operation.perform(null, MirGlobal.localizer().dataModel().adapterModel().makeEntityAdapter("content", anArticle));\r
+      if (filterRule!=null) {\r
+        logger.debug("Match for " + filterRule.getType() + " rule '" + filterRule.getExpression()+"'");\r
+        filterRule.setLastHit(new GregorianCalendar().getTime());\r
+        MirGlobal.performArticleOperation(null, anArticle, filterRule.getArticleAction());\r
         setCookie(aResponse);\r
+        save();\r
       }\r
 \r
       logger.info("checkArticle: " + (System.currentTimeMillis()-time) + "ms");\r
     }\r
     catch (Throwable t) {\r
+      t.printStackTrace(logger.asPrintWriter(logger.DEBUG_MESSAGE));\r
       logger.error("Abuse.checkArticle: " + t.toString());\r
     }\r
   }\r
@@ -242,7 +216,8 @@ public class Abuse {
   }\r
 \r
   public void setLogEnabled(boolean anEnabled) {\r
-    logEnabled = anEnabled;\r
+    if (!configuration.getString("Abuse.DisallowIPLogging", "0").equals("1"))\r
+      logEnabled = anEnabled;\r
     truncateLog();\r
   }\r
 \r
@@ -295,32 +270,52 @@ public class Abuse {
     commentBlockAction = anAction;\r
   }\r
 \r
-\r
   public List getLog() {\r
     synchronized(log) {\r
-      List result = new Vector();\r
+      try {\r
+        List result = new Vector();\r
+\r
+        Iterator i = log.iterator();\r
+        while (i.hasNext()) {\r
+          LogEntry logEntry = (LogEntry) i.next();\r
+          Map entry = new HashMap();\r
+\r
+          entry.put("ip", logEntry.getIpNumber());\r
+          entry.put("id", logEntry.getId());\r
+          entry.put("timestamp", new GeneratorFormatAdapters.DateFormatAdapter(logEntry.getTimeStamp(), MirPropertiesConfiguration.instance().getString("Mir.DefaultTimezone")));\r
+          if (logEntry.getIsArticle())\r
+            entry.put("type", "content");\r
+          else\r
+            entry.put("type", "comment");\r
+          entry.put("browser", logEntry.getBrowserString());\r
+\r
+          result.add(entry);\r
+        }\r
 \r
-      Iterator i = log.iterator();\r
-      while (i.hasNext()) {\r
-        LogEntry logEntry = (LogEntry) i.next();\r
-        Map entry = new HashMap();\r
-\r
-        entry.put("ip", logEntry.getIpNumber());\r
-        entry.put("id", logEntry.getId());\r
-        entry.put("timestamp", new DateToMapAdapter(logEntry.getTimeStamp()));\r
-        if (logEntry.getIsArticle())\r
-          entry.put("type", "content");\r
-        else\r
-          entry.put("type", "comment");\r
-        entry.put("browser", logEntry.getBrowserString());\r
-\r
-        result.add(entry);\r
+        return result;\r
+      }\r
+      catch (Throwable t) {\r
+        throw new RuntimeException(t.toString());\r
       }\r
-\r
-      return result;\r
     }\r
   }\r
 \r
+  public void logComment(Entity aComment, Request aRequest) {\r
+    String ipAddress = aRequest.getHeader("ip");\r
+    String id = aComment.getId();\r
+    String browser = aRequest.getHeader("User-Agent");\r
+\r
+    logComment(ipAddress, id, new Date(), browser);\r
+  }\r
+\r
+  public void logArticle(Entity anArticle, Request aRequest) {\r
+    String ipAddress = aRequest.getHeader("ip");\r
+    String id = anArticle.getId();\r
+    String browser = aRequest.getHeader("User-Agent");\r
+\r
+    logArticle(ipAddress, id, new Date(), browser);\r
+  }\r
+\r
   public void logComment(String anIp, String anId, Date aTimeStamp, String aBrowser) {\r
     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, false));\r
   }\r
@@ -330,64 +325,75 @@ public class Abuse {
   }\r
 \r
   public void load() {\r
-    try {\r
-      ExtendedProperties configuration = new ExtendedProperties();\r
-\r
+    synchronized (filterRules) {\r
       try {\r
-        configuration = new ExtendedProperties(configFile);\r
-      }\r
-      catch (FileNotFoundException e) {\r
-      }\r
+        ExtendedProperties configuration = new ExtendedProperties();\r
 \r
-      getFilterConfig(filters, "abuse.filter", configuration);\r
+        try {\r
+          configuration = new ExtendedProperties(configFile);\r
+        }\r
+        catch (FileNotFoundException e) {\r
+        }\r
 \r
-      setOpenPostingDisabled(configuration.getString("abuse.openPostingDisabled", "0").equals("1"));\r
-      setOpenPostingPassword(configuration.getString("abuse.openPostingPassword", "0").equals("1"));\r
-      setCookieOnBlock(configuration.getString("abuse.cookieOnBlock", "0").equals("1"));\r
-      setLogEnabled(configuration.getString("abuse.logEnabled", "0").equals("1"));\r
-      setLogSize(configuration.getInt("abuse.logSize", 10));\r
-      setArticleBlockAction(configuration.getString("abuse.articleBlockAction", ""));\r
-      setCommentBlockAction(configuration.getString("abuse.commentBlockAction", ""));\r
-    }\r
-    catch (Throwable t) {\r
-      throw new RuntimeException(t.toString());\r
+        getFilterConfig(filterRules, "abuse.filter", configuration);\r
+\r
+        setOpenPostingDisabled(configuration.getString("abuse.openPostingDisabled", "0").equals("1"));\r
+        setOpenPostingPassword(configuration.getString("abuse.openPostingPassword", "0").equals("1"));\r
+        setCookieOnBlock(configuration.getString("abuse.cookieOnBlock", "0").equals("1"));\r
+        setLogEnabled(configuration.getString("abuse.logEnabled", "0").equals("1"));\r
+        setLogSize(configuration.getInt("abuse.logSize", 10));\r
+        setArticleBlockAction(configuration.getString("abuse.articleBlockAction", ""));\r
+        setCommentBlockAction(configuration.getString("abuse.commentBlockAction", ""));\r
+      }\r
+      catch (Throwable t) {\r
+        throw new RuntimeException(t.toString());\r
+      }\r
     }\r
   }\r
+\r
   public void save() {\r
-    try {\r
-      ExtendedProperties configuration = new ExtendedProperties();\r
+    synchronized (filterRules) {\r
+      try {\r
+        ExtendedProperties configuration = new ExtendedProperties();\r
 \r
-      setFilterConfig(filters, "abuse.filter", configuration);\r
+        setFilterConfig(filterRules, "abuse.filter", configuration);\r
 \r
-      configuration.addProperty("abuse.openPostingDisabled", getOpenPostingDisabled()?"1":"0");\r
-      configuration.addProperty("abuse.openPostingPassword", getOpenPostingPassword()?"1":"0");\r
-      configuration.addProperty("abuse.cookieOnBlock", getCookieOnBlock()?"1":"0");\r
-      configuration.addProperty("abuse.logEnabled", getLogEnabled()?"1":"0");\r
-      configuration.addProperty("abuse.logSize", Integer.toString(getLogSize()));\r
-      configuration.addProperty("abuse.articleBlockAction", getArticleBlockAction());\r
-      configuration.addProperty("abuse.commentBlockAction", getCommentBlockAction());\r
+        configuration.addProperty("abuse.openPostingDisabled", getOpenPostingDisabled() ? "1" : "0");\r
+        configuration.addProperty("abuse.openPostingPassword", getOpenPostingPassword() ? "1" : "0");\r
+        configuration.addProperty("abuse.cookieOnBlock", getCookieOnBlock() ? "1" : "0");\r
+        configuration.addProperty("abuse.logEnabled", getLogEnabled() ? "1" : "0");\r
+        configuration.addProperty("abuse.logSize", Integer.toString(getLogSize()));\r
+        configuration.addProperty("abuse.articleBlockAction", getArticleBlockAction());\r
+        configuration.addProperty("abuse.commentBlockAction", getCommentBlockAction());\r
 \r
-      configuration.save(new FileOutputStream(new File(configFile)), "Anti abuse configuration");\r
-    }\r
-    catch (Throwable t) {\r
-      throw new RuntimeException(t.toString());\r
+        configuration.save(new FileOutputStream(new File(configFile)), "Anti abuse configuration");\r
+      }\r
+      catch (Throwable t) {\r
+        throw new RuntimeException(t.toString());\r
+      }\r
     }\r
   }\r
 \r
   public List getFilterTypes() {\r
-    List result = new Vector();\r
+    try {\r
+      List result = new Vector();\r
+\r
+      Iterator i = filterTypeIds.iterator();\r
+      while (i.hasNext()) {\r
+        String id = (String) i.next();\r
 \r
-    Map entry = new HashMap();\r
-    entry.put("resource", "ip");\r
-    entry.put("id", IP_FILTER_TYPE);\r
-    result.add(entry);\r
+        Map action = new HashMap();\r
+        action.put("resource", id);\r
+        action.put("identifier", id);\r
 \r
-    entry = new HashMap();\r
-    entry.put("resource", "regexp");\r
-    entry.put("id", REGEXP_FILTER_TYPE);\r
-    result.add(entry);\r
+        result.add(action);\r
+      }\r
 \r
-    return result;\r
+      return result;\r
+    }\r
+    catch (Throwable t) {\r
+      throw new RuntimeException("can't get article actions");\r
+    }\r
   }\r
 \r
   public List getArticleActions() {\r
@@ -437,71 +443,101 @@ public class Abuse {
   }\r
 \r
   public List getFilters() {\r
-    return getFiltersAsMaps(filters);\r
+    List result = new Vector();\r
+\r
+    synchronized(filterRules) {\r
+      Iterator i = filterRules.iterator();\r
+      while (i.hasNext()) {\r
+        FilterRule filter = (FilterRule) i.next();\r
+        result.add(filter.clone());\r
+      }\r
+      return result;\r
+    }\r
   }\r
 \r
-  public void addFilter(String aType, String anExpression) {\r
-    addFilter(filters, aType, anExpression);\r
+  public String addFilter(String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {\r
+    return addFilter(aType, anExpression, aComments, aCommentAction, anArticleAction, null);\r
   }\r
 \r
-  public void setFilter(String anIdentifier, String aType, String anExpression) {\r
-    setFilter(filters, anIdentifier, aType, anExpression);\r
+  public String addFilter(String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction, Date aListHit) {\r
+    return addFilter(filterRules, aType, anExpression, aComments, aCommentAction, anArticleAction, aListHit);\r
   }\r
 \r
-  public void deleteFilter(String anIdentifier) {\r
-    deleteFilter(filters, anIdentifier);\r
+  public FilterRule getFilter(String anId) {\r
+    synchronized (filterRules) {\r
+      FilterRule result = (FilterRule) findFilter(filterRules, anId);\r
+      if (result==null)\r
+        return result;\r
+      else\r
+        return (FilterRule) result.clone();\r
+    }\r
   }\r
 \r
-  public void validateIpFilter(String anIdentifier, String anArticleAction, String aCommentAction) throws Exception {\r
+  public String setFilter(String anIdentifier, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {\r
+    return setFilter(filterRules, anIdentifier, aType, anExpression, aComments, aCommentAction, anArticleAction);\r
   }\r
 \r
-  private List getFiltersAsMaps(List aFilters) {\r
-    synchronized(aFilters) {\r
-      List result = new Vector();\r
+  public void deleteFilter(String anIdentifier) {\r
+    deleteFilter(filterRules, anIdentifier);\r
+  }\r
 \r
-      Iterator i = aFilters.iterator();\r
-      while (i.hasNext()) {\r
-        Filter filter = (Filter) i.next();\r
-        Map map = new HashMap();\r
+  private String addFilter(List aFilters, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction, Date aLastHit) {\r
+    MirAntiAbuseFilterType type = (MirAntiAbuseFilterType) filterTypes.get(aType);\r
 \r
-        map.put("id", filter.getId());\r
-        map.put("expression", filter.getExpression());\r
-        map.put("type", filter.getType());\r
+    if (type==null)\r
+      return "invalidtype";\r
 \r
-        result.add(map);\r
-      }\r
-      return result;\r
+    if (!type.validate(anExpression)) {\r
+      return "invalidexpression";\r
     }\r
-  }\r
 \r
-  private void addFilter(List aFilters, String aType, String anExpression) {\r
-    Filter filter = new Filter();\r
+    FilterRule filter = new FilterRule();\r
 \r
     filter.setId(generateId());\r
     filter.setExpression(anExpression);\r
     filter.setType(aType);\r
+    filter.setComments(aComments);\r
+    filter.setArticleAction(anArticleAction);\r
+    filter.setCommentAction(aCommentAction);\r
+    filter.setLastHit(aLastHit);\r
 \r
     synchronized (aFilters) {\r
       aFilters.add(filter);\r
     }\r
+\r
+    return null;\r
   }\r
 \r
-  private void setFilter(List aFilters, String anIdentifier, String aType, String anExpression) {\r
+  private String setFilter(List aFilters, String anIdentifier, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {\r
+    MirAntiAbuseFilterType type = (MirAntiAbuseFilterType) filterTypes.get(aType);\r
+\r
+    if (type==null)\r
+      return "invalidtype";\r
+\r
+    if (!type.validate(anExpression)) {\r
+      return "invalidexpression";\r
+    }\r
+\r
     synchronized (aFilters) {\r
-      Filter filter = findFilter(aFilters, anIdentifier);\r
+      FilterRule filter = findFilter(aFilters, anIdentifier);\r
 \r
       if (filter!=null) {\r
         filter.setExpression(anExpression);\r
         filter.setType(aType);\r
+        filter.setCommentAction(aCommentAction);\r
+        filter.setArticleAction(anArticleAction);\r
+        filter.setComments(aComments);\r
       }\r
+\r
+      return null;\r
     }\r
   }\r
 \r
-  private Filter findFilter(List aFilters, String anIdentifier) {\r
+  private FilterRule findFilter(List aFilters, String anIdentifier) {\r
     synchronized (aFilters) {\r
       Iterator i = aFilters.iterator();\r
       while (i.hasNext()) {\r
-        Filter filter = (Filter) i.next();\r
+        FilterRule filter = (FilterRule) i.next();\r
 \r
         if (filter.getId().equals(anIdentifier)) {\r
           return filter;\r
@@ -514,7 +550,7 @@ public class Abuse {
 \r
   private void deleteFilter(List aFilters, String anIdentifier) {\r
     synchronized (aFilters) {\r
-      Filter filter = findFilter(aFilters, anIdentifier);\r
+      FilterRule filter = findFilter(aFilters, anIdentifier);\r
 \r
       if (filter!=null) {\r
         aFilters.remove(filter);\r
@@ -530,15 +566,31 @@ public class Abuse {
     }\r
   }\r
 \r
-  private static class Filter {\r
+  public class FilterRule {\r
     private String identifier;\r
     private String expression;\r
     private String type;\r
+    private String comments;\r
+    private String articleAction;\r
+    private String commentAction;\r
+    private Date lastHit;\r
+\r
+    public FilterRule() {\r
+      expression = "";\r
+      type = "";\r
+      identifier = "";\r
+      comments = "";\r
+      articleAction = articleBlockAction;\r
+      commentAction = commentBlockAction;\r
+      lastHit = null;\r
+    }\r
 \r
-    public Filter() {\r
-      expression="";\r
-      type="";\r
-      identifier="";\r
+    public Date getLastHit() {\r
+      return lastHit;\r
+    }\r
+\r
+    public void setLastHit(Date aDate) {\r
+      lastHit = aDate;\r
     }\r
 \r
     public String getId() {\r
@@ -564,6 +616,56 @@ public class Abuse {
     public void setType(String aType) {\r
       type = aType;\r
     }\r
+\r
+    public void setComments(String aComments) {\r
+      comments = aComments;\r
+    }\r
+\r
+    public String getComments() {\r
+      return comments;\r
+    }\r
+\r
+    public String getArticleAction() {\r
+      return articleAction;\r
+    }\r
+\r
+    public void setArticleAction(String anArticleAction) {\r
+      articleAction = anArticleAction;\r
+    }\r
+\r
+    public String getCommentAction() {\r
+      return commentAction;\r
+    }\r
+\r
+    public void setCommentAction(String aCommentAction) {\r
+      commentAction = aCommentAction;\r
+    }\r
+\r
+    public boolean test(Entity anEntity, Request aRequest) {\r
+      MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) filterTypes.get(type);\r
+      try {\r
+        if (filterType != null)\r
+          return filterType.test(expression, anEntity, aRequest);\r
+      }\r
+      catch (Throwable t) {\r
+        logger.error("error while testing "+type+"-filter '"+expression+"'");\r
+      }\r
+\r
+      return false;\r
+    };\r
+\r
+    public Object clone() {\r
+      FilterRule result = new FilterRule();\r
+      result.setComments(getComments());\r
+      result.setExpression(getExpression());\r
+      result.setId(getId());\r
+      result.setType(getType());\r
+      result.setArticleAction(getArticleAction());\r
+      result.setCommentAction(getCommentAction());\r
+      result.setLastHit(getLastHit());\r
+\r
+      return result;\r
+    }\r
   }\r
 \r
   private void setFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {\r
@@ -571,9 +673,19 @@ public class Abuse {
       Iterator i = aFilters.iterator();\r
 \r
       while (i.hasNext()) {\r
-        Filter filter = (Filter) i.next();\r
+        FilterRule filter = (FilterRule) i.next();\r
+\r
+        String filterconfig =\r
+            StringRoutines.replaceStringCharacters(filter.getType(), new char[] { '\\', ':'}, new String[] { "\\\\", "\\:"} ) + ":" +\r
+            StringRoutines.replaceStringCharacters(filter.getExpression(), new char[] { '\\', ':'}, new String[] { "\\\\", "\\:"} ) + ":" +\r
+            StringRoutines.replaceStringCharacters(filter.getArticleAction(), new char[] { '\\', ':'}, new String[] { "\\\\", "\\:"} ) + ":" +\r
+            StringRoutines.replaceStringCharacters(filter.getCommentAction(), new char[] { '\\', ':'}, new String[] { "\\\\", "\\:"} ) + ":" +\r
+            StringRoutines.replaceStringCharacters(filter.getComments(), new char[] { '\\', ':'}, new String[] { "\\\\", "\\:"})  + ":";\r
+\r
+        if (filter.getLastHit()!=null)\r
+          filterconfig = filterconfig + filter.getLastHit().getTime();\r
 \r
-        aConfiguration.addProperty(aConfigKey, filter.getType()+":"+filter.getExpression());\r
+        aConfiguration.addProperty(aConfigKey, filterconfig);\r
       }\r
     }\r
   }\r
@@ -582,14 +694,36 @@ public class Abuse {
     synchronized(aFilters) {\r
       aFilters.clear();\r
 \r
-      Iterator i = Arrays.asList(aConfiguration.getStringArray(aConfigKey)).iterator();\r
+      if (aConfiguration.getStringArray(aConfigKey)!=null) {\r
 \r
-      while (i.hasNext()) {\r
-        String filter = (String) i.next();\r
-        List parts = StringRoutines.separateString(filter, ":");\r
+        Iterator i = Arrays.asList(aConfiguration.getStringArray(aConfigKey)).\r
+            iterator();\r
+\r
+        while (i.hasNext()) {\r
+          String filter = (String) i.next();\r
+          List parts = StringRoutines.splitStringWithEscape(filter, ':', '\\');\r
+          if (parts.size() == 2) {\r
+            parts.add(articleBlockAction);\r
+            parts.add(commentBlockAction);\r
+            parts.add("");\r
+            parts.add("");\r
+          }\r
+\r
+          if (parts.size() >= 5) {\r
+            Date lastHit = null;\r
+\r
+            if (parts.size()>=6) {\r
+              String lastHitString = (String) parts.get(5);\r
 \r
-        if (parts.size()==2) {\r
-          addFilter( (String) parts.get(0), (String) parts.get(1));\r
+              try {\r
+                lastHit = new Date(Long.parseLong(lastHitString));\r
+              }\r
+              catch (Throwable t) {\r
+              }\r
+            }\r
+\r
+            addFilter( (String) parts.get(0), (String) parts.get(1), (String) parts.get(4), (String) parts.get(3), (String) parts.get(2), lastHit);\r
+          }\r
         }\r
       }\r
     }\r
@@ -652,4 +786,15 @@ public class Abuse {
     }\r
   }\r
 \r
+  public void logAdminUsage(EntityUsers aUser, String aDescription) {\r
+    try {\r
+      String user = "unknown (" + aUser.toString() +")";\r
+      if (user!=null)\r
+        user = aUser.getValue("login");\r
+      adminUsageLogger.info(user + ": " + aDescription);\r
+    }\r
+    catch (Throwable t) {\r
+      logger.error("Error while logging admin usage ("+aUser.toString()+", "+aDescription+"): " +t.toString());\r
+    }\r
+  }\r
 }
\ No newline at end of file