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