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