tiny fixes here and there
[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 mir.config.MirPropertiesConfiguration;
34 import mir.entity.Entity;
35 import mir.entity.adapter.EntityAdapterModel;
36 import mir.log.LoggerWrapper;
37 import mir.session.Request;
38 import mir.util.DateTimeRoutines;
39 import mir.util.EntityUtility;
40 import mir.util.GeneratorFormatAdapters;
41 import mir.util.StringRoutines;
42 import mircoders.abuse.FilterEngine;
43 import mircoders.entity.EntityComment;
44 import mircoders.entity.EntityContent;
45 import mircoders.localizer.MirAdminInterfaceLocalizer;
46 import mircoders.module.ModuleComment;
47 import mircoders.module.ModuleContent;
48 import org.apache.commons.collections.ExtendedProperties;
49
50 import javax.servlet.http.Cookie;
51 import javax.servlet.http.HttpServletResponse;
52 import java.io.BufferedOutputStream;
53 import java.io.File;
54 import java.io.FileNotFoundException;
55 import java.io.FileOutputStream;
56 import java.util.ArrayList;
57 import java.util.Date;
58 import java.util.GregorianCalendar;
59 import java.util.HashMap;
60 import java.util.Iterator;
61 import java.util.List;
62 import java.util.Map;
63 import java.util.Random;
64
65 /**
66  *  This class manages abuse (spam, offending material, etc.). This
67  *  is done by using a set of filters managed by the FilterEngine class.
68  *  Filters may be of different types (IP, throttle, regexp...), 
69  *  but are created and configured in a single user interface (web page),
70  *  and are stored in a single database table called "filter". 
71  */
72 public class Abuse {
73   private LoggerWrapper logger;
74   private int logSize;
75   private boolean logEnabled;
76   private boolean openPostingDisabled;
77   private boolean openPostingPassword;
78   private boolean cookieOnBlock;
79   private String articleBlockAction;
80   private String commentBlockAction;
81   private List log;
82   private File configFile = MirGlobal.config().getFile("Abuse.Config");
83   private FilterEngine filterEngine;
84
85   private MirPropertiesConfiguration configuration;
86
87   private static String cookieName = MirGlobal.config().getString("Abuse.CookieName");
88   private static int cookieMaxAge = 60 * 60 * MirGlobal.config().getInt("Abuse.CookieMaxAge");
89   private EntityAdapterModel model;
90
91   public Abuse(EntityAdapterModel aModel) {
92     logger = new LoggerWrapper("Global.Abuse");
93     filterEngine = new FilterEngine(aModel);
94     model = aModel;
95
96     log = new ArrayList();
97
98     try {
99       configuration = MirPropertiesConfiguration.instance();
100     }
101     catch (Throwable e) {
102       throw new RuntimeException("Can't get configuration: " + e.getMessage());
103     }
104
105     logSize = 100;
106     logEnabled = false;
107     articleBlockAction = "";
108     commentBlockAction = "";
109     openPostingPassword = false;
110     openPostingDisabled = false;
111     cookieOnBlock = false;
112
113     load();
114   }
115
116   public FilterEngine getFilterEngine() {
117     return filterEngine;
118   }
119
120   private void setCookie(HttpServletResponse aResponse) {
121     Random random = new Random();
122
123     Cookie cookie = new Cookie(cookieName, Integer.toString(random.nextInt(1000000000)));
124     cookie.setMaxAge(cookieMaxAge);
125     cookie.setPath("/");
126
127     if (aResponse != null)
128       aResponse.addCookie(cookie);
129   }
130
131   private boolean checkCookie(List aCookies) {
132     if (getCookieOnBlock()) {
133       Iterator i = aCookies.iterator();
134
135       while (i.hasNext()) {
136         Cookie cookie = (Cookie) i.next();
137
138         if (cookie.getName().equals(cookieName)) {
139           logger.debug("cookie match");
140           return true;
141         }
142       }
143     }
144
145     return false;
146   }
147   /** Checks if there is a filter that matches a comment and takes 
148    * appropriate action (as configured in the xxxxxaction field of 
149    * the filter table). The actual matching is delegated to the 
150    * FilterEngine class. 
151    */
152   public void checkComment(EntityComment aComment, Request aRequest, HttpServletResponse aResponse) {
153     try {
154       long time = System.currentTimeMillis();
155
156       FilterEngine.Filter matchingFilter = filterEngine.testPosting(aComment, aRequest);
157
158       if (matchingFilter != null) {
159         logger.debug("Match for " + matchingFilter.getTag());
160         matchingFilter.updateLastHit(new GregorianCalendar().getTime());
161
162         StringBuffer line = new StringBuffer();
163
164         line.append(DateTimeRoutines.advancedDateFormat(
165             configuration.getString("Mir.DefaultDateTimeFormat"),
166             (new GregorianCalendar()).getTime(), configuration.getString("Mir.DefaultTimezone")));
167
168         line.append(" ");
169         line.append(matchingFilter.getTag());
170         EntityUtility.appendLineToField(aComment, "comment", line.toString());
171
172         MirGlobal.performCommentOperation(null, aComment, matchingFilter.getCommentAction());
173         setCookie(aResponse);
174         save();
175         logComment(aComment, aRequest, matchingFilter.getTag());
176       }
177       else {
178         logComment(aComment, aRequest);
179       }
180
181       logger.debug("checkComment: " + (System.currentTimeMillis() - time) + "ms");
182     }
183     catch (Throwable t) {
184       logger.error("Exception thrown while checking comment", t);
185     }
186   }
187   /** Checks if there is a filter that matches an articleand takes 
188    * appropriate action (as configured in the xxxxxaction field of 
189    * the filter table). The actual matching is delegated to the 
190    * FilterEngine class. 
191    */
192   public void checkArticle(EntityContent anArticle, Request aRequest, HttpServletResponse aResponse) {
193     try {
194       long time = System.currentTimeMillis();
195
196       FilterEngine.Filter matchingFilter = filterEngine.testPosting(anArticle, aRequest);
197
198       if (matchingFilter != null) {
199         logger.debug("Match for " + matchingFilter.getTag());
200 //        matchingFilter.updateLastHit(new GregorianCalendar().getTime());
201
202         StringBuffer line = new StringBuffer();
203
204         line.append(DateTimeRoutines.advancedDateFormat(
205             configuration.getString("Mir.DefaultDateTimeFormat"),
206             (new GregorianCalendar()).getTime(), configuration.getString("Mir.DefaultTimezone")));
207
208         line.append(" ");
209         line.append(matchingFilter.getTag());
210         EntityUtility.appendLineToField(anArticle, "comment", line.toString());
211
212         MirGlobal.performArticleOperation(null, anArticle, matchingFilter.getArticleAction());
213         setCookie(aResponse);
214         save();
215         logArticle(anArticle, aRequest, matchingFilter.getTag());
216       }
217       else {
218         logArticle(anArticle, aRequest);
219       }
220
221       logger.info("checkArticle: " + (System.currentTimeMillis() - time) + "ms");
222     }
223     catch (Throwable t) {
224       logger.error("Exception thrown while checking article", t);
225     }
226   }
227
228   public boolean getLogEnabled() {
229     return logEnabled;
230   }
231
232   public void setLogEnabled(boolean anEnabled) {
233     if (!configuration.getString("Abuse.DisallowIPLogging", "0").equals("1"))
234       logEnabled = anEnabled;
235     truncateLog();
236   }
237
238   public int getLogSize() {
239     return logSize;
240   }
241
242   public void setLogSize(int aSize) {
243     logSize = aSize;
244     truncateLog();
245   }
246
247   public boolean getOpenPostingDisabled() {
248     return openPostingDisabled;
249   }
250
251   public void setOpenPostingDisabled(boolean anOpenPostingDisabled) {
252     openPostingDisabled = anOpenPostingDisabled;
253   }
254
255   public boolean getOpenPostingPassword() {
256     return openPostingPassword;
257   }
258
259   public void setOpenPostingPassword(boolean anOpenPostingPassword) {
260     openPostingPassword = anOpenPostingPassword;
261   }
262
263   public boolean getCookieOnBlock() {
264     return cookieOnBlock;
265   }
266
267   public void setCookieOnBlock(boolean aCookieOnBlock) {
268     cookieOnBlock = aCookieOnBlock;
269   }
270
271   public String getArticleBlockAction() {
272     return articleBlockAction;
273   }
274
275   public void setArticleBlockAction(String anAction) {
276     articleBlockAction = anAction;
277   }
278
279   public String getCommentBlockAction() {
280     return commentBlockAction;
281   }
282
283   public void setCommentBlockAction(String anAction) {
284     commentBlockAction = anAction;
285   }
286
287   public List getLog() {
288     ModuleContent contentModule = new ModuleContent();
289     ModuleComment commentModule = new ModuleComment();
290
291     synchronized (log) {
292       try {
293         List result = new ArrayList();
294
295         Iterator i = log.iterator();
296         while (i.hasNext()) {
297           LogEntry logEntry = (LogEntry) i.next();
298           Map entry = new HashMap();
299
300           entry.put("ip", logEntry.getIpNumber());
301           entry.put("id", logEntry.getId());
302           entry.put("timestamp", new GeneratorFormatAdapters.DateFormatAdapter(logEntry.getTimeStamp(), MirPropertiesConfiguration.instance().getString("Mir.DefaultTimezone")));
303
304           if (logEntry.getIsArticle()) {
305             entry.put("type", "content");
306             entry.put("object",
307                 model.makeEntityAdapter("content", contentModule.getById(logEntry.getId())));
308           }
309           else {
310             entry.put("type", "comment");
311             entry.put("object",
312                 model.makeEntityAdapter("comment", commentModule.getById(logEntry.getId())));
313           }
314
315           entry.put("browser", logEntry.getBrowserString());
316           entry.put("filtertag", logEntry.getMatchingFilterTag());
317
318           result.add(entry);
319         }
320
321         return result;
322       }
323       catch (Throwable t) {
324         throw new RuntimeException(t.toString());
325       }
326     }
327   }
328
329   public void logComment(Entity aComment, Request aRequest) {
330     logComment(aComment, aRequest, null);
331   }
332
333   public void logComment(Entity aComment, Request aRequest, String aMatchingFilterTag) {
334     String ipAddress = aRequest.getHeader("ip");
335     String id = aComment.getId();
336     String browser = aRequest.getHeader("User-Agent");
337
338     logComment(ipAddress, id, new Date(), browser, aMatchingFilterTag);
339   }
340
341   public void logArticle(Entity anArticle, Request aRequest) {
342     logArticle(anArticle, aRequest, null);
343   }
344
345   public void logArticle(Entity anArticle, Request aRequest, String aMatchingFilterTag) {
346     String ipAddress = aRequest.getHeader("ip");
347     String id = anArticle.getId();
348     String browser = aRequest.getHeader("User-Agent");
349
350     logArticle(ipAddress, id, new Date(), browser, aMatchingFilterTag);
351   }
352
353   public void logComment(String anIp, String anId, Date aTimeStamp, String aBrowser, String aMatchingFilterTag) {
354     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, false, aMatchingFilterTag));
355   }
356
357   public void logArticle(String anIp, String anId, Date aTimeStamp, String aBrowser, String aMatchingFilterTag) {
358     appendLog(new LogEntry(aTimeStamp, anIp, aBrowser, anId, true, aMatchingFilterTag));
359   }
360
361   public synchronized void load() {
362     try {
363       ExtendedProperties configuration = new ExtendedProperties();
364
365       try {
366         configuration = new ExtendedProperties(configFile.getAbsolutePath());
367       }
368       catch (FileNotFoundException e) {
369       }
370
371       setOpenPostingDisabled(configuration.getString("abuse.openPostingDisabled", "0").equals("1"));
372       setOpenPostingPassword(configuration.getString("abuse.openPostingPassword", "0").equals("1"));
373       setCookieOnBlock(configuration.getString("abuse.cookieOnBlock", "0").equals("1"));
374       setLogEnabled(configuration.getString("abuse.logEnabled", "0").equals("1"));
375       setLogSize(configuration.getInt("abuse.logSize", 10));
376       setArticleBlockAction(configuration.getString("abuse.articleBlockAction", ""));
377       setCommentBlockAction(configuration.getString("abuse.commentBlockAction", ""));
378     }
379     catch (Throwable t) {
380       throw new RuntimeException(t.toString());
381     }
382   }
383
384   public synchronized void save() {
385     try {
386       ExtendedProperties configuration = new ExtendedProperties();
387
388       configuration.addProperty("abuse.openPostingDisabled", getOpenPostingDisabled() ? "1" : "0");
389       configuration.addProperty("abuse.openPostingPassword", getOpenPostingPassword() ? "1" : "0");
390       configuration.addProperty("abuse.cookieOnBlock", getCookieOnBlock() ? "1" : "0");
391       configuration.addProperty("abuse.logEnabled", getLogEnabled() ? "1" : "0");
392       configuration.addProperty("abuse.logSize", Integer.toString(getLogSize()));
393       configuration.addProperty("abuse.articleBlockAction", getArticleBlockAction());
394       configuration.addProperty("abuse.commentBlockAction", getCommentBlockAction());
395
396       configuration.save(new BufferedOutputStream(new FileOutputStream(configFile),8192), "Anti abuse configuration");
397     }
398     catch (Throwable t) {
399       throw new RuntimeException(t.toString());
400     }
401   }
402
403   public List getArticleActions() {
404     try {
405       List result = new ArrayList();
406
407       Iterator i = MirGlobal.localizer().adminInterface().simpleArticleOperations().iterator();
408       while (i.hasNext()) {
409         MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
410             (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
411
412         Map action = new HashMap();
413         action.put("resource", operation.getName());
414         action.put("identifier", operation.getName());
415
416         result.add(action);
417       }
418
419       return result;
420     }
421     catch (Throwable t) {
422       throw new RuntimeException("can't get article actions");
423     }
424   }
425
426   public List getCommentActions() {
427     try {
428       List result = new ArrayList();
429
430       Iterator i = MirGlobal.localizer().adminInterface().simpleCommentOperations().iterator();
431       while (i.hasNext()) {
432         MirAdminInterfaceLocalizer.MirSimpleEntityOperation operation =
433             (MirAdminInterfaceLocalizer.MirSimpleEntityOperation) i.next();
434
435         Map action = new HashMap();
436         action.put("resource", operation.getName());
437         action.put("identifier", operation.getName());
438
439         result.add(action);
440       }
441
442       return result;
443     }
444     catch (Throwable t) {
445       throw new RuntimeException("can't get comment actions");
446     }
447   }
448
449   private String escapeConfigListEntry(String aFilterPart) {
450     return StringRoutines.replaceStringCharacters(aFilterPart,
451         new char[] {'\\', ':'},
452         new String[] {"\\\\", "\\:"});
453   }
454
455   private String escapeFilterPart(String aFilterPart) {
456     return StringRoutines.replaceStringCharacters(aFilterPart,
457         new char[] {'\\', '\n', '\r', '\t', ' '},
458         new String[] {"\\\\", "\\n", "\\r", "\\t", "\\ "});
459   }
460
461   private String deescapeFilterPart(String aFilterPart) {
462     return StringRoutines.replaceEscapedStringCharacters(aFilterPart,
463         '\\',
464         new char[] {'\\', ':', 'n', 'r', 't', ' '},
465         new String[] {"\\", ":", "\n", "\r", "\t", " "});
466   }
467
468   private static class LogEntry {
469     private String ipNumber;
470     private String browserString;
471     private String id;
472     private Date timeStamp;
473     private boolean isArticle;
474     private String matchingFilterTag;
475
476     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle, String aMatchingFilterTag) {
477       ipNumber = anIpNumber;
478       browserString = aBrowserString;
479       id = anId;
480       isArticle = anIsArticle;
481       timeStamp = aTimeStamp;
482       matchingFilterTag = aMatchingFilterTag;
483     }
484
485     public LogEntry(Date aTimeStamp, String anIpNumber, String aBrowserString, String anId, boolean anIsArticle) {
486       this(aTimeStamp, anIpNumber, aBrowserString, anId, anIsArticle, null);
487     }
488
489     public String getIpNumber() {
490       return ipNumber;
491     }
492
493     public String getBrowserString() {
494       return browserString;
495     }
496
497     public String getId() {
498       return id;
499     }
500
501     public String getMatchingFilterTag() {
502       return matchingFilterTag;
503     }
504
505     public Date getTimeStamp() {
506       return timeStamp;
507     }
508
509     public boolean getIsArticle() {
510       return isArticle;
511     }
512   }
513
514   private void truncateLog() {
515     synchronized (log) {
516       if (!logEnabled)
517         log.clear();
518       else {
519         while (log.size() > 0 && log.size() > logSize) {
520           log.remove(log.size()-1);
521         }
522       }
523     }
524   }
525
526   private void appendLog(LogEntry anEntry) {
527     synchronized (log) {
528       if (logEnabled) {
529         log.add(0, anEntry);
530         truncateLog();
531       }
532     }
533   }
534 }