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 "protocol.h"

/******************************************************************************
 * Flat-file database read/write
 ******************************************************************************/

/* structure for single entry */
struct EntryDescriptor
{
	std::string name, ts, topicset, topicsetby, topic, modes;
};
/** reads the channel database and returns structure with it */
class DatabaseReader
{
	/* the entry descriptor */
	EntryDescriptor entry;
	/* file */
	FILE *fd;
 public:
	/* constructor, opens the file */
	DatabaseReader (std::string filename)
	{
		/* initialize */
		fd = NULL;
		/* open the file */
		fd = fopen (filename.c_str ( ), "r");
		/* if we can't open the file, return. */
		if (!fd) return;
	}
	/* destructor will close the file */
	~DatabaseReader ( )
	{
		/* if fd is not null, close it */
		if (fd) fclose (fd);
	}
	/* get next entry */
	EntryDescriptor *next ( )
	{
		/* if fd is NULL, fake eof */
		if (!fd) return 0;
		/* clear data */
		entry.name = "";
		entry.ts = "";
		entry.topicset = "";
		entry.topicsetby = "";
		entry.modes = "";
		entry.topic = "";
		std::string str;
		/* read single characters from the file and add them to the string, end at EOF or \n, ignore \r */
		while (1)
		{
			int c = fgetc (fd);
			if ((c == EOF) || ((unsigned char)c == '\n')) break;
			if ((unsigned char)c == '\r') continue;
			str.push_back ((unsigned char)c);
		}
		/* ready to parse the line */
		if (str == "") return 0;
		std::string token;
		irc::spacesepstream sep(str, false);
		/* get first one */
		/* malformed if it is not chaninfo */
		if (!sep.GetToken (token) || token != "chaninfo")
		{
			ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
			return 0;
		}
		/* okay, read channel name */
		/* name was get, but if it's the last token, database is malformed */
		if (!sep.GetToken (token))
		{
			ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
			return 0;
		}
		/* save name */
		entry.name = token;
		/* set channel timestamp */
		sep.GetToken (entry.ts);
		/* initial entry read, read next lines in a loop until end, eof means malformed database again */
		while (1)
		{
			str.clear ( );
			/* read the line */
			while (1)
			{
				int c = fgetc (fd);
				if (c == EOF)
				{
					ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
					return 0;
				}
					unsigned char c2 = (unsigned char)c;
				if (c2 == '\n') break;
				if (c2 == '\r') continue;
				str.push_back (c2);
			}
			irc::spacesepstream sep2(str, false);
			/* get the token */
			if (str == "")
			{
				ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
				return 0;
			}
			sep2.GetToken (token);
			/* break the loop if token is "end" */
			if (token == "end") break;
			/* it is not, so the large if statement there */
			else if (token == "topic")
			{
				/* topic info, if end then it is malformed */
				if (sep2.StreamEnd ( ))
				{
					ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed topic declaration in channel database for channel %s", entry.name.c_str ( ));
				}
				/* if not, then read topic set time */
				/* if end then malformed */
				if (!sep2.GetToken (token))
				{
					ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed topic declaration in channel database for channel %s", entry.name.c_str ( ));
				}
				/* save that */
				entry.topicset = token;
				/* get next token */
				/* if last then malformed */
				if (!sep2.GetToken (token))
				{
					ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed topic declaration in channel database for channel %s", entry.name.c_str ( ));
				}
				entry.topicsetby = token;
				/* get rest of the declaration into the topic string, this is a topic */
				entry.topic = sep2.GetRemaining ( );
			} else if (token == "modes")
			{
				/* modes token, used for representing modes, it's the easier one, just load remaining data */
				if (sep2.StreamEnd ( ))
				{
					ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed modes declaration in channel database for channel %s", entry.name.c_str ( ));
					continue;
				}
				entry.modes = sep2.GetRemaining ( );
			}
		}
		/* return entry address */
		return &entry;
	}
};
/* class being a database writer, gets a database file name on construct */
class DatabaseWriter
{
	/* file stream */
	FILE *fd;
	std::string dbname, tmpname;
	/* public */
	public:
	/* constructor */
	DatabaseWriter (std::string filename)
	{
		fd = NULL;
		dbname = filename;
		tmpname = filename + ".tmp";
		/* ready, open temporary database */
		fd = fopen (tmpname.c_str ( ), "w");
		if (!fd)
		{
			ServerInstance->Logs->Log ("MODULE", DEFAULT, "cannot save to the channel database");
		}
	}
	/* destructor */
	~DatabaseWriter ( )
	{
		if (fd)
		{
			/* saving has been ended, close the database and flush buffers */
			fclose (fd);
			/* rename the database file */
			if (rename (tmpname.c_str ( ), dbname.c_str ( )) == -1)
			{
				ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't rename the database file");
			}
		}
	}
	/* save the single channel */
	void next (Channel *chan)
	{
		if (!fd) return;
		/* first, construct the chaninfo line */
		std::string line;
		line.append ("chaninfo ").append (chan->name).append (" ").append (ConvToStr(chan->age)).append ("\n");
		if (fputs (line.c_str ( ), fd) == EOF)
		{
			ServerInstance->Logs->Log ("MODULE", DEFAULT, "unable to write the channel entry");
			fclose (fd);
			fd = NULL;
			return;
		}
		/* now, write the topic if present */
		if (!chan->topic.empty ( ))
		{
			line.clear ( );
			line.append ("topic ").append (ConvToStr (chan->topicset)).append (" ").append (chan->setby).append (" ").append (chan->topic).append ("\n");
			if (fputs (line.c_str ( ), fd) == EOF)
			{
				ServerInstance->Logs->Log ("MODULE", DEFAULT, "unable to write channel's topic declaration");
				fclose (fd);
				fd = NULL;
				return;
			}
		}
		/* get all channel modes */
		line.clear ( );
		irc::modestacker ms;
		chan->ChanModes (ms, MODELIST_FULL);
		line.append ("modes ").append (ms.popModeLine (FORMAT_PERSIST, INT_MAX, INT_MAX)).append ("\nend\n");
		if (fputs (line.c_str ( ), fd) == EOF)
		{
			ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't write mode declaration");
			fclose (fd);
			fd = NULL;
			return;
		}
	}
};

class FlatFileChannelDB : public Module
{
 private:
	std::string filedb;
	bool dirty; // filedb needs to be flushed to disk
	bool storeregistered, storepermanent;

	/** Whether or not to store a channel to the database */
	bool ShouldStoreChannel(Channel* c)
	{
		return (storeregistered && c->IsModeSet("registered")) || (storepermanent && c->IsModeSet("permanent"));
	}

	void WriteFileDatabase ( )
	{
		// Dump entire database; open/close in constructor/destructor
		DatabaseWriter db (filedb);
		for (chan_hash::const_iterator i = ServerInstance->chanlist->begin(); i != ServerInstance->chanlist->end(); i++)
		{
			if (ShouldStoreChannel(i->second))
				db.next(i->second);
		}
	}

	/** Read flat-file database */
	void ReadFileDatabase ( )
	{
		/* create the reader object and open the database */
		DatabaseReader db (filedb);
		/* start the database read loop */
		EntryDescriptor *entry;
		while ((entry = db.next ( )))
		{
			/* we get a database entry */
			/* so if channel name doesn't start with #, give an error and continue */
			if (entry->name[0] != '#')
			{
				ServerInstance->Logs->Log ("MODULE", DEFAULT, "invalid channel name in channel database");
				continue;
			}
			/* entry is valid */
			/* try to find the channel */
			Channel *chan = ServerInstance->FindChan (entry->name);
			time_t ourTS = atol (entry->ts.c_str ( ));
			/* now, things that will be done depends on if channel was found or not */
			if (!chan)
			{
				/* channel does not exist, allocate it, it will be constructed and added to the network as an empty channel */
				chan = new Channel (entry->name, ourTS);
				/* empty modeless channel added and allocated */
			}
			else if(chan->age > ourTS)
			{
				chan = Channel::Nuke(chan, entry->name, ourTS);
			}
			else if(chan->age < ourTS)
			{
				continue; // there's a channel older than the one we want to load, so we don't load it
			}
			/* so, channel exists in the network, then if our topic is newer than the current, set data
			 * if no topic was ever set, topicset is 0 */
			time_t topicTS = atol (entry->topicset.c_str ( ));
			if (topicTS && topicTS >= chan->topicset)
			{
				chan->topic = entry->topic;
				chan->topicset = atol (entry->topicset.c_str ( ));
				chan->setby = entry->topicsetby;
				chan->WriteChannelWithServ(chan->setby, "TOPIC %s :%s", chan->name.c_str(), chan->topic.c_str());
			}
			/* if modestring is not empty, then set all modes in it */
			if (!entry->modes.empty ( ))
			{
				/* create sepstream */
				irc::spacesepstream sep(entry->modes);
				/* create modestacker for stacking all mode changes */
				irc::modestacker ms;
				/* spacesepstream is used to iterate through all modes that were saved for a channel */
				/* modestacker ms is used to stack them */
				/* iterate through the mode list */
				while (!sep.StreamEnd ( ))
				{
					/* get the mode to be applied */
					std::string token, name, value;
					sep.GetToken (token);
					/* the mode looks like name=value unless it doesn't have any value then it's just name */
					/* find the position of = sign */
					size_t namepos = token.find ('=');
					/* if we didn't find anything, set name to the token because mode is... valueless */
					if (namepos == std::string::npos) name = token;
					/* if it found that = sign, we have both name and value */
					else
					{
						/* it found it, name is string before the character, value is the string after it */
						name = token.substr (0, namepos);
						value = token.substr (namepos + 1);
					}
					/* mode name and value found, we can now add it to the modestacker */
					/* hmm, or we can't, reason is that letters are separate in user and channel modes, but mode names are not, so what if this thing we're setting is
					an user mode? */
					/* to check that, we must find the mode */
					ModeHandler *mc = ServerInstance->Modes->FindMode (name);
					/* those actions will be taken only if mode was found and if it's the channel mode */
					if (mc && mc->GetModeType ( ) == MODETYPE_CHANNEL)
					{
						/* mode was found and is not an user mode but a channel mode, push it to the modestacker, we use id instead of name for quicker push because we
						have the mode handler now */
						ms.push (irc::modechange (mc->id, value));
					}
				}
				/* okay, all modes were pushed, modestacker can now produce the mode line */
				/* so, process all mode changes and apply them on a channel as a merge */
				ServerInstance->Modes->Process (ServerInstance->FakeClient, chan, ms, true);
				ServerInstance->Modes->Send (ServerInstance->FakeClient, chan, ms);
				ServerInstance->PI->SendMode (ServerInstance->FakeClient, chan, ms, true);
			}
		}
	}

 public:
	/* module constructor, for initializing stuff */
	FlatFileChannelDB() : dirty(true)
	{
	}
	/* get module version and flags. */
	Version GetVersion()
	{
		return Version("Provides channel database storage in a flat-file format", VF_VENDOR);
	}
	void init ( )
	{
		Implementation eventlist[] = {
			I_OnBackgroundTimer, I_OnMode, I_OnPostTopicChange
		};

		ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));

		ReadFileDatabase();
	}
	/* rehash event */
	void ReadConfig(ConfigReadStatus& status)
	{
		ConfigTag *tag = ServerInstance->Config->GetTag ("chandb");
		filedb = tag->getString ("dbfile");
		storeregistered = tag->getBool("storeregistered", true);
		storepermanent = tag->getBool("storepermanent", false);
	}
	/* called on background timer to write all channels to disk if they were changed */
	void OnBackgroundTimer (time_t cur)
	{
		/* if not dirty then don't do anything */
		if (!dirty)
			return;
		/* dirty, one of registered channels was changed, save it */
		WriteFileDatabase();
		/* clear dirty to prevent next savings */
		dirty = false;
	}
	/* after the topic has been changed, call this to check if channel was registered */
	void OnPostTopicChange (User *user, Channel *chan, const std::string &topic)
	{
		if (ShouldStoreChannel(chan)) dirty = true;
	}
	/* this event is called after each modechange */
	void OnMode (User *user, Extensible *target, const irc::modestacker &modes)
	{
		Channel* chan = IS_CHANNEL(target);
		if (chan && ShouldStoreChannel(chan)) dirty = true;
	}

	void Prioritize()
	{
		// database reading may depend on channel modes being loaded
		ServerInstance->Modules->SetPriority(this, I_ModuleInit, PRIORITY_LAST);
	}
};
/* register the module */
MODULE_INIT(FlatFileChannelDB)