# HG changeset patch # User Bram Moolenaar # Date 1569532505 -7200 # Node ID 4ac8161e92e0d2ac1a3babd1cee3522c0300dcc4 # Parent 6ad8949a205d5bf10d8044ca1bcf719d5d595476 patch 8.1.2080: the terminal API is limited and can't be disabled Commit: https://github.com/vim/vim/commit/d2842ea60bd608b7f9ec93c77d3f36a8e3bf5fe9 Author: Bram Moolenaar Date: Thu Sep 26 23:08:54 2019 +0200 patch 8.1.2080: the terminal API is limited and can't be disabled Problem: The terminal API is limited and can't be disabled. Solution: Add term_setapi() to set the function prefix. (Ozaki Kiichi, closes #2907) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1,4 +1,4 @@ -*eval.txt* For Vim version 8.1. Last change: 2019 Sep 19 +*eval.txt* For Vim version 8.1. Last change: 2019 Sep 26 VIM REFERENCE MANUAL by Bram Moolenaar @@ -2819,6 +2819,7 @@ term_gettty({buf}, [{input}]) String get term_list() List get the list of terminal buffers term_scrape({buf}, {row}) List get row of a terminal screen term_sendkeys({buf}, {keys}) none send keystrokes to a terminal +term_setapi({buf}, {expr}) none set |terminal-api| function name prefix term_setansicolors({buf}, {colors}) none set ANSI palette in GUI color mode term_setkill({buf}, {how}) none set signal to stop job in terminal @@ -3262,9 +3263,14 @@ bufnr([{expr} [, {create}]]) The result is the number of a buffer, as it is displayed by the ":ls" command. For the use of {expr}, see |bufname()| above. + If the buffer doesn't exist, -1 is returned. Or, if the {create} argument is present and not zero, a new, unlisted, - buffer is created and its number is returned. + buffer is created and its number is returned. Example: > + let newbuf = bufnr('Scratch001', 1) +< Using an empty name uses the current buffer. To create a new + buffer with an empty name use |bufadd()|. + bufnr("$") is the last buffer: > :let last_buffer = bufnr("$") < The result is a Number, which is the highest buffer number diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt --- a/runtime/doc/terminal.txt +++ b/runtime/doc/terminal.txt @@ -1,4 +1,4 @@ -*terminal.txt* For Vim version 8.1. Last change: 2019 Sep 20 +*terminal.txt* For Vim version 8.1. Last change: 2019 Sep 26 VIM REFERENCE MANUAL by Bram Moolenaar @@ -222,7 +222,7 @@ Command syntax ~ Vim width (no window left or right of the terminal window) this value is ignored. - ++eof={text} when using [range]: text to send after + ++eof={text} When using [range]: text to send after the last line was written. Cannot contain white space. A CR is appended. For MS-Windows the default @@ -234,6 +234,10 @@ Command syntax ~ ++type={pty} (MS-Windows only): Use {pty} as the virtual console. See 'termwintype' for the values. + ++api={expr} Permit the function name starting with + {expr} to be called as |terminal-api| + function. If {expr} is empty then no + function can be called. If you want to use more options use the |term_start()| function. @@ -701,6 +705,15 @@ term_sendkeys({buf}, {keys}) *term_se GetBufnr()->term_sendkeys(keys) +term_setapi({buf}, {expr}) *term_setapi()* + Set the function name prefix to be used for the |terminal-api| + function in terminal {buf}. For example: > + :call term_setapi(buf, "Myapi_") + :call term_setapi(buf, "") +< + The default is "Tapi_". When {expr} is an empty string then + no |terminal-api| function can be used for {buf}. + term_setansicolors({buf}, {colors}) *term_setansicolors()* Set the ANSI color palette used by terminal {buf}. {colors} must be a List of 16 valid color names or hexadecimal @@ -843,6 +856,9 @@ term_start({cmd} [, {options}]) *term_ color modes. See |g:terminal_ansi_colors|. "tty_type" (MS-Windows only): Specify which pty to use. See 'termwintype' for the values. + "term_api" function name prefix for the + |terminal-api| function. See + |term_setapi()|. Can also be used as a |method|: > GetCommand()->term_start() @@ -902,9 +918,9 @@ Currently supported commands: Call a user defined function with {argument}. The function is called with two arguments: the buffer number of the terminal and {argument}, the decoded JSON argument. - The function name must start with "Tapi_" to avoid + By default, the function name must start with "Tapi_" to avoid accidentally calling a function not meant to be used for the - terminal API. + terminal API. This can be changed with |term_setapi()|. The user function should sanity check the argument. The function can use |term_sendkeys()| to send back a reply. Example in JSON: > diff --git a/src/channel.c b/src/channel.c --- a/src/channel.c +++ b/src/channel.c @@ -5144,6 +5144,14 @@ get_job_options(typval_T *tv, jobopt_T * memcpy(opt->jo_ansi_colors, rgb, sizeof(rgb)); } # endif + else if (STRCMP(hi->hi_key, "term_api") == 0) + { + if (!(supported2 & JO2_TERM_API)) + break; + opt->jo_set2 |= JO2_TERM_API; + opt->jo_term_api = tv_get_string_buf_chk(item, + opt->jo_term_api_buf); + } #endif else if (STRCMP(hi->hi_key, "env") == 0) { diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -787,6 +787,7 @@ static funcentry_T global_functions[] = # if defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS) {"term_setansicolors", 2, 2, FEARG_1, f_term_setansicolors}, # endif + {"term_setapi", 2, 2, FEARG_1, f_term_setapi}, {"term_setkill", 2, 2, FEARG_1, f_term_setkill}, {"term_setrestore", 2, 2, FEARG_1, f_term_setrestore}, {"term_setsize", 3, 3, FEARG_1, f_term_setsize}, diff --git a/src/proto/terminal.pro b/src/proto/terminal.pro --- a/src/proto/terminal.pro +++ b/src/proto/terminal.pro @@ -50,6 +50,7 @@ void f_term_scrape(typval_T *argvars, ty void f_term_sendkeys(typval_T *argvars, typval_T *rettv); void f_term_getansicolors(typval_T *argvars, typval_T *rettv); void f_term_setansicolors(typval_T *argvars, typval_T *rettv); +void f_term_setapi(typval_T *argvars, typval_T *rettv); void f_term_setrestore(typval_T *argvars, typval_T *rettv); void f_term_setkill(typval_T *argvars, typval_T *rettv); void f_term_start(typval_T *argvars, typval_T *rettv); diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -1938,6 +1938,7 @@ struct channel_S { #define JO2_ANSI_COLORS 0x8000 // "ansi_colors" #define JO2_TTY_TYPE 0x10000 // "tty_type" #define JO2_BUFNR 0x20000 // "bufnr" +#define JO2_TERM_API 0x40000 // "term_api" #define JO_MODE_ALL (JO_MODE + JO_IN_MODE + JO_OUT_MODE + JO_ERR_MODE) #define JO_CB_ALL \ @@ -2007,6 +2008,8 @@ typedef struct long_u jo_ansi_colors[16]; # endif int jo_tty_type; // first character of "tty_type" + char_u *jo_term_api; + char_u jo_term_api_buf[NUMBUFLEN]; #endif } jobopt_T; diff --git a/src/terminal.c b/src/terminal.c --- a/src/terminal.c +++ b/src/terminal.c @@ -109,6 +109,7 @@ struct terminal_S { #define TL_FINISH_OPEN 'o' /* ++open */ char_u *tl_opencmd; char_u *tl_eof_chars; + char_u *tl_api; // prefix for terminal API function char_u *tl_arg0_cmd; // To format the status bar @@ -641,6 +642,11 @@ term_start( term->tl_kill = vim_strnsave(opt->jo_term_kill, p - opt->jo_term_kill); } + if (opt->jo_term_api != NULL) + term->tl_api = vim_strsave(opt->jo_term_api); + else + term->tl_api = vim_strsave((char_u *)"Tapi_"); + /* System dependent: setup the vterm and maybe start the job in it. */ if (argv == NULL && argvar->v_type == VAR_STRING @@ -708,44 +714,58 @@ ex_terminal(exarg_T *eap) cmd += 2; p = skiptowhite(cmd); ep = vim_strchr(cmd, '='); - if (ep != NULL && ep < p) - p = ep; - - if ((int)(p - cmd) == 5 && STRNICMP(cmd, "close", 5) == 0) + if (ep != NULL) + { + if (ep < p) + p = ep; + else + ep = NULL; + } + +# define OPTARG_HAS(name) ((int)(p - cmd) == sizeof(name) - 1 \ + && STRNICMP(cmd, name, sizeof(name) - 1) == 0) + if (OPTARG_HAS("close")) opt.jo_term_finish = 'c'; - else if ((int)(p - cmd) == 7 && STRNICMP(cmd, "noclose", 7) == 0) + else if (OPTARG_HAS("noclose")) opt.jo_term_finish = 'n'; - else if ((int)(p - cmd) == 4 && STRNICMP(cmd, "open", 4) == 0) + else if (OPTARG_HAS("open")) opt.jo_term_finish = 'o'; - else if ((int)(p - cmd) == 6 && STRNICMP(cmd, "curwin", 6) == 0) + else if (OPTARG_HAS("curwin")) opt.jo_curwin = 1; - else if ((int)(p - cmd) == 6 && STRNICMP(cmd, "hidden", 6) == 0) + else if (OPTARG_HAS("hidden")) opt.jo_hidden = 1; - else if ((int)(p - cmd) == 9 && STRNICMP(cmd, "norestore", 9) == 0) + else if (OPTARG_HAS("norestore")) opt.jo_term_norestore = 1; - else if ((int)(p - cmd) == 4 && STRNICMP(cmd, "kill", 4) == 0 - && ep != NULL) + else if (OPTARG_HAS("kill") && ep != NULL) { opt.jo_set2 |= JO2_TERM_KILL; opt.jo_term_kill = ep + 1; p = skiptowhite(cmd); } - else if ((int)(p - cmd) == 4 && STRNICMP(cmd, "rows", 4) == 0 - && ep != NULL && isdigit(ep[1])) + else if (OPTARG_HAS("api")) + { + opt.jo_set2 |= JO2_TERM_API; + if (ep != NULL) + { + opt.jo_term_api = ep + 1; + p = skiptowhite(cmd); + } + else + opt.jo_term_api = NULL; + } + else if (OPTARG_HAS("rows") && ep != NULL && isdigit(ep[1])) { opt.jo_set2 |= JO2_TERM_ROWS; opt.jo_term_rows = atoi((char *)ep + 1); p = skiptowhite(cmd); } - else if ((int)(p - cmd) == 4 && STRNICMP(cmd, "cols", 4) == 0 - && ep != NULL && isdigit(ep[1])) + else if (OPTARG_HAS("cols") && ep != NULL && isdigit(ep[1])) { opt.jo_set2 |= JO2_TERM_COLS; opt.jo_term_cols = atoi((char *)ep + 1); p = skiptowhite(cmd); } - else if ((int)(p - cmd) == 3 && STRNICMP(cmd, "eof", 3) == 0 - && ep != NULL) + else if (OPTARG_HAS("eof") && ep != NULL) { char_u *buf = NULL; char_u *keys; @@ -785,6 +805,7 @@ ex_terminal(exarg_T *eap) semsg(_("E181: Invalid attribute: %s"), cmd); goto theend; } +# undef OPTARG_HAS cmd = skipwhite(p); } if (*cmd == NUL) @@ -933,6 +954,7 @@ free_unused_terminals() free_scrollback(term); term_free_vterm(term); + vim_free(term->tl_api); vim_free(term->tl_title); #ifdef FEAT_SESSION vim_free(term->tl_command); @@ -3770,6 +3792,15 @@ handle_drop_command(listitem_T *item) } /* + * Return TRUE if "func" starts with "pat" and "pat" isn't empty. + */ + static int +is_permitted_term_api(char_u *func, char_u *pat) +{ + return pat != NULL && *pat != NUL && STRNICMP(func, pat, STRLEN(pat)) == 0; +} + +/* * Handles a function call from the job running in a terminal. * "item" is the function name, "item->li_next" has the arguments. */ @@ -3788,9 +3819,9 @@ handle_call_command(term_T *term, channe } func = tv_get_string(&item->li_tv); - if (STRNCMP(func, "Tapi_", 5) != 0) - { - ch_log(channel, "Invalid function name: %s", func); + if (!is_permitted_term_api(func, term->tl_api)) + { + ch_log(channel, "Unpermitted function: %s", func); return; } @@ -5546,6 +5577,27 @@ f_term_setansicolors(typval_T *argvars, #endif /* + * "term_setapi(buf, api)" function + */ + void +f_term_setapi(typval_T *argvars, typval_T *rettv UNUSED) +{ + buf_T *buf = term_get_buf(argvars, "term_setapi()"); + term_T *term; + char_u *api; + + if (buf == NULL) + return; + term = buf->b_term; + vim_free(term->tl_api); + api = tv_get_string_chk(&argvars[1]); + if (api != NULL) + term->tl_api = vim_strsave(api); + else + term->tl_api = NULL; +} + +/* * "term_setrestore(buf, command)" function */ void @@ -5608,7 +5660,7 @@ f_term_start(typval_T *argvars, typval_T + JO2_TERM_COLS + JO2_TERM_ROWS + JO2_VERTICAL + JO2_CURWIN + JO2_CWD + JO2_ENV + JO2_EOF_CHARS + JO2_NORESTORE + JO2_TERM_KILL - + JO2_ANSI_COLORS + JO2_TTY_TYPE) == FAIL) + + JO2_ANSI_COLORS + JO2_TTY_TYPE + JO2_TERM_API) == FAIL) return; buf = term_start(&argvars[0], NULL, &opt, 0); diff --git a/src/testdir/term_util.vim b/src/testdir/term_util.vim --- a/src/testdir/term_util.vim +++ b/src/testdir/term_util.vim @@ -61,11 +61,16 @@ func RunVimInTerminal(arguments, options let cmd = GetVimCommandCleanTerm() .. a:arguments - let buf = term_start(cmd, { + let options = { \ 'curwin': 1, \ 'term_rows': rows, \ 'term_cols': cols, - \ }) + \ } + " Accept other options whose name starts with 'term_'. + call extend(options, filter(copy(a:options), 'v:key =~# "^term_"')) + + let buf = term_start(cmd, options) + if &termwinsize == '' " in the GUI we may end up with a different size, try to set it. if term_getsize(buf) != [rows, cols] diff --git a/src/testdir/test_terminal.vim b/src/testdir/test_terminal.vim --- a/src/testdir/test_terminal.vim +++ b/src/testdir/test_terminal.vim @@ -1353,30 +1353,90 @@ endfunc func Test_terminal_api_call() CheckRunVimInTerminal +call ch_logfile('logfile', 'w') + unlet! g:called_bufnum + unlet! g:called_arg + call WriteApiCall('Tapi_TryThis') + + " Default let buf = RunVimInTerminal('-S Xscript', {}) call WaitFor({-> exists('g:called_bufnum')}) call assert_equal(buf, g:called_bufnum) call assert_equal(['hello', 123], g:called_arg) + call StopVimInTerminal(buf) + unlet! g:called_bufnum + unlet! g:called_arg + + " Enable explicitly + let buf = RunVimInTerminal('-S Xscript', {'term_api': 'Tapi_Try'}) + call WaitFor({-> exists('g:called_bufnum')}) + call assert_equal(buf, g:called_bufnum) + call assert_equal(['hello', 123], g:called_arg) call StopVimInTerminal(buf) + + unlet! g:called_bufnum + unlet! g:called_arg + + func! ApiCall_TryThis(bufnum, arg) + let g:called_bufnum2 = a:bufnum + let g:called_arg2 = a:arg + endfunc + + call WriteApiCall('ApiCall_TryThis') + + " Use prefix match + let buf = RunVimInTerminal('-S Xscript', {'term_api': 'ApiCall_'}) + call WaitFor({-> exists('g:called_bufnum2')}) + call assert_equal(buf, g:called_bufnum2) + call assert_equal(['hello', 123], g:called_arg2) + call StopVimInTerminal(buf) + + unlet! g:called_bufnum2 + unlet! g:called_arg2 + call delete('Xscript') - unlet g:called_bufnum - unlet g:called_arg + delfunction! ApiCall_TryThis + unlet! g:called_bufnum2 + unlet! g:called_arg2 endfunc func Test_terminal_api_call_fails() CheckRunVimInTerminal + func! TryThis(bufnum, arg) + let g:called_bufnum3 = a:bufnum + let g:called_arg3 = a:arg + endfunc + call WriteApiCall('TryThis') - call ch_logfile('Xlog', 'w') - let buf = RunVimInTerminal('-S Xscript', {}) - call WaitForAssert({-> assert_match('Invalid function name: TryThis', string(readfile('Xlog')))}) + + unlet! g:called_bufnum3 + unlet! g:called_arg3 + " Not permitted + call ch_logfile('Xlog', 'w') + let buf = RunVimInTerminal('-S Xscript', {'term_api': ''}) + call WaitForAssert({-> assert_match('Unpermitted function: TryThis', string(readfile('Xlog')))}) + call assert_false(exists('g:called_bufnum3')) + call assert_false(exists('g:called_arg3')) call StopVimInTerminal(buf) + + " No match + call ch_logfile('Xlog', 'w') + let buf = RunVimInTerminal('-S Xscript', {'term_api': 'TryThat'}) + call WaitFor({-> string(readfile('Xlog')) =~ 'Unpermitted function: TryThis'}) + call assert_false(exists('g:called_bufnum3')) + call assert_false(exists('g:called_arg3')) + call StopVimInTerminal(buf) + call delete('Xscript') - call ch_logfile('', '') + call ch_logfile('') call delete('Xlog') + delfunction! TryThis + unlet! g:called_bufnum3 + unlet! g:called_arg3 endfunc let s:caught_e937 = 0 @@ -2061,3 +2121,34 @@ func Test_terminal_altscreen() exe buf . "bwipe!" call delete('Xtext') endfunc + +func Test_terminal_setapi_and_call() + if !CanRunVimInTerminal() + return + endif + + call WriteApiCall('Tapi_TryThis') + call ch_logfile('Xlog', 'w') + + unlet! g:called_bufnum + unlet! g:called_arg + + let buf = RunVimInTerminal('-S Xscript', {'term_api': 0}) + call WaitForAssert({-> assert_match('Unpermitted function: Tapi_TryThis', string(readfile('Xlog')))}) + call assert_false(exists('g:called_bufnum')) + call assert_false(exists('g:called_arg')) + + call term_setapi(buf, 'Tapi_TryThis') + call term_sendkeys(buf, ":set notitle\") + call term_sendkeys(buf, ":source Xscript\") + call WaitFor({-> exists('g:called_bufnum')}) + call assert_equal(buf, g:called_bufnum) + call assert_equal(['hello', 123], g:called_arg) + call StopVimInTerminal(buf) + + call delete('Xscript') + call ch_logfile('') + call delete('Xlog') + unlet! g:called_bufnum + unlet! g:called_arg +endfunc diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -758,6 +758,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 2080, +/**/ 2079, /**/ 2078,