/* Lua interface (scripting engine) */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include "elinks.h" #include "bfu/dialog.h" #include "cache/cache.h" #include "config/home.h" #include "config/kbdbind.h" #include "config/options.h" #include "config/opttypes.h" #include "document/document.h" #include "document/renderer.h" #include "document/view.h" #include "intl/charsets.h" #include "intl/gettext/libintl.h" #include "main/event.h" #include "main/module.h" #include "osdep/signals.h" #include "protocol/uri.h" #include "scripting/lua/core.h" #include "session/location.h" #include "session/session.h" #include "session/task.h" #include "terminal/terminal.h" #include "util/color.h" #include "util/conv.h" #include "util/file.h" #include "util/memory.h" #include "util/string.h" #include "viewer/dump/dump.h" #include "viewer/text/view.h" #include "viewer/text/vs.h" #define LUA_HOOKS_FILENAME "hooks.lua" lua_State *lua_state; static struct session *lua_ses; static struct terminal *errterm; static sigjmp_buf errjmp; #define L lua_state #define LS lua_State *S static void handle_standard_lua_returns(unsigned char *from); static void handle_ref(LS, struct session *ses, int func_ref, unsigned char *from, int num_args, int unref); /* * Functions exported to the lua_State. */ static int l_alert(LS) { unsigned char *msg = (unsigned char *) lua_tostring(S, 1); /* Don't crash if a script calls e.g. error(nil) or error(error). */ if (msg == NULL) msg = "(cannot convert the error message to a string)"; alert_lua_error(msg); return 0; } static int l_current_url(LS) { if (lua_ses && have_location(lua_ses)) { struct view_state *vs = &cur_loc(lua_ses)->vs; unsigned char *url = get_uri_string(vs->uri, URI_ORIGINAL); if (url) { lua_pushstring(S, url); mem_free(url); return 1; } } lua_pushnil(S); return 1; } static int l_current_link(LS) { struct link *link = get_current_session_link(lua_ses); if (link) { lua_pushstring(S, link->where); } else { lua_pushnil(S); } return 1; } static int l_current_title(LS) { struct document_view *doc_view = current_frame(lua_ses); if (doc_view && doc_view->document->title) { unsigned char *clean_title = stracpy(doc_view->document->title); if (clean_title) { sanitize_title(clean_title); lua_pushstring(S, clean_title); mem_free(clean_title); return 1; } } lua_pushnil(S); return 1; } static int l_current_document(LS) { if (lua_ses) { struct cache_entry *cached = find_in_cache(cur_loc(lua_ses)->vs.uri); struct fragment *f = cached ? cached->frag.next : NULL; if (f && f->length) { lua_pushlstring(S, f->data, f->length); return 1; } } lua_pushnil(S); return 1; } /* XXX: This function is mostly copied from `dump_to_file'. */ static int l_current_document_formatted(LS) { struct document_view *doc_view; struct string buffer; int width, old_width = 0; if (lua_gettop(S) == 0) width = -1; else if (!lua_isnumber(S, 1)) goto lua_error; else if ((width = lua_tonumber(S, 1)) <= 0) goto lua_error; if (!lua_ses || !(doc_view = current_frame(lua_ses))) goto lua_error; if (width > 0) { old_width = lua_ses->tab->term->width; lua_ses->tab->term->width = width; render_document_frames(lua_ses, 0); } if (init_string(&buffer)) { add_document_to_string(&buffer, doc_view->document); lua_pushlstring(S, buffer.source, buffer.length); done_string(&buffer); } if (width > 0) { lua_ses->tab->term->width = old_width; render_document_frames(lua_ses, 0); } return 1; lua_error: lua_pushnil(S); return 1; } static int l_pipe_read(LS) { FILE *fp; unsigned char *s = NULL; size_t len = 0; if (!lua_isstring(S, 1)) goto lua_error; fp = popen(lua_tostring(S, 1), "r"); if (!fp) goto lua_error; while (!feof(fp)) { unsigned char buf[1024]; size_t l = fread(buf, 1, sizeof(buf), fp); if (l > 0) { unsigned char *news = mem_realloc(s, len + l); if (!news) goto lua_error; s = news; memcpy(s + len, buf, l); len += l; } else if (l < 0) { goto lua_error; } } pclose(fp); lua_pushlstring(S, s, len); mem_free_if(s); return 1; lua_error: mem_free_if(s); lua_pushnil(S); return 1; } static int l_execute(LS) { if (lua_isstring(S, 1)) { exec_on_terminal(lua_ses->tab->term, (unsigned char *) lua_tostring(S, 1), "", 0); lua_pushnumber(S, 0); return 1; } lua_pushnil(L); return 1; } static int l_tmpname(LS) { unsigned char *fn = tempnam(NULL, "elinks"); if (fn) { lua_pushstring(S, fn); free(fn); return 1; } alert_lua_error("Error generating temporary file name"); lua_pushnil(S); return 1; } /* * Helper to run Lua functions bound to keystrokes. */ static enum evhook_status run_lua_func(va_list ap, void *data) { struct session *ses = va_arg(ap, struct session *); int func_ref = (long) data; if (func_ref == LUA_NOREF) { alert_lua_error("key bound to nothing (internal error)"); return EVENT_HOOK_STATUS_NEXT; } handle_ref(L, ses, func_ref, "keyboard function", 0, 0); return EVENT_HOOK_STATUS_NEXT; } static int l_bind_key(LS) { int ref; int event_id; unsigned char *err = NULL; struct string event_name = NULL_STRING; if (!lua_isstring(S, 1) || !lua_isstring(S, 2) || !lua_isfunction(S, 3)) { alert_lua_error("bad arguments to bind_key"); goto lua_error; } if (!init_string(&event_name)) goto lua_error; lua_pushvalue(S, 3); ref = luaL_ref(S, LUA_REGISTRYINDEX); add_format_to_string(&event_name, "lua-run-func %i", ref); event_id = bind_key_to_event_name((unsigned char *) lua_tostring(S, 1), (unsigned char *) lua_tostring(S, 2), event_name.source, &err); done_string(&event_name); if (!err) { event_id = register_event_hook(event_id, run_lua_func, 0, (void *) (long) ref); if (event_id == EVENT_NONE) err = gettext("Error registering event hook"); } if (err) { luaL_unref(S, LUA_REGISTRYINDEX, ref); alert_lua_error2("error in bind_key: ", err); goto lua_error; } lua_pushnumber(S, 1); return 1; lua_error: lua_pushnil(S); return 1; } /* Begin very hackish bit for bookmark editing dialog. */ /* XXX: Add history and generalise. */ struct lua_dlg_data { lua_State *state; unsigned char cat[MAX_STR_LEN]; unsigned char name[MAX_STR_LEN]; unsigned char url[MAX_STR_LEN]; int func_ref; }; static void dialog_run_lua(void *data_) { struct lua_dlg_data *data = data_; lua_State *s = data->state; lua_pushstring(s, data->cat); lua_pushstring(s, data->name); lua_pushstring(s, data->url); handle_ref(s, lua_ses, data->func_ref, "post dialog function", 3, 1); } static int l_edit_bookmark_dialog(LS) { struct terminal *term = lua_ses->tab->term; struct dialog *dlg; struct lua_dlg_data *data; if (!lua_isstring(S, 1) || !lua_isstring(S, 2) || !lua_isstring(S, 3) || !lua_isfunction(S, 4)) { lua_pushnil(S); return 1; } #define L_EDIT_BMK_WIDGETS_COUNT 5 dlg = calloc_dialog(L_EDIT_BMK_WIDGETS_COUNT, sizeof(*data)); if (!dlg) return 0; data = (struct lua_dlg_data *) get_dialog_offset(dlg, L_EDIT_BMK_WIDGETS_COUNT); data->state = S; safe_strncpy(data->cat, (unsigned char *) lua_tostring(S, 1), MAX_STR_LEN-1); safe_strncpy(data->name, (unsigned char *) lua_tostring(S, 2), MAX_STR_LEN-1); safe_strncpy(data->url, (unsigned char *) lua_tostring(S, 3), MAX_STR_LEN-1); lua_pushvalue(S, 4); data->func_ref = luaL_ref(S, LUA_REGISTRYINDEX); dlg->title = _("Edit bookmark", term); dlg->layouter = generic_dialog_layouter; dlg->layout.maximize_width = 1; add_dlg_field(dlg, _("Name", term), 0, 0, NULL, MAX_STR_LEN, data->cat, NULL); add_dlg_field(dlg, _("Name", term), 0, 0, NULL, MAX_STR_LEN, data->name, NULL); add_dlg_field(dlg, _("URL", term), 0, 0, NULL, MAX_STR_LEN, data->url, NULL); add_dlg_ok_button(dlg, _("~OK", term), B_ENTER, dialog_run_lua, data); add_dlg_button(dlg, _("~Cancel", term), B_ESC, cancel_dialog, NULL); add_dlg_end(dlg, L_EDIT_BMK_WIDGETS_COUNT); do_dialog(term, dlg, getml(dlg, NULL)); lua_pushnumber(S, 1); return 1; } /* End very hackish bit. */ /* Begin hackish bit for half-generalised dialog. */ /* XXX: Add history and custom labels. */ #define XDIALOG_MAX_FIELDS 5 struct lua_xdialog_data { lua_State *state; int func_ref; int nfields; unsigned char fields[XDIALOG_MAX_FIELDS][MAX_STR_LEN]; }; static void xdialog_run_lua(void *data_) { struct lua_xdialog_data *data = data_; lua_State *s = data->state; int i; for (i = 0; i < data->nfields; i++) lua_pushstring(s, data->fields[i]); handle_ref(s, lua_ses, data->func_ref, "post xdialog function", data->nfields, 1); } static int l_xdialog(LS) { struct terminal *term; struct dialog *dlg; struct lua_xdialog_data *data; int nargs, nfields, nitems; int i = 0; if (!lua_ses) return 0; term = lua_ses->tab->term; nargs = lua_gettop(S); nfields = nargs - 1; nitems = nfields + 2; if ((nfields < 1) || (nfields > XDIALOG_MAX_FIELDS)) goto lua_error; for (i = 1; i < nargs; i++) if (!lua_isstring(S, i)) goto lua_error; if (!lua_isfunction(S, nargs)) goto lua_error; dlg = calloc_dialog(nitems, sizeof(*data)); if (!dlg) return 0; data = (struct lua_xdialog_data *) get_dialog_offset(dlg, nitems); data->state = S; data->nfields = nfields; for (i = 0; i < nfields; i++) safe_strncpy(data->fields[i], (unsigned char *) lua_tostring(S, i+1), MAX_STR_LEN-1); lua_pushvalue(S, nargs); data->func_ref = luaL_ref(S, LUA_REGISTRYINDEX); dlg->title = _("User dialog", term); dlg->layouter = generic_dialog_layouter; dlg->layout.maximize_width = 1; for (i = 0; i < nfields; i++) add_dlg_field(dlg, _("Name", term), 0, 0, NULL, MAX_STR_LEN, data->fields[i], NULL); add_dlg_ok_button(dlg, _("~OK", term), B_ENTER, xdialog_run_lua, data); add_dlg_button(dlg, _("~Cancel", term), B_ESC, cancel_dialog, NULL); add_dlg_end(dlg, nitems); do_dialog(term, dlg, getml(dlg, NULL)); lua_pushnumber(S, 1); return 1; lua_error: lua_pushnil(S); return 1; } /* End xdialog bit. */ /* Set/get option */ static int l_set_option(LS) { int nargs; struct option *opt, *current; const char *name; nargs = lua_gettop(S); if (nargs != 2) goto lua_error; /* Get option record */ name = lua_tostring(S, 1); opt = get_opt_rec(config_options, (unsigned char *) name); if (opt == NULL) goto lua_error; /* Set option */ switch (opt->type) { case OPT_BOOL: { /* option_types[OPT_BOOL].set expects a long even though it * saves the value to opt->value.number, which is an int. */ long value = lua_toboolean(S, 2); option_types[opt->type].set(opt, (unsigned char *) (&value)); break; } case OPT_INT: case OPT_LONG: { /* option_types[OPT_INT].set expects a long even though it * saves the value to opt->value.number, which is an int. * option_types[OPT_LONG].set of course wants a long too. */ long value = lua_tonumber(S, 2); option_types[opt->type].set(opt, (unsigned char *) (&value)); break; } case OPT_STRING: case OPT_CODEPAGE: case OPT_LANGUAGE: case OPT_COLOR: option_types[opt->type].set(opt, (unsigned char *) lua_tostring(S, 2)); break; default: goto lua_error; } opt->flags |= OPT_TOUCHED; /* Call hook */ current = opt; call_change_hooks(lua_ses, current, opt); return 1; lua_error: lua_pushnil(S); return 1; } static int l_get_option(LS) { int nargs; struct option *opt; const char *name; /* Get option record */ nargs = lua_gettop(S); if (nargs != 1) goto lua_error; name = lua_tostring(S, 1); opt = get_opt_rec(config_options, (unsigned char *) name); if (opt == NULL) goto lua_error; /* Convert to an appropriate Lua type */ switch (opt->type) { case OPT_BOOL: lua_pushboolean(S, opt->value.number); break; case OPT_INT: lua_pushnumber(S, opt->value.number); break; case OPT_LONG: lua_pushnumber(S, opt->value.big_number); break; case OPT_STRING: lua_pushstring(S, opt->value.string); break; case OPT_CODEPAGE: { unsigned char *cp_name; cp_name = get_cp_mime_name(opt->value.number); lua_pushstring(S, cp_name); break; } case OPT_LANGUAGE: { unsigned char *lang; #ifdef ENABLE_NLS lang = language_to_name(current_language); #else lang = "System"; #endif lua_pushstring(S, lang); break; } case OPT_COLOR: { color_T color; unsigned char hexcolor[8]; unsigned char *strcolor; color = opt->value.color; strcolor = get_color_string(color, hexcolor); lua_pushstring(S, strcolor); break; } case OPT_COMMAND: default: goto lua_error; } return 1; lua_error: lua_pushnil(S); return 1; } /* End of set/get option */ int eval_function(LS, int num_args, int num_results) { int err; err = lua_pcall(S, num_args, num_results, 0); if (err) { alert_lua_error((unsigned char *) lua_tostring(L, -1)); lua_pop(L, 1); } return err; } /* Initialisation */ static void do_hooks_file(LS, unsigned char *prefix, unsigned char *filename) { unsigned char *file = straconcat(prefix, "/", filename, NULL); if (!file) return; /* Test file existence to avoid Lua error reporting (under version 5.x) * Fixes debian bug #231760 ('dbug 231760' using URI rewrite) */ if (file_can_read(file)) { int oldtop = lua_gettop(S); if (lua_dofile(S, file) != 0) sleep(3); /* Let some time to see error messages. */ lua_settop(S, oldtop); } mem_free(file); } void init_lua(struct module *module) { L = lua_open(); luaopen_base(L); luaopen_table(L); luaopen_io(L); luaopen_string(L); luaopen_math(L); lua_register(L, LUA_ALERT, l_alert); lua_register(L, "current_url", l_current_url); lua_register(L, "current_link", l_current_link); lua_register(L, "current_title", l_current_title); lua_register(L, "current_document", l_current_document); lua_register(L, "current_document_formatted", l_current_document_formatted); lua_register(L, "pipe_read", l_pipe_read); lua_register(L, "execute", l_execute); lua_register(L, "tmpname", l_tmpname); lua_register(L, "bind_key", l_bind_key); lua_register(L, "edit_bookmark_dialog", l_edit_bookmark_dialog); lua_register(L, "xdialog", l_xdialog); lua_register(L, "set_option", l_set_option); lua_register(L, "get_option", l_get_option); lua_pushstring(L, elinks_home ? elinks_home : (unsigned char *) CONFDIR); lua_setglobal(L, "elinks_home"); do_hooks_file(L, CONFDIR, LUA_HOOKS_FILENAME); if (elinks_home) do_hooks_file(L, elinks_home, LUA_HOOKS_FILENAME); } static void free_lua_console_history_entries(void); void cleanup_lua(struct module *module) { free_lua_console_history_entries(); lua_close(L); } /* Attempt to handle infinite loops by trapping SIGINT. If we get a * SIGINT, we longjump to where prepare_lua was called. finish_lua() * disables the trapping. */ static void handle_sigint(void *data) { finish_lua(); siglongjmp(errjmp, -1); } int prepare_lua(struct session *ses) { lua_ses = ses; errterm = lua_ses ? lua_ses->tab->term : NULL; /* XXX this uses the wrong term, I think */ install_signal_handler(SIGINT, (void (*)(void *)) handle_sigint, NULL, 1); return sigsetjmp(errjmp, 1); } void finish_lua(void) { /* XXX should save previous handler instead of assuming this one */ install_signal_handler(SIGINT, (void (*)(void *)) sig_ctrl_c, errterm, 0); } /* Error reporting. */ void alert_lua_error(unsigned char *msg) { if (errterm) { info_box(errterm, MSGBOX_NO_TEXT_INTL | MSGBOX_FREE_TEXT, N_("Lua Error"), ALIGN_LEFT, stracpy(msg)); return; } usrerror("Lua: %s", msg); sleep(3); } void alert_lua_error2(unsigned char *msg, unsigned char *msg2) { unsigned char *tmp = stracpy(msg); if (!tmp) return; add_to_strn(&tmp, msg2); alert_lua_error(tmp); mem_free(tmp); } /* The following stuff is to handle the return values of * lua_console_hook and keystroke functions, and also the xdialog * function. It expects two values on top of the stack. */ static void handle_ret_eval(struct session *ses) { const unsigned char *expr = lua_tostring(L, -1); if (expr) { int oldtop = lua_gettop(L); if (prepare_lua(ses) == 0) { lua_dostring(L, expr); lua_settop(L, oldtop); finish_lua(); } return; } alert_lua_error("bad argument for eval"); } static void handle_ret_run(struct session *ses) { unsigned char *cmd = (unsigned char *) lua_tostring(L, -1); if (cmd) { exec_on_terminal(ses->tab->term, cmd, "", 1); return; } alert_lua_error("bad argument for run"); } static void handle_ret_goto_url(struct session *ses) { unsigned char *url = (unsigned char *) lua_tostring(L, -1); if (url) { goto_url_with_hook(ses, url); return; } alert_lua_error("bad argument for goto_url"); } static void handle_standard_lua_returns(unsigned char *from) { const unsigned char *act = lua_tostring(L, -2); if (act) { if (!strcmp(act, "eval")) handle_ret_eval(lua_ses); else if (!strcmp(act, "run")) handle_ret_run(lua_ses); else if (!strcmp(act, "goto_url")) handle_ret_goto_url(lua_ses); else alert_lua_error2("unrecognised return value from ", from); } else if (!lua_isnil(L, -2)) alert_lua_error2("bad return type from ", from); lua_pop(L, 2); } static void handle_ref_on_stack(LS, struct session *ses, unsigned char *from, int num_args) { int err; if (prepare_lua(ses)) return; err = eval_function(S, num_args, 2); finish_lua(); if (!err) handle_standard_lua_returns(from); } static void handle_ref(LS, struct session *ses, int func_ref, unsigned char *from, int num_args, int unref) { lua_rawgeti(S, LUA_REGISTRYINDEX, func_ref); /* The function must be below the arguments on the stack. */ if (num_args != 0) lua_insert(S, -(num_args + 1)); handle_ref_on_stack(S, ses, from, num_args); if (unref) luaL_unref(S, LUA_REGISTRYINDEX, func_ref); } /* Console stuff. */ static INIT_INPUT_HISTORY(lua_console_history); static void lua_console(struct session *ses, unsigned char *expr) { lua_getglobal(L, "lua_console_hook"); if (lua_isnil(L, -1)) { lua_pop(L, 1); handle_ret_eval(ses); return; } lua_pushstring(L, expr); handle_ref_on_stack(L, ses, "lua_console_hook", 1); } /* TODO: Make this a "Scripting console" instead, with a radiobutton below the * inputbox selecting the appropriate scripting backend to use to evaluate the * expression. --pasky */ enum evhook_status dialog_lua_console(va_list ap, void *data) { struct session *ses = va_arg(ap, struct session *); if (get_cmd_opt_bool("anonymous")) return EVENT_HOOK_STATUS_NEXT; input_dialog(ses->tab->term, NULL, N_("Lua Console"), N_("Enter expression"), ses, &lua_console_history, MAX_STR_LEN, "", 0, 0, NULL, (void (*)(void *, unsigned char *)) lua_console, NULL); return EVENT_HOOK_STATUS_NEXT; } static void free_lua_console_history_entries(void) { free_list(lua_console_history.entries); } enum evhook_status free_lua_console_history(va_list ap, void *data) { free_lua_console_history_entries(); return EVENT_HOOK_STATUS_NEXT; }