diff --git a/doc/sphinx_source/using/tcl-commands.rst b/doc/sphinx_source/using/tcl-commands.rst index 9d0f53df1..c284bd871 100644 --- a/doc/sphinx_source/using/tcl-commands.rst +++ b/doc/sphinx_source/using/tcl-commands.rst @@ -3585,6 +3585,14 @@ The following is a list of bind types and how they work. Below each bind type is Description: triggered when a server sends an IRCv3 spec CHGHOST message to change a user's hostmask. The new host is matched against mask in the form of "#channel nick!user\@host" and can contain wildcards. The specified proc will be called with the nick of the user whose hostmask changed; the hostmask the affected user had before the change, the handle of the affected user (or * if no handle is present), the channel the user was on when the bind triggered, and the new hostmask of the affected user. This bind will trigger once for each channel the user is on. +(57) CHANSET + + bind chanset + + procname + + Description: triggered when a channel setting is set via the partyline. flags is ignored, mask is the name of channel setting (not including any +/- prefix) and can contain wildcards. The proc will be called with the channel that the setting was set on, the text name of the setting that was changed, and the value it was set to (0/1 for -/+, string, or X:Y formatted value). + ^^^^^^^^^^^^^ Return Values ^^^^^^^^^^^^^ @@ -3639,6 +3647,8 @@ Here's a list of the bindings that use the return value from procs they trigger: (19) RAWT Return 1 to ask the bot not to process the server text. This can affet the bot's performance by causing it to miss things that it would normally act on -- you have been warned. Again. +(20) CHANSET Return 1 to prevent the channel setting from being changed. + Control Procedures ------------------ diff --git a/src/mod/channels.mod/channels.c b/src/mod/channels.mod/channels.c index dc202dc22..885b86539 100644 --- a/src/mod/channels.mod/channels.c +++ b/src/mod/channels.mod/channels.c @@ -32,6 +32,8 @@ static Function *global = NULL; static char chanfile[121], glob_chanmode[65]; static char *lastdeletedmask; +static p_tcl_bind_list H_chanset; + static struct udef_struct *udef; static int use_info, chan_hack, quiet_save, global_revenge_mode, @@ -241,6 +243,27 @@ static void get_mode_protect(struct chanset_t *chan, char *s) } } +static int builtin_chanset STDVAR +{ + Function F = (Function) cd; + + BADARGS(3, 3, " chan setting value"); + + CHECKVALIDITY(builtin_chanset); + F(argv[1], argv[2], argv[3]); + return TCL_OK; +} + +int check_tcl_chanset(const char *chan, const char *setting, const char *value) +{ + Tcl_SetVar(interp, "_chanset1", (char *) chan, 0); + Tcl_SetVar(interp, "_chanset2", (char *) setting, 0); + Tcl_SetVar(interp, "_chanset3", (char *) value, 0); + + return BIND_EXEC_LOG == check_tcl_bind(H_chanset, setting, 0, " $_chanset1 $_chanset2 $_chanset3", + MATCH_MASK | BIND_STACKABLE | BIND_STACKRET | BIND_WANTRET); +} + /* Returns true if this is one of the channel masks */ static int ismodeline(masklist *m, char *user) @@ -1007,6 +1030,7 @@ char *channels_start(Function *global_funcs) Tcl_TraceVar(interp, "default-chanset", TCL_TRACE_READS | TCL_TRACE_WRITES | TCL_TRACE_UNSETS, traced_globchanset, NULL); + H_chanset = add_bind_table("chanset", HT_STACKABLE, builtin_chanset); add_builtins(H_chon, my_chon); add_builtins(H_dcc, C_dcc_irc); add_tcl_commands(channels_cmds); diff --git a/src/mod/channels.mod/channels.h b/src/mod/channels.mod/channels.h index a165e20be..86cab4902 100644 --- a/src/mod/channels.mod/channels.h +++ b/src/mod/channels.mod/channels.h @@ -118,6 +118,8 @@ static void setudef(struct udef_struct *, char *, intptr_t); static void remove_channel(struct chanset_t *); static intptr_t ngetudef(char *, char *); static int expired_mask(struct chanset_t *chan, char *who); +static int check_tcl_chanset(const char *, const char *, const char *); + #else diff --git a/src/mod/channels.mod/cmdschan.c b/src/mod/channels.mod/cmdschan.c index a267dfb08..670a1e6d6 100644 --- a/src/mod/channels.mod/cmdschan.c +++ b/src/mod/channels.mod/cmdschan.c @@ -1472,10 +1472,10 @@ static void cmd_chaninfo(struct userrec *u, int idx, char *par) static void cmd_chanset(struct userrec *u, int idx, char *par) { - char *chname = NULL, answers[512], *parcpy; - char *list[2], *bak, *buf; + char *chname = NULL, answers[1024], *parcpy; + char *list[2], value[2], *bak, *buf; struct chanset_t *chan = NULL; - int all = 0; + int len, all = 0; if (!par[0]) dprintf(idx, "Usage: chanset [%schannel] \n", CHANMETA); @@ -1534,8 +1534,10 @@ static void cmd_chanset(struct userrec *u, int idx, char *par) return; } if (tcl_channel_modify(0, chan, 1, list) == TCL_OK) { - strcat(answers, list[0]); - strcat(answers, " "); + strlcpy(value, list[0], 2); + len = strlen(answers); + egg_snprintf(answers + len, (sizeof answers) - len, + (len == 0) ? "%s" : " %s", list[0]); /* Concatenation */ } else if (!all || !chan->next) dprintf(idx, "Error trying to set %s for %s, invalid mode.\n", list[0], all ? "all channels" : chname); @@ -1562,7 +1564,7 @@ static void cmd_chanset(struct userrec *u, int idx, char *par) strcpy(parcpy, par); irp = Tcl_CreateInterp(); if (tcl_channel_modify(irp, chan, 2, list) == TCL_OK) { - int len = strlen(answers); + len = strlen(answers); egg_snprintf(answers + len, (sizeof answers) - len, "%s { %s }", list[0], parcpy); /* Concatenation */ } else if (!all || !chan->next) dprintf(idx, "Error trying to set %s for %s, %s\n", diff --git a/src/mod/channels.mod/tclchan.c b/src/mod/channels.mod/tclchan.c index debfb1368..af91753cd 100644 --- a/src/mod/channels.mod/tclchan.c +++ b/src/mod/channels.mod/tclchan.c @@ -1318,6 +1318,48 @@ static int tcl_channel_modify(Tcl_Interp *irp, struct chanset_t *chan, module_entry *me; for (i = 0; i < items; i++) { + if (item[i][0] == '+' || item[i][0] == '-') { + if (check_tcl_chanset(chan->dname, item[i] + 1, item[i][0] == '+' ? "1" : "0")) { + if (irp) { + Tcl_ResetResult(irp); + Tcl_AppendResult(irp, "Channel setting ", item[i], " rejected by Tcl script", NULL); + } + return TCL_ERROR; + } + } else { + // otherwise invalid later, missing value + if (i < items - 1) { + int free_value = 0; + char *value; + + if (!strncmp("need-", item[i], 5)) { + value = item[i + 1]; + } else { + char *sep = strchr(item[i + 1], ' '); + + if (sep) { + value = nmalloc(sep - item[i + 1] + 1); + strlcpy(value, item[i + 1], sep - item[i + 1] + 1); + free_value = 1; + } else { + value = item[i + 1]; + } + } + if (check_tcl_chanset(chan->dname, item[i], value)) { + if (free_value) { + nfree(value); + } + if (irp) { + Tcl_ResetResult(irp); + Tcl_AppendResult(irp, "Channel setting ", item[i], " to ", value, " rejected by Tcl script", NULL); + } + return TCL_ERROR; + } + if (free_value) { + nfree(value); + } + } + } if (!strcmp(item[i], "need-op")) { i++; if (i >= items) { diff --git a/src/tcl.c b/src/tcl.c index bbd12fcbc..b7e7fb4d7 100644 --- a/src/tcl.c +++ b/src/tcl.c @@ -319,19 +319,30 @@ static void tcl_cleanup_stringinfo(ClientData cd) } /* Compatibility wrapper that calls Tcl functions with String API */ +/* + * can call itself recursively, so argv is dynamically allocated + * incrrefcount is needed to preserve the strings we get from Tcl_GetString from being cleaned up + * if Tcl is invoked from this + */ static int tcl_call_stringproc_cd(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) { - static int max; - static const char **argv; - int i; + const char **argv; + int i, ret; struct tcl_call_stringinfo *info = cd; + /* The string API guarantees argv[argc] == NULL, unlike the obj API */ - if (objc + 1 > max) - argv = nrealloc(argv, (objc + 1) * sizeof *argv); - for (i = 0; i < objc; i++) + argv = nmalloc((objc + 1) * sizeof *argv); + for (i = 0; i < objc; i++) { + Tcl_IncrRefCount(objv[i]); argv[i] = Tcl_GetString(objv[i]); + } argv[objc] = NULL; - return (info->proc)(info->cd, interp, objc, argv); + ret = (info->proc)(info->cd, interp, objc, argv); + for (i = 0; i < objc; i++) { + Tcl_DecrRefCount(objv[i]); + } + nfree(argv); + return ret; } /* The standard case of no actual cd */