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