]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_filter.cpp
Update copyright headers.
[user/henk/code/inspircd.git] / src / modules / m_filter.cpp
1 /*
2  * InspIRCd -- Internet Relay Chat Daemon
3  *
4  *   Copyright (C) 2019 Matt Schatz <genius3000@g3k.solutions>
5  *   Copyright (C) 2019 Filippo Cortigiani <simos@simosnap.org>
6  *   Copyright (C) 2018-2019 linuxdaemon <linuxdaemon.irc@gmail.com>
7  *   Copyright (C) 2018 Michael Hazell <michaelhazell@hotmail.com>
8  *   Copyright (C) 2017 B00mX0r <b00mx0r@aureus.pw>
9  *   Copyright (C) 2012-2014, 2016 Attila Molnar <attilamolnar@hush.com>
10  *   Copyright (C) 2012-2013, 2017-2021 Sadie Powell <sadie@witchery.services>
11  *   Copyright (C) 2012, 2018-2019 Robby <robby@chatbelgie.be>
12  *   Copyright (C) 2011 Adam <Adam@anope.org>
13  *   Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
14  *   Copyright (C) 2009 Robin Burchell <robin+git@viroteck.net>
15  *   Copyright (C) 2009 Matt Smith <dz@inspircd.org>
16  *   Copyright (C) 2009 John Brooks <special@inspircd.org>
17  *   Copyright (C) 2007 Dennis Friis <peavey@inspircd.org>
18  *   Copyright (C) 2006-2010 Craig Edwards <brain@inspircd.org>
19  *
20  * This file is part of InspIRCd.  InspIRCd is free software: you can
21  * redistribute it and/or modify it under the terms of the GNU General Public
22  * License as published by the Free Software Foundation, version 2.
23  *
24  * This program is distributed in the hope that it will be useful, but WITHOUT
25  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
26  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
27  * details.
28  *
29  * You should have received a copy of the GNU General Public License
30  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
31  */
32
33
34 #include "inspircd.h"
35 #include "xline.h"
36 #include "modules/regex.h"
37 #include "modules/server.h"
38 #include "modules/shun.h"
39 #include "modules/stats.h"
40 #include "modules/account.h"
41
42 #include <fstream>
43
44 enum FilterFlags
45 {
46         FLAG_PART = 2,
47         FLAG_QUIT = 4,
48         FLAG_PRIVMSG = 8,
49         FLAG_NOTICE = 16
50 };
51
52 enum FilterAction
53 {
54         FA_GLINE,
55         FA_ZLINE,
56         FA_WARN,
57         FA_BLOCK,
58         FA_SILENT,
59         FA_KILL,
60         FA_SHUN,
61         FA_NONE
62 };
63
64 class FilterResult
65 {
66  public:
67         Regex* regex;
68         std::string freeform;
69         std::string reason;
70         FilterAction action;
71         unsigned long duration;
72         bool from_config;
73
74         bool flag_no_opers;
75         bool flag_part_message;
76         bool flag_quit_message;
77         bool flag_privmsg;
78         bool flag_notice;
79         bool flag_strip_color;
80         bool flag_no_registered;
81
82         FilterResult(dynamic_reference<RegexFactory>& RegexEngine, const std::string& free, const std::string& rea, FilterAction act, unsigned long gt, const std::string& fla, bool cfg)
83                 : freeform(free)
84                 , reason(rea)
85                 , action(act)
86                 , duration(gt)
87                 , from_config(cfg)
88         {
89                 if (!RegexEngine)
90                         throw ModuleException("Regex module implementing '"+RegexEngine.GetProvider()+"' is not loaded!");
91                 regex = RegexEngine->Create(free);
92                 this->FillFlags(fla);
93         }
94
95         char FillFlags(const std::string &fl)
96         {
97                 flag_no_opers = flag_part_message = flag_quit_message = flag_privmsg =
98                         flag_notice = flag_strip_color = flag_no_registered = false;
99
100                 for (std::string::const_iterator n = fl.begin(); n != fl.end(); ++n)
101                 {
102                         switch (*n)
103                         {
104                                 case 'o':
105                                         flag_no_opers = true;
106                                 break;
107                                 case 'P':
108                                         flag_part_message = true;
109                                 break;
110                                 case 'q':
111                                         flag_quit_message = true;
112                                 break;
113                                 case 'p':
114                                         flag_privmsg = true;
115                                 break;
116                                 case 'n':
117                                         flag_notice = true;
118                                 break;
119                                 case 'c':
120                                         flag_strip_color = true;
121                                 break;
122                                 case 'r':
123                                         flag_no_registered = true;
124                                 break;
125                                 case '*':
126                                         flag_no_opers = flag_part_message = flag_quit_message =
127                                                 flag_privmsg = flag_notice = flag_strip_color = true;
128                                 break;
129                                 default:
130                                         return *n;
131                                 break;
132                         }
133                 }
134                 return 0;
135         }
136
137         std::string GetFlags() const
138         {
139                 std::string flags;
140                 if (flag_no_opers)
141                         flags.push_back('o');
142                 if (flag_part_message)
143                         flags.push_back('P');
144                 if (flag_quit_message)
145                         flags.push_back('q');
146                 if (flag_privmsg)
147                         flags.push_back('p');
148                 if (flag_notice)
149                         flags.push_back('n');
150
151                 /* Order is important here, as the logic in FillFlags() stops parsing when it encounters
152                  * an unknown character. So the following characters must be last in the string.
153                  * 'c' is unsupported on < 2.0.10
154                  * 'r' is unsupported on < 3.2.0
155                  */
156                 if (flag_strip_color)
157                         flags.push_back('c');
158                 if (flag_no_registered)
159                         flags.push_back('r');
160
161                 if (flags.empty())
162                         flags.push_back('-');
163
164                 return flags;
165         }
166
167         FilterResult()
168         {
169         }
170 };
171
172 class CommandFilter : public Command
173 {
174  public:
175         CommandFilter(Module* f)
176                 : Command(f, "FILTER", 1, 5)
177         {
178                 flags_needed = 'o';
179                 this->syntax = "<pattern> [<action> <flags> [<duration>] :<reason>]";
180         }
181         CmdResult Handle(User* user, const Params& parameters) CXX11_OVERRIDE;
182
183         RouteDescriptor GetRouting(User* user, const Params& parameters) CXX11_OVERRIDE
184         {
185                 return ROUTE_BROADCAST;
186         }
187 };
188
189 class ModuleFilter
190         : public Module
191         , public ServerProtocol::SyncEventListener
192         , public Stats::EventListener
193         , public Timer
194 {
195         typedef insp::flat_set<std::string, irc::insensitive_swo> ExemptTargetSet;
196
197         bool initing;
198         bool notifyuser;
199         bool warnonselfmsg;
200         bool dirty;
201         std::string filterconf;
202         RegexFactory* factory;
203         void FreeFilters();
204
205  public:
206         CommandFilter filtcommand;
207         dynamic_reference<RegexFactory> RegexEngine;
208
209         std::vector<FilterResult> filters;
210         int flags;
211
212         // List of channel names excluded from filtering.
213         ExemptTargetSet exemptedchans;
214
215         // List of target nicknames excluded from filtering.
216         ExemptTargetSet exemptednicks;
217
218         ModuleFilter();
219         void init() CXX11_OVERRIDE;
220         CullResult cull() CXX11_OVERRIDE;
221         ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE;
222         FilterResult* FilterMatch(User* user, const std::string &text, int flags);
223         bool DeleteFilter(const std::string& freeform, std::string& reason);
224         std::pair<bool, std::string> AddFilter(const std::string& freeform, FilterAction type, const std::string& reason, unsigned long duration, const std::string& flags, bool config = false);
225         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE;
226         Version GetVersion() CXX11_OVERRIDE;
227         std::string EncodeFilter(FilterResult* filter);
228         FilterResult DecodeFilter(const std::string &data);
229         void OnSyncNetwork(ProtocolInterface::Server& server) CXX11_OVERRIDE;
230         void OnDecodeMetaData(Extensible* target, const std::string &extname, const std::string &extdata) CXX11_OVERRIDE;
231         ModResult OnStats(Stats::Context& stats) CXX11_OVERRIDE;
232         ModResult OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated) CXX11_OVERRIDE;
233         void OnUnloadModule(Module* mod) CXX11_OVERRIDE;
234         bool Tick(time_t) CXX11_OVERRIDE;
235         bool AppliesToMe(User* user, FilterResult* filter, int flags);
236         void ReadFilters();
237         static bool StringToFilterAction(const std::string& str, FilterAction& fa);
238         static std::string FilterActionToString(FilterAction fa);
239 };
240
241 CmdResult CommandFilter::Handle(User* user, const Params& parameters)
242 {
243         if (parameters.size() == 1)
244         {
245                 /* Deleting a filter */
246                 Module* me = creator;
247                 std::string reason;
248
249                 if (static_cast<ModuleFilter*>(me)->DeleteFilter(parameters[0], reason))
250                 {
251                         user->WriteNotice("*** Removed filter '" + parameters[0] + "': " + reason);
252                         ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F', "%s removed filter '%s': %s",
253                                 user->nick.c_str(), parameters[0].c_str(), reason.c_str());
254                         return CMD_SUCCESS;
255                 }
256                 else
257                 {
258                         user->WriteNotice("*** Filter '" + parameters[0] + "' not found on the list.");
259                         return CMD_FAILURE;
260                 }
261         }
262         else
263         {
264                 /* Adding a filter */
265                 if (parameters.size() >= 4)
266                 {
267                         const std::string& freeform = parameters[0];
268                         FilterAction type;
269                         const std::string& flags = parameters[2];
270                         unsigned int reasonindex;
271                         unsigned long duration = 0;
272
273                         if (!ModuleFilter::StringToFilterAction(parameters[1], type))
274                         {
275                                 if (ServerInstance->XLines->GetFactory("SHUN"))
276                                         user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', 'kill', and 'shun'.");
277                                 else
278                                         user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', and 'kill'.");
279                                 return CMD_FAILURE;
280                         }
281
282                         if (type == FA_GLINE || type == FA_ZLINE || type == FA_SHUN)
283                         {
284                                 if (parameters.size() >= 5)
285                                 {
286                                         if (!InspIRCd::Duration(parameters[3], duration))
287                                         {
288                                                 user->WriteNotice("*** Invalid duration for filter");
289                                                 return CMD_FAILURE;
290                                         }
291                                         reasonindex = 4;
292                                 }
293                                 else
294                                 {
295                                         user->WriteNotice("*** Not enough parameters: When setting a '" + parameters[1] + "' type filter, a duration must be specified as the third parameter.");
296                                         return CMD_FAILURE;
297                                 }
298                         }
299                         else
300                         {
301                                 reasonindex = 3;
302                         }
303
304                         Module* me = creator;
305                         std::pair<bool, std::string> result = static_cast<ModuleFilter*>(me)->AddFilter(freeform, type, parameters[reasonindex], duration, flags);
306                         if (result.first)
307                         {
308                                 const std::string message = InspIRCd::Format("'%s', type '%s'%s, flags '%s', reason: %s",
309                                         freeform.c_str(), parameters[1].c_str(),
310                                         (duration ? InspIRCd::Format(", duration '%s'",
311                                                 InspIRCd::DurationString(duration).c_str()).c_str()
312                                         : ""), flags.c_str(), parameters[reasonindex].c_str());
313
314                                 user->WriteNotice("*** Added filter " + message);
315                                 ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F',
316                                         "%s added filter %s", user->nick.c_str(), message.c_str());
317
318                                 return CMD_SUCCESS;
319                         }
320                         else
321                         {
322                                 user->WriteNotice("*** Filter '" + freeform + "' could not be added: " + result.second);
323                                 return CMD_FAILURE;
324                         }
325                 }
326                 else
327                 {
328                         user->WriteNotice("*** Not enough parameters.");
329                         return CMD_FAILURE;
330                 }
331
332         }
333 }
334
335 bool ModuleFilter::AppliesToMe(User* user, FilterResult* filter, int iflags)
336 {
337         const AccountExtItem* accountext = GetAccountExtItem();
338
339         if ((filter->flag_no_opers) && user->IsOper())
340                 return false;
341         if ((filter->flag_no_registered) && accountext && accountext->get(user))
342                 return false;
343         if ((iflags & FLAG_PRIVMSG) && (!filter->flag_privmsg))
344                 return false;
345         if ((iflags & FLAG_NOTICE) && (!filter->flag_notice))
346                 return false;
347         if ((iflags & FLAG_QUIT)   && (!filter->flag_quit_message))
348                 return false;
349         if ((iflags & FLAG_PART)   && (!filter->flag_part_message))
350                 return false;
351         return true;
352 }
353
354 ModuleFilter::ModuleFilter()
355         : ServerProtocol::SyncEventListener(this)
356         , Stats::EventListener(this)
357         , Timer(0, true)
358         , initing(true)
359         , dirty(false)
360         , filtcommand(this)
361         , RegexEngine(this, "regex")
362 {
363 }
364
365 void ModuleFilter::init()
366 {
367         ServerInstance->SNO->EnableSnomask('f', "FILTER");
368 }
369
370 CullResult ModuleFilter::cull()
371 {
372         FreeFilters();
373         return Module::cull();
374 }
375
376 void ModuleFilter::FreeFilters()
377 {
378         for (std::vector<FilterResult>::const_iterator i = filters.begin(); i != filters.end(); ++i)
379                 delete i->regex;
380
381         filters.clear();
382         dirty = true;
383 }
384
385 ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtarget, MessageDetails& details)
386 {
387         // Leave remote users and servers alone
388         if (!IS_LOCAL(user))
389                 return MOD_RES_PASSTHRU;
390
391         flags = (details.type == MSG_PRIVMSG) ? FLAG_PRIVMSG : FLAG_NOTICE;
392
393         FilterResult* f = this->FilterMatch(user, details.text, flags);
394         if (f)
395         {
396                 bool is_selfmsg = false;
397                 switch (msgtarget.type)
398                 {
399                         case MessageTarget::TYPE_USER:
400                         {
401                                 User* t = msgtarget.Get<User>();
402                                 // Check if the target nick is exempted, if yes, ignore this message
403                                 if (exemptednicks.count(t->nick))
404                                         return MOD_RES_PASSTHRU;
405
406                                 if (user == t)
407                                         is_selfmsg = true;
408                                 break;
409                         }
410                         case MessageTarget::TYPE_CHANNEL:
411                         {
412                                 Channel* t = msgtarget.Get<Channel>();
413                                 if (exemptedchans.count(t->name))
414                                         return MOD_RES_PASSTHRU;
415                                 break;
416                         }
417                         case MessageTarget::TYPE_SERVER:
418                                 return MOD_RES_PASSTHRU;
419                 }
420
421                 if (is_selfmsg && warnonselfmsg)
422                 {
423                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("WARNING: %s's self message matched %s (%s)",
424                                 user->nick.c_str(), f->freeform.c_str(), f->reason.c_str()));
425                         return MOD_RES_PASSTHRU;
426                 }
427                 else if (f->action == FA_WARN)
428                 {
429                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("WARNING: %s's message to %s matched %s (%s)",
430                                 user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
431                         return MOD_RES_PASSTHRU;
432                 }
433                 else if (f->action == FA_BLOCK)
434                 {
435                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s had their message to %s filtered as it matched %s (%s)",
436                                 user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
437                         if (notifyuser)
438                         {
439                                 if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
440                                         user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<Channel>(), InspIRCd::Format("Your message to this channel was blocked: %s.", f->reason.c_str())));
441                                 else
442                                         user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
443                         }
444                         else
445                                 details.echo_original = true;
446                 }
447                 else if (f->action == FA_SILENT)
448                 {
449                         if (notifyuser)
450                         {
451                                 if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
452                                         user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<Channel>(), InspIRCd::Format("Your message to this channel was blocked: %s.", f->reason.c_str())));
453                                 else
454                                         user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
455                         }
456                         else
457                                 details.echo_original = true;
458                 }
459                 else if (f->action == FA_KILL)
460                 {
461                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was killed because their message to %s matched %s (%s)",
462                                 user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
463                         ServerInstance->Users->QuitUser(user, "Filtered: " + f->reason);
464                 }
465                 else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
466                 {
467                         Shun* sh = new Shun(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
468                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was shunned for %s (expires on %s) because their message to %s matched %s (%s)",
469                                 user->nick.c_str(), sh->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
470                                 InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
471                                 msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
472                         if (ServerInstance->XLines->AddLine(sh, NULL))
473                         {
474                                 ServerInstance->XLines->ApplyLines();
475                         }
476                         else
477                                 delete sh;
478                 }
479                 else if (f->action == FA_GLINE)
480                 {
481                         GLine* gl = new GLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), "*", user->GetIPString());
482                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was G-lined for %s (expires on %s) because their message to %s matched %s (%s)",
483                                 user->nick.c_str(), gl->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
484                                 InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
485                                 msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
486                         if (ServerInstance->XLines->AddLine(gl,NULL))
487                         {
488                                 ServerInstance->XLines->ApplyLines();
489                         }
490                         else
491                                 delete gl;
492                 }
493                 else if (f->action == FA_ZLINE)
494                 {
495                         ZLine* zl = new ZLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
496                         ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was Z-lined for %s (expires on %s) because their message to %s matched %s (%s)",
497                                 user->nick.c_str(), zl->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
498                                 InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
499                                 msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
500                         if (ServerInstance->XLines->AddLine(zl,NULL))
501                         {
502                                 ServerInstance->XLines->ApplyLines();
503                         }
504                         else
505                                 delete zl;
506                 }
507
508                 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, user->nick + " had their message filtered, target was " + msgtarget.GetName() + ": " + f->reason + " Action: " + ModuleFilter::FilterActionToString(f->action));
509                 return MOD_RES_DENY;
510         }
511         return MOD_RES_PASSTHRU;
512 }
513
514 ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated)
515 {
516         if (validated)
517         {
518                 flags = 0;
519                 bool parting;
520
521                 if (command == "QUIT")
522                 {
523                         /* QUIT with no reason: nothing to do */
524                         if (parameters.size() < 1)
525                                 return MOD_RES_PASSTHRU;
526
527                         parting = false;
528                         flags = FLAG_QUIT;
529                 }
530                 else if (command == "PART")
531                 {
532                         /* PART with no reason: nothing to do */
533                         if (parameters.size() < 2)
534                                 return MOD_RES_PASSTHRU;
535
536                         if (exemptedchans.count(parameters[0]))
537                                 return MOD_RES_PASSTHRU;
538
539                         parting = true;
540                         flags = FLAG_PART;
541                 }
542                 else
543                         /* We're only messing with PART and QUIT */
544                         return MOD_RES_PASSTHRU;
545
546                 FilterResult* f = this->FilterMatch(user, parameters[parting ? 1 : 0], flags);
547                 if (!f)
548                         /* PART or QUIT reason doesnt match a filter */
549                         return MOD_RES_PASSTHRU;
550
551                 /* We cant block a part or quit, so instead we change the reason to 'Reason filtered' */
552                 parameters[parting ? 1 : 0] = "Reason filtered";
553
554                 /* We're warning or blocking, OR they're quitting and its a KILL action
555                  * (we cant kill someone whos already quitting, so filter them anyway)
556                  */
557                 if ((f->action == FA_WARN) || (f->action == FA_BLOCK) || (((!parting) && (f->action == FA_KILL))) || (f->action == FA_SILENT))
558                 {
559                         return MOD_RES_PASSTHRU;
560                 }
561                 else
562                 {
563                         /* Are they parting, if so, kill is applicable */
564                         if ((parting) && (f->action == FA_KILL))
565                         {
566                                 user->WriteNotice("*** Your PART message was filtered: " + f->reason);
567                                 ServerInstance->Users->QuitUser(user, "Filtered: " + f->reason);
568                         }
569                         if (f->action == FA_GLINE)
570                         {
571                                 /* Note: We G-line *@IP so that if their host doesn't resolve the G-line still applies. */
572                                 GLine* gl = new GLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), "*", user->GetIPString());
573                                 ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was G-lined for %s (expires on %s) because their %s message matched %s (%s)",
574                                         user->nick.c_str(), gl->Displayable().c_str(),
575                                         InspIRCd::DurationString(f->duration).c_str(),
576                                         InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
577                                         command.c_str(), f->freeform.c_str(), f->reason.c_str()));
578
579                                 if (ServerInstance->XLines->AddLine(gl,NULL))
580                                 {
581                                         ServerInstance->XLines->ApplyLines();
582                                 }
583                                 else
584                                         delete gl;
585                         }
586                         if (f->action == FA_ZLINE)
587                         {
588                                 ZLine* zl = new ZLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
589                                 ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was Z-lined for %s (expires on %s) because their %s message matched %s (%s)",
590                                         user->nick.c_str(), zl->Displayable().c_str(),
591                                         InspIRCd::DurationString(f->duration).c_str(),
592                                         InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
593                                         command.c_str(), f->freeform.c_str(), f->reason.c_str()));
594
595                                 if (ServerInstance->XLines->AddLine(zl,NULL))
596                                 {
597                                         ServerInstance->XLines->ApplyLines();
598                                 }
599                                 else
600                                         delete zl;
601                         }
602                         else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
603                         {
604                                 /* Note: We shun *!*@IP so that if their host doesnt resolve the shun still applies. */
605                                 Shun* sh = new Shun(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
606                                 ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was shunned for %s (expires on %s) because their %s message matched %s (%s)",
607                                         user->nick.c_str(), sh->Displayable().c_str(),
608                                         InspIRCd::DurationString(f->duration).c_str(),
609                                         InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
610                                         command.c_str(), f->freeform.c_str(), f->reason.c_str()));
611
612                                 if (ServerInstance->XLines->AddLine(sh, NULL))
613                                 {
614                                         ServerInstance->XLines->ApplyLines();
615                                 }
616                                 else
617                                         delete sh;
618                         }
619                         return MOD_RES_DENY;
620                 }
621         }
622         return MOD_RES_PASSTHRU;
623 }
624
625 void ModuleFilter::ReadConfig(ConfigStatus& status)
626 {
627         ConfigTagList tags = ServerInstance->Config->ConfTags("exemptfromfilter");
628         exemptedchans.clear();
629         exemptednicks.clear();
630
631         for (ConfigIter i = tags.first; i != tags.second; ++i)
632         {
633                 ConfigTag* tag = i->second;
634
635                 // If "target" is not found, try the old "channel" key to keep compatibility with 2.0 configs
636                 const std::string target = tag->getString("target", tag->getString("channel"), 1);
637                 if (!target.empty())
638                 {
639                         if (target[0] == '#')
640                                 exemptedchans.insert(target);
641                         else
642                                 exemptednicks.insert(target);
643                 }
644         }
645
646         ConfigTag* tag = ServerInstance->Config->ConfValue("filteropts");
647         std::string newrxengine = tag->getString("engine");
648         notifyuser = tag->getBool("notifyuser", true);
649         warnonselfmsg = tag->getBool("warnonselfmsg");
650         filterconf = tag->getString("filename");
651         if (!filterconf.empty())
652                 filterconf = ServerInstance->Config->Paths.PrependConfig(filterconf);
653         SetInterval(tag->getDuration("saveperiod", 5));
654
655         factory = RegexEngine ? (RegexEngine.operator->()) : NULL;
656
657         if (newrxengine.empty())
658                 RegexEngine.SetProvider("regex");
659         else
660                 RegexEngine.SetProvider("regex/" + newrxengine);
661
662         if (!RegexEngine)
663         {
664                 if (newrxengine.empty())
665                         ServerInstance->SNO->WriteGlobalSno('f', "WARNING: No regex engine loaded - Filter functionality disabled until this is corrected.");
666                 else
667                         ServerInstance->SNO->WriteGlobalSno('f', "WARNING: Regex engine '%s' is not loaded - Filter functionality disabled until this is corrected.", newrxengine.c_str());
668
669                 initing = false;
670                 FreeFilters();
671                 return;
672         }
673
674         if ((!initing) && (RegexEngine.operator->() != factory))
675         {
676                 ServerInstance->SNO->WriteGlobalSno('f', "Dumping all filters due to regex engine change");
677                 FreeFilters();
678         }
679
680         initing = false;
681         ReadFilters();
682 }
683
684 Version ModuleFilter::GetVersion()
685 {
686         return Version("Adds the /FILTER command which allows server operators to define regex matches for inappropriate phrases that are not allowed to be used in channel messages, private messages, part messages, or quit messages.", VF_VENDOR | VF_COMMON, RegexEngine ? RegexEngine->name : "");
687 }
688
689 std::string ModuleFilter::EncodeFilter(FilterResult* filter)
690 {
691         std::ostringstream stream;
692         std::string x = filter->freeform;
693
694         /* Hax to allow spaces in the freeform without changing the design of the irc protocol */
695         for (std::string::iterator n = x.begin(); n != x.end(); n++)
696                 if (*n == ' ')
697                         *n = '\7';
698
699         stream << x << " " << FilterActionToString(filter->action) << " " << filter->GetFlags() << " " << filter->duration << " :" << filter->reason;
700         return stream.str();
701 }
702
703 FilterResult ModuleFilter::DecodeFilter(const std::string &data)
704 {
705         std::string filteraction;
706         FilterResult res;
707         irc::tokenstream tokens(data);
708         tokens.GetMiddle(res.freeform);
709         tokens.GetMiddle(filteraction);
710         if (!StringToFilterAction(filteraction, res.action))
711                 throw ModuleException("Invalid action: " + filteraction);
712
713         std::string filterflags;
714         tokens.GetMiddle(filterflags);
715         char c = res.FillFlags(filterflags);
716         if (c != 0)
717                 throw ModuleException("Invalid flag: '" + std::string(1, c) + "'");
718
719         std::string duration;
720         tokens.GetMiddle(duration);
721         res.duration = ConvToNum<unsigned long>(duration);
722
723         tokens.GetTrailing(res.reason);
724
725         /* Hax to allow spaces in the freeform without changing the design of the irc protocol */
726         for (std::string::iterator n = res.freeform.begin(); n != res.freeform.end(); n++)
727                 if (*n == '\7')
728                         *n = ' ';
729
730         return res;
731 }
732
733 void ModuleFilter::OnSyncNetwork(ProtocolInterface::Server& server)
734 {
735         for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
736         {
737                 FilterResult& filter = *i;
738                 if (filter.from_config)
739                         continue;
740
741                 server.SendMetaData("filter", EncodeFilter(&filter));
742         }
743 }
744
745 void ModuleFilter::OnDecodeMetaData(Extensible* target, const std::string &extname, const std::string &extdata)
746 {
747         if ((target == NULL) && (extname == "filter"))
748         {
749                 try
750                 {
751                         FilterResult data = DecodeFilter(extdata);
752                         this->AddFilter(data.freeform, data.action, data.reason, data.duration, data.GetFlags());
753                 }
754                 catch (ModuleException& e)
755                 {
756                         ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Error when unserializing filter: " + e.GetReason());
757                 }
758         }
759 }
760
761 FilterResult* ModuleFilter::FilterMatch(User* user, const std::string &text, int flgs)
762 {
763         static std::string stripped_text;
764         stripped_text.clear();
765
766         for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
767         {
768                 FilterResult* filter = &*i;
769
770                 /* Skip ones that dont apply to us */
771                 if (!AppliesToMe(user, filter, flgs))
772                         continue;
773
774                 if ((filter->flag_strip_color) && (stripped_text.empty()))
775                 {
776                         stripped_text = text;
777                         InspIRCd::StripColor(stripped_text);
778                 }
779
780                 if (filter->regex->Matches(filter->flag_strip_color ? stripped_text : text))
781                         return filter;
782         }
783         return NULL;
784 }
785
786 bool ModuleFilter::DeleteFilter(const std::string& freeform, std::string& reason)
787 {
788         for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
789         {
790                 if (i->freeform == freeform)
791                 {
792                         reason.assign(i->reason);
793                         delete i->regex;
794                         filters.erase(i);
795                         dirty = true;
796                         return true;
797                 }
798         }
799         return false;
800 }
801
802 std::pair<bool, std::string> ModuleFilter::AddFilter(const std::string& freeform, FilterAction type, const std::string& reason, unsigned long duration, const std::string& flgs, bool config)
803 {
804         for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
805         {
806                 if (i->freeform == freeform)
807                 {
808                         return std::make_pair(false, "Filter already exists");
809                 }
810         }
811
812         try
813         {
814                 filters.push_back(FilterResult(RegexEngine, freeform, reason, type, duration, flgs, config));
815                 dirty = true;
816         }
817         catch (ModuleException &e)
818         {
819                 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Error in regular expression '%s': %s", freeform.c_str(), e.GetReason().c_str());
820                 return std::make_pair(false, e.GetReason());
821         }
822         return std::make_pair(true, "");
823 }
824
825 bool ModuleFilter::StringToFilterAction(const std::string& str, FilterAction& fa)
826 {
827         if (stdalgo::string::equalsci(str, "gline"))
828                 fa = FA_GLINE;
829         else if (stdalgo::string::equalsci(str, "zline"))
830                 fa = FA_ZLINE;
831         else if (stdalgo::string::equalsci(str, "warn"))
832                 fa = FA_WARN;
833         else if (stdalgo::string::equalsci(str, "block"))
834                 fa = FA_BLOCK;
835         else if (stdalgo::string::equalsci(str, "silent"))
836                 fa = FA_SILENT;
837         else if (stdalgo::string::equalsci(str, "kill"))
838                 fa = FA_KILL;
839         else if (stdalgo::string::equalsci(str, "shun") && (ServerInstance->XLines->GetFactory("SHUN")))
840                 fa = FA_SHUN;
841         else if (stdalgo::string::equalsci(str, "none"))
842                 fa = FA_NONE;
843         else
844                 return false;
845
846         return true;
847 }
848
849 std::string ModuleFilter::FilterActionToString(FilterAction fa)
850 {
851         switch (fa)
852         {
853                 case FA_GLINE:  return "gline";
854                 case FA_ZLINE:  return "zline";
855                 case FA_WARN:   return "warn";
856                 case FA_BLOCK:  return "block";
857                 case FA_SILENT: return "silent";
858                 case FA_KILL:   return "kill";
859                 case FA_SHUN:   return "shun";
860                 default:                return "none";
861         }
862 }
863
864 void ModuleFilter::ReadFilters()
865 {
866         insp::flat_set<std::string> removedfilters;
867
868         for (std::vector<FilterResult>::iterator filter = filters.begin(); filter != filters.end(); )
869         {
870                 if (filter->from_config)
871                 {
872                         removedfilters.insert(filter->freeform);
873                         delete filter->regex;
874                         filter = filters.erase(filter);
875                         continue;
876                 }
877
878                 // The filter is not from the config.
879                 filter++;
880         }
881
882         ConfigTagList tags = ServerInstance->Config->ConfTags("keyword");
883         for (ConfigIter i = tags.first; i != tags.second; ++i)
884         {
885                 std::string pattern = i->second->getString("pattern");
886                 std::string reason = i->second->getString("reason");
887                 std::string action = i->second->getString("action");
888                 std::string flgs = i->second->getString("flags");
889                 unsigned long duration = i->second->getDuration("duration", 10*60, 1);
890                 if (flgs.empty())
891                         flgs = "*";
892
893                 FilterAction fa;
894                 if (!StringToFilterAction(action, fa))
895                         fa = FA_NONE;
896
897                 std::pair<bool, std::string> result = static_cast<ModuleFilter*>(this)->AddFilter(pattern, fa, reason, duration, flgs, !i->second->getBool("generated"));
898                 if (result.first)
899                         removedfilters.erase(pattern);
900                 else
901                         ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Filter '%s' could not be added: %s", pattern.c_str(), result.second.c_str());
902         }
903
904         if (!removedfilters.empty())
905         {
906                 for (insp::flat_set<std::string>::const_iterator it = removedfilters.begin(); it != removedfilters.end(); ++it)
907                         ServerInstance->SNO->WriteGlobalSno('f', "Removing filter '" + *(it) + "' due to config rehash.");
908         }
909 }
910
911 ModResult ModuleFilter::OnStats(Stats::Context& stats)
912 {
913         if (stats.GetSymbol() == 's')
914         {
915                 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
916                 {
917                         stats.AddRow(223, RegexEngine.GetProvider(), i->freeform, i->GetFlags(), FilterActionToString(i->action), i->duration, i->reason);
918                 }
919                 for (ExemptTargetSet::const_iterator i = exemptedchans.begin(); i != exemptedchans.end(); ++i)
920                 {
921                         stats.AddRow(223, "EXEMPT "+(*i));
922                 }
923                 for (ExemptTargetSet::const_iterator i = exemptednicks.begin(); i != exemptednicks.end(); ++i)
924                 {
925                         stats.AddRow(223, "EXEMPT "+(*i));
926                 }
927         }
928         return MOD_RES_PASSTHRU;
929 }
930
931 void ModuleFilter::OnUnloadModule(Module* mod)
932 {
933         // If the regex engine became unavailable or has changed, remove all filters
934         if (!RegexEngine)
935         {
936                 FreeFilters();
937         }
938         else if (RegexEngine.operator->() != factory)
939         {
940                 factory = RegexEngine.operator->();
941                 FreeFilters();
942         }
943 }
944
945 bool ModuleFilter::Tick(time_t)
946 {
947                 if (!dirty) // No need to write.
948                         return true;
949
950                 if (filterconf.empty()) // Nothing to write to.
951                 {
952                         dirty = false;
953                         return true;
954                 }
955
956                 const std::string newfilterconf = filterconf + ".tmp";
957                 std::ofstream stream(newfilterconf.c_str());
958                 if (!stream.is_open()) // Filesystem probably not writable.
959                 {
960                         ServerInstance->SNO->WriteToSnoMask('f', "Unable to save filters to \"%s\": %s (%d)",
961                                         newfilterconf.c_str(), strerror(errno), errno);
962                         return true;
963                 }
964
965                 stream
966                         << "# This file was automatically generated by the " << INSPIRCD_VERSION << " filter module on " << InspIRCd::TimeString(ServerInstance->Time()) << "." << std::endl
967                         << "# Any changes to this file will be automatically overwritten." << std::endl
968                         << "# If you want to convert this to a normal config file you *MUST* remove the generated=\"yes\" keys!" << std::endl
969                         << std::endl
970                         << "<config format=\"xml\">" << std::endl;
971
972                 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
973                 {
974                         // # <keyword reason="You qwertied!" action="block" flags="pn">
975                         const FilterResult& filter = (*i);
976                         if (filter.from_config)
977                                 continue;
978
979                         stream << "<keyword generated=\"yes"
980                         << "\" pattern=\"" << ServerConfig::Escape(filter.freeform)
981                         << "\" reason=\"" << ServerConfig::Escape(filter.reason)
982                         << "\" action=\"" << FilterActionToString(filter.action)
983                         << "\" flags=\"" << filter.GetFlags();
984                         if (filter.duration)
985                                 stream << "\" duration=\"" << InspIRCd::DurationString(filter.duration);
986                         stream << "\">" << std::endl;
987                 }
988
989                 if (stream.fail()) // Filesystem probably not writable.
990                 {
991                         ServerInstance->SNO->WriteToSnoMask('f', "Unable to save filters to \"%s\": %s (%d)",
992                                 newfilterconf.c_str(), strerror(errno), errno);
993                         return true;
994                 }
995                 stream.close();
996
997 #ifdef _WIN32
998                 remove(filterconf.c_str());
999 #endif
1000
1001                 // Use rename to move temporary to new db - this is guaranteed not to fuck up, even in case of a crash.
1002                 if (rename(newfilterconf.c_str(), filterconf.c_str()) < 0)
1003                 {
1004                         ServerInstance->SNO->WriteToSnoMask('f', "Unable to replace old filter config \"%s\" with \"%s\": %s (%d)",
1005                                 filterconf.c_str(), newfilterconf.c_str(), strerror(errno), errno);
1006                         return true;
1007                 }
1008
1009                 dirty = false;
1010                 return true;
1011 }
1012
1013 MODULE_INIT(ModuleFilter)