ip log order reversed
[mir.git] / source / mircoders / global / Abuse.java
1 /*
2  * Copyright (C) 2001, 2002 The Mir-coders group
3  *
4  * This file is part of Mir.
5  *
6  * Mir is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * Mir is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Mir; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  *
20  * In addition, as a special exception, The Mir-coders gives permission to link
21  * the code of this program with  any library licensed under the Apache Software License,
22  * The Sun (tm) Java Advanced Imaging library (JAI), The Sun JIMI library
23  * (or with modified versions of the above that use the same license as the above),
24  * and distribute linked combinations including the two.  You must obey the
25  * GNU General Public License in all respects for all of the code used other than
26  * the above mentioned libraries.  If you modify this file, you may extend this
27  * exception to your version of the file, but you are not obligated to do so.
28  * If you do not wish to do so, delete this exception statement from your version.
29  */
30
31 package mircoders.global;
32
33 import java.io.BufferedOutputStream;
34 import java.io.File;
35 import java.io.FileNotFoundException;
36 import java.io.FileOutputStream;
37 import java.util.Arrays;
38 import java.util.Date;
39 import java.util.GregorianCalendar;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Random;
45 import java.util.Vector;
46
47 import javax.servlet.http.Cookie;
48 import javax.servlet.http.HttpServletResponse;
49
50 import mir.config.MirPropertiesConfiguration;
51 import mir.entity.Entity;
52 import mir.log.LoggerWrapper;
53 import mir.session.Request;
54 import mir.util.DateTimeFunctions;
55 import mir.util.GeneratorFormatAdapters;
56 import mir.util.StringRoutines;
57 import mircoders.entity.EntityComment;
58 import mircoders.entity.EntityContent;
59 import mircoders.localizer.MirAdminInterfaceLocalizer;
60 import mircoders.localizer.MirAntiAbuseFilterType;
61
62 import org.apache.commons.collections.ExtendedProperties;
63
64
65 public class Abuse {
66   private List filterRules;
67   private Map filterTypes;
68   private List filterTypeIds;
69   private int maxIdentifier;
70   private LoggerWrapper logger;
71   private int logSize;
72   private boolean logEnabled;
73   private boolean openPostingDisabled;
74   private boolean openPostingPassword;
75   private boolean cookieOnBlock;
76   private String articleBlockAction;
77   private String commentBlockAction;
78   private List log;
79   private String configFile = MirGlobal.config().getStringWithHome("Abuse.Config");
80
81   private MirPropertiesConfiguration configuration;
82
83   private static String cookieName = MirGlobal.config().getString("Abuse.CookieName");
84   private static int cookieMaxAge = 60 * 60 * MirGlobal.config().getInt("Abuse.CookieMaxAge");
85
86   public Abuse() {
87     logger = new LoggerWrapper("Global.Abuse");
88     filterRules = new Vector();
89     maxIdentifier = 0;
90     log = new Vector();
91
92     try {
93       configuration = MirPropertiesConfiguration.instance();
94     }
95     catch (Throwable e) {
96       throw new RuntimeException("Can't get configuration: " + e.getMessage());
97     }
98
99     logSize = 100;
100     logEnabled = false;
101     articleBlockAction = "";
102     commentBlockAction = "";
103     openPostingPassword = false;
104     openPostingDisabled = false;
105     cookieOnBlock = false;
106
107     try {
108       filterTypes = new HashMap();
109       filterTypeIds = new Vector();
110
111       Iterator i = MirGlobal.localizer().openPostings().getAntiAbuseFilterTypes().iterator();
112
113       while (i.hasNext()) {
114         MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) i.next();
115         filterTypes.put(filterType.getName(), filterType);
116         filterTypeIds.add(filterType.getName());
117       }
118     }
119     catch (Throwable t) {
120       throw new RuntimeException("Can't get filter types: " + t.getMessage());
121     }
122
123     load();
124   }
125
126   private void setCookie(HttpServletResponse aResponse) {
127     Random random = new Random();
128
129     Cookie cookie = new Cookie(cookieName, Integer.toString(random.nextInt(1000000000)));
130     cookie.setMaxAge(cookieMaxAge);
131     cookie.setPath("/");
132
133     if (aResponse != null)
134       aResponse.addCookie(cookie);
135   }
136
137   private boolean checkCookie(List aCookies) {
138     if (getCookieOnBlock()) {
139       Iterator i = aCookies.iterator();
140
141       while (i.hasNext()) {
142         Cookie cookie = (Cookie) i.next();
143
144         if (cookie.getName().equals(cookieName)) {
145           logger.debug("cookie match");
146           return true;
147         }
148       }
149     }
150
151     return false;
152   }
153
154   FilterRule findMatchingFilter(Entity anEntity, Request aRequest) {
155     Iterator iterator = filterRules.iterator();
156
157     while (iterator.hasNext()) {
158       FilterRule rule = (FilterRule) iterator.next();
159
160       if (rule.test(anEntity, aRequest))
161         return rule;
162     }
163
164     return null;
165   }
166
167   public void checkComment(EntityComment aComment, Request aRequest, HttpServletResponse aResponse) {
168     try {
169       long time = System.currentTimeMillis();
170
171       FilterRule filterRule = findMatchingFilter(aComment, aRequest);
172
173       if (filterRule != null) {
174         logger.debug("Match for " + filterRule.getType() + " rule '" + filterRule.getExpression() + "'");
175         filterRule.setLastHit(new GregorianCalendar().getTime());
176
177         StringBuffer line = new StringBuffer();
178
179         line.append(DateTimeFunctions.advancedDateFormat(
180             configuration.getString("Mir.DefaultDateTimeFormat"),
181             (new GregorianCalendar()).getTime(), configuration.getString("Mir.DefaultTimezone")));
182
183         line.append(" ");
184         line.append("filter");
185
186         line.append(" ");
187         line.append(filterRule.getType() +" ("+ filterRule.getExpression()+")");
188         aComment.appendLineToField("comment", line.toString());
189
190         MirGlobal.performCommentOperation(null, aComment, filterRule.getCommentAction());
191         setCookie(aResponse);
192         save();
193         logComment(aComment, aRequest, filterRule.getType(), filterRule.getExpression());
194       }
195       else
196         logComment(aComment, aRequest);
197
198
199       logger.info("checkComment: " + (System.currentTimeMillis() - time) + "ms");
200     }
201     catch (Throwable t) {
202       t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
203       logger.error("Abuse.checkComment: " + t.toString());
204     }
205   }
206
207   public void checkArticle(EntityContent anArticle, Request aRequest, HttpServletResponse aResponse) {
208     try {
209       long time = System.currentTimeMillis();
210
211       FilterRule filterRule = findMatchingFilter(anArticle, aRequest);
212
213       if (filterRule != null) {
214         logger.debug("Match for " + filterRule.getType() + " rule '" + filterRule.getExpression() + "'");
215         filterRule.setLastHit(new GregorianCalendar().getTime());
216
217         StringBuffer line = new StringBuffer();
218
219         line.append(DateTimeFunctions.advancedDateFormat(
220             configuration.getString("Mir.DefaultDateTimeFormat"),
221             (new GregorianCalendar()).getTime(), configuration.getString("Mir.DefaultTimezone")));
222
223         line.append(" ");
224         line.append("filter");
225
226         line.append(" ");
227         line.append(filterRule.getType() +" ("+ filterRule.getExpression()+")");
228         anArticle.appendLineToField("comment", line.toString());
229
230         MirGlobal.performArticleOperation(null, anArticle, filterRule.getArticleAction());
231         setCookie(aResponse);
232         save();
233         logArticle(anArticle, aRequest, filterRule.getType(), filterRule.getExpression());
234       }
235       else
236         logArticle(anArticle, aRequest);
237
238       logger.info("checkArticle: " + (System.currentTimeMillis() - time) + "ms");
239     }
240     catch (Throwable t) {
241       t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
242       logger.error("Abuse.checkArticle: " + t.toString());
243     }
244   }
245
246   public boolean getLogEnabled() {
247     return logEnabled;
248   }
249
250   public void setLogEnabled(boolean anEnabled) {
251     if (!configuration.getString("Abuse.DisallowIPLogging", "0").equals("1"))
252       logEnabled = anEnabled;
253     truncateLog();
254   }
255
256   public int getLogSize() {
257     return logSize;
258   }
259
260   public void setLogSize(int aSize) {
261     logSize = aSize;
262     truncateLog();
263   }
264
265   public boolean getOpenPostingDisabled() {
266     return openPostingDisabled;
267   }
268
269   public void setOpenPostingDisabled(boolean anOpenPostingDisabled) {
270     openPostingDisabled = anOpenPostingDisabled;
271   }
272
273   public boolean getOpenPostingPassword() {
274     return openPostingPassword;
275   }
276
277   public void setOpenPostingPassword(boolean anOpenPostingPassword) {
278     openPostingPassword = anOpenPostingPassword;
279   }
280
281   public boolean getCookieOnBlock() {
282     return cookieOnBlock;
283   }
284
285   public void setCookieOnBlock(boolean aCookieOnBlock) {
286     cookieOnBlock = aCookieOnBlock;
287   }
288
289   public String getArticleBlockAction() {
290     return articleBlockAction;
291   }
292
293   public void setArticleBlockAction(String anAction) {
294     articleBlockAction = anAction;
295   }
296
297   public String getCommentBlockAction() {
298     return commentBlockAction;
299   }
300
301   public void setCommentBlockAction(String anAction) {
302     commentBlockAction = anAction;
303   }
304
305   public List getLog() {
306     synchronized (log) {
307       try {
308         List result = new Vector();
309
310         Iterator i = log.iterator();
311         while (i.hasNext()) {
312           LogEntry logEntry = (LogEntry) i.next();
313           Map entry = new HashMap();
314
315           entry.put("ip", logEntry.getIpNumber());
316           entry.put("id", logEntry.getId());
317           entry.put("timestamp", new GeneratorFormatAdapters.DateFormatAdapter(logEntry.getTimeStamp(), MirPropertiesConfiguration.instance().getString("Mir.DefaultTimezone")));
318           if (logEntry.getIsArticle())
319             entry.put("type", "content");
320           else
321             entry.put("type", "comment");
322           entry.put("browser", logEntry.getBrowserString());
323           entry.put("hitfiltertype", logEntry.getHitFilterType());
324           entry.put("hitfilterexpression", logEntry.getHitFilterExpression());
325
326           result.add(entry);
327         }
328
329         return result;
330       }
331       catch (Throwable t) {
332         throw new RuntimeException(t.toString());
333       }
334     }
335   }
336
337   public void logComment(Entity aComment, Request aRequest) {
338     logComment(aComment, aRequest, null, null);
339   }
340
341   public void logComment(Entity aComment, Request aRequest, String aHitFilterType, String aHitFilterExpression) {
342     String ipAddress = aRequest.getHeader("ip");
343     String id = aComment.getId();
344     String browser = aRequest.getHeader("User-Agent");
345
346     logComment(ipAddress, id, new Date(), browser, aHitFilterType, aHitFilterExpression);
347   }
348
349   public void logArticle(Entity anArticle, Request aRequest) {
350     logArticle(anArticle, aRequest, null, null);
351   }
352
353   public void logArticle(Entity anArticle, Request aRequest, String aHitFilterType, String aHitFilterExpression) {
354     String ipAddress = aRequest.getHeader("ip");
355     String id = anArticle.getId();
356     String browser = aRequest.getHeader("User-Agent");
357
358     logArticle(ipAddress, id, new Date(), browser, aHitFilterType, aHitFilterExpression);
359   }
360
361   public void logComment(String anIp, String anId, Date aTimeStamp, String aBrowser, String aHitFilterType, String aHitFilterExpression) {
362     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, false, aHitFilterType, aHitFilterExpression));
363   }
364
365   public void logArticle(String anIp, String anId, Date aTimeStamp, String aBrowser, String aHitFilterType, String aHitFilterExpression) {
366     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, true, aHitFilterType, aHitFilterExpression));
367   }
368
369   public void load() {
370     synchronized (filterRules) {
371       try {
372         ExtendedProperties configuration = new ExtendedProperties();
373
374         try {
375           configuration = new ExtendedProperties(configFile);
376         }
377         catch (FileNotFoundException e) {
378         }
379
380         getFilterConfig(filterRules, "abuse.filter", configuration);
381
382         setOpenPostingDisabled(configuration.getString("abuse.openPostingDisabled", "0").equals("1"));
383         setOpenPostingPassword(configuration.getString("abuse.openPostingPassword", "0").equals("1"));
384         setCookieOnBlock(configuration.getString("abuse.cookieOnBlock", "0").equals("1"));
385         setLogEnabled(configuration.getString("abuse.logEnabled", "0").equals("1"));
386         setLogSize(configuration.getInt("abuse.logSize", 10));
387         setArticleBlockAction(configuration.getString("abuse.articleBlockAction", ""));
388         setCommentBlockAction(configuration.getString("abuse.commentBlockAction", ""));
389       }
390       catch (Throwable t) {
391         throw new RuntimeException(t.toString());
392       }
393     }
394   }
395
396   public void save() {
397     synchronized (filterRules) {
398       try {
399         ExtendedProperties configuration = new ExtendedProperties();
400
401         setFilterConfig(filterRules, "abuse.filter", configuration);
402
403         configuration.addProperty("abuse.openPostingDisabled", getOpenPostingDisabled() ? "1" : "0");
404         configuration.addProperty("abuse.openPostingPassword", getOpenPostingPassword() ? "1" : "0");
405         configuration.addProperty("abuse.cookieOnBlock", getCookieOnBlock() ? "1" : "0");
406         configuration.addProperty("abuse.logEnabled", getLogEnabled() ? "1" : "0");
407         configuration.addProperty("abuse.logSize", Integer.toString(getLogSize()));
408         configuration.addProperty("abuse.articleBlockAction", getArticleBlockAction());
409         configuration.addProperty("abuse.commentBlockAction", getCommentBlockAction());
410
411         configuration.save(new BufferedOutputStream(new FileOutputStream(new File(configFile)),8192), "Anti abuse configuration");
412       }
413       catch (Throwable t) {
414         throw new RuntimeException(t.toString());
415       }
416     }
417   }
418
419   public List getFilterTypes() {
420     try {
421       List result = new Vector();
422
423       Iterator i = filterTypeIds.iterator();
424       while (i.hasNext()) {
425         String id = (String) i.next();
426
427         Map action = new HashMap();
428         action.put("resource", id);
429         action.put("identifier", id);
430
431         result.add(action);
432       }
433
434       return result;
435     }
436     catch (Throwable t) {
437       throw new RuntimeException("can't get article actions");
438     }
439   }
440
441   public List getArticleActions() {
442     try {
443       List result = new Vector();
444
445       Iterator i = MirGlobal.localizer().adminInterface().simpleArticleOperations().iterator();
446       while (i.hasNext()) {
447         MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
448             (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
449
450         Map action = new HashMap();
451         action.put("resource", operation.getName());
452         action.put("identifier", operation.getName());
453
454         result.add(action);
455       }
456
457       return result;
458     }
459     catch (Throwable t) {
460       throw new RuntimeException("can't get article actions");
461     }
462   }
463
464   public List getCommentActions() {
465     try {
466       List result = new Vector();
467
468       Iterator i = MirGlobal.localizer().adminInterface().simpleCommentOperations().iterator();
469       while (i.hasNext()) {
470         MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
471             (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
472
473         Map action = new HashMap();
474         action.put("resource", operation.getName());
475         action.put("identifier", operation.getName());
476
477         result.add(action);
478       }
479
480       return result;
481     }
482     catch (Throwable t) {
483       throw new RuntimeException("can't get comment actions");
484     }
485   }
486
487   public List getFilters() {
488     List result = new Vector();
489
490     synchronized (filterRules) {
491       Iterator i = filterRules.iterator();
492       while (i.hasNext()) {
493         FilterRule filter = (FilterRule) i.next();
494         result.add(filter.clone());
495       }
496       return result;
497     }
498   }
499
500   public String addFilter(String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {
501     return addFilter(aType, anExpression, aComments, aCommentAction, anArticleAction, null);
502   }
503
504   public String addFilter(String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction, Date aListHit) {
505     return addFilter(filterRules, aType, anExpression, aComments, aCommentAction, anArticleAction, aListHit);
506   }
507
508   public FilterRule getFilter(String anId) {
509     synchronized (filterRules) {
510       FilterRule result = (FilterRule) findFilter(filterRules, anId);
511       if (result == null)
512         return result;
513       else
514         return (FilterRule) result.clone();
515     }
516   }
517
518   public String setFilter(String anIdentifier, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {
519     return setFilter(filterRules, anIdentifier, aType, anExpression, aComments, aCommentAction, anArticleAction);
520   }
521
522   public void deleteFilter(String anIdentifier) {
523     deleteFilter(filterRules, anIdentifier);
524   }
525
526   public void moveFilterUp(String anIdentifier) {
527     moveFilter(filterRules, anIdentifier, -1);
528   }
529
530   public void moveFilterDown(String anIdentifier) {
531     moveFilter(filterRules, anIdentifier, 1);
532   }
533
534   private String addFilter(List aFilters, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction, Date aLastHit) {
535     MirAntiAbuseFilterType type = (MirAntiAbuseFilterType) filterTypes.get(aType);
536
537     if (type == null)
538       return "invalidtype";
539
540     if (!type.validate(anExpression)) {
541       return "invalidexpression";
542     }
543
544     FilterRule filter = new FilterRule();
545
546     filter.setId(generateId());
547     filter.setExpression(anExpression);
548     filter.setType(aType);
549     filter.setComments(aComments);
550     filter.setArticleAction(anArticleAction);
551     filter.setCommentAction(aCommentAction);
552     filter.setLastHit(aLastHit);
553
554     synchronized (aFilters) {
555       aFilters.add(filter);
556     }
557
558     return null;
559   }
560
561   private String setFilter(List aFilters, String anIdentifier, String aType, String anExpression, String aComments, String aCommentAction, String anArticleAction) {
562     MirAntiAbuseFilterType type = (MirAntiAbuseFilterType) filterTypes.get(aType);
563
564     if (type == null)
565       return "invalidtype";
566
567     if (!type.validate(anExpression)) {
568       return "invalidexpression";
569     }
570
571     synchronized (aFilters) {
572       FilterRule filter = findFilter(aFilters, anIdentifier);
573
574       if (filter != null) {
575         filter.setExpression(anExpression);
576         filter.setType(aType);
577         filter.setCommentAction(aCommentAction);
578         filter.setArticleAction(anArticleAction);
579         filter.setComments(aComments);
580       }
581
582       return null;
583     }
584   }
585
586   private FilterRule findFilter(List aFilters, String anIdentifier) {
587     synchronized (aFilters) {
588       Iterator i = aFilters.iterator();
589       while (i.hasNext()) {
590         FilterRule filter = (FilterRule) i.next();
591
592         if (filter.getId().equals(anIdentifier)) {
593           return filter;
594         }
595       }
596     }
597
598     return null;
599   }
600
601   private void moveFilter(List aFilters, String anIdentifier, int aDirection) {
602     synchronized (aFilters) {
603       for (int i = 0; i < aFilters.size(); i++) {
604         FilterRule rule = (FilterRule) aFilters.get(i);
605
606         if (rule.getId().equals(anIdentifier) && (i + aDirection >= 0) && (i + aDirection < aFilters.size())) {
607           aFilters.remove(rule);
608           aFilters.add(i + aDirection, rule);
609           break;
610         }
611       }
612     }
613   }
614
615   private void deleteFilter(List aFilters, String anIdentifier) {
616     synchronized (aFilters) {
617       FilterRule filter = findFilter(aFilters, anIdentifier);
618
619       if (filter != null) {
620         aFilters.remove(filter);
621       }
622     }
623   }
624
625   private String generateId() {
626     synchronized (this) {
627       maxIdentifier = maxIdentifier + 1;
628
629       return Integer.toString(maxIdentifier);
630     }
631   }
632
633   public class FilterRule {
634     private String identifier;
635     private String expression;
636     private String type;
637     private String comments;
638     private String articleAction;
639     private String commentAction;
640     private Date lastHit;
641
642     public FilterRule() {
643       expression = "";
644       type = "";
645       identifier = "";
646       comments = "";
647       articleAction = articleBlockAction;
648       commentAction = commentBlockAction;
649       lastHit = null;
650     }
651
652     public Date getLastHit() {
653       return lastHit;
654     }
655
656     public void setLastHit(Date aDate) {
657       lastHit = aDate;
658     }
659
660     public String getId() {
661       return identifier;
662     }
663
664     public void setId(String anId) {
665       identifier = anId;
666     }
667
668     public String getExpression() {
669       return expression;
670     }
671
672     public void setExpression(String anExpression) {
673       expression = anExpression;
674     }
675
676     public String getType() {
677       return type;
678     }
679
680     public void setType(String aType) {
681       type = aType;
682     }
683
684     public void setComments(String aComments) {
685       comments = aComments;
686     }
687
688     public String getComments() {
689       return comments;
690     }
691
692     public String getArticleAction() {
693       return articleAction;
694     }
695
696     public void setArticleAction(String anArticleAction) {
697       articleAction = anArticleAction;
698     }
699
700     public String getCommentAction() {
701       return commentAction;
702     }
703
704     public void setCommentAction(String aCommentAction) {
705       commentAction = aCommentAction;
706     }
707
708     public boolean test(Entity anEntity, Request aRequest) {
709       MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) filterTypes.get(type);
710       try {
711         if (filterType != null)
712           return filterType.test(expression, anEntity, aRequest);
713       }
714       catch (Throwable t) {
715         logger.error("error while testing " + type + "-filter '" + expression + "'");
716       }
717
718       return false;
719     };
720
721     public Object clone() {
722       FilterRule result = new FilterRule();
723       result.setComments(getComments());
724       result.setExpression(getExpression());
725       result.setId(getId());
726       result.setType(getType());
727       result.setArticleAction(getArticleAction());
728       result.setCommentAction(getCommentAction());
729       result.setLastHit(getLastHit());
730
731       return result;
732     }
733   }
734
735   private String escapeFilterPart(String aFilterPart) {
736     return StringRoutines.replaceStringCharacters(aFilterPart,
737         new char[] {'\\', ':', '\n', '\r', '\t', ' '},
738         new String[] {"\\\\", "\\:", "\\n", "\\r", "\\t", "\\ "});
739   }
740
741   private String deescapeFilterPart(String aFilterPart) {
742     return StringRoutines.replaceEscapedStringCharacters(aFilterPart,
743         '\\',
744         new char[] {'\\', ':', 'n', 'r', 't', ' '},
745         new String[] {"\\", ":", "\n", "\r", "\t", " "});
746   }
747
748   private void setFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
749     synchronized (aFilters) {
750       Iterator i = aFilters.iterator();
751
752       while (i.hasNext()) {
753         FilterRule filter = (FilterRule) i.next();
754
755         String filterconfig =
756             escapeFilterPart(filter.getType()) + ":" +
757             escapeFilterPart(filter.getExpression()) + ":" +
758             escapeFilterPart(filter.getArticleAction()) + ":" +
759             escapeFilterPart(filter.getCommentAction()) + ":" +
760             escapeFilterPart(filter.getComments()) + ":";
761
762         if (filter.getLastHit() != null)
763           filterconfig = filterconfig + filter.getLastHit().getTime();
764
765         aConfiguration.addProperty(aConfigKey, filterconfig);
766       }
767     }
768   }
769
770   private void getFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
771     synchronized (aFilters) {
772       aFilters.clear();
773
774       if (aConfiguration.getStringArray(aConfigKey) != null) {
775
776         Iterator i = Arrays.asList(aConfiguration.getStringArray(aConfigKey)).
777             iterator();
778
779         while (i.hasNext()) {
780           String filter = (String) i.next();
781           List parts = StringRoutines.splitStringWithEscape(filter, ':', '\\');
782           if (parts.size() == 2) {
783             parts.add(articleBlockAction);
784             parts.add(commentBlockAction);
785             parts.add("");
786             parts.add("");
787           }
788
789           if (parts.size() >= 5) {
790             Date lastHit = null;
791
792             if (parts.size() >= 6) {
793               String lastHitString = (String) parts.get(5);
794
795               try {
796                 lastHit = new Date(Long.parseLong(lastHitString));
797               }
798               catch (Throwable t) {
799               }
800             }
801
802             addFilter(deescapeFilterPart( (String) parts.get(0)),
803                       deescapeFilterPart( (String) parts.get(1)),
804                       deescapeFilterPart( (String) parts.get(4)),
805                       deescapeFilterPart( (String) parts.get(3)),
806                       deescapeFilterPart( (String) parts.get(2)), lastHit);
807           }
808         }
809       }
810     }
811   }
812
813   private static class LogEntry {
814     private String ipNumber;
815     private String browserString;
816     private String id;
817     private Date timeStamp;
818     private boolean isArticle;
819     private String hitFilterType;
820     private String hitFilterExpression;
821
822     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle, String aHitFilterType, String aHitFilterExpression) {
823       ipNumber = anIpNumber;
824       browserString = aBrowserString;
825       id = anId;
826       isArticle = anIsArticle;
827       timeStamp = aTimeStamp;
828       hitFilterType = aHitFilterType;
829       hitFilterExpression = aHitFilterExpression;
830     }
831
832     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle) {
833       this(aTimeStamp, anIpNumber, aBrowserString, anId, anIsArticle, null, null);
834     }
835
836     public String getIpNumber() {
837       return ipNumber;
838     }
839
840     public String getBrowserString() {
841       return browserString;
842     }
843
844     public String getId() {
845       return id;
846     }
847
848     public String getHitFilterType() {
849       return hitFilterType;
850     }
851
852     public String getHitFilterExpression() {
853       return hitFilterExpression;
854     }
855
856     public Date getTimeStamp() {
857       return timeStamp;
858     }
859
860     public boolean getIsArticle() {
861       return isArticle;
862     }
863   }
864
865   private void truncateLog() {
866     synchronized (log) {
867       if (!logEnabled)
868         log.clear();
869       else {
870         while (log.size() > 0 && log.size() > logSize) {
871           log.remove(log.size()-1);
872         }
873       }
874     }
875   };
876
877   private void appendLog(LogEntry anEntry) {
878     synchronized (log) {
879       if (logEnabled) {
880         log.add(0, anEntry);
881         truncateLog();
882       }
883     }
884   }
885 }