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