summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xautogen.sh2
-rw-r--r--src/common/cfgfiles.c12
-rw-r--r--src/common/cfgfiles.h2
-rw-r--r--src/common/common.vcxproj4
-rw-r--r--src/common/dcc.c3
-rw-r--r--src/common/hexchat.c2
-rw-r--r--src/common/inbound.c13
-rw-r--r--src/common/outbound.c6
-rw-r--r--src/common/plugin.c74
-rw-r--r--src/common/plugin.h8
-rw-r--r--src/common/proto-irc.c20
-rw-r--r--src/common/servlist.c12
-rw-r--r--src/common/text.c18
-rw-r--r--src/common/userlist.c4
-rw-r--r--src/common/util.c78
-rw-r--r--src/common/util.h2
-rw-r--r--src/fe-gtk/fe-gtk.vcxproj4
-rw-r--r--src/fe-gtk/setup.c9
-rw-r--r--src/fe-gtk/xtext.c94
-rw-r--r--src/fe-text/fe-text.c2
-rw-r--r--win32/hexchat.props4
-rw-r--r--win32/installer/hexchat.iss.tt137
-rw-r--r--win32/installer/installer.vcxproj4
23 files changed, 343 insertions, 171 deletions
diff --git a/autogen.sh b/autogen.sh
index 607aa949..324870a3 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
 have_automake=false
 
 if automake --version < /dev/null > /dev/null 2>&1 ; then
diff --git a/src/common/cfgfiles.c b/src/common/cfgfiles.c
index 725b9632..7d0e9c91 100644
--- a/src/common/cfgfiles.c
+++ b/src/common/cfgfiles.c
@@ -41,7 +41,7 @@
 #define DEF_FONT "Monospace 9"
 #define DEF_FONT_ALTER "Arial Unicode MS,Lucida Sans Unicode,MS Gothic,Unifont"
 
-const char const *languages[LANGUAGES_LENGTH] = {
+const char * const languages[LANGUAGES_LENGTH] = {
 	"af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN",   /*  0 ..  9 */
 	"zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl", /* 10 .. 19 */
 	"de", "el", "gu", "hi", "hu", "id", "it", "ja", "kn", "rw",       /* 20 .. 29 */
@@ -630,7 +630,7 @@ convert_with_fallback (const char *str, const char *fallback)
 }
 
 static int
-find_language_number (const char const *lang)
+find_language_number (const char * const lang)
 {
 	int i;
 
@@ -1085,12 +1085,10 @@ set_showval (session *sess, const struct prefs *var, char *tbuf)
 
 	len = strlen (var->name);
 	memcpy (tbuf, var->name, len);
-	dots = 29 - len;
-
-	if (dots < 0)
-	{
+	if (len > 29)
 		dots = 0;
-	}
+	else
+		dots = 29 - len;
 
 	tbuf[len++] = '\003';
 	tbuf[len++] = '2';
diff --git a/src/common/cfgfiles.h b/src/common/cfgfiles.h
index 83db9656..79bc0582 100644
--- a/src/common/cfgfiles.h
+++ b/src/common/cfgfiles.h
@@ -27,7 +27,7 @@
 #define LANGUAGES_LENGTH 52
 
 extern char *xdir;
-extern const char const *languages[LANGUAGES_LENGTH];
+extern const char * const languages[LANGUAGES_LENGTH];
 
 char *cfg_get_str (char *cfg, const char *var, char *dest, int dest_len);
 int cfg_get_bool (char *var);
diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj
index 13610cee..39c50222 100644
--- a/src/common/common.vcxproj
+++ b/src/common/common.vcxproj
@@ -159,8 +159,8 @@
       <Command>

       <![CDATA[

 SET SOLUTIONDIR=$(SolutionDir)..\

-"%PROGRAMFILES%\Common Files\microsoft shared\TextTemplating\12.0\TextTransform.exe" -out "%SOLUTIONDIR%config-win32.h" "%SOLUTIONDIR%config-win32.h.tt"

-"%PROGRAMFILES%\Common Files\microsoft shared\TextTemplating\12.0\TextTransform.exe" -out "%SOLUTIONDIR%win32\version.txt" "%SOLUTIONDIR%win32\version.txt.tt"

+$(TextTransformPath) -out "%SOLUTIONDIR%config-win32.h" "%SOLUTIONDIR%config-win32.h.tt"

+$(TextTransformPath) -out "%SOLUTIONDIR%win32\version.txt" "%SOLUTIONDIR%win32\version.txt.tt"

       ]]>

       </Command>

     </PreBuildEvent>

diff --git a/src/common/dcc.c b/src/common/dcc.c
index 57354ba9..ca1be140 100644
--- a/src/common/dcc.c
+++ b/src/common/dcc.c
@@ -557,8 +557,7 @@ dcc_chat_line (struct DCC *dcc, char *line)
 	for (i = 5; i < PDIWORDS; i++)
 		word[i] = "\000";
 
-	ret = plugin_emit_print (sess, word) 
-		+ plugin_emit_print_attrs (sess, word, 0);
+	ret = plugin_emit_print (sess, word, 0);
 
 	/* did the plugin close it? */
 	if (!g_slist_find (dcc_list, dcc))
diff --git a/src/common/hexchat.c b/src/common/hexchat.c
index 17309dee..5b23621f 100644
--- a/src/common/hexchat.c
+++ b/src/common/hexchat.c
@@ -232,7 +232,7 @@ find_channel (server *serv, char *chan)
 	while (list)
 	{
 		sess = list->data;
-		if ((!serv || serv == sess->server) && sess->type != SESS_DIALOG)
+		if ((!serv || serv == sess->server) && sess->type == SESS_CHANNEL)
 		{
 			if (!serv->p_cmp (chan, sess->channel))
 				return sess;
diff --git a/src/common/inbound.c b/src/common/inbound.c
index b9503ddb..8d51df94 100644
--- a/src/common/inbound.c
+++ b/src/common/inbound.c
@@ -467,7 +467,7 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
 
 	if (fromme)
 	{
-  		if (prefs.hex_away_auto_unmark && serv->is_away)
+		if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp)
 			sess->server->p_set_back (sess->server);
 		EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,
 									  0, tags_data->timestamp);
@@ -1439,10 +1439,15 @@ inbound_banlist (session *sess, time_t stamp, char *chan, char *mask,
 	server *serv = sess->server;
 	char *nl;
 
-	if ((nl = strchr (time_str, '\n')))
-		*nl = 0;
-	if (stamp == 0)
+	if (stamp <= 0)
+	{
 		time_str = "";
+	}
+	else
+	{
+		if ((nl = strchr (time_str, '\n')))
+			*nl = 0;
+	}
 
 	sess = find_channel (serv, chan);
 	if (!sess)
diff --git a/src/common/outbound.c b/src/common/outbound.c
index 8e412186..96425f8f 100644
--- a/src/common/outbound.c
+++ b/src/common/outbound.c
@@ -3947,9 +3947,9 @@ const struct commands xc_cmds[] = {
 	 N_("INVITE <nick> [<channel>], invites someone to a channel, by default the current channel (needs chanop)")},
 	{"JOIN", cmd_join, 1, 0, 0, N_("JOIN <channel>, joins the channel")},
 	{"KICK", cmd_kick, 1, 1, 1,
-	 N_("KICK <nick>, kicks the nick from the current channel (needs chanop)")},
+	 N_("KICK <nick> [reason], kicks the nick from the current channel (needs chanop)")},
 	{"KICKBAN", cmd_kickban, 1, 1, 1,
-	 N_("KICKBAN <nick>, bans then kicks the nick from the current channel (needs chanop)")},
+	 N_("KICKBAN <nick> [reason], bans then kicks the nick from the current channel (needs chanop)")},
 	{"KILLALL", cmd_killall, 0, 0, 1, "KILLALL, immediately exit"},
 	{"LAGCHECK", cmd_lagcheck, 0, 0, 1,
 	 N_("LAGCHECK, forces a new lag check")},
@@ -3978,7 +3978,7 @@ const struct commands xc_cmds[] = {
 	{"MSG", cmd_msg, 0, 0, 1, N_("MSG <nick> <message>, sends a private message, message \".\" to send to last nick or prefix with \"=\" for dcc chat")},
 
 	{"NAMES", cmd_names, 1, 0, 1,
-	 N_("NAMES, Lists the nicks on the current channel")},
+	 N_("NAMES [channel], Lists the nicks on the channel")},
 	{"NCTCP", cmd_nctcp, 1, 0, 1,
 	 N_("NCTCP <nick> <message>, Sends a CTCP notice")},
 	{"NEWSERVER", cmd_newserver, 0, 0, 1, N_("NEWSERVER [-noconnect] <hostname> [<port>]")},
diff --git a/src/common/plugin.c b/src/common/plugin.c
index 228983f3..ee3b26c8 100644
--- a/src/common/plugin.c
+++ b/src/common/plugin.c
@@ -102,16 +102,21 @@ enum
 	LIST_USERS
 };
 
+/* We use binary flags here because it makes it possible for plugin_hook_find()
+ * to match several types of hooks.  This is used so that plugin_hook_run()
+ * match both HOOK_SERVER and HOOK_SERVER_ATTRS hooks when plugin_emit_server()
+ * is called.
+ */
 enum
 {
-	HOOK_COMMAND,      /* /command */
-	HOOK_SERVER,       /* PRIVMSG, NOTICE, numerics */
-	HOOK_SERVER_ATTRS, /* same as above, with attributes */
-	HOOK_PRINT,        /* All print events */
-	HOOK_PRINT_ATTRS,  /* same as above, with attributes */
-	HOOK_TIMER,        /* timeouts */
-	HOOK_FD,           /* sockets & fds */
-	HOOK_DELETED       /* marked for deletion */
+	HOOK_COMMAND      = 1 << 0, /* /command */
+	HOOK_SERVER       = 1 << 1, /* PRIVMSG, NOTICE, numerics */
+	HOOK_SERVER_ATTRS = 1 << 2, /* same as above, with attributes */
+	HOOK_PRINT        = 1 << 3, /* All print events */
+	HOOK_PRINT_ATTRS  = 1 << 4, /* same as above, with attributes */
+	HOOK_TIMER        = 1 << 5, /* timeouts */
+	HOOK_FD           = 1 << 6, /* sockets & fds */
+	HOOK_DELETED      = 1 << 7  /* marked for deletion */
 };
 
 GSList *plugin_list = NULL;	/* export for plugingui.c */
@@ -560,16 +565,14 @@ plugin_hook_find (GSList *list, int type, char *name)
 	while (list)
 	{
 		hook = list->data;
-		if (hook && hook->type == type)
+		if (hook && (hook->type & type))
 		{
 			if (g_ascii_strcasecmp (hook->name, name) == 0)
 				return list;
 
-			if (type == HOOK_SERVER)
-			{
-				if (g_ascii_strcasecmp (hook->name, "RAW LINE") == 0)
+			if ((type & HOOK_SERVER)
+				&& g_ascii_strcasecmp (hook->name, "RAW LINE") == 0)
 					return list;
-			}
 		}
 		list = list->next;
 	}
@@ -599,7 +602,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
 		hook->pl->context = sess;
 
 		/* run the plugin's callback function */
-		switch (type)
+		switch (hook->type)
 		{
 		case HOOK_COMMAND:
 			ret = ((hexchat_cmd_cb *)hook->callback) (word, word_eol, hook->userdata);
@@ -676,39 +679,30 @@ hexchat_event_attrs_free (hexchat_plugin *ph, hexchat_event_attrs *attrs)
 }
 
 /* got a server PRIVMSG, NOTICE, numeric etc... */
-int
-plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[])
-{
-	return plugin_hook_run (sess, name, word, word_eol, NULL, HOOK_SERVER);
-}
 
 int
-plugin_emit_server_attrs (session *sess, char *name, char *word[], char *word_eol[],
-						  time_t server_time)
+plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[],
+					time_t server_time)
 {
 	hexchat_event_attrs attrs;
 
 	attrs.server_time_utc = server_time;
 
-	return plugin_hook_run (sess, name, word, word_eol, &attrs, HOOK_SERVER_ATTRS);
+	return plugin_hook_run (sess, name, word, word_eol, &attrs, 
+							HOOK_SERVER | HOOK_SERVER_ATTRS);
 }
 
 /* see if any plugins are interested in this print event */
 
 int
-plugin_emit_print (session *sess, char *word[])
-{
-	return plugin_hook_run (sess, word[0], word, NULL, NULL, HOOK_PRINT);
-}
-
-int
-plugin_emit_print_attrs (session *sess, char *word[], time_t server_time)
+plugin_emit_print (session *sess, char *word[], time_t server_time)
 {
 	hexchat_event_attrs attrs;
 
 	attrs.server_time_utc = server_time;
 
-	return plugin_hook_run (sess, word[0], word, NULL, &attrs, HOOK_PRINT_ATTRS);
+	return plugin_hook_run (sess, word[0], word, NULL, &attrs,
+							HOOK_PRINT | HOOK_PRINT_ATTRS);
 }
 
 int
@@ -783,12 +777,27 @@ plugin_insert_hook (hexchat_hook *new_hook)
 {
 	GSList *list;
 	hexchat_hook *hook;
+	int new_hook_type;
+ 
+	switch (new_hook->type)
+	{
+		case HOOK_PRINT:
+		case HOOK_PRINT_ATTRS:
+			new_hook_type = HOOK_PRINT | HOOK_PRINT_ATTRS;
+			break;
+		case HOOK_SERVER:
+		case HOOK_SERVER_ATTRS:
+			new_hook_type = HOOK_SERVER | HOOK_PRINT_ATTRS;
+			break;
+		default:
+			new_hook_type = new_hook->type;
+	}
 
 	list = hook_list;
 	while (list)
 	{
 		hook = list->data;
-		if (hook && hook->type == new_hook->type && hook->pri <= new_hook->pri)
+		if (hook && (hook->type & new_hook_type) && hook->pri <= new_hook->pri)
 		{
 			hook_list = g_slist_insert_before (hook_list, list, new_hook);
 			return;
@@ -1548,7 +1557,8 @@ hexchat_list_int (hexchat_plugin *ph, hexchat_list *xlist, const char *name)
 {
 	guint32 hash = str_hash (name);
 	gpointer data = ph->context;
-	int tmp, type = LIST_CHANNELS;
+	int tmp = 0;
+	int type = LIST_CHANNELS;
 
 	/* a NULL xlist is a shortcut to current "channels" context */
 	if (xlist)
diff --git a/src/common/plugin.h b/src/common/plugin.h
index ee9da8c1..cd3f70a8 100644
--- a/src/common/plugin.h
+++ b/src/common/plugin.h
@@ -170,11 +170,9 @@ int plugin_kill (char *name, int by_filename);
 void plugin_kill_all (void);
 void plugin_auto_load (session *sess);
 int plugin_emit_command (session *sess, char *name, char *word[], char *word_eol[]);
-int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[]);
-int plugin_emit_server_attrs (session *sess, char *name, char *word[],
-							  char *word_eol[], time_t server_time);
-int plugin_emit_print (session *sess, char *word[]);
-int plugin_emit_print_attrs (session *sess, char *word[], time_t server_time);
+int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[],
+						time_t server_time);
+int plugin_emit_print (session *sess, char *word[], time_t server_time);
 int plugin_emit_dummy_print (session *sess, char *name);
 int plugin_emit_keypress (session *sess, unsigned int state, unsigned int keyval, int len, char *string);
 GList* plugin_command_list(GList *tmp_list);
diff --git a/src/common/proto-irc.c b/src/common/proto-irc.c
index eb60a2e6..6d7c7fc1 100644
--- a/src/common/proto-irc.c
+++ b/src/common/proto-irc.c
@@ -1503,9 +1503,6 @@ irc_inline (server *serv, char *buf, int len)
 
 	if (buf[0] == ':')
 	{
-		int eat1;
-		int eat2;
-
 		/* find a context for this message */
 		if (is_channel (serv, word[3]))
 		{
@@ -1520,11 +1517,8 @@ irc_inline (server *serv, char *buf, int len)
 		word[0] = type;
 		word_eol[1] = buf;	/* keep the ":" for plugins */
 
-		eat1 = plugin_emit_server (sess, type, word, word_eol);
-		eat2 = plugin_emit_server_attrs (sess, type, word, word_eol, 
-										 tags_data.timestamp);
-
-		if (eat1 || eat2)
+		if (plugin_emit_server (sess, type, word, word_eol,
+								tags_data.timestamp))
 			goto xit;
 
 		word[1]++;
@@ -1532,16 +1526,10 @@ irc_inline (server *serv, char *buf, int len)
 
 	} else
 	{
-		int eat1;
-		int eat2;
-
 		word[0] = type = word[1];
 
-		eat1 = plugin_emit_server (sess, type, word, word_eol);
-		eat2 = plugin_emit_server_attrs (sess, type, word, word_eol,
-										 tags_data.timestamp);
-
-		if (eat1 || eat2)
+		if (plugin_emit_server (sess, type, word, word_eol,
+								tags_data.timestamp))
 			goto xit;
 	}
 
diff --git a/src/common/servlist.c b/src/common/servlist.c
index c85f893a..46f99c5d 100644
--- a/src/common/servlist.c
+++ b/src/common/servlist.c
@@ -264,6 +264,12 @@ static const struct defaultserver def[] =
 	{0,			"irc.indirectirc.com/+6697"},
 #endif
 	{0,			"irc.indirectirc.com"},
+	
+	{"Interlinked", 0, 0, 0, LOGIN_SASL},
+#ifdef USE_OPENSSL
+	{0,			"irc.interlinked.me/+6697"},
+#endif
+	{0,			"irc.interlinked.me"},
 
 	{"IRCHighWay",	0},
 #ifdef USE_OPENSSL
@@ -457,6 +463,12 @@ static const struct defaultserver def[] =
 #endif
 	{0,			"irc.swiftirc.net/6667"},
 
+	{"Techman's World IRC",		0},
+#ifdef USE_OPENSSL
+	{0,			"irc.techmansworld.com/+6697"},
+#endif
+	{0,			"irc.techmansworld.com/6667"},
+
 	{"TinyCrab", 0, 0, 0, LOGIN_SASL},
 	{0,			"irc.tinycrab.net"},
 
diff --git a/src/common/text.c b/src/common/text.c
index 0b66c5fd..1afb0c18 100644
--- a/src/common/text.c
+++ b/src/common/text.c
@@ -542,7 +542,7 @@ log_create_pathname (char *servname, char *channame, char *netname)
 	/* insert time/date */
 	now = time (NULL);
 	tm = localtime (&now);
-	strftime (fnametime, sizeof (fnametime), fname, tm);
+	strftime_validated (fnametime, sizeof (fnametime), fname, tm);
 
 	/* create final path/filename */
 	if (logmask_is_fullpath ())
@@ -649,14 +649,7 @@ get_stamp_str (char *fmt, time_t tim, char **ret)
 			fmt = loc;
 	}
 
-	len = strftime (dest, sizeof (dest), fmt, localtime (&tim));
-#ifdef WIN32
-	if (!len)
-	{
-		/* use failsafe format until a correct one is specified */
-		len = strftime (dest, sizeof (dest), "[%H:%M:%S]", localtime (&tim));
-	}
-#endif
+	len = strftime_validated (dest, sizeof (dest), fmt, localtime (&tim));
 	if (len)
 	{
 		if (prefs.utf8_locale)
@@ -2081,8 +2074,6 @@ text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
 	int i;
 	unsigned int stripcolor_args = (chanopt_is_set (prefs.hex_text_stripcolor_msg, sess->text_strip) ? 0xFFFFFFFF : 0);
 	char tbuf[NICKLEN + 4];
-	int eat1;
-	int eat2;
 
 	if (prefs.hex_text_color_nicks && (index == XP_TE_CHANACTION || index == XP_TE_CHANMSG))
 	{
@@ -2099,10 +2090,7 @@ text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
 	for (i = 5; i < PDIWORDS; i++)
 		word[i] = "\000";
 
-	eat1 = plugin_emit_print (sess, word);
-	eat2 = plugin_emit_print_attrs (sess, word, timestamp);
-
-	if (eat1 || eat2)
+	if (plugin_emit_print (sess, word, timestamp))
 		return;
 
 	/* If a plugin's callback executes "/close", 'sess' may be invalid */
diff --git a/src/common/userlist.c b/src/common/userlist.c
index 4e3a2615..7565fcd4 100644
--- a/src/common/userlist.c
+++ b/src/common/userlist.c
@@ -150,7 +150,7 @@ userlist_add_hostname (struct session *sess, char *nick, char *hostname,
 				do_rehash = TRUE;
 			user->hostname = strdup (hostname);
 		}
-		if (!user->realname && realname)
+		if (!user->realname && realname && *realname)
 			user->realname = strdup (realname);
 		if (!user->servername && servername)
 			user->servername = strdup (servername);
@@ -418,7 +418,7 @@ userlist_add (struct session *sess, char *name, char *hostname,
 	{
 		if (account && strcmp (account, "*") != 0)
 			user->account = strdup (account);
-		if (realname)
+		if (realname && *realname)
 			user->realname = strdup (realname);
 	}
 
diff --git a/src/common/util.c b/src/common/util.c
index cb6181c4..6e912169 100644
--- a/src/common/util.c
+++ b/src/common/util.c
@@ -25,6 +25,7 @@
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
+#include <time.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 
@@ -2287,3 +2288,80 @@ challengeauth_response (char *username, char *password, char *challenge)
 	return (char *) digest;
 }
 #endif
+
+/**
+* \brief Wrapper around strftime for Windows
+*
+* Prevents crashing when using an invalid format by escaping them.
+*
+* Behaves the same as strftime with the addition that
+* it returns 0 if the escaped format string is too large.
+*
+* Based upon work from znc-msvc project.
+*/
+size_t
+strftime_validated (char *dest, size_t destsize, const char *format, const struct tm *time)
+{
+#ifndef WIN32
+	return strftime (dest, destsize, format, time);
+#else
+	char safe_format[64];
+	const char *p = format;
+	int i = 0;
+
+	if (strlen (format) >= sizeof(safe_format))
+		return 0;
+
+	memset (safe_format, 0, sizeof(safe_format));
+
+	while (*p)
+	{
+		if (*p == '%')
+		{
+			int has_hash = (*(p + 1) == '#');
+			char c = *(p + (has_hash ? 2 : 1));
+
+			if (i >= sizeof (safe_format))
+				return 0;
+
+			switch (c)
+			{
+			case 'a': case 'A': case 'b': case 'B': case 'c': case 'd': case 'H': case 'I': case 'j': case 'm': case 'M':
+			case 'p': case 'S': case 'U': case 'w': case 'W': case 'x': case 'X': case 'y': case 'Y': case 'z': case 'Z':
+			case '%':
+				/* formatting code is fine */
+				break;
+			default:
+				/* replace bad formatting code with itself, escaped, e.g. "%V" --> "%%V" */
+				g_strlcat (safe_format, "%%", sizeof(safe_format));
+				i += 2;
+				p++;
+				break;
+			}
+
+			/* the current loop run will append % (and maybe #) and the next one will do the actual char. */
+			if (has_hash)
+			{
+				safe_format[i] = *p;
+				p++;
+				i++;
+			}
+			if (c == '%')
+			{
+				safe_format[i] = *p;
+				p++;
+				i++;
+			}
+		}
+
+		if (*p)
+		{
+			safe_format[i] = *p;
+			p++;
+			i++;
+		}
+	}
+
+	return strftime (dest, destsize, safe_format, time);
+#endif
+}
diff --git a/src/common/util.h b/src/common/util.h
index 6b8d359c..0c54411b 100644
--- a/src/common/util.h
+++ b/src/common/util.h
@@ -83,5 +83,5 @@ char *encode_sasl_pass_plain (char *user, char *pass);
 char *encode_sasl_pass_blowfish (char *user, char *pass, char *data);
 char *encode_sasl_pass_aes (char *user, char *pass, char *data);
 char *challengeauth_response (char *username, char *password, char *challenge);
-
+size_t strftime_validated (char *dest, size_t destsize, const char *format, const struct tm *time);
 #endif
diff --git a/src/fe-gtk/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj
index 7cc40da8..f7341904 100644
--- a/src/fe-gtk/fe-gtk.vcxproj
+++ b/src/fe-gtk/fe-gtk.vcxproj
@@ -100,8 +100,8 @@
       <Command>

       <![CDATA[

 SET SOLUTIONDIR=$(SolutionDir)..\

-"%PROGRAMFILES%\Common Files\microsoft shared\TextTemplating\12.0\TextTransform.exe" -out hexchat.rc hexchat.rc.tt

-$(DepsRoot)\bin\glib-compile-resources.exe --generate-source --sourcedir $(DataDir) --target "$(ProjectDir)resources.c" "$(DataDir)hexchat.gresource.xml"

+$(TextTransformPath) -out hexchat.rc hexchat.rc.tt

+"$(DepsRoot)\bin\glib-compile-resources.exe" --generate-source --sourcedir $(DataDir) --target "$(ProjectDir)resources.c" "$(DataDir)hexchat.gresource.xml"

       ]]>

       </Command>

       <Message>Build hexchat.rc and gresource file</Message>

diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c
index b42dbc8a..be5448cb 100644
--- a/src/fe-gtk/setup.c
+++ b/src/fe-gtk/setup.c
@@ -2114,7 +2114,6 @@ setup_apply (struct hexchatprefs *pr)
 	PangoFontDescription *old_desc;
 	PangoFontDescription *new_desc;
 	char buffer[4 * FONTNAMELEN + 1];
-	time_t rawtime;
 #endif
 	int new_pix = FALSE;
 	int noapply = FALSE;
@@ -2192,14 +2191,6 @@ setup_apply (struct hexchatprefs *pr)
 	g_free (old_desc);
 	g_free (new_desc);
 	*/
-
-	/* workaround for strftime differences between POSIX and MSVC */
-	time (&rawtime);
-
-	if (!strftime (buffer, sizeof (buffer), prefs.hex_stamp_text_format, localtime (&rawtime)) || !strftime (buffer, sizeof (buffer), prefs.hex_stamp_log_format, localtime (&rawtime)))
-	{
-		fe_message (_("Invalid time stamp format! See the strftime MSDN article for details."), FE_MSG_ERROR);
-	}
 #endif
 
 	if (prefs.hex_irc_real_name[0] == 0)
diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c
index 48d4ec61..f9161b7b 100644
--- a/src/fe-gtk/xtext.c
+++ b/src/fe-gtk/xtext.c
@@ -139,6 +139,8 @@ static void gtk_xtext_search_textentry_fini (gpointer, gpointer);
 static void gtk_xtext_search_fini (xtext_buffer *);
 static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk_xtext_search_flags flags, GError **perr);
 
+/* Avoid warning messages for this unused function */
+#if 0
 /* gives width of a 8bit string - with no mIRC codes in it */
 
 static int
@@ -155,6 +157,7 @@ gtk_xtext_text_width_8bit (GtkXText *xtext, unsigned char *str, int len)
 
 	return width;
 }
+#endif
 
 #define xtext_draw_bg(xt,x,y,w,h) gdk_draw_rectangle(xt->draw_buf, xt->bgc, 1, x, y, w, h);
 
@@ -1319,51 +1322,37 @@ gtk_xtext_timeout_ms (GtkXText *xtext, int pixes)
 	if (apixes < 20) return 20;
 	return 10;
 }
-
 static gint
 gtk_xtext_scrolldown_timeout (GtkXText * xtext)
 {
 	int p_y, win_height;
 	xtext_buffer *buf = xtext->buffer;
 	GtkAdjustment *adj = xtext->adj;
-	textentry *ent;
-
-	if (buf->last_ent_end == NULL)	/* If context has changed */
-	{
-		xtext->scroll_tag = 0;
-		return 0;
-	}
 
 	gdk_window_get_pointer (GTK_WIDGET (xtext)->window, 0, &p_y, 0);
 	gdk_drawable_get_size (GTK_WIDGET (xtext)->window, 0, &win_height);
 
-	if (p_y > win_height &&
-		 xtext->adj->value < (xtext->adj->upper - xtext->adj->page_size))
-	{
-		xtext->adj->value += buf->pagetop_ent->lines_taken;
-		ent = buf->last_ent_end->next;
-		if (ent)
-		{
-			gtk_adjustment_value_changed (xtext->adj);
-		}
-		else
-		{
-			buf->scrollbar_down = TRUE;
-		}
-		xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y - win_height),
-														(GSourceFunc)
-														gtk_xtext_scrolldown_timeout,
-														xtext);
-		xtext->select_start_y -= (adj->value - xtext->select_start_adj) * xtext->fontsize;
-		xtext->select_start_adj = adj->value;
-		gtk_xtext_selection_draw (xtext, NULL, TRUE);
-		gtk_xtext_render_ents (xtext, ent, buf->last_ent_end);
-	}
-	else
+	if (buf->last_ent_end == NULL ||	/* If context has changed OR */
+		 buf->pagetop_ent == NULL ||	/* pagetop_ent is reset OR */
+		 p_y <= win_height ||			/* pointer not below bottom margin OR */
+		 adj->value >= adj->upper - adj->page_size) 	/* we're scrolled to bottom */
 	{
 		xtext->scroll_tag = 0;
+		return 0;
 	}
 
+	adj->value = (int)adj->value;	/* Align to line boundary */
+	xtext->select_start_y -= xtext->fontsize;
+	xtext->select_start_adj++;
+	adj->value++;
+	gtk_adjustment_value_changed (adj);
+	gtk_xtext_selection_draw (xtext, NULL, TRUE);
+	gtk_xtext_render_ents (xtext, buf->pagetop_ent->next, buf->last_ent_end);
+	xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y - win_height),
+													(GSourceFunc)
+													gtk_xtext_scrolldown_timeout,
+													xtext);
+
 	return 0;
 }
 
@@ -1373,39 +1362,30 @@ gtk_xtext_scrollup_timeout (GtkXText * xtext)
 	int p_y;
 	xtext_buffer *buf = xtext->buffer;
 	GtkAdjustment *adj = xtext->adj;
-	textentry *ent;
-
-	if (buf->last_ent_start == NULL)	/* If context has changed */
-	{
-		xtext->scroll_tag = 0;
-		return 0;
-	}
 
 	gdk_window_get_pointer (GTK_WIDGET (xtext)->window, 0, &p_y, 0);
 
-	if (p_y < 0 && adj->value >= 0)
-	{
-		buf->scrollbar_down = FALSE;
-		ent = buf->last_ent_start->prev;
-		if (ent)
-		{
-			adj->value -= ent->lines_taken;
-			gtk_adjustment_value_changed (adj);
-		}
-		xtext->select_start_y -= (adj->value - xtext->select_start_adj) * xtext->fontsize;
-		xtext->select_start_adj = adj->value;
-		gtk_xtext_selection_draw (xtext, NULL, TRUE);
-		gtk_xtext_render_ents (xtext, ent, buf->last_ent_end);
-		xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y),
-														(GSourceFunc)
-														gtk_xtext_scrollup_timeout,
-														xtext);
-	}
-	else
+	if (buf->last_ent_start == NULL ||	/* If context has changed OR */
+		 buf->pagetop_ent == NULL ||		/* pagetop_ent is reset OR */
+		 p_y >= 0 ||							/* not above top margin OR */
+		 adj->value == 0)						/* we're scrolled to the top */
 	{
 		xtext->scroll_tag = 0;
+		return 0;
 	}
 
+	adj->value = (int)adj->value;	/* Align to line boundary */
+	xtext->select_start_y += xtext->fontsize;
+	xtext->select_start_adj--;
+	adj->value--;
+	gtk_adjustment_value_changed (adj);
+	gtk_xtext_selection_draw (xtext, NULL, TRUE);
+	gtk_xtext_render_ents (xtext, buf->pagetop_ent->prev, buf->last_ent_end);
+	xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y),
+													(GSourceFunc)
+													gtk_xtext_scrollup_timeout,
+													xtext);
+
 	return 0;
 }
 
diff --git a/src/fe-text/fe-text.c b/src/fe-text/fe-text.c
index 6f197916..76f93d8d 100644
--- a/src/fe-text/fe-text.c
+++ b/src/fe-text/fe-text.c
@@ -121,7 +121,7 @@ fe_new_window (struct session *sess, int focus)
 static int
 get_stamp_str (time_t tim, char *dest, int size)
 {
-	return strftime (dest, size, prefs.hex_stamp_text_format, localtime (&tim));
+	return strftime_validated (dest, size, prefs.hex_stamp_text_format, localtime (&tim));
 }
 
 static int
diff --git a/win32/hexchat.props b/win32/hexchat.props
index 57f77648..3acfd913 100644
--- a/win32/hexchat.props
+++ b/win32/hexchat.props
@@ -94,6 +94,10 @@ copy "$(HexChatBin)hcsysinfo.dll" "$(HexChatRel)\plugins"
 xcopy /q /s /i "$(HexChatBin)locale" "$(HexChatRel)\share\locale"

 xcopy /q /s /i "$(DepsRoot)\share\locale" "$(HexChatRel)\share\locale"

 		</HexChatCopy>

+		<TextTransformPath Condition="'$(Platform)'=='Win32'">"%PROGRAMFILES%\Common Files\microsoft shared\TextTemplating\12.0\TextTransform.exe"</TextTransformPath>

+		<TextTransformPath Condition="'$(Platform)'=='x64'">"%PROGRAMFILES(x86)%\Common Files\microsoft shared\TextTemplating\12.0\TextTransform.exe"</TextTransformPath>

+		<IsccPath Condition="'$(Platform)'=='Win32'">"%PROGRAMFILES%\Inno Setup 5\iscc.exe"</IsccPath>

+		<IsccPath Condition="'$(Platform)'=='x64'">"%PROGRAMFILES(x86)%\Inno Setup 5\iscc.exe"</IsccPath>

 	</PropertyGroup>

 

 	<ItemDefinitionGroup>

diff --git a/win32/installer/hexchat.iss.tt b/win32/installer/hexchat.iss.tt
index 1f2e1491..1ba55303 100644
--- a/win32/installer/hexchat.iss.tt
+++ b/win32/installer/hexchat.iss.tt
@@ -4,6 +4,9 @@
 ;#define APPARCH "x64"
 ;#define PROJECTDIR "C:\...\hexchat\win32\installer\"
 
+;http://mitrich.net23.net/?/inno-download-plugin.html
+#include <idp.iss>
+
 [Setup]
 AppName=HexChat
 AppVersion={#APPVER}
@@ -57,8 +60,9 @@ Name: "custom"; Description: "Custom Installation"; Flags: iscustom
 Name: "libs"; Description: "HexChat"; Types: normal minimal custom; Flags: fixed
 Name: "gtktheme"; Description: "GTK+ Theme"; Types: normal custom; Flags: disablenouninstallwarning
 Name: "xctext"; Description: "HexChat-Text"; Types: custom; Flags: disablenouninstallwarning
-Name: "xtm"; Description: "HexChat Theme Manager (Requires .NET 4.0)"; Types: normal custom; Flags: disablenouninstallwarning
+Name: "xtm"; Description: "HexChat Theme Manager"; Types: normal custom; Flags: disablenouninstallwarning
 Name: "translations"; Description: "Translations"; Types: normal custom; Flags: disablenouninstallwarning
+Name: "spell"; Description: "Spelling Dictionaries"; Types: custom; Flags: disablenouninstallwarning
 Name: "plugins"; Description: "Plugins"; Types: custom; Flags: disablenouninstallwarning
 Name: "plugins\checksum"; Description: "Checksum"; Types: custom; Flags: disablenouninstallwarning
 Name: "plugins\dns"; Description: "DNS"; Types: custom; Flags: disablenouninstallwarning
@@ -76,7 +80,7 @@ Name: "langs\python\python2"; Description: "Python (requires Python 2.7)"; Types
 Name: "langs\python\python3"; Description: "Python (requires Python 3.3)"; Types: custom; Flags: disablenouninstallwarning exclusive
 
 [Tasks]
-Name: portable; Description: "Yes"; GroupDescription: "Portable Install (no Registry entries, no Start Menu icons, no uninstaller):"; Flags: unchecked
+Name: portable; Description: "Yes"; GroupDescription: "Portable Mode: Stores configuration files within install directory for portable drives."; Flags: unchecked
 
 [Registry]
 Root: HKCR; Subkey: "irc"; ValueType: none; ValueName: ""; ValueData: ""; Flags: deletekey uninsdeletekey; Tasks: not portable
@@ -95,12 +99,12 @@ Root: HKCR; Subkey: ".hct\shell\open\command"; ValueType: string; ValueName: "";
 
 [Run]
 Filename: "{app}\hexchat.exe"; Description: "Run HexChat after closing the Wizard"; Flags: nowait postinstall skipifsilent
-Filename: "https://www.microsoft.com/en-us/download/details.aspx?id=39315"; Description: "Download Visual C++ 2013 Redistributable"; Flags: shellexec runasoriginaluser postinstall skipifsilent
-Filename: "http://www.microsoft.com/en-us/download/details.aspx?id=17851"; Description: "Download .NET 4.0 for theme manager"; Components: xtm; Flags: shellexec runasoriginaluser postinstall skipifsilent
 Filename: "http://docs.hexchat.org/en/latest/changelog.html"; Description: "See what's changed"; Flags: shellexec runasoriginaluser postinstall skipifsilent unchecked
-Filename: "http://hexchat.org/downloads.html"; Description: "Download Perl"; Flags: shellexec runasoriginaluser postinstall skipifsilent unchecked; Components: langs\perl and not langs\python
-Filename: "http://hexchat.org/downloads.html"; Description: "Download Python"; Flags: shellexec runasoriginaluser postinstall skipifsilent unchecked; Components: langs\python and not langs\perl
-Filename: "http://hexchat.org/downloads.html"; Description: "Download Perl and Python"; Flags: shellexec runasoriginaluser postinstall skipifsilent unchecked; Components: langs\perl and langs\python
+Filename: "{tmp}\vcredist.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "Installing Visual C++ Redist"; Flags: skipifdoesntexist; Tasks: not portable
+Filename: "{tmp}\dotnet4.exe"; Parameters: "/q /norestart"; StatusMsg: "Installing .NET"; Components: xtm; Flags: skipifdoesntexist; Tasks: not portable
+Filename: "{tmp}\perl.msi"; StatusMsg: "Installing Perl"; Components: langs\perl; Flags: shellexec skipifdoesntexist; Tasks: not portable
+Filename: "{tmp}\python.msi"; StatusMsg: "Installing Python"; Components: langs\python; Flags: shellexec skipifdoesntexist; Tasks: not portable
+Filename: "{tmp}\spelling-dicts.exe"; Parameters: "/verysilent"; StatusMsg: "Installing Spelling Dictionaries"; Components: spell; Flags: skipifdoesntexist; Tasks: not portable
 
 [Files]
 Source: "portable-mode"; DestDir: "{app}"; Tasks: portable
@@ -182,8 +186,126 @@ BeveledLabel= {#APPNAM}
 procedure InitializeWizard;
 begin
 	WizardForm.LicenseAcceptedRadio.Checked := True;
+
+	idpDownloadAfter(wpReady);
+end;
+
+/////////////////////////////////////////////////////////////////////
+function GetSysDir(): String;
+begin
+#if APPARCH != "x64"
+	if IsWin64 then
+		Result := ExpandConstant('{syswow64}\')
+	else
+		Result := ExpandConstant('{sys}\');
+#else
+	Result := ExpandConstant('{sys}\');
+#endif
+end;
+
+/////////////////////////////////////////////////////////////////////
+function CheckDLL(DLLName: String): Boolean;
+var
+	ResultCode: Integer;
+begin
+	if ExecAsOriginalUser(GetSysDir() + 'where.exe', '/Q ' + DLLName,
+						'', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
+		Result := ResultCode = 0 // 0 is success
+	else
+		Result := False;
+end;
+
+/////////////////////////////////////////////////////////////////////
+function CheckVCInstall(): Boolean;
+begin
+	Result := FileExists(GetSysDir() + 'msvcr120.dll');;
+end;
+
+/////////////////////////////////////////////////////////////////////
+function CheckSpellInstall(): Boolean;
+begin
+	Result := DirExists(ExpandConstant('{localappdata}') + '\enchant');;
+end;
+
+/////////////////////////////////////////////////////////////////////
+function CheckDotNetInstall(): Boolean;
+begin
+	Result := RegKeyExists(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4');
 end;
 
+/////////////////////////////////////////////////////////////////////
+// Sets up the automatic downloads
+/////////////////////////////////////////////////////////////////////
+procedure CurPageChanged(CurPageID: Integer);
+var
+	REDIST: String;
+	PERL: String;
+	PY2: String;
+	PY3: String;
+	DOTNET: String;
+	SPELL: String;
+begin
+  if(CurPageID = wpReady) then
+  begin
+    idpClearFiles;
+	
+	if not IsTaskSelected('portable') then
+	begin
+	
+#if APPARCH == "x64"
+		REDIST := 'http://dl.hexchat.net/misc/vcredist_2013_x64.exe';
+		PERL := 'http://dl.hexchat.net/misc/perl/Perl%205.18.0%20x64.msi';
+		PY2 := 'http://python.org/ftp/python/2.7.5/python-2.7.5.amd64.msi';
+		PY3 := 'http://python.org/ftp/python/3.3.2/python-3.3.2.amd64.msi';
+#else
+		REDIST := 'http://dl.hexchat.net/misc/vcredist_2013_x86.exe';
+		PERL := 'http://dl.hexchat.net/misc/perl/Perl%205.18.0%20x86.msi';
+		PY2 := 'http://python.org/ftp/python/2.7.5/python-2.7.5.msi';
+		PY3 := 'http://python.org/ftp/python/3.3.2/python-3.3.2.msi';
+#endif
+		DOTNET := 'http://dl.hexchat.net/misc/dotnet_40.exe';
+		SPELL := 'http://dl.hexchat.net/hexchat/HexChat%20Spelling%20Dictionaries%20r2.exe';
+
+		if not CheckVCInstall() then
+			idpAddFile(REDIST, ExpandConstant('{tmp}\vcredist.exe'));
+
+		if IsComponentSelected('xtm') and not CheckDotNetInstall() then
+			idpAddFile(DOTNET, ExpandConstant('{tmp}\dotnet4.exe'));
+
+		if IsComponentSelected('spell') and not CheckSpellInstall() then
+			idpAddFile(SPELL, ExpandConstant('{tmp}\spelling-dicts.exe'));
+			
+		if IsComponentSelected('langs\perl') and not CheckDLL('perl518.dll') then
+			idpAddFile(PERL, ExpandConstant('{tmp}\perl.msi'));
+			
+		if IsComponentSelected('langs\python\python2') and not CheckDLL('python27.dll') then
+			idpAddFile(PY2, ExpandConstant('{tmp}\python.msi'));
+			
+		if IsComponentSelected('langs\python\python3') and not CheckDLL('python33.dll') then
+			idpAddFile(PY3, ExpandConstant('{tmp}\python.msi'));
+	end;
+  end;
+end;
+
+/////////////////////////////////////////////////////////////////////
+// Disable portable-mode if installing to program files
+/////////////////////////////////////////////////////////////////////
+function NextButtonClick(CurPageID: Integer): Boolean;
+begin
+	if (CurPageID = wpSelectTasks) then
+		if (WizardForm.TasksList.Checked[1] = True) then
+#if APPARCH == "x64"
+			if (WizardDirValue() = ExpandConstant('{pf64}\HexChat')) then
+#else
+			if (WizardDirValue() = ExpandConstant('{pf32}\HexChat')) then
+#endif
+			begin
+				WizardForm.TasksList.Checked[1] := False
+				MsgBox('Portable mode is only intended for use on portable drives and has been disabled.', mbInformation, MB_OK)
+			end;
+
+	Result := True; // Always continue
+end;
 
 /////////////////////////////////////////////////////////////////////
 // these are required for x86->x64 or reverse upgrades
@@ -211,7 +333,6 @@ begin
 	Result := sUnInstallString;
 end;
 
-
 /////////////////////////////////////////////////////////////////////
 function IsUpgrade(): Boolean;
 begin
diff --git a/win32/installer/installer.vcxproj b/win32/installer/installer.vcxproj
index 453e87bd..09654ab0 100644
--- a/win32/installer/installer.vcxproj
+++ b/win32/installer/installer.vcxproj
@@ -64,10 +64,10 @@
       <Command>

       <![CDATA[

 SET SOLUTIONDIR=$(SolutionDir)..\

-"%PROGRAMFILES%\Common Files\microsoft shared\TextTemplating\12.0\TextTransform.exe" -out "%SOLUTIONDIR%win32\installer\hexchat.iss" "%SOLUTIONDIR%win32\installer\hexchat.iss.tt"

+$(TextTransformPath) -out "%SOLUTIONDIR%win32\installer\hexchat.iss" "%SOLUTIONDIR%win32\installer\hexchat.iss.tt"

 del "$(OutDir)hexchat.iss"

 type hexchat.iss >> "$(OutDir)hexchat.iss"

-"$(ProgramFiles)\Inno Setup 5\iscc.exe" /dPROJECTDIR="$(ProjectDir)" /dAPPARCH="$(Platform)" "$(OutDir)hexchat.iss"

+$(IsccPath) /dPROJECTDIR="$(ProjectDir)" /dAPPARCH="$(Platform)" "$(OutDir)hexchat.iss"

       ]]>

       </Command>

     </PreBuildEvent>