1 /*
2 * slashexec - A CLI for libpurple clients
3 * Copyright (C) 2004-2008 Gary Kramlich
4 * Copyright (C) 2005-2008 Peter Lawler
5 * Copyright (C) 2005-2008 Daniel Atallah
6 * Copyright (C) 2005-2008 John Bailey
7 * Copyright (C) 2006-2008 Sadrul Habib Chowdhury
8 *
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 2
12 * of the License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
22 */
23
24 /* XXX: Translations of strings - Several strings are NOT marked as needing to
25 * be translated. This is because they go only to the debug window. I only
26 * care to translate messages users will see in the main interface. Debug
27 * window messages are not important. - rekkanoryo */
28
29 /* If you can't figure out what this line is for, DON'T TOUCH IT. */
30 #include "../common/pp_internal.h"
31
32 #include <stdarg.h>
33 #include <string.h>
34
35 #ifndef _WIN32
36 # include <pwd.h>
37 # include <unistd.h>
38 #endif
39
40 #include <cmds.h>
41 #include <conversation.h>
42 #include <debug.h>
43 #include <notify.h>
44 #include <plugin.h>
45 #include <util.h>
46
47 #ifdef _WIN32
48 /* Windows 2000 and earlier allow only 2047 bytes in an argv vector for cmd.exe
49 * so we need to make sure we don't exceed that. 2036 allows "cmd.exe /c " to
50 * fit inside the vector. */
51 # define MAX_CMD_LEN 2036
52 #else
53 /* Some unixes allow only 8192 chars in an argv vector; allow some space for
54 * the shell command itself to fit in the vector. Besides, if anyone is
55 * crazy enough to use /exec for a command this long they need shot to
56 * begin with, or educated on the beauty of shell scripts. */
57 # define MAX_CMD_LEN 8000
58 #endif
59
60 #define PREF_PREFIX "/plugins/core/slashexec"
61 #define PREF_SLASH PREF_PREFIX "/slash"
62 #define PREF_BANG PREF_PREFIX "/bang"
63
64 static PurpleCmdId se_cmd;
65 static gchar *shell;
66
67 static void /* replace a character at the end of a string with' \0' */
se_replace_ending_char(gchar * string,gchar replace)68 se_replace_ending_char(gchar *string, gchar replace)
69 {
70 gint stringlen;
71 gchar *replace_in_string;
72
73 stringlen = strlen(string);
74 replace_in_string = g_utf8_strrchr(string, -1, replace);
75
76 /* continue to replace the character at the end of the string until the
77 * current last character in the string is not the character that needs to
78 * be replaced. */
79 while(replace_in_string && replace_in_string == &(string[stringlen - 1])) {
80 purple_debug_info("slashexec", "Replacing %c at position %d\n",
81 replace, stringlen - 1);
82
83 /* a \0 is the logical end of the string, even if characters exist in
84 * the string after it. Replacing the bad character with a \0 causes
85 * the string to technically be shortened by one character even though
86 * the allocated size of the string doesn't change. */
87 *replace_in_string = '\0';
88
89 /* update the string length due to the new \0 */
90 stringlen = strlen(string);
91
92 /* find, if one exists, another instance of the bad character */
93 replace_in_string = g_utf8_strrchr(string, -1, replace);
94 }
95
96 return;
97 }
98
99 static gchar *
se_strdelimit(gchar * string,gchar newdelim)100 se_strdelimit(gchar *string, gchar newdelim)
101 { /* This function borrowed and tweaked from glib to suit my purposes */
102
103 /* these are the decimal representations of the ascii control characters that
104 * we need to remove to prevent bad behavior, such as XMPP disconnects */
105 gchar delimiters[] = { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17,
106 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 };
107 gchar *c;
108
109 g_return_val_if_fail(string !=NULL, NULL);
110
111 for(c = string; *c; c = g_utf8_next_char(c))
112 if(strchr(delimiters, *c))
113 *c = newdelim;
114
115 return string;
116 }
117
118 static gboolean
se_do_action(PurpleConversation * conv,gchar * args,gboolean send)119 se_do_action(PurpleConversation *conv, gchar *args, gboolean send)
120 {
121 GError *parse_error = NULL, *exec_error = NULL;
122 gchar *spawn_cmd = NULL, **cmd_argv = NULL, *cmd_stdout = NULL,
123 *cmd_stderr = NULL;
124 gint cmd_argc = 0;
125 #ifdef _WIN32
126 const gchar *shell_flag = "/c";
127 #else
128 const gchar *shell_flag = "-c";
129 #endif
130
131 /* we can still end up with 0 "args", and we need to make sure we don't
132 * go over the max command length */
133 if(strlen(args) == 0 || strlen(args) > MAX_CMD_LEN)
134 return FALSE;
135
136 /* remove trailing \ characters to prevent some nasty crashes */
137 se_replace_ending_char(args, '\\');
138
139 #ifndef _WIN32
140 /* escape remaining special characters; this is to ensure the shell gets
141 * what the user actually typed. */
142 args = g_strescape(args, "");
143 #endif
144
145 /* if we get this far with a NULL, there's a problem. */
146 if(!args) {
147 purple_debug_info("slashexec", "args NULL!\n");
148
149 return FALSE;
150 }
151
152 /* make sure the string passes the UTF8 validation in glib */
153 if(!g_utf8_validate(args, -1, NULL)) {
154 purple_debug_info("slashexec", "invalid UTF8: %s\n",
155 args ? args : "null");
156
157 return FALSE;
158 }
159
160 #ifdef _WIN32
161 spawn_cmd = g_strdup_printf("%s %s %s", shell, shell_flag, args);
162 #else
163 spawn_cmd = g_strdup_printf("%s %s \"%s\"", shell, shell_flag, args);
164 #endif
165
166 /* We need the command parsed into a proper argv for it to be executed */
167 if(!g_shell_parse_argv(spawn_cmd, &cmd_argc, &cmd_argv, &parse_error)) {
168 /* everything in here is error checking and information for the user */
169
170 char *errmsg = NULL;
171
172 /* the command string isn't NULL, so give it to the user */
173 if(spawn_cmd) {
174 errmsg = g_strdup_printf(_("Unable to parse \"%s\""), spawn_cmd);
175
176 purple_debug_info("slashexec", "%s\n", errmsg);
177 purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
178 time(NULL));
179
180 g_free(errmsg);
181 }
182
183 /* the GError isn't NULL, so give its information to the user */
184 if(parse_error) {
185 errmsg = g_strdup_printf(_("Parse error message: %s"),
186 parse_error->message ? parse_error->message : "null");
187
188 purple_debug_info("slashexec", "%s\n", errmsg);
189 purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
190 time(NULL));
191
192 g_free(errmsg);
193 g_error_free(parse_error);
194 }
195
196 if(cmd_argv)
197 g_strfreev(cmd_argv);
198
199 return FALSE;
200 }
201
202 /* I may eventually add a pref to show this to the user; for now it's fine
203 * going just to the debug bucket */
204 purple_debug_info("slashexec", "Spawn command: %s\n", spawn_cmd);
205
206 /* now we actually execute the command. everything inside the block
207 * is error checking and information for the user. */
208 if(!g_spawn_sync(NULL, cmd_argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
209 &cmd_stdout, &cmd_stderr, NULL, &exec_error))
210 {
211 char *errmsg = NULL;
212
213 /* the command isn't NULL, so let the user see it */
214 if(spawn_cmd) {
215 errmsg = g_strdup_printf(_("Unable to execute \"%s\""), spawn_cmd);
216
217 purple_debug_info("slashexec", "%s\n", errmsg);
218 purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
219 time(NULL));
220
221 g_free(errmsg);
222 }
223
224 /* the GError isn't NULL, so let's show the user its information */
225 if(exec_error) {
226 errmsg = g_strdup_printf(_("Execute error message: %s"),
227 exec_error->message ? exec_error->message : "NULL");
228
229 purple_debug_info("slashexec", "%s\n", errmsg);
230 purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
231 time(NULL));
232
233 g_free(errmsg);
234 g_error_free(exec_error);
235 }
236
237 g_free(cmd_stdout);
238 g_free(cmd_stderr);
239 g_strfreev(cmd_argv);
240
241 return FALSE;
242 }
243
244 /* if we get this far then the command actually executed */
245 if(parse_error)
246 g_error_free(parse_error);
247
248 if(exec_error)
249 g_error_free(exec_error);
250
251 if(cmd_stderr)
252 purple_debug_info("slashexec", "command stderr: %s\n", cmd_stderr);
253
254 g_strfreev(cmd_argv);
255 g_free(cmd_stderr);
256
257 if(cmd_stdout && strcmp(cmd_stdout, "") && strcmp(cmd_stdout, "\n")) {
258 g_strchug(cmd_stdout);
259
260 if(g_str_has_suffix(cmd_stdout, "\n"))
261 cmd_stdout[strlen(cmd_stdout) - 1] = '\0';
262
263 if(send) {
264 purple_debug_info("slashexec", "Command stdout: %s\n", cmd_stdout);
265
266 if(!g_utf8_validate(cmd_stdout, -1, NULL)) {
267 purple_debug_error("slashexec", "Output failed UTF-8 verification!\n");
268
269 return FALSE;
270 } else {
271 cmd_stdout = se_strdelimit(cmd_stdout, ' ');
272
273 g_strstrip(cmd_stdout);
274
275 purple_debug_info("slashexec", "Sanitized command stdout: %s\n", cmd_stdout);
276 }
277
278 switch(purple_conversation_get_type(conv)) {
279 case PURPLE_CONV_TYPE_IM:
280 purple_conv_im_send(PURPLE_CONV_IM(conv), cmd_stdout);
281 break;
282 case PURPLE_CONV_TYPE_CHAT:
283 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), cmd_stdout);
284 break;
285 default:
286 return FALSE;
287 }
288
289 } else
290 purple_conversation_write(conv, NULL, cmd_stdout, PURPLE_MESSAGE_SYSTEM,
291 time(NULL));
292 } else {
293 purple_debug_info("slashexec", "Error executing \"%s\"\n", spawn_cmd);
294 purple_conversation_write(conv, NULL,
295 _("There was an error executing your command."),
296 PURPLE_MESSAGE_SYSTEM, time(NULL));
297
298 g_free(spawn_cmd);
299 g_free(cmd_stdout);
300
301 return FALSE;
302 }
303
304 g_free(cmd_stdout);
305 g_free(spawn_cmd);
306
307 return TRUE;
308 }
309
310 static PurpleCmdRet
se_cmd_cb(PurpleConversation * conv,const gchar * cmd,gchar ** args,gchar ** error,gpointer data)311 se_cmd_cb(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error,
312 gpointer data)
313 {
314 gboolean send = FALSE;
315 char *string = args[0];
316
317 if(!purple_prefs_get_bool(PREF_SLASH))
318 return PURPLE_CMD_RET_CONTINUE;
319
320 if(string && !strncmp(string, "-o ", 3)) {
321 send = TRUE;
322 string += 3;
323 }
324
325 if(se_do_action(conv, string, send))
326 return PURPLE_CMD_RET_OK;
327 else
328 return PURPLE_CMD_RET_FAILED;
329 }
330
331 static void
se_sending_msg_helper(PurpleConversation * conv,char ** message)332 se_sending_msg_helper(PurpleConversation *conv, char **message)
333 {
334 char *string = *message, *strip = NULL;
335 gboolean send = TRUE;
336
337 if(conv == NULL || !purple_prefs_get_bool(PREF_BANG)) return;
338
339 strip = purple_markup_strip_html(string);
340
341 if(strip && *strip != '!') {
342 g_free(strip);
343 return;
344 }
345
346 *message = NULL;
347
348 g_free(string);
349
350 /* this is refactored quite a bit to simplify things since sending-im-msg
351 * allows changing the text that will be sent by simply giving *message a
352 * new pointer */
353 if(strncmp(strip, "!!!", 3) == 0) {
354 char *new_msg, *conv_sys_msg;
355
356 new_msg = g_strdup(strip + 2);
357
358 *message = new_msg;
359
360 /* I really want to eventually make this cleaner, like by making it
361 * change the actual message that gets printed to the conv window... */
362 conv_sys_msg = g_strdup_printf(_("The following text was sent: %s"),
363 new_msg);
364
365 purple_conversation_write(conv, NULL, conv_sys_msg, PURPLE_MESSAGE_SYSTEM,
366 time(NULL));
367
368 g_free(strip);
369 g_free(conv_sys_msg);
370
371 return;
372 }
373
374 if(*(strip + 1) == '!')
375 send = FALSE;
376
377 if(send)
378 se_do_action(conv, strip + 1, send);
379 else
380 se_do_action(conv, strip + 2, send);
381
382 g_free(strip);
383 }
384
385 static void
se_sending_chat_msg_cb(PurpleAccount * account,char ** message,int id)386 se_sending_chat_msg_cb(PurpleAccount *account, char **message, int id)
387 {
388 PurpleConversation *conv;
389 if (message == NULL || *message == NULL) /* It's possible if some other callback to
390 to the signal resets the message */
391 return;
392 conv = purple_find_chat(account->gc, id);
393 se_sending_msg_helper(conv, message);
394 }
395
396 static void
se_sending_im_msg_cb(PurpleAccount * account,const char * who,char ** message)397 se_sending_im_msg_cb(PurpleAccount *account, const char *who, char **message)
398 {
399 PurpleConversation *conv;
400 if (message == NULL || *message == NULL) /* It's possible if some other callback to
401 to the signal resets the message */
402 return;
403 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account);
404 se_sending_msg_helper(conv, message);
405 }
406
407 static gboolean
se_load(PurplePlugin * plugin)408 se_load(PurplePlugin *plugin) {
409 #ifndef _WIN32
410 struct passwd *pw = NULL;
411 #endif /* _WIN32 */
412 const gchar *help = _("exec [-o] <command>, runs the command.\n"
413 "If the -o flag is used then output is sent to the"
414 "current conversation; otherwise it is printed to the "
415 "current text box.");
416
417 se_cmd = purple_cmd_register("exec", "s", PURPLE_CMD_P_PLUGIN,
418 PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_CHAT, NULL,
419 se_cmd_cb, help, NULL);
420
421 /* these signals are needed for the bangexec features we took on */
422 purple_signal_connect(purple_conversations_get_handle(), "sending-im-msg", plugin,
423 PURPLE_CALLBACK(se_sending_im_msg_cb), NULL);
424 purple_signal_connect(purple_conversations_get_handle(), "sending-chat-msg", plugin,
425 PURPLE_CALLBACK(se_sending_chat_msg_cb), NULL);
426
427 /* this gets the user's shell. If this is built on Windows, force cmd.exe,
428 * which will make this plugin not work on Windows 98/ME */
429 #ifdef _WIN32
430 shell = g_strdup("cmd.exe");
431 #else
432 pw = getpwuid(getuid());
433
434 if(pw)
435 if(pw->pw_shell)
436 shell = g_strdup(pw->pw_shell);
437 else
438 shell = g_strdup("/bin/sh");
439 else
440 shell = g_strdup("/bin/sh");
441 #endif /* _WIN32 */
442
443 return TRUE;
444 }
445
446 static gboolean
se_unload(PurplePlugin * plugin)447 se_unload(PurplePlugin *plugin) {
448 purple_cmd_unregister(se_cmd);
449
450 g_free(shell);
451
452 return TRUE;
453 }
454
455 static PurplePluginPrefFrame *
get_plugin_pref_frame(PurplePlugin * plugin)456 get_plugin_pref_frame(PurplePlugin *plugin)
457 {
458 PurplePluginPrefFrame *frame;
459 PurplePluginPref *pref;
460
461 frame = purple_plugin_pref_frame_new();
462
463 pref = purple_plugin_pref_new_with_label(_("Execute commands starting with: "));
464 purple_plugin_pref_frame_add(frame, pref);
465
466 pref = purple_plugin_pref_new_with_name_and_label(PREF_SLASH,
467 _("/exec Command (/exec someCommand)"));
468 purple_plugin_pref_frame_add(frame, pref);
469
470 pref = purple_plugin_pref_new_with_name_and_label(PREF_BANG,
471 _("Exclamation point (!someCommand)"));
472 purple_plugin_pref_frame_add(frame, pref);
473
474 return frame;
475 }
476
477 static PurplePluginUiInfo prefs_info = {
478 get_plugin_pref_frame,
479 0,
480 NULL,
481
482 /* padding */
483 NULL,
484 NULL,
485 NULL,
486 NULL
487 };
488
489 static PurplePluginInfo se_info = {
490 PURPLE_PLUGIN_MAGIC,
491 PURPLE_MAJOR_VERSION,
492 PURPLE_MINOR_VERSION,
493 PURPLE_PLUGIN_STANDARD, /* type */
494 NULL, /* ui requirement */
495 0, /* flags */
496 NULL, /* dependencies */
497 PURPLE_PRIORITY_DEFAULT, /* priority */
498 "core-plugin_pack-slashexec", /* id */
499 "/exec", /* name */
500 PP_VERSION, /* version */
501 NULL, /* summary */
502 NULL, /* description */
503
504 "Gary Kramlich <grim@reaperworld.com>,\n"
505 "Peter Lawler <bleeter from users.sf.net>,\n"
506 "Daniel Atallah,\nSadrul Habib Chowdhury <sadrul@users.sf.net>,\n"
507 "John Bailey <rekkanoryo@rekkanoryo.org>", /* authors */
508
509 PP_WEBSITE, /* homepage */
510 se_load, /* load */
511 se_unload, /* unload */
512 NULL, /* destroy */
513 NULL, /* ui info */
514 NULL, /* extra info */
515 &prefs_info, /* prefs info */
516 NULL, /* actions info */
517 NULL,
518 NULL,
519 NULL,
520 NULL
521 };
522
523 static void
init_plugin(PurplePlugin * plugin)524 init_plugin(PurplePlugin *plugin) {
525 #ifdef ENABLE_NLS
526 bindtextdomain(GETTEXT_PACKAGE, PP_LOCALEDIR);
527 bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
528 #endif /* ENABLE_NLS */
529 se_info.summary = _("/exec a la UNIX IRC CLI");
530 se_info.description = _("A plugin that adds the /exec command line"
531 " interpreter like most UNIX/Linux IRC"
532 " clients have. Also included is the ability to"
533 " execute commands with an exclamation point"
534 " (!uptime, for instance).\n");
535 purple_prefs_add_none(PREF_PREFIX);
536 purple_prefs_add_bool(PREF_SLASH, TRUE);
537 purple_prefs_add_bool(PREF_BANG, FALSE);
538 }
539
540 PURPLE_INIT_PLUGIN(slashexec, init_plugin, se_info)
541