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