aboutsummaryrefslogtreecommitdiff
/*       +------------------------------------+
 *       | Inspire Internet Relay Chat Daemon |
 *       +------------------------------------+
 *
 *  InspIRCd: (C) 2002-2011 InspIRCd Development Team
 * See: http://wiki.inspircd.org/Credits
 *
 * This program is free but copyrighted software; see
 *            the file COPYING for details.
 *
 * ---------------------------------------------------
 */


#include "inspircd.h"
#include "account.h"

/* $ModDesc: Provides channel mode +r for channel registration */

static dynamic_reference<AccountDBProvider> db("accountdb");

/* Some function for checking if the selected account is a registrant for a selected channel, for writing less code later */
static bool IsRegistrant (ModeHandler *mh, Channel *chan, const irc::string &account)
{
	irc::string token;
	irc::commasepstream registrantnames(chan->GetModeParameter (mh));
	while (registrantnames.GetToken (token))
	{
		if (token == account)
			return true;
	}
	return false;
}

/* The last_activity extension item: last part from the channel, used to implement expiry */
class ChanExpiryExtItem : public ExtensionItem
{
	public:
	ChanExpiryExtItem (Module *owner) : ExtensionItem (EXTENSIBLE_CHANNEL, "last_activity", owner)
	{

	}
	/* a function to get pointer to the timestamp for a given channel */
	time_t get (const Extensible *container) const
	{
		return reinterpret_cast<time_t> (get_raw (container));
	}
	void set (Extensible *container, time_t value)
	{
		set_raw (container, reinterpret_cast<void *> (value));
	}
	void unset (Extensible *container)
	{
		unset_raw (container);
	}
	void free (void *value)
	{
	}
	/* task of this function is to make the given string into the time_t and to put it in the extension item for the given container */
	void unserialize (SerializeFormat format, Extensible *container, const std::string &value)
	{
		/* independently of the format, the string is always the timestamp */
		/* if nothing happens, we are sure that no one will normally send the metadata except netburst */
		/* if this will be a netburst, we are sure no one will unset it, it just won't be send at all */
		/* so string can't be empty, and thus, the extension item's value can be not found, but can't be 0 */
		/* get the null terminated string from the std::string class, convert it to the integer/time_t */
		time_t newtime = atol (value.c_str ( ));
		/* if newtime is less or equal than the current time set then don't change */
		if (newtime <= get (container))
			return;
		/* if not */
		set (container, newtime);
	}
	std::string serialize (SerializeFormat format, const Extensible *container, void *item) const
	{
		/* we got a value, if it is null then return empty string to deny sending metadata */
		/* if it is not null, it's a pointer to time_t, so cast it, convert to string and return */
		if (!item) return "";
		time_t v = reinterpret_cast<time_t> (item);
		return ConvToStr (v);
	}
};

/* class for handling +r mode */
class RegisterModeHandler : public ParamChannelModeHandler
{
 public:
	ChanExpiryExtItem last_activity;
	dynamic_reference<AccountProvider> account;
	bool verbose;
	int chanlimit;
	RegisterModeHandler(Module *me) : ParamChannelModeHandler(me, "registered", 'r'),
		last_activity(me), account("account"), verbose(true), chanlimit(10)
	{
		fixed_letter = false;
	}

	void OnParameterMissing (User *user, User*, Channel *chan, std::string &param)
	{
		// put the user's own account name in the parameter, if missing
		if (account)
			param = account->GetAccountName(user);
	}
	/* make access checks */
	void AccessCheck(ModePermissionData& perm)
	{
		irc::string acctname = account ? account->GetAccountName(perm.source) : "";

		if (perm.mc.adding)
		{
			if (perm.chan->IsModeSet(this))
			{
				/* changing registration: must be a registrant */
				if (acctname.empty())
				{
					// not logged in: different error message
					perm.ErrorNumeric(ERR_CHANOPRIVSNEEDED, "%s :You must be logged in to an account to reregister a channel",
						perm.chan->name.c_str());
					perm.result = MOD_RES_DENY;
					return;
				}
				if (!IsRegistrant (this, perm.chan, acctname))
				{
					// no matching account name
					perm.ErrorNumeric(ERR_CHANOPRIVSNEEDED, "%s :Only a registrant of a channel may reregister it",
						perm.chan->name.c_str());
					perm.result = MOD_RES_DENY;
					return;
				}
			}
			if (perm.source->HasPrivPermission("channels/set-registration", false))
				return;
			/* new functionality, you can't register any channel if you don't have the oper privilege and chanlimit == 0 */
			if (chanlimit == 0)
			{
				perm.ErrorNumeric (ERR_NOPRIVILEGES, ":Permission denied - only IRC operators may register channels");
				perm.result = MOD_RES_DENY;
				return;
			}
			/* calculate how many channels the user has registered himself */
			int chans = 0;
			for (chan_hash::const_iterator it = ServerInstance->chanlist->begin ( ); it != ServerInstance->chanlist->end ( ); it++)
			{
				if (it->second != perm.chan && it->second->IsModeSet (this))
				{
					irc::commasepstream registrantnames (it->second->GetModeParameter (this));
					irc::string token;
					registrantnames.GetToken (token);
					if (token == acctname) chans++;
				}
			}
			/* check for limit */
			if (chans >= chanlimit)
			{
				perm.ErrorNumeric(ERR_CHANOPRIVSNEEDED, "%s :You can't register more than %i channels under one account", perm.chan->name.c_str(), chans);
				perm.result = MOD_RES_DENY;
				return;
			}
			/* otherwise, you can only set it to your own account name */
			irc::commasepstream registrantnames(perm.mc.value);
			irc::string registrantname;
			/* if the account name was not given, is empty or is not equal to the given parameter, deny */
			if (acctname.empty() || !registrantnames.GetToken(registrantname) || acctname != registrantname)
			{
				perm.ErrorNumeric(ERR_CHANOPRIVSNEEDED, "%s :The first account in the %sregistrant list must be your own",
					perm.chan->name.c_str(), perm.chan->IsModeSet(this) ? "new " : "");
				perm.result = MOD_RES_DENY;
			}
		}
		else
		{
			/* removing a mode: must be a registrant */
			if (acctname.empty())
			{
				// not logged in: different error message
				perm.ErrorNumeric(ERR_CHANOPRIVSNEEDED, "%s :You must be logged in to an account to unregister a channel",
					perm.chan->name.c_str());
				perm.result = MOD_RES_DENY;
				return;
			}
			if (IsRegistrant (this, perm.chan, acctname))
				return;
			// no matching account name
			perm.ErrorNumeric(ERR_CHANOPRIVSNEEDED, "%s :Only a registrant of a channel may unregister it",
				perm.chan->name.c_str());
			perm.result = MOD_RES_DENY;
		}
	}

	ModeAction OnModeChange (User *source, User*, Channel *chan, std::string &param, bool adding)
	{
		if (adding)
		{
			std::string now = chan->GetModeParameter(this), token;
			if(!IS_SERVER(source) && db)
			{
				irc::commasepstream registrantnames(param);
				std::vector<irc::string> registrants;
				while (registrantnames.GetToken (token))
					if (db->GetAccount(token, false))
						registrants.push_back(token);
				if(registrants.empty())
					return MODEACTION_DENY;
				param.clear();
				for(std::vector<irc::string>::iterator i = registrants.begin(); i != registrants.end(); ++i)
					param.append(*i).push_back(',');
				param = param.substr(0, param.size() - 1);
			}
			if (param == now)
				return MODEACTION_DENY;
			if (!now.empty())
			{
				ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(source) || source == ServerInstance->FakeClient ? 'r' : 'R', "%s changed registration of %s from %s to %s",
					source->GetFullHost().c_str(), chan->name.c_str(), now.c_str(), param.c_str());
				if (verbose)
					chan->WriteChannel(ServerInstance->FakeClient, "NOTICE %s :This channel has been reregistered (%s -> %s)",
						chan->name.c_str(), now.c_str(), param.c_str());
			}
			/* servers are expected to notice their reasons themselves */
			else if (!IS_SERVER(source))
			{
				/* first, send a message to all ircops with snomask +r set */
				ServerInstance->SNO->WriteToSnoMask (IS_LOCAL (source) ? 'r' : 'R', "%s registered channel %s to account %s", source->GetFullHost ( ).c_str ( ), chan->name.c_str ( ), param.c_str ( ));
				/* now, send similar to channel */
				if (verbose)
					chan->WriteChannel (ServerInstance->FakeClient, "NOTICE %s :This channel has been registered",
					chan->name.c_str ( ));
			}
			last_activity.set(chan, ServerInstance->Time());
			chan->SetModeParam (this, param);
		}
		else
		{
			/* servers are expected to notice their reasons themselves */
			if (!IS_SERVER(source))
			{
				/* it is set, so first send a server notice to all ircops using +r snomask that this channel has been unregistered */
				ServerInstance->SNO->WriteToSnoMask (IS_LOCAL (source) ? 'r' : 'R', "channel %s unregistered by %s", chan->name.c_str ( ), source->GetFullHost ( ).c_str ( ));
				/* then send this important message to the channel as the notice */
				if (verbose)
					chan->WriteChannel (ServerInstance->FakeClient, "NOTICE %s :This channel has been unregistered",
					chan->name.c_str ( ));
			}
			chan->SetModeParam(this, "");
			last_activity.unset(chan);
			/* now, if usercount is 0, delete the channel */
			if (!chan->GetUserCounter())
				chan->DelUser(ServerInstance->FakeClient);
		}
		return MODEACTION_ALLOW;
	}

	void set_prefixrequired (ModeHandler *mh)
	{
		levelrequired = mh->GetPrefixRank();
	}
};

class ChannelRegistrationModule : public Module
{
 private:
	RegisterModeHandler mh;

	time_t expiretime;

	/* check if the channel given as a parameter expired */
	bool Expired (Channel *chan)
	{
		// can't expire if +r isn't set
		if (!chan->IsModeSet(&mh))
			return false;
		// won't expire if +P is set
		if (chan->IsModeSet("permanent"))
			return false;
		// is it too old?
		if (ServerInstance->Time() - expiretime >= mh.last_activity.get(chan))
			return true;
		return false;
	}

 public:
	/* module constructor, for initializing stuff */
	ChannelRegistrationModule() : mh(this)
	{
	}
	/* get module version and flags. */
	Version GetVersion()
	{
		return Version("Provides channel mode +r for channel registration", VF_VENDOR);
	}
	void init ( )
	{
		ServerInstance->SNO->EnableSnomask ('r', "CHANREGISTER");
		Implementation eventlist[] = {
			I_OnCheckJoin, I_OnPermissionCheck, I_OnChannelPreDelete,
			I_OnUserQuit, I_OnUserPart, I_OnUserKick, I_OnGarbageCollect
		};

		ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
		ServerInstance->Modules->AddService(mh);
		ServerInstance->Modules->AddService(mh.last_activity);
	}
	/* rehash event */
	void ReadConfig(ConfigReadStatus& status)
	{
		ConfigTag *tag = ServerInstance->Config->GetTag ("chanregister");

		/* expiration is in user-readable time format */
		expiretime = ServerInstance->Duration (tag->getString ("expiretime", "21d"));
		mh.verbose = tag->getBool ("verbose", true);
		mh.chanlimit = tag->getInt ("limit", 10);

		std::string prefixmode = tag->getString("prefix", "op");
		ModeHandler *prefixmodehandler = ServerInstance->Modes->FindMode(prefixmode);
		if (prefixmodehandler)
			mh.set_prefixrequired (prefixmodehandler);
		else
			status.ReportError(tag, "The given prefix level was not found");
	}
	/* OnCheckJoin - this is an event for checking permissions of some user to join some channel, it is used to allow joining by registrants even when
banned */
	void OnCheckJoin (ChannelPermissionData &joindata)
	{
		/* if channel is null, it's not something that is done on channels, or it is but channel is being created, so no registrant in it */
		if (!joindata.chan)
			return;
		/* return if mode is not set and channel is unregistered */
		if (!joindata.chan->IsModeSet (&mh))
			return;
		/* get user's account name */
		irc::string acctname = mh.account ? mh.account->GetAccountName(joindata.source) : "";
		/* if account is not found or empty, we can be really sure that we really aren't registrant of any channel */
		if (acctname.empty())
			return;
		/* it is set and we have parameter, get it */
		if (IsRegistrant (&mh, joindata.chan, acctname))
		{
			/* if registrantname and account name are the same, override */
			joindata.result = MOD_RES_ALLOW;
			return;
		}
	}
	/* check permissions */
	void OnPermissionCheck (PermissionData &perm)
	{
		/* if channel is null, proceed normally */
		if (!perm.chan)
			return;
		/* if not, but name of permission is join, proceed normally because it was checked */
		if (perm.name == "join")
			return;
		/* if permission is implicit, proceed normally, as registrants may not want these permissions */
		if (perm.implicit)
			return;
		/* if +r mode is being changed, return: we keep the normal checks here */
		if (perm.name == "mode/registered")
			return;
		/* if anything is ok, but mode +r is unset, return and check perms normally */
		if (!perm.chan->IsModeSet (&mh))
			return;
		/* if set, get the registrant account name */
		/* get account name of the current user */
		irc::string acctname = mh.account ? mh.account->GetAccountName(perm.source) : "";
		/* if user is not logged in then return */
		if (acctname.empty())
			return;
		if (IsRegistrant (&mh, perm.chan, acctname))
		{
			/* if ok, then set result to allow if registrant name matches account name */
			perm.result = MOD_RES_ALLOW;
			return;
		}
	}
	/* called before channel is being deleted */
	ModResult OnChannelPreDelete (Channel *chan)
	{
		/* return 1 to prevent channel deletion if channel is registered */
		if (chan->IsModeSet(&mh))
			return MOD_RES_DENY;
		/* in other case, return 0, to allow channel deletion */
		return MOD_RES_PASSTHRU;
	}
	/* when someone unsets +r, OnMode event won't mark the database as requiring saving, but it requires to be saved, and because of this, this event
	handler is needed */
	/* this is called when some user parts a channel and is used to record the time user parted as the last activity time */
	/* this is done only if channel is registered, and is used only when it's empty */
	void OnUserPart (Membership *memb, std::string &msg, CUList &except_list)
	{
		/* we have some membership, let's set this last part time now */
		if (memb->chan->IsModeSet (&mh))
			mh.last_activity.set (memb->chan, ServerInstance->Time ( ));
	}
	/* called when the user is being kicked, it's also used for setting last activity time */
	void OnUserKick (User *source, Membership *memb, const std::string &reason, CUList &except_list)
	{
		/* the same */
		if (memb->chan->IsModeSet (&mh))
			mh.last_activity.set (memb->chan, ServerInstance->Time ( ));
	}
	/* if user quits, set last activity time for each channel he was in that are registered */
	void OnUserQuit (User *user, const std::string &msg, const std::string &opermsg)
	{
		/* iterate through a channel list */
		for (UCListIter it = user->chans.begin ( ); it != user->chans.end ( ); it++)
		{
			/* for each membership, check if channel is registered, if it is, set last activity time for it */
			if (it->chan->IsModeSet (&mh))
				mh.last_activity.set (it->chan, ServerInstance->Time ( ));
		}
	}

	void Prioritize()
	{
		ServerInstance->Modules->SetPriority(this, I_OnGarbageCollect, PRIORITY_AFTER, ServerInstance->Modules->Find("m_account_register.so"));
	}

	/* called once a hour to expire expired channels */
	void OnGarbageCollect ( )
	{
		chan_hash::const_iterator it = ServerInstance->chanlist->begin();
		std::vector<irc::string> registrants;
		bool listChanged;
		while (it != ServerInstance->chanlist->end())
		{
			Channel* c = it++->second;
			/* check if the channel was expired and is empty at the same time */
			if (c->GetUserCounter() == 0 && Expired(c))
			{
				/* send a notice */
				ServerInstance->SNO->WriteGlobalSno('r', "Channel %s was expired", c->name.c_str());
				/* send mode -r */
				irc::modestacker ms;
				ms.push(irc::modechange(mh.id, "", false));
				ServerInstance->SendMode(ServerInstance->FakeClient, c, ms, true);
			}

			if(!c->IsModeSet(&mh))
				continue;
			if(!db)			// if we're using a services packages for nick registration rather than the account system,
				continue;	// then we can't check whether registrants exist anymore, so we just go to the next channel
			irc::string token;
			irc::commasepstream registrantnames(c->GetModeParameter (&mh));
			registrants.clear();
			listChanged = false;
			registrantnames.GetToken(token);
			if(db->GetAccount(token, false))
				registrants.push_back(token);
			else
			{
				/* send a notice */
				ServerInstance->SNO->WriteGlobalSno('r', "Channel %s was deregistered because the primary registrant account was missing", c->name.c_str());
				/* send mode -r */
				irc::modestacker ms;
				ms.push(irc::modechange(mh.id, "", false));
				ServerInstance->SendMode(ServerInstance->FakeClient, c, ms, true);
				continue;
			}
			while (registrantnames.GetToken (token))
			{
				if (db->GetAccount(token, false))
					registrants.push_back(token);
				else
					listChanged = true;
			}
			if(listChanged)
			{
				std::string newvalue;
				for(std::vector<irc::string>::iterator i = registrants.begin(); i != registrants.end(); ++i)
					newvalue.append(*i).push_back(',');
				irc::modestacker ms;
				ms.push(irc::modechange(mh.id, newvalue.substr(0, newvalue.length() - 1), true));
				ServerInstance->SendMode(ServerInstance->FakeClient, c, ms, true);
			}
		}
	}
};
/* register the module */
MODULE_INIT(ChannelRegistrationModule)