-- sandbox.lua function __execute(script_func, ctx, response) -- Store context and response globally for function access __ctx = ctx __response = response _G.ctx = ctx -- Create a coroutine for script execution to handle early exits local co = coroutine.create(function() return script_func() end) local ok, result = coroutine.resume(co) -- Clean up __ctx = nil __response = nil if not ok then -- Real error during script execution error(result, 0) end -- Check if exit was requested if result == "__EXIT__" then return {nil, response} end return {result, response} end -- Exit sentinel using coroutine yield instead of error function exit() coroutine.yield("__EXIT__") end -- ====================================================================== -- HTTP RESPONSE FUNCTIONS -- ====================================================================== function http_set_status(code) __response.status = code end function http_set_header(name, value) __response.headers = __response.headers or {} __response.headers[name] = value end function http_set_content_type(ct) __response.headers = __response.headers or {} __response.headers["Content-Type"] = ct end function http_set_metadata(key, value) __response.metadata = __response.metadata or {} __response.metadata[key] = value end function http_redirect(url, status) __response.status = status or 302 __response.headers = __response.headers or {} __response.headers["Location"] = url coroutine.yield("__EXIT__") end -- ====================================================================== -- COOKIE FUNCTIONS -- ====================================================================== function cookie_set(name, value, options) __response.cookies = __response.cookies or {} local opts = options or {} local cookie = { name = name, value = value or "", path = opts.path or "/", domain = opts.domain, secure = opts.secure ~= false, http_only = opts.http_only ~= false } if opts.expires and opts.expires > 0 then cookie.max_age = opts.expires end table.insert(__response.cookies, cookie) end function cookie_get(name) return __ctx._request_cookies and __ctx._request_cookies[name] end function cookie_delete(name, path, domain) return cookie_set(name, "", {expires = -1, path = path or "/", domain = domain}) end -- ====================================================================== -- SESSION FUNCTIONS -- ====================================================================== function session_set(key, value) __response.session = __response.session or {} __response.session[key] = value if __ctx.session and __ctx.session.data then __ctx.session.data[key] = value end end function session_get(key) return __ctx.session and __ctx.session.data and __ctx.session.data[key] end function session_id() return __ctx.session and __ctx.session.id end function session_get_all() if __ctx.session and __ctx.session.data then local copy = {} for k, v in pairs(__ctx.session.data) do copy[k] = v end return copy end return {} end function session_delete(key) __response.session = __response.session or {} __response.session[key] = "__DELETE__" if __ctx.session and __ctx.session.data then __ctx.session.data[key] = nil end end function session_clear() __response.session = {__clear_all = true} if __ctx.session and __ctx.session.data then for k in pairs(__ctx.session.data) do __ctx.session.data[k] = nil end end end -- ====================================================================== -- CSRF FUNCTIONS -- ====================================================================== function csrf_generate() local token = generate_token(32) session_set("_csrf_token", token) return token end function csrf_field() local token = session_get("_csrf_token") if not token then token = csrf_generate() end return string.format('', html_special_chars(token)) end function csrf_validate() local token = __ctx.session and __ctx.session.data and __ctx.session.data["_csrf_token"] if not token then __response.status = 403 coroutine.yield("__EXIT__") end local request_token = (__ctx._request_form and __ctx._request_form._csrf_token) or (__ctx._request_headers and (__ctx._request_headers["x-csrf-token"] or __ctx._request_headers["csrf-token"])) if not request_token or request_token ~= token then __response.status = 403 coroutine.yield("__EXIT__") end return true end -- ====================================================================== -- CONTENT TYPE HELPERS -- ====================================================================== function send_html(content) http_set_content_type("text/html") return content end function send_json(content) http_set_content_type("application/json") return content end function send_text(content) http_set_content_type("text/plain") return content end function send_xml(content) http_set_content_type("application/xml") return content end function send_javascript(content) http_set_content_type("application/javascript") return content end function send_css(content) http_set_content_type("text/css") return content end function send_svg(content) http_set_content_type("image/svg+xml") return content end function send_csv(content) http_set_content_type("text/csv") return content end function send_binary(content, mime_type) http_set_content_type(mime_type or "application/octet-stream") return content end -- ====================================================================== -- TEMPLATE RENDER FUNCTIONS -- ====================================================================== -- Template processing with code execution function render(template_str, env) local function is_control_structure(code) -- Check if code is a control structure that doesn't produce output local trimmed = code:match("^%s*(.-)%s*$") return trimmed == "else" or trimmed == "end" or trimmed:match("^if%s") or trimmed:match("^elseif%s") or trimmed:match("^for%s") or trimmed:match("^while%s") or trimmed:match("^repeat%s*$") or trimmed:match("^until%s") or trimmed:match("^do%s*$") or trimmed:match("^local%s") or trimmed:match("^function%s") or trimmed:match(".*=%s*function%s*%(") or trimmed:match(".*then%s*$") or trimmed:match(".*do%s*$") end local pos, chunks = 1, {} while pos <= #template_str do local unescaped_start = template_str:find("{{{", pos, true) local escaped_start = template_str:find("{{", pos, true) local start, tag_type, open_len if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then start, tag_type, open_len = unescaped_start, "-", 3 elseif escaped_start then start, tag_type, open_len = escaped_start, "=", 2 else table.insert(chunks, template_str:sub(pos)) break end if start > pos then table.insert(chunks, template_str:sub(pos, start-1)) end pos = start + open_len local close_tag = tag_type == "-" and "}}}" or "}}" local close_start, close_stop = template_str:find(close_tag, pos, true) if not close_start then error("Failed to find closing tag at position " .. pos) end local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") local is_control = is_control_structure(code) table.insert(chunks, {tag_type, code, pos, is_control}) pos = close_stop + 1 end local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} for _, chunk in ipairs(chunks) do local t = type(chunk) if t == "string" then table.insert(buffer, "_b_i = _b_i + 1\n") table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") else local tag_type, code, pos, is_control = chunk[1], chunk[2], chunk[3], chunk[4] if is_control then -- Control structure - just insert as raw Lua code table.insert(buffer, "--[[" .. pos .. "]] " .. code .. "\n") elseif tag_type == "=" then -- Simple variable check if code:match("^[%w_]+$") then table.insert(buffer, "_b_i = _b_i + 1\n") table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") else -- Expression output with escaping table.insert(buffer, "_b_i = _b_i + 1\n") table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") end elseif tag_type == "-" then -- Unescaped output table.insert(buffer, "_b_i = _b_i + 1\n") table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _tostring(" .. code .. ")\n") end end end table.insert(buffer, "return _b") local generated_code = table.concat(buffer) -- DEBUG: Uncomment to see generated code -- print("Generated Lua code:") -- print(generated_code) -- print("---") local fn, err = loadstring(generated_code) if not fn then print("Generated code that failed to compile:") print(generated_code) error(err) end env = env or {} local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end}) setfenv(fn, runtime_env) local output_buffer = {} fn(tostring, html_special_chars, output_buffer, 0) return table.concat(output_buffer) end -- Named placeholder processing function parse(template_str, env) local pos, output = 1, {} env = env or {} while pos <= #template_str do local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos) local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos) local next_pos, placeholder_end, name, escaped if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false elseif escaped_start then next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true else local text = template_str:sub(pos) if text and #text > 0 then table.insert(output, text) end break end local text = template_str:sub(pos, next_pos - 1) if text and #text > 0 then table.insert(output, text) end local value = env[name] local str = tostring(value or "") if escaped then str = html_special_chars(str) end table.insert(output, str) pos = placeholder_end + 1 end return table.concat(output) end -- Indexed placeholder processing function iparse(template_str, values) local pos, output, value_index = 1, {}, 1 values = values or {} while pos <= #template_str do local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true) local escaped_start, escaped_end = template_str:find("{{}}", pos, true) local next_pos, placeholder_end, escaped if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false elseif escaped_start then next_pos, placeholder_end, escaped = escaped_start, escaped_end, true else local text = template_str:sub(pos) if text and #text > 0 then table.insert(output, text) end break end local text = template_str:sub(pos, next_pos - 1) if text and #text > 0 then table.insert(output, text) end local value = values[value_index] local str = tostring(value or "") if escaped then str = html_special_chars(str) end table.insert(output, str) pos = placeholder_end + 1 value_index = value_index + 1 end return table.concat(output) end -- ====================================================================== -- PASSWORD FUNCTIONS -- ====================================================================== -- Hash a password using Argon2id -- Options: -- memory: Amount of memory to use in KB (default: 128MB) -- iterations: Number of iterations (default: 4) -- parallelism: Number of threads (default: 4) -- salt_length: Length of salt in bytes (default: 16) -- key_length: Length of the derived key in bytes (default: 32) function password_hash(plain_password, options) if type(plain_password) ~= "string" then error("password_hash: expected string password", 2) end return __password_hash(plain_password, options) end -- Verify a password against a hash function password_verify(plain_password, hash_string) if type(plain_password) ~= "string" then error("password_verify: expected string password", 2) end if type(hash_string) ~= "string" then error("password_verify: expected string hash", 2) end return __password_verify(plain_password, hash_string) end -- ====================================================================== -- SESSION FLASH FUNCTIONS -- ====================================================================== function session_flash(key, value) __response.flash = __response.flash or {} __response.flash[key] = value end function session_get_flash(key) -- Check current flash data first if __response.flash and __response.flash[key] ~= nil then return __response.flash[key] end -- Check session flash data if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then return __ctx.session.flash[key] end return nil end function session_has_flash(key) -- Check current flash if __response.flash and __response.flash[key] ~= nil then return true end -- Check session flash if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then return true end return false end function session_get_all_flash() local flash = {} -- Add session flash data first if __ctx.session and __ctx.session.flash then for k, v in pairs(__ctx.session.flash) do flash[k] = v end end -- Add current response flash (overwrites session flash if same key) if __response.flash then for k, v in pairs(__response.flash) do flash[k] = v end end return flash end function session_flash_now(key, value) -- Flash for current request only (not persisted) _G._current_flash = _G._current_flash or {} _G._current_flash[key] = value end function session_get_flash_now(key) return _G._current_flash and _G._current_flash[key] end -- ====================================================================== -- FLASH HELPER FUNCTIONS -- ====================================================================== function flash_success(message) session_flash("success", message) end function flash_error(message) session_flash("error", message) end function flash_warning(message) session_flash("warning", message) end function flash_info(message) session_flash("info", message) end function flash_message(type, message) session_flash(type, message) end -- Get flash messages by type function get_flash_success() return session_get_flash("success") end function get_flash_error() return session_get_flash("error") end function get_flash_warning() return session_get_flash("warning") end function get_flash_info() return session_get_flash("info") end -- Check if flash messages exist function has_flash_success() return session_has_flash("success") end function has_flash_error() return session_has_flash("error") end function has_flash_warning() return session_has_flash("warning") end function has_flash_info() return session_has_flash("info") end -- Convenience function for redirects with flash function redirect_with_flash(url, type, message, status) session_flash(type or "info", message) http_redirect(url, status) end function redirect_with_success(url, message, status) redirect_with_flash(url, "success", message, status) end function redirect_with_error(url, message, status) redirect_with_flash(url, "error", message, status) end