2 * InspIRCd -- Internet Relay Chat Daemon
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>
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.
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
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/>.
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"
71 unsigned long duration;
75 bool flag_part_message;
76 bool flag_quit_message;
79 bool flag_strip_color;
80 bool flag_no_registered;
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)
90 throw ModuleException("Regex module implementing '"+RegexEngine.GetProvider()+"' is not loaded!");
91 regex = RegexEngine->Create(free);
95 char FillFlags(const std::string &fl)
97 flag_no_opers = flag_part_message = flag_quit_message = flag_privmsg =
98 flag_notice = flag_strip_color = flag_no_registered = false;
100 for (std::string::const_iterator n = fl.begin(); n != fl.end(); ++n)
105 flag_no_opers = true;
108 flag_part_message = true;
111 flag_quit_message = true;
120 flag_strip_color = true;
123 flag_no_registered = true;
126 flag_no_opers = flag_part_message = flag_quit_message =
127 flag_privmsg = flag_notice = flag_strip_color = true;
137 std::string GetFlags() const
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');
147 flags.push_back('p');
149 flags.push_back('n');
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
156 if (flag_strip_color)
157 flags.push_back('c');
158 if (flag_no_registered)
159 flags.push_back('r');
162 flags.push_back('-');
172 class CommandFilter : public Command
175 CommandFilter(Module* f)
176 : Command(f, "FILTER", 1, 5)
179 this->syntax = "<pattern> [<action> <flags> [<duration>] :<reason>]";
181 CmdResult Handle(User* user, const Params& parameters) CXX11_OVERRIDE;
183 RouteDescriptor GetRouting(User* user, const Params& parameters) CXX11_OVERRIDE
185 return ROUTE_BROADCAST;
191 , public ServerProtocol::SyncEventListener
192 , public Stats::EventListener
195 typedef insp::flat_set<std::string, irc::insensitive_swo> ExemptTargetSet;
201 std::string filterconf;
202 RegexFactory* factory;
206 CommandFilter filtcommand;
207 dynamic_reference<RegexFactory> RegexEngine;
209 std::vector<FilterResult> filters;
212 // List of channel names excluded from filtering.
213 ExemptTargetSet exemptedchans;
215 // List of target nicknames excluded from filtering.
216 ExemptTargetSet exemptednicks;
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);
237 static bool StringToFilterAction(const std::string& str, FilterAction& fa);
238 static std::string FilterActionToString(FilterAction fa);
241 CmdResult CommandFilter::Handle(User* user, const Params& parameters)
243 if (parameters.size() == 1)
245 /* Deleting a filter */
246 Module* me = creator;
249 if (static_cast<ModuleFilter*>(me)->DeleteFilter(parameters[0], reason))
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());
258 user->WriteNotice("*** Filter '" + parameters[0] + "' not found on the list.");
264 /* Adding a filter */
265 if (parameters.size() >= 4)
267 const std::string& freeform = parameters[0];
269 const std::string& flags = parameters[2];
270 unsigned int reasonindex;
271 unsigned long duration = 0;
273 if (!ModuleFilter::StringToFilterAction(parameters[1], type))
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'.");
278 user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', and 'kill'.");
282 if (type == FA_GLINE || type == FA_ZLINE || type == FA_SHUN)
284 if (parameters.size() >= 5)
286 if (!InspIRCd::Duration(parameters[3], duration))
288 user->WriteNotice("*** Invalid duration for filter");
295 user->WriteNotice("*** Not enough parameters: When setting a '" + parameters[1] + "' type filter, a duration must be specified as the third parameter.");
304 Module* me = creator;
305 std::pair<bool, std::string> result = static_cast<ModuleFilter*>(me)->AddFilter(freeform, type, parameters[reasonindex], duration, flags);
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());
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());
322 user->WriteNotice("*** Filter '" + freeform + "' could not be added: " + result.second);
328 user->WriteNotice("*** Not enough parameters.");
335 bool ModuleFilter::AppliesToMe(User* user, FilterResult* filter, int iflags)
337 const AccountExtItem* accountext = GetAccountExtItem();
339 if ((filter->flag_no_opers) && user->IsOper())
341 if ((filter->flag_no_registered) && accountext && accountext->get(user))
343 if ((iflags & FLAG_PRIVMSG) && (!filter->flag_privmsg))
345 if ((iflags & FLAG_NOTICE) && (!filter->flag_notice))
347 if ((iflags & FLAG_QUIT) && (!filter->flag_quit_message))
349 if ((iflags & FLAG_PART) && (!filter->flag_part_message))
354 ModuleFilter::ModuleFilter()
355 : ServerProtocol::SyncEventListener(this)
356 , Stats::EventListener(this)
361 , RegexEngine(this, "regex")
365 void ModuleFilter::init()
367 ServerInstance->SNO->EnableSnomask('f', "FILTER");
370 CullResult ModuleFilter::cull()
373 return Module::cull();
376 void ModuleFilter::FreeFilters()
378 for (std::vector<FilterResult>::const_iterator i = filters.begin(); i != filters.end(); ++i)
385 ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtarget, MessageDetails& details)
387 // Leave remote users and servers alone
389 return MOD_RES_PASSTHRU;
391 flags = (details.type == MSG_PRIVMSG) ? FLAG_PRIVMSG : FLAG_NOTICE;
393 FilterResult* f = this->FilterMatch(user, details.text, flags);
396 bool is_selfmsg = false;
397 switch (msgtarget.type)
399 case MessageTarget::TYPE_USER:
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;
410 case MessageTarget::TYPE_CHANNEL:
412 Channel* t = msgtarget.Get<Channel>();
413 if (exemptedchans.count(t->name))
414 return MOD_RES_PASSTHRU;
417 case MessageTarget::TYPE_SERVER:
418 return MOD_RES_PASSTHRU;
421 if (is_selfmsg && warnonselfmsg)
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;
427 else if (f->action == FA_WARN)
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;
433 else if (f->action == FA_BLOCK)
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()));
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())));
442 user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
445 details.echo_original = true;
447 else if (f->action == FA_SILENT)
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())));
454 user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
457 details.echo_original = true;
459 else if (f->action == FA_KILL)
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);
465 else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
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))
474 ServerInstance->XLines->ApplyLines();
479 else if (f->action == FA_GLINE)
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))
488 ServerInstance->XLines->ApplyLines();
493 else if (f->action == FA_ZLINE)
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))
502 ServerInstance->XLines->ApplyLines();
508 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, user->nick + " had their message filtered, target was " + msgtarget.GetName() + ": " + f->reason + " Action: " + ModuleFilter::FilterActionToString(f->action));
511 return MOD_RES_PASSTHRU;
514 ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated)
521 if (command == "QUIT")
523 /* QUIT with no reason: nothing to do */
524 if (parameters.size() < 1)
525 return MOD_RES_PASSTHRU;
530 else if (command == "PART")
532 /* PART with no reason: nothing to do */
533 if (parameters.size() < 2)
534 return MOD_RES_PASSTHRU;
536 if (exemptedchans.count(parameters[0]))
537 return MOD_RES_PASSTHRU;
543 /* We're only messing with PART and QUIT */
544 return MOD_RES_PASSTHRU;
546 FilterResult* f = this->FilterMatch(user, parameters[parting ? 1 : 0], flags);
548 /* PART or QUIT reason doesnt match a filter */
549 return MOD_RES_PASSTHRU;
551 /* We cant block a part or quit, so instead we change the reason to 'Reason filtered' */
552 parameters[parting ? 1 : 0] = "Reason filtered";
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)
557 if ((f->action == FA_WARN) || (f->action == FA_BLOCK) || (((!parting) && (f->action == FA_KILL))) || (f->action == FA_SILENT))
559 return MOD_RES_PASSTHRU;
563 /* Are they parting, if so, kill is applicable */
564 if ((parting) && (f->action == FA_KILL))
566 user->WriteNotice("*** Your PART message was filtered: " + f->reason);
567 ServerInstance->Users->QuitUser(user, "Filtered: " + f->reason);
569 if (f->action == FA_GLINE)
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()));
579 if (ServerInstance->XLines->AddLine(gl,NULL))
581 ServerInstance->XLines->ApplyLines();
586 if (f->action == FA_ZLINE)
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()));
595 if (ServerInstance->XLines->AddLine(zl,NULL))
597 ServerInstance->XLines->ApplyLines();
602 else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
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()));
612 if (ServerInstance->XLines->AddLine(sh, NULL))
614 ServerInstance->XLines->ApplyLines();
622 return MOD_RES_PASSTHRU;
625 void ModuleFilter::ReadConfig(ConfigStatus& status)
627 ConfigTagList tags = ServerInstance->Config->ConfTags("exemptfromfilter");
628 exemptedchans.clear();
629 exemptednicks.clear();
631 for (ConfigIter i = tags.first; i != tags.second; ++i)
633 ConfigTag* tag = i->second;
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);
639 if (target[0] == '#')
640 exemptedchans.insert(target);
642 exemptednicks.insert(target);
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));
655 factory = RegexEngine ? (RegexEngine.operator->()) : NULL;
657 if (newrxengine.empty())
658 RegexEngine.SetProvider("regex");
660 RegexEngine.SetProvider("regex/" + newrxengine);
664 if (newrxengine.empty())
665 ServerInstance->SNO->WriteGlobalSno('f', "WARNING: No regex engine loaded - Filter functionality disabled until this is corrected.");
667 ServerInstance->SNO->WriteGlobalSno('f', "WARNING: Regex engine '%s' is not loaded - Filter functionality disabled until this is corrected.", newrxengine.c_str());
674 if ((!initing) && (RegexEngine.operator->() != factory))
676 ServerInstance->SNO->WriteGlobalSno('f', "Dumping all filters due to regex engine change");
684 Version ModuleFilter::GetVersion()
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 : "");
689 std::string ModuleFilter::EncodeFilter(FilterResult* filter)
691 std::ostringstream stream;
692 std::string x = filter->freeform;
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++)
699 stream << x << " " << FilterActionToString(filter->action) << " " << filter->GetFlags() << " " << filter->duration << " :" << filter->reason;
703 FilterResult ModuleFilter::DecodeFilter(const std::string &data)
705 std::string filteraction;
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);
713 std::string filterflags;
714 tokens.GetMiddle(filterflags);
715 char c = res.FillFlags(filterflags);
717 throw ModuleException("Invalid flag: '" + std::string(1, c) + "'");
719 std::string duration;
720 tokens.GetMiddle(duration);
721 res.duration = ConvToNum<unsigned long>(duration);
723 tokens.GetTrailing(res.reason);
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++)
733 void ModuleFilter::OnSyncNetwork(ProtocolInterface::Server& server)
735 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
737 FilterResult& filter = *i;
738 if (filter.from_config)
741 server.SendMetaData("filter", EncodeFilter(&filter));
745 void ModuleFilter::OnDecodeMetaData(Extensible* target, const std::string &extname, const std::string &extdata)
747 if ((target == NULL) && (extname == "filter"))
751 FilterResult data = DecodeFilter(extdata);
752 this->AddFilter(data.freeform, data.action, data.reason, data.duration, data.GetFlags());
754 catch (ModuleException& e)
756 ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Error when unserializing filter: " + e.GetReason());
761 FilterResult* ModuleFilter::FilterMatch(User* user, const std::string &text, int flgs)
763 static std::string stripped_text;
764 stripped_text.clear();
766 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
768 FilterResult* filter = &*i;
770 /* Skip ones that dont apply to us */
771 if (!AppliesToMe(user, filter, flgs))
774 if ((filter->flag_strip_color) && (stripped_text.empty()))
776 stripped_text = text;
777 InspIRCd::StripColor(stripped_text);
780 if (filter->regex->Matches(filter->flag_strip_color ? stripped_text : text))
786 bool ModuleFilter::DeleteFilter(const std::string& freeform, std::string& reason)
788 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
790 if (i->freeform == freeform)
792 reason.assign(i->reason);
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)
804 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
806 if (i->freeform == freeform)
808 return std::make_pair(false, "Filter already exists");
814 filters.push_back(FilterResult(RegexEngine, freeform, reason, type, duration, flgs, config));
817 catch (ModuleException &e)
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());
822 return std::make_pair(true, "");
825 bool ModuleFilter::StringToFilterAction(const std::string& str, FilterAction& fa)
827 if (stdalgo::string::equalsci(str, "gline"))
829 else if (stdalgo::string::equalsci(str, "zline"))
831 else if (stdalgo::string::equalsci(str, "warn"))
833 else if (stdalgo::string::equalsci(str, "block"))
835 else if (stdalgo::string::equalsci(str, "silent"))
837 else if (stdalgo::string::equalsci(str, "kill"))
839 else if (stdalgo::string::equalsci(str, "shun") && (ServerInstance->XLines->GetFactory("SHUN")))
841 else if (stdalgo::string::equalsci(str, "none"))
849 std::string ModuleFilter::FilterActionToString(FilterAction fa)
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";
864 void ModuleFilter::ReadFilters()
866 insp::flat_set<std::string> removedfilters;
868 for (std::vector<FilterResult>::iterator filter = filters.begin(); filter != filters.end(); )
870 if (filter->from_config)
872 removedfilters.insert(filter->freeform);
873 delete filter->regex;
874 filter = filters.erase(filter);
878 // The filter is not from the config.
882 ConfigTagList tags = ServerInstance->Config->ConfTags("keyword");
883 for (ConfigIter i = tags.first; i != tags.second; ++i)
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);
894 if (!StringToFilterAction(action, fa))
897 std::pair<bool, std::string> result = static_cast<ModuleFilter*>(this)->AddFilter(pattern, fa, reason, duration, flgs, !i->second->getBool("generated"));
899 removedfilters.erase(pattern);
901 ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Filter '%s' could not be added: %s", pattern.c_str(), result.second.c_str());
904 if (!removedfilters.empty())
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.");
911 ModResult ModuleFilter::OnStats(Stats::Context& stats)
913 if (stats.GetSymbol() == 's')
915 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
917 stats.AddRow(223, RegexEngine.GetProvider(), i->freeform, i->GetFlags(), FilterActionToString(i->action), i->duration, i->reason);
919 for (ExemptTargetSet::const_iterator i = exemptedchans.begin(); i != exemptedchans.end(); ++i)
921 stats.AddRow(223, "EXEMPT "+(*i));
923 for (ExemptTargetSet::const_iterator i = exemptednicks.begin(); i != exemptednicks.end(); ++i)
925 stats.AddRow(223, "EXEMPT "+(*i));
928 return MOD_RES_PASSTHRU;
931 void ModuleFilter::OnUnloadModule(Module* mod)
933 // If the regex engine became unavailable or has changed, remove all filters
938 else if (RegexEngine.operator->() != factory)
940 factory = RegexEngine.operator->();
945 bool ModuleFilter::Tick(time_t)
947 if (!dirty) // No need to write.
950 if (filterconf.empty()) // Nothing to write to.
956 const std::string newfilterconf = filterconf + ".tmp";
957 std::ofstream stream(newfilterconf.c_str());
958 if (!stream.is_open()) // Filesystem probably not writable.
960 ServerInstance->SNO->WriteToSnoMask('f', "Unable to save filters to \"%s\": %s (%d)",
961 newfilterconf.c_str(), strerror(errno), errno);
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
970 << "<config format=\"xml\">" << std::endl;
972 for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
974 // # <keyword reason="You qwertied!" action="block" flags="pn">
975 const FilterResult& filter = (*i);
976 if (filter.from_config)
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();
985 stream << "\" duration=\"" << InspIRCd::DurationString(filter.duration);
986 stream << "\">" << std::endl;
989 if (stream.fail()) // Filesystem probably not writable.
991 ServerInstance->SNO->WriteToSnoMask('f', "Unable to save filters to \"%s\": %s (%d)",
992 newfilterconf.c_str(), strerror(errno), errno);
998 remove(filterconf.c_str());
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)
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);
1013 MODULE_INIT(ModuleFilter)