/*
 * slashexec - A CLI for libpurple clients
 * Copyright (C) 2004-2008 Gary Kramlich
 * Copyright (C) 2005-2008 Peter Lawler
 * Copyright (C) 2005-2008 Daniel Atallah
 * Copyright (C) 2005-2008 John Bailey
 * Copyright (C) 2006-2008 Sadrul Habib Chowdhury
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
 */

/* XXX: Translations of strings - Several strings are NOT marked as needing to
 * be translated.  This is because they go only to the debug window.  I only
 * care to translate messages users will see in the main interface.  Debug
 * window messages are not important. - rekkanoryo */

/* If you can't figure out what this line is for, DON'T TOUCH IT. */
#include "../common/pp_internal.h"

#include <stdarg.h>
#include <string.h>

#ifndef _WIN32
# include <pwd.h>
# include <unistd.h>
#endif

#include <cmds.h>
#include <conversation.h>
#include <debug.h>
#include <notify.h>
#include <plugin.h>
#include <util.h>

#ifdef _WIN32
/* Windows 2000 and earlier allow only 2047 bytes in an argv vector for cmd.exe
 * so we need to make sure we don't exceed that. 2036 allows "cmd.exe /c " to
 * fit inside the vector. */
# define MAX_CMD_LEN 2036
#else
/* Some unixes allow only 8192 chars in an argv vector; allow some space for
 * the shell command itself to fit in the vector.  Besides, if anyone is
 * crazy enough to use /exec for a command this long they need shot to 
 * begin with, or educated on the beauty of shell scripts. */
# define MAX_CMD_LEN 8000
#endif

#define PREF_PREFIX		"/plugins/core/slashexec"
#define PREF_SLASH		PREF_PREFIX "/slash"
#define PREF_BANG		PREF_PREFIX "/bang"

static PurpleCmdId se_cmd;
static gchar *shell;

static void /* replace a character at the end of a string with' \0' */
se_replace_ending_char(gchar *string, gchar replace)
{
	gint stringlen;
	gchar *replace_in_string;

	stringlen = strlen(string);
	replace_in_string = g_utf8_strrchr(string, -1, replace);

	/* continue to replace the character at the end of the string until the
	 * current last character in the string is not the character that needs to
	 * be replaced. */
	while(replace_in_string && replace_in_string == &(string[stringlen - 1])) {
		purple_debug_info("slashexec", "Replacing %c at position %d\n",
				replace, stringlen - 1);

		/* a \0 is the logical end of the string, even if characters exist in
		 * the string after it.  Replacing the bad character with a \0 causes
		 * the string to technically be shortened by one character even though
		 * the allocated size of the string doesn't change. */
		*replace_in_string = '\0';

		/* update the string length due to the new \0 */
		stringlen = strlen(string);

		/* find, if one exists, another instance of the bad character */
		replace_in_string = g_utf8_strrchr(string, -1, replace);
	}

	return;
}

static gchar *
se_strdelimit(gchar *string, gchar newdelim)
{ /* This function borrowed and tweaked from glib to suit my purposes */

	/* these are the decimal representations of the ascii control characters that
	 * we need to remove to prevent bad behavior, such as XMPP disconnects */
	gchar delimiters[] = { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17,
						18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 };
	gchar *c;

	g_return_val_if_fail(string !=NULL, NULL);

	for(c = string; *c; c = g_utf8_next_char(c))
		if(strchr(delimiters, *c))
			*c = newdelim;

	return string;
}

static gboolean
se_do_action(PurpleConversation *conv, gchar *args, gboolean send)
{
	GError *parse_error = NULL, *exec_error = NULL;
	gchar *spawn_cmd = NULL, **cmd_argv = NULL, *cmd_stdout = NULL,
		  *cmd_stderr = NULL;
	gint cmd_argc = 0;
#ifdef _WIN32
	const gchar *shell_flag = "/c";
#else
	const gchar *shell_flag = "-c";
#endif

	/* we can still end up with 0 "args", and we need to make sure we don't
	 * go over the max command length */
	if(strlen(args) == 0 || strlen(args) > MAX_CMD_LEN)
		return FALSE;

	/* remove trailing \ characters to prevent some nasty crashes */
	se_replace_ending_char(args, '\\');

#ifndef _WIN32
	/* escape remaining special characters; this is to ensure the shell gets
	 * what the user actually typed. */
	args = g_strescape(args, "");
#endif

	/* if we get this far with a NULL, there's a problem. */
	if(!args) {
		purple_debug_info("slashexec", "args NULL!\n");
		
		return FALSE;
	}

	/* make sure the string passes the UTF8 validation in glib */
	if(!g_utf8_validate(args, -1, NULL)) {
		purple_debug_info("slashexec", "invalid UTF8: %s\n",
				args ? args : "null");

		return FALSE;
	}

#ifdef _WIN32
	spawn_cmd = g_strdup_printf("%s %s %s", shell, shell_flag, args);
#else
	spawn_cmd = g_strdup_printf("%s %s \"%s\"", shell, shell_flag, args);
#endif

	/* We need the command parsed into a proper argv for it to be executed */
	if(!g_shell_parse_argv(spawn_cmd, &cmd_argc, &cmd_argv, &parse_error)) {
		/* everything in here is error checking and information for the user */

		char *errmsg = NULL;

		/* the command string isn't NULL, so give it to the user */
		if(spawn_cmd) {
			errmsg = g_strdup_printf(_("Unable to parse \"%s\""), spawn_cmd);

			purple_debug_info("slashexec", "%s\n", errmsg);
			purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
					time(NULL));

			g_free(errmsg);
		}

		/* the GError isn't NULL, so give its information to the user */
		if(parse_error) {
			errmsg = g_strdup_printf(_("Parse error message: %s"),
					parse_error->message ? parse_error->message : "null");

			purple_debug_info("slashexec", "%s\n", errmsg);
			purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
					time(NULL));

			g_free(errmsg);
			g_error_free(parse_error);
		}

		if(cmd_argv)
			g_strfreev(cmd_argv);

		return FALSE;
	}

	/* I may eventually add a pref to show this to the user; for now it's fine
	 * going just to the debug bucket */
	purple_debug_info("slashexec", "Spawn command: %s\n", spawn_cmd);

	/* now we actually execute the command.  everything inside the block
	 * is error checking and information for the user. */
	if(!g_spawn_sync(NULL, cmd_argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
				&cmd_stdout, &cmd_stderr, NULL, &exec_error))
	{
		char *errmsg = NULL;

		/* the command isn't NULL, so let the user see it */
		if(spawn_cmd) {
			errmsg = g_strdup_printf(_("Unable to execute \"%s\""),	spawn_cmd);

			purple_debug_info("slashexec", "%s\n", errmsg);
			purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
					time(NULL));

			g_free(errmsg);
		}

		/* the GError isn't NULL, so let's show the user its information */
		if(exec_error) {
			errmsg = g_strdup_printf(_("Execute error message: %s"),
					exec_error->message ? exec_error->message : "NULL");

			purple_debug_info("slashexec", "%s\n", errmsg);
			purple_conversation_write(conv, NULL, errmsg,	PURPLE_MESSAGE_SYSTEM,
					time(NULL));

			g_free(errmsg);
			g_error_free(exec_error);
		}

		g_free(cmd_stdout);
		g_free(cmd_stderr);
		g_strfreev(cmd_argv);
		
		return FALSE;
	}

	/* if we get this far then the command actually executed */
	if(parse_error)
		g_error_free(parse_error);

	if(exec_error)
		g_error_free(exec_error);

	if(cmd_stderr)
		purple_debug_info("slashexec", "command stderr: %s\n", cmd_stderr);

	g_strfreev(cmd_argv);
	g_free(cmd_stderr);

	if(cmd_stdout && strcmp(cmd_stdout, "") && strcmp(cmd_stdout, "\n")) {
		g_strchug(cmd_stdout);

		if(g_str_has_suffix(cmd_stdout, "\n"))
			cmd_stdout[strlen(cmd_stdout) - 1] = '\0';

		if(send) {
			purple_debug_info("slashexec", "Command stdout: %s\n", cmd_stdout);

			if(!g_utf8_validate(cmd_stdout, -1, NULL)) {
				purple_debug_error("slashexec", "Output failed UTF-8 verification!\n");

				return FALSE;
			} else {
				cmd_stdout = se_strdelimit(cmd_stdout, ' ');

				g_strstrip(cmd_stdout);

				purple_debug_info("slashexec", "Sanitized command stdout: %s\n", cmd_stdout);
			}

			switch(purple_conversation_get_type(conv)) {
				case PURPLE_CONV_TYPE_IM:
					purple_conv_im_send(PURPLE_CONV_IM(conv), cmd_stdout);
					break;
				case PURPLE_CONV_TYPE_CHAT:
					purple_conv_chat_send(PURPLE_CONV_CHAT(conv), cmd_stdout);
					break;
				default:
					return FALSE;
			}

		} else
			purple_conversation_write(conv, NULL, cmd_stdout, PURPLE_MESSAGE_SYSTEM,
					time(NULL));
	} else {
		purple_debug_info("slashexec", "Error executing \"%s\"\n", spawn_cmd);
		purple_conversation_write(conv, NULL,
				_("There was an error executing your command."),
				PURPLE_MESSAGE_SYSTEM, time(NULL));
			
		g_free(spawn_cmd);
		g_free(cmd_stdout);

		return FALSE;
	}
	
	g_free(cmd_stdout);
	g_free(spawn_cmd);

	return TRUE;
}

static PurpleCmdRet
se_cmd_cb(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error,
			gpointer data)
{
	gboolean send = FALSE;
	char *string = args[0];

	if(!purple_prefs_get_bool(PREF_SLASH))
		return PURPLE_CMD_RET_CONTINUE;

	if(string && !strncmp(string, "-o ", 3)) {
		send = TRUE;
		string += 3;
	}

	if(se_do_action(conv, string, send))
		return PURPLE_CMD_RET_OK;
	else
		return PURPLE_CMD_RET_FAILED;
}

static void
se_sending_msg_helper(PurpleConversation *conv, char **message)
{
	char *string = *message, *strip = NULL;
	gboolean send = TRUE;

	if(conv == NULL || !purple_prefs_get_bool(PREF_BANG)) return;

	strip = purple_markup_strip_html(string);

	if(strip && *strip != '!') {
		g_free(strip);
		return;
	}

	*message = NULL;

	g_free(string);

	/* this is refactored quite a bit to simplify things since sending-im-msg
	 * allows changing the text that will be sent by simply giving *message a
	 * new pointer */
	if(strncmp(strip, "!!!", 3) == 0) {
		char *new_msg, *conv_sys_msg;
		
		new_msg = g_strdup(strip + 2);

		*message = new_msg;

		/* I really want to eventually make this cleaner, like by making it
		 * change the actual message that gets printed to the conv window... */
		conv_sys_msg = g_strdup_printf(_("The following text was sent: %s"),
				new_msg);

		purple_conversation_write(conv, NULL, conv_sys_msg, PURPLE_MESSAGE_SYSTEM,
				time(NULL));

		g_free(strip);
		g_free(conv_sys_msg);

		return;
	}

	if(*(strip + 1) == '!')
		send = FALSE;

	if(send)
		se_do_action(conv, strip + 1, send);
	else
		se_do_action(conv, strip + 2, send);

	g_free(strip);
}

static void
se_sending_chat_msg_cb(PurpleAccount *account, char **message, int id)
{
	PurpleConversation *conv;
	if (message == NULL || *message == NULL)  /* It's possible if some other callback to
	                                           to the signal resets the message */
		return;
	conv = purple_find_chat(account->gc, id);
	se_sending_msg_helper(conv, message);
}

static void
se_sending_im_msg_cb(PurpleAccount *account, const char *who, char **message)
{
	PurpleConversation *conv;
	if (message == NULL || *message == NULL)  /* It's possible if some other callback to
												 to the signal resets the message */
		return;
	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account);
	se_sending_msg_helper(conv, message);
}

static gboolean
se_load(PurplePlugin *plugin) {
#ifndef _WIN32
	struct passwd *pw = NULL;
#endif /* _WIN32 */
	const gchar *help = _("exec [-o] &lt;command&gt;, runs the command.\n"
						"If the -o flag is used then output is sent to the"
						"current conversation; otherwise it is printed to the "
						"current text box.");

	se_cmd = purple_cmd_register("exec", "s", PURPLE_CMD_P_PLUGIN,
								PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_CHAT, NULL,
								se_cmd_cb, help, NULL);

	/* these signals are needed for the bangexec features we took on */
	purple_signal_connect(purple_conversations_get_handle(), "sending-im-msg", plugin,
				PURPLE_CALLBACK(se_sending_im_msg_cb), NULL);
	purple_signal_connect(purple_conversations_get_handle(), "sending-chat-msg", plugin,
				PURPLE_CALLBACK(se_sending_chat_msg_cb), NULL);

	/* this gets the user's shell.  If this is built on Windows, force cmd.exe,
	 * which will make this plugin not work on Windows 98/ME */
#ifdef _WIN32
	shell = g_strdup("cmd.exe");
#else
	pw = getpwuid(getuid());
	
	if(pw)
		if(pw->pw_shell)
			shell = g_strdup(pw->pw_shell);
		else
			shell = g_strdup("/bin/sh");
	else
		shell = g_strdup("/bin/sh");
#endif /* _WIN32 */

	return TRUE;
}

static gboolean
se_unload(PurplePlugin *plugin) {
	purple_cmd_unregister(se_cmd);

	g_free(shell);

	return TRUE;
}

static PurplePluginPrefFrame *
get_plugin_pref_frame(PurplePlugin *plugin)
{
	PurplePluginPrefFrame *frame;
	PurplePluginPref *pref;

	frame = purple_plugin_pref_frame_new();

	pref = purple_plugin_pref_new_with_label(_("Execute commands starting with: "));
	purple_plugin_pref_frame_add(frame, pref);

	pref = purple_plugin_pref_new_with_name_and_label(PREF_SLASH,
					_("/exec Command (/exec someCommand)"));
	purple_plugin_pref_frame_add(frame, pref);

	pref = purple_plugin_pref_new_with_name_and_label(PREF_BANG,
					_("Exclamation point (!someCommand)"));
	purple_plugin_pref_frame_add(frame, pref);

	return frame;
}

static PurplePluginUiInfo prefs_info = {
	get_plugin_pref_frame,
	0,
	NULL,

	/* padding */
	NULL,
	NULL,
	NULL,
	NULL
};

static PurplePluginInfo se_info = {
	PURPLE_PLUGIN_MAGIC,
	PURPLE_MAJOR_VERSION,
	PURPLE_MINOR_VERSION,
	PURPLE_PLUGIN_STANDARD,								/* type				*/
	NULL,												/* ui requirement	*/
	0,													/* flags			*/
	NULL,												/* dependencies		*/
	PURPLE_PRIORITY_DEFAULT,							/* priority			*/
	"core-plugin_pack-slashexec",						/* id				*/
	"/exec",											/* name				*/
	PP_VERSION,											/* version			*/
	NULL,												/* summary			*/
	NULL,												/* description		*/

	"Gary Kramlich <grim@reaperworld.com>,\n"
	"Peter Lawler <bleeter from users.sf.net>,\n"
	"Daniel Atallah,\nSadrul Habib Chowdhury <sadrul@users.sf.net>,\n"
	"John Bailey <rekkanoryo@rekkanoryo.org>",			/* authors			*/

	PP_WEBSITE,											/* homepage			*/
	se_load,											/* load				*/
	se_unload,											/* unload			*/
	NULL,												/* destroy			*/
	NULL,												/* ui info			*/
	NULL,												/* extra info		*/
	&prefs_info,										/* prefs info		*/
	NULL,												/* actions info		*/
	NULL,
	NULL,
	NULL,
	NULL
};

static void
init_plugin(PurplePlugin *plugin) {
#ifdef ENABLE_NLS
	bindtextdomain(GETTEXT_PACKAGE, PP_LOCALEDIR);
	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
#endif /* ENABLE_NLS */
	se_info.summary = _("/exec a la UNIX IRC CLI");
	se_info.description = _("A plugin that adds the /exec command line"
							" interpreter like most UNIX/Linux IRC"
							" clients have.  Also included is the ability to"
							" execute commands with an exclamation point"
							" (!uptime, for instance).\n");
	purple_prefs_add_none(PREF_PREFIX);
	purple_prefs_add_bool(PREF_SLASH, TRUE);
	purple_prefs_add_bool(PREF_BANG, FALSE);
}

PURPLE_INIT_PLUGIN(slashexec, init_plugin, se_info)
