19859a357eb7bb082959768c5574baa4b9946d89
[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 String configFile = MirGlobal.config().getStringWithHome("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);
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(new File(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 = (FilterRule) 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   private void moveFilter(List aFilters, String anIdentifier, int aDirection) {
631     synchronized (aFilters) {
632       for (int i = 0; i < aFilters.size(); i++) {
633         FilterRule rule = (FilterRule) aFilters.get(i);
634
635         if (rule.getId().equals(anIdentifier) && (i + aDirection >= 0) && (i + aDirection < aFilters.size())) {
636           aFilters.remove(rule);
637           aFilters.add(i + aDirection, rule);
638           break;
639         }
640       }
641     }
642   }
643
644   private void deleteFilter(List aFilters, String anIdentifier) {
645     synchronized (aFilters) {
646       FilterRule filter = findFilter(aFilters, anIdentifier);
647
648       if (filter != null) {
649         aFilters.remove(filter);
650       }
651     }
652   }
653
654   private String generateId() {
655     synchronized (this) {
656       maxIdentifier = maxIdentifier + 1;
657
658       return Integer.toString(maxIdentifier);
659     }
660   }
661
662   public class FilterRule {
663     private String identifier;
664     private String expression;
665     private String type;
666     private String comments;
667     private String articleAction;
668     private String commentAction;
669     private Date lastHit;
670
671     public FilterRule() {
672       expression = "";
673       type = "";
674       identifier = "";
675       comments = "";
676       articleAction = articleBlockAction;
677       commentAction = commentBlockAction;
678       lastHit = null;
679     }
680
681     public Date getLastHit() {
682       return lastHit;
683     }
684
685     public void setLastHit(Date aDate) {
686       lastHit = aDate;
687     }
688
689     public String getId() {
690       return identifier;
691     }
692
693     public void setId(String anId) {
694       identifier = anId;
695     }
696
697     public String getExpression() {
698       return expression;
699     }
700
701     public void setExpression(String anExpression) {
702       expression = anExpression;
703     }
704
705     public String getType() {
706       return type;
707     }
708
709     public void setType(String aType) {
710       type = aType;
711     }
712
713     public void setComments(String aComments) {
714       comments = aComments;
715     }
716
717     public String getComments() {
718       return comments;
719     }
720
721     public String getArticleAction() {
722       return articleAction;
723     }
724
725     public void setArticleAction(String anArticleAction) {
726       articleAction = anArticleAction;
727     }
728
729     public String getCommentAction() {
730       return commentAction;
731     }
732
733     public void setCommentAction(String aCommentAction) {
734       commentAction = aCommentAction;
735     }
736
737     public boolean test(Entity anEntity, Request aRequest) {
738       MirAntiAbuseFilterType filterType = (MirAntiAbuseFilterType) filterTypes.get(type);
739       try {
740         if (filterType != null)
741           return filterType.test(expression, anEntity, aRequest);
742       }
743       catch (Throwable t) {
744         logger.error("error while testing " + type + "-filter '" + expression + "'");
745       }
746
747       return false;
748     };
749
750     public Object clone() {
751       FilterRule result = new FilterRule();
752       result.setComments(getComments());
753       result.setExpression(getExpression());
754       result.setId(getId());
755       result.setType(getType());
756       result.setArticleAction(getArticleAction());
757       result.setCommentAction(getCommentAction());
758       result.setLastHit(getLastHit());
759
760       return result;
761     }
762   }
763
764   private String escapeFilterPart(String aFilterPart) {
765     return StringRoutines.replaceStringCharacters(aFilterPart,
766         new char[] {'\\', ':', '\n', '\r', '\t', ' '},
767         new String[] {"\\\\", "\\:", "\\n", "\\r", "\\t", "\\ "});
768   }
769
770   private String deescapeFilterPart(String aFilterPart) {
771     return StringRoutines.replaceEscapedStringCharacters(aFilterPart,
772         '\\',
773         new char[] {'\\', ':', 'n', 'r', 't', ' '},
774         new String[] {"\\", ":", "\n", "\r", "\t", " "});
775   }
776
777   private void setFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
778     synchronized (aFilters) {
779       Iterator i = aFilters.iterator();
780
781       while (i.hasNext()) {
782         FilterRule filter = (FilterRule) i.next();
783
784         String filterconfig =
785             escapeFilterPart(filter.getType()) + ":" +
786             escapeFilterPart(filter.getExpression()) + ":" +
787             escapeFilterPart(filter.getArticleAction()) + ":" +
788             escapeFilterPart(filter.getCommentAction()) + ":" +
789             escapeFilterPart(filter.getComments()) + ":";
790
791         if (filter.getLastHit() != null)
792           filterconfig = filterconfig + filter.getLastHit().getTime();
793
794         aConfiguration.addProperty(aConfigKey, filterconfig);
795       }
796     }
797   }
798
799   private void getFilterConfig(List aFilters, String aConfigKey, ExtendedProperties aConfiguration) {
800     synchronized (aFilters) {
801       aFilters.clear();
802
803       if (aConfiguration.getStringArray(aConfigKey) != null) {
804
805         Iterator i = Arrays.asList(aConfiguration.getStringArray(aConfigKey)).
806             iterator();
807
808         while (i.hasNext()) {
809           String filter = (String) i.next();
810           List parts = StringRoutines.splitStringWithEscape(filter, ':', '\\');
811           if (parts.size() == 2) {
812             parts.add(articleBlockAction);
813             parts.add(commentBlockAction);
814             parts.add("");
815             parts.add("");
816           }
817
818           if (parts.size() >= 5) {
819             Date lastHit = null;
820
821             if (parts.size() >= 6) {
822               String lastHitString = (String) parts.get(5);
823
824               try {
825                 lastHit = new Date(Long.parseLong(lastHitString));
826               }
827               catch (Throwable t) {
828               }
829             }
830
831             addFilter(deescapeFilterPart( (String) parts.get(0)),
832                       deescapeFilterPart( (String) parts.get(1)),
833                       deescapeFilterPart( (String) parts.get(4)),
834                       deescapeFilterPart( (String) parts.get(3)),
835                       deescapeFilterPart( (String) parts.get(2)), lastHit);
836           }
837         }
838       }
839     }
840   }
841
842   private static class LogEntry {
843     private String ipNumber;
844     private String browserString;
845     private String id;
846     private Date timeStamp;
847     private boolean isArticle;
848     private String hitFilterType;
849     private String hitFilterExpression;
850
851     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle, String aHitFilterType, String aHitFilterExpression) {
852       ipNumber = anIpNumber;
853       browserString = aBrowserString;
854       id = anId;
855       isArticle = anIsArticle;
856       timeStamp = aTimeStamp;
857       hitFilterType = aHitFilterType;
858       hitFilterExpression = aHitFilterExpression;
859     }
860
861     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle) {
862       this(aTimeStamp, anIpNumber, aBrowserString, anId, anIsArticle, null, null);
863     }
864
865     public String getIpNumber() {
866       return ipNumber;
867     }
868
869     public String getBrowserString() {
870       return browserString;
871     }
872
873     public String getId() {
874       return id;
875     }
876
877     public String getHitFilterType() {
878       return hitFilterType;
879     }
880
881     public String getHitFilterExpression() {
882       return hitFilterExpression;
883     }
884
885     public Date getTimeStamp() {
886       return timeStamp;
887     }
888
889     public boolean getIsArticle() {
890       return isArticle;
891     }
892   }
893
894   private void truncateLog() {
895     synchronized (log) {
896       if (!logEnabled)
897         log.clear();
898       else {
899         while (log.size() > 0 && log.size() > logSize) {
900           log.remove(log.size()-1);
901         }
902       }
903     }
904   };
905
906   private void appendLog(LogEntry anEntry) {
907     synchronized (log) {
908       if (logEnabled) {
909         log.add(0, anEntry);
910         truncateLog();
911       }
912     }
913   }
914 }