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