diff --git a/core/runner/embed.go b/core/runner/embed.go index 55569c3..e4d2c8b 100644 --- a/core/runner/embed.go +++ b/core/runner/embed.go @@ -22,119 +22,69 @@ var sqliteLuaCode string //go:embed fs.lua var fsLuaCode string -// Global bytecode cache to improve performance +//go:embed util.lua +var utilLuaCode string + +//go:embed string.lua +var stringLuaCode string + +// ModuleInfo holds information about an embeddable Lua module +type ModuleInfo struct { + Name string // Module name + Code string // Module source code + Bytecode atomic.Pointer[[]byte] // Cached bytecode + Once sync.Once // For one-time compilation +} + var ( - sandboxBytecode atomic.Pointer[[]byte] - jsonBytecode atomic.Pointer[[]byte] - sqliteBytecode atomic.Pointer[[]byte] - fsBytecode atomic.Pointer[[]byte] - bytecodeOnce sync.Once - jsonBytecodeOnce sync.Once - sqliteBytecodeOnce sync.Once - fsBytecodeOnce sync.Once + sandbox = ModuleInfo{Name: "sandbox", Code: sandboxLuaCode} + modules = []ModuleInfo{ + {Name: "json", Code: jsonLuaCode}, + {Name: "sqlite", Code: sqliteLuaCode}, + {Name: "fs", Code: fsLuaCode}, + {Name: "util", Code: utilLuaCode}, + {Name: "string", Code: stringLuaCode}, + } ) -// precompileSandboxCode compiles the sandbox.lua code to bytecode once -func precompileSandboxCode() { - tempState := luajit.New() - if tempState == nil { - logger.Fatal("Failed to create temp Lua state for bytecode compilation") - } - defer tempState.Close() - defer tempState.Cleanup() +// precompileModule compiles a module's code to bytecode once +func precompileModule(m *ModuleInfo) { + m.Once.Do(func() { + tempState := luajit.New() + if tempState == nil { + logger.Fatal("Failed to create temp Lua state for %s module compilation", m.Name) + return + } + defer tempState.Close() + defer tempState.Cleanup() - code, err := tempState.CompileBytecode(sandboxLuaCode, "sandbox.lua") - if err != nil { - logger.Error("Failed to compile sandbox code: %v", err) - return - } - - bytecode := make([]byte, len(code)) - copy(bytecode, code) - sandboxBytecode.Store(&bytecode) - - logger.Debug("Successfully precompiled sandbox.lua to bytecode (%d bytes)", len(code)) -} - -// precompileJsonModule compiles the json.lua code to bytecode once -func precompileJsonModule() { - tempState := luajit.New() - if tempState == nil { - logger.Fatal("Failed to create temp Lua state for JSON module compilation") - } - defer tempState.Close() - defer tempState.Cleanup() - - code, err := tempState.CompileBytecode(jsonLuaCode, "json.lua") - if err != nil { - logger.Error("Failed to compile JSON module: %v", err) - return - } - - bytecode := make([]byte, len(code)) - copy(bytecode, code) - jsonBytecode.Store(&bytecode) - - logger.Debug("Successfully precompiled json.lua to bytecode (%d bytes)", len(code)) -} - -// precompileSqliteModule compiles the sqlite.lua code to bytecode once -func precompileSqliteModule() { - tempState := luajit.New() - if tempState == nil { - logger.Fatal("Failed to create temp Lua state for SQLite module compilation") - } - defer tempState.Close() - defer tempState.Cleanup() - - code, err := tempState.CompileBytecode(sqliteLuaCode, "sqlite.lua") - if err != nil { - logger.Error("Failed to compile SQLite module: %v", err) - return - } - - bytecode := make([]byte, len(code)) - copy(bytecode, code) - sqliteBytecode.Store(&bytecode) - - logger.Debug("Successfully precompiled sqlite.lua to bytecode (%d bytes)", len(code)) -} - -func precompileFsModule() { - tempState := luajit.New() - if tempState == nil { - logger.Fatal("Failed to create temp Lua state for FS module compilation") - } - defer tempState.Close() - defer tempState.Cleanup() - - code, err := tempState.CompileBytecode(fsLuaCode, "fs.lua") - if err != nil { - logger.Error("Failed to compile FS module: %v", err) - return - } - - bytecode := make([]byte, len(code)) - copy(bytecode, code) - fsBytecode.Store(&bytecode) - - logger.Debug("Successfully precompiled fs.lua to bytecode (%d bytes)", len(code)) -} - -// loadSandboxIntoState loads the sandbox code into a Lua state -func loadSandboxIntoState(state *luajit.State, verbose bool) error { - bytecodeOnce.Do(precompileSandboxCode) - jsonBytecodeOnce.Do(precompileJsonModule) - sqliteBytecodeOnce.Do(precompileSqliteModule) - - // First load and execute the JSON module - jsBytecode := jsonBytecode.Load() - if jsBytecode != nil && len(*jsBytecode) > 0 { - if verbose { - logger.Debug("Loading json.lua from precompiled bytecode") + code, err := tempState.CompileBytecode(m.Code, m.Name+".lua") + if err != nil { + logger.Error("Failed to compile %s module: %v", m.Name, err) + return } - if err := state.LoadBytecode(*jsBytecode, "json.lua"); err != nil { + bytecode := make([]byte, len(code)) + copy(bytecode, code) + m.Bytecode.Store(&bytecode) + + logger.Debug("Successfully precompiled %s.lua to bytecode (%d bytes)", m.Name, len(code)) + }) +} + +// loadModule loads a module into a Lua state +func loadModule(state *luajit.State, m *ModuleInfo, verbose bool) error { + // Ensure bytecode is compiled + precompileModule(m) + + // Attempt to load from bytecode + bytecode := m.Bytecode.Load() + if bytecode != nil && len(*bytecode) > 0 { + if verbose { + logger.Debug("Loading %s.lua from precompiled bytecode", m.Name) + } + + if err := state.LoadBytecode(*bytecode, m.Name+".lua"); err != nil { return err } @@ -142,75 +92,38 @@ func loadSandboxIntoState(state *luajit.State, verbose bool) error { return err } - state.SetGlobal("json") + state.SetGlobal(m.Name) } else { + // Fallback to interpreting the source if verbose { - logger.Warning("Using non-precompiled json.lua") + logger.Warning("Using non-precompiled %s.lua", m.Name) } - if err := state.DoString(jsonLuaCode); err != nil { + if err := state.DoString(m.Code); err != nil { return err } } - // Initialize active connections tracking + return nil +} + +// loadSandboxIntoState loads all modules and sandbox into a Lua state +func loadSandboxIntoState(state *luajit.State, verbose bool) error { + // Load all modules first + for i := range modules { + if err := loadModule(state, &modules[i], verbose); err != nil { + return err + } + } + + // Initialize active connections tracking (specific to SQLite) if err := state.DoString(`__active_sqlite_connections = {}`); err != nil { return err } - // Load SQLite module - sqlBytecode := sqliteBytecode.Load() - if sqlBytecode != nil && len(*sqlBytecode) > 0 { - if verbose { - logger.Debug("Loading sqlite.lua from precompiled bytecode") - } - - if err := state.LoadBytecode(*sqlBytecode, "sqlite.lua"); err != nil { - return err - } - - if err := state.RunBytecodeWithResults(1); err != nil { - return err - } - - state.SetGlobal("sqlite") - } else { - if verbose { - logger.Warning("Using non-precompiled sqlite.lua") - } - - if err := state.DoString(sqliteLuaCode); err != nil { - return err - } - } - - fsBytecodeOnce.Do(precompileFsModule) - fsBytecode := fsBytecode.Load() - if fsBytecode != nil && len(*fsBytecode) > 0 { - if verbose { - logger.Debug("Loading fs.lua from precompiled bytecode") - } - - if err := state.LoadBytecode(*fsBytecode, "fs.lua"); err != nil { - return err - } - - if err := state.RunBytecodeWithResults(1); err != nil { - return err - } - - state.SetGlobal("fs") - } else { - if verbose { - logger.Warning("Using non-precompiled fs.lua") - } - - if err := state.DoString(fsLuaCode); err != nil { - return err - } - } - - bytecode := sandboxBytecode.Load() + // Load the sandbox last + precompileModule(&sandbox) + bytecode := sandbox.Bytecode.Load() if bytecode != nil && len(*bytecode) > 0 { if verbose { logger.Debug("Loading sandbox.lua from precompiled bytecode") diff --git a/core/runner/sandbox.go b/core/runner/sandbox.go index 341bd98..bc8122c 100644 --- a/core/runner/sandbox.go +++ b/core/runner/sandbox.go @@ -113,6 +113,10 @@ func (s *Sandbox) registerCoreFunctions(state *luajit.State) error { return err } + if err := RegisterUtilFunctions(state); err != nil { + return err + } + return nil } diff --git a/core/runner/sandbox.lua b/core/runner/sandbox.lua index 5eede95..3319e1e 100644 --- a/core/runner/sandbox.lua +++ b/core/runner/sandbox.lua @@ -1,9 +1,5 @@ --[[ -Moonshark Lua Sandbox Environment - -This file contains all the Lua code needed for the sandbox environment, -including core modules and utilities. It's designed to be embedded in the -Go binary at build time. +sandbox.lua ]]-- __http_response = {} @@ -421,60 +417,6 @@ local csrf = { end } --- ====================================================================== --- UTIL MODULE --- ====================================================================== - --- Utility module implementation -local util = { - generate_token = function(length) - return __generate_token(length or 32) - end, - - -- Deep copy of tables - deep_copy = function(obj) - if type(obj) ~= 'table' then return obj end - local res = {} - for k, v in pairs(obj) do res[k] = util.deep_copy(v) end - return res - end, - - -- Merge tables - merge_tables = function(t1, t2) - if type(t1) ~= 'table' or type(t2) ~= 'table' then - error("Both arguments must be tables", 2) - end - - local result = util.deep_copy(t1) - for k, v in pairs(t2) do - if type(v) == 'table' and type(result[k]) == 'table' then - result[k] = util.merge_tables(result[k], v) - else - result[k] = v - end - end - return result - end, - - -- String utilities - string = { - -- Trim whitespace - trim = function(s) - return (s:gsub("^%s*(.-)%s*$", "%1")) - end, - - -- Split string - split = function(s, delimiter) - delimiter = delimiter or "," - local result = {} - for match in (s..delimiter):gmatch("(.-)"..delimiter) do - table.insert(result, match) - end - return result - end - } -} - -- ====================================================================== -- TEMPLATE RENDER FUNCTION -- ====================================================================== @@ -619,5 +561,4 @@ _G.http = http _G.session = session _G.csrf = csrf _G.cookie = cookie -_G.util = util _G.password = password diff --git a/core/runner/string.lua b/core/runner/string.lua new file mode 100644 index 0000000..c29960d --- /dev/null +++ b/core/runner/string.lua @@ -0,0 +1,197 @@ +--[[ +string.lua - Extended string library functions +]]-- + +local string_ext = {} + +-- ====================================================================== +-- STRING UTILITY FUNCTIONS +-- ====================================================================== + +-- Trim whitespace from both ends +function string_ext.trim(s) + if type(s) ~= "string" then return s end + return s:match("^%s*(.-)%s*$") +end + +-- Split string by delimiter +function string_ext.split(s, delimiter) + if type(s) ~= "string" then return {} end + + delimiter = delimiter or "," + local result = {} + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match) + end + return result +end + +-- Check if string starts with prefix +function string_ext.starts_with(s, prefix) + if type(s) ~= "string" or type(prefix) ~= "string" then return false end + return s:sub(1, #prefix) == prefix +end + +-- Check if string ends with suffix +function string_ext.ends_with(s, suffix) + if type(s) ~= "string" or type(suffix) ~= "string" then return false end + return suffix == "" or s:sub(-#suffix) == suffix +end + +-- Left pad a string +function string_ext.pad_left(s, len, char) + if type(s) ~= "string" or type(len) ~= "number" then return s end + + char = char or " " + if #s >= len then return s end + + return string.rep(char:sub(1,1), len - #s) .. s +end + +-- Right pad a string +function string_ext.pad_right(s, len, char) + if type(s) ~= "string" or type(len) ~= "number" then return s end + + char = char or " " + if #s >= len then return s end + + return s .. string.rep(char:sub(1,1), len - #s) +end + +-- Center a string +function string_ext.center(s, width, char) + if type(s) ~= "string" or width <= #s then return s end + + char = char or " " + local pad_len = width - #s + local left_pad = math.floor(pad_len / 2) + local right_pad = pad_len - left_pad + + return string.rep(char:sub(1,1), left_pad) .. s .. string.rep(char:sub(1,1), right_pad) +end + +-- Count occurrences of substring +function string_ext.count(s, substr) + if type(s) ~= "string" or type(substr) ~= "string" or #substr == 0 then return 0 end + + local count, pos = 0, 1 + while true do + pos = s:find(substr, pos, true) + if not pos then break end + count = count + 1 + pos = pos + 1 + end + return count +end + +-- Capitalize first letter +function string_ext.capitalize(s) + if type(s) ~= "string" or #s == 0 then return s end + return s:sub(1,1):upper() .. s:sub(2) +end + +-- Capitalize all words +function string_ext.title(s) + if type(s) ~= "string" then return s end + + return s:gsub("(%w)([%w]*)", function(first, rest) + return first:upper() .. rest:lower() + end) +end + +-- Insert string at position +function string_ext.insert(s, pos, insert_str) + if type(s) ~= "string" or type(insert_str) ~= "string" then return s end + + pos = math.max(1, math.min(pos, #s + 1)) + return s:sub(1, pos - 1) .. insert_str .. s:sub(pos) +end + +-- Remove substring +function string_ext.remove(s, start, length) + if type(s) ~= "string" then return s end + + length = length or 1 + if start < 1 or start > #s then return s end + + return s:sub(1, start - 1) .. s:sub(start + length) +end + +-- Replace substring once +function string_ext.replace(s, old, new, n) + if type(s) ~= "string" or type(old) ~= "string" or #old == 0 then return s end + + new = new or "" + n = n or 1 + + return s:gsub(old:gsub("[%-%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1"), new, n) +end + +-- Check if string contains substring +function string_ext.contains(s, substr) + if type(s) ~= "string" or type(substr) ~= "string" then return false end + return s:find(substr, 1, true) ~= nil +end + +-- Escape pattern magic characters +function string_ext.escape_pattern(s) + if type(s) ~= "string" then return s end + return s:gsub("[%-%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1") +end + +-- Wrap text at specified width +function string_ext.wrap(s, width, indent_first, indent_rest) + if type(s) ~= "string" or type(width) ~= "number" then return s end + + width = math.max(1, width) + indent_first = indent_first or "" + indent_rest = indent_rest or indent_first + + local result = {} + local line_prefix = indent_first + local pos = 1 + + while pos <= #s do + local line_width = width - #line_prefix + local end_pos = math.min(pos + line_width - 1, #s) + + if end_pos < #s then + local last_space = s:sub(pos, end_pos):match(".*%s()") + if last_space then + end_pos = pos + last_space - 2 + end + end + + table.insert(result, line_prefix .. s:sub(pos, end_pos)) + pos = end_pos + 1 + + -- Skip leading spaces on next line + while s:sub(pos, pos) == " " do + pos = pos + 1 + end + + line_prefix = indent_rest + end + + return table.concat(result, "\n") +end + +-- Limit string length with ellipsis +function string_ext.truncate(s, length, ellipsis) + if type(s) ~= "string" then return s end + + ellipsis = ellipsis or "..." + if #s <= length then return s end + + return s:sub(1, length - #ellipsis) .. ellipsis +end + +-- ====================================================================== +-- INSTALL EXTENSIONS INTO STRING LIBRARY +-- ====================================================================== + +for name, func in pairs(string_ext) do + string[name] = func +end + +return string_ext \ No newline at end of file diff --git a/core/runner/util.go b/core/runner/util.go new file mode 100644 index 0000000..e8c3010 --- /dev/null +++ b/core/runner/util.go @@ -0,0 +1,116 @@ +package runner + +import ( + "encoding/base64" + "html" + "strings" + + luajit "git.sharkk.net/Sky/LuaJIT-to-Go" +) + +// RegisterUtilFunctions registers utility functions with the Lua state +func RegisterUtilFunctions(state *luajit.State) error { + // HTML special chars + if err := state.RegisterGoFunction("__html_special_chars", htmlSpecialChars); err != nil { + return err + } + + // HTML entities + if err := state.RegisterGoFunction("__html_entities", htmlEntities); err != nil { + return err + } + + // Base64 encode + if err := state.RegisterGoFunction("__base64_encode", base64Encode); err != nil { + return err + } + + // Base64 decode + if err := state.RegisterGoFunction("__base64_decode", base64Decode); err != nil { + return err + } + + return nil +} + +// htmlSpecialChars converts special characters to HTML entities +func htmlSpecialChars(state *luajit.State) int { + if !state.IsString(1) { + state.PushNil() + return 1 + } + + input := state.ToString(1) + result := html.EscapeString(input) + state.PushString(result) + return 1 +} + +// htmlEntities is a more comprehensive version of htmlSpecialChars +func htmlEntities(state *luajit.State) int { + if !state.IsString(1) { + state.PushNil() + return 1 + } + + input := state.ToString(1) + // First use HTML escape for standard entities + result := html.EscapeString(input) + + // Additional entities beyond what html.EscapeString handles + replacements := map[string]string{ + "©": "©", + "®": "®", + "™": "™", + "€": "€", + "£": "£", + "¥": "¥", + "—": "—", + "–": "–", + "…": "…", + "•": "•", + "°": "°", + "±": "±", + "¼": "¼", + "½": "½", + "¾": "¾", + } + + for char, entity := range replacements { + result = strings.ReplaceAll(result, char, entity) + } + + state.PushString(result) + return 1 +} + +// base64Encode encodes a string to base64 +func base64Encode(state *luajit.State) int { + if !state.IsString(1) { + state.PushNil() + return 1 + } + + input := state.ToString(1) + result := base64.StdEncoding.EncodeToString([]byte(input)) + state.PushString(result) + return 1 +} + +// base64Decode decodes a base64 string +func base64Decode(state *luajit.State) int { + if !state.IsString(1) { + state.PushNil() + return 1 + } + + input := state.ToString(1) + result, err := base64.StdEncoding.DecodeString(input) + if err != nil { + state.PushNil() + return 1 + } + + state.PushString(string(result)) + return 1 +} diff --git a/core/runner/util.lua b/core/runner/util.lua new file mode 100644 index 0000000..ed4c219 --- /dev/null +++ b/core/runner/util.lua @@ -0,0 +1,335 @@ +--[[ +util.lua - Utility functions for the Lua sandbox +Enhanced with web development utilities +]]-- + +local util = {} + +-- ====================================================================== +-- CORE UTILITY FUNCTIONS +-- ====================================================================== + +-- Generate a random token +function util.generate_token(length) + return __generate_token(length or 32) +end + +-- Deep copy of tables +function util.deep_copy(obj) + if type(obj) ~= 'table' then return obj end + local res = {} + for k, v in pairs(obj) do res[k] = util.deep_copy(v) end + return res +end + +-- Merge tables +function util.merge_tables(t1, t2) + if type(t1) ~= 'table' or type(t2) ~= 'table' then + error("Both arguments must be tables", 2) + end + + local result = util.deep_copy(t1) + for k, v in pairs(t2) do + if type(v) == 'table' and type(result[k]) == 'table' then + result[k] = util.merge_tables(result[k], v) + else + result[k] = v + end + end + return result +end + +-- ====================================================================== +-- HTML ENTITY FUNCTIONS +-- ====================================================================== + +-- HTML entity mapping for common characters +local html_entities = { + ["&"] = "&", + ["<"] = "<", + [">"] = ">", + ['"'] = """, + ["'"] = "'", + ["/"] = "/", + ["`"] = "`", + ["="] = "=" +} + +-- Convert special characters to HTML entities (like htmlspecialchars) +function util.html_special_chars(str) + if type(str) ~= "string" then + return str + end + + return __html_special_chars(str) +end + +-- Convert all applicable characters to HTML entities (like htmlentities) +function util.html_entities(str) + if type(str) ~= "string" then + return str + end + + return __html_entities(str) +end + +-- Convert HTML entities back to characters (simple version) +function util.html_entity_decode(str) + if type(str) ~= "string" then + return str + end + + str = str:gsub("<", "<") + str = str:gsub(">", ">") + str = str:gsub(""", '"') + str = str:gsub("'", "'") + str = str:gsub("&", "&") + + return str +end + +-- Convert newlines to
tags +function util.nl2br(str) + if type(str) ~= "string" then + return str + end + + return str:gsub("\r\n", "
"):gsub("\n", "
"):gsub("\r", "
") +end + +-- ====================================================================== +-- URL FUNCTIONS +-- ====================================================================== + +-- URL encode a string +function util.url_encode(str) + if type(str) ~= "string" then + return str + end + + str = str:gsub("\n", "\r\n") + str = str:gsub("([^%w %-%_%.%~])", function(c) + return string.format("%%%02X", string.byte(c)) + end) + str = str:gsub(" ", "+") + return str +end + +-- URL decode a string +function util.url_decode(str) + if type(str) ~= "string" then + return str + end + + str = str:gsub("+", " ") + str = str:gsub("%%(%x%x)", function(h) + return string.char(tonumber(h, 16)) + end) + return str +end + +-- ====================================================================== +-- VALIDATION FUNCTIONS +-- ====================================================================== + +-- Email validation +function util.is_email(str) + if type(str) ~= "string" then + return false + end + + -- Simple email validation pattern + local pattern = "^[%w%.%%%+%-]+@[%w%.%%%+%-]+%.%w%w%w?%w?$" + return str:match(pattern) ~= nil +end + +-- URL validation +function util.is_url(str) + if type(str) ~= "string" then + return false + end + + -- Simple URL validation + local pattern = "^https?://[%w-_%.%?%.:/%+=&%%]+$" + return str:match(pattern) ~= nil +end + +-- IP address validation (IPv4) +function util.is_ipv4(str) + if type(str) ~= "string" then + return false + end + + local pattern = "^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$" + local a, b, c, d = str:match(pattern) + + if not (a and b and c and d) then + return false + end + + a, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d) + return a <= 255 and b <= 255 and c <= 255 and d <= 255 +end + +-- Integer validation +function util.is_int(str) + if type(str) == "number" then + return math.floor(str) == str + elseif type(str) ~= "string" then + return false + end + + return str:match("^-?%d+$") ~= nil +end + +-- Float validation +function util.is_float(str) + if type(str) == "number" then + return true + elseif type(str) ~= "string" then + return false + end + + return str:match("^-?%d+%.?%d*$") ~= nil +end + +-- Boolean validation +function util.is_bool(value) + if type(value) == "boolean" then + return true + elseif type(value) ~= "string" and type(value) ~= "number" then + return false + end + + local v = type(value) == "string" and value:lower() or value + return v == "1" or v == "true" or v == "on" or v == "yes" or + v == "0" or v == "false" or v == "off" or v == "no" or + v == 1 or v == 0 +end + +-- Convert to boolean +function util.to_bool(value) + if type(value) == "boolean" then + return value + elseif type(value) ~= "string" and type(value) ~= "number" then + return false + end + + local v = type(value) == "string" and value:lower() or value + return v == "1" or v == "true" or v == "on" or v == "yes" or v == 1 +end + +-- Sanitize string (simple version) +function util.sanitize_string(str) + if type(str) ~= "string" then + return "" + end + + return util.html_special_chars(str) +end + +-- Sanitize to integer +function util.sanitize_int(value) + if type(value) ~= "string" and type(value) ~= "number" then + return 0 + end + + value = tostring(value) + local result = value:match("^-?%d+") + return result and tonumber(result) or 0 +end + +-- Sanitize to float +function util.sanitize_float(value) + if type(value) ~= "string" and type(value) ~= "number" then + return 0 + end + + value = tostring(value) + local result = value:match("^-?%d+%.?%d*") + return result and tonumber(result) or 0 +end + +-- Sanitize URL +function util.sanitize_url(str) + if type(str) ~= "string" then + return "" + end + + -- Basic sanitization by removing control characters + str = str:gsub("[\000-\031]", "") + + -- Make sure it's a valid URL + if util.is_url(str) then + return str + end + + -- Try to prepend http:// if it's missing + if not str:match("^https?://") and util.is_url("http://" .. str) then + return "http://" .. str + end + + return "" +end + +-- Sanitize email +function util.sanitize_email(str) + if type(str) ~= "string" then + return "" + end + + -- Remove all characters except common email characters + str = str:gsub("[^%a%d%!%#%$%%%&%'%*%+%-%/%=%?%^%_%`%{%|%}%~%@%.%[%]]", "") + + -- Return only if it's a valid email + if util.is_email(str) then + return str + end + + return "" +end + +-- ====================================================================== +-- SECURITY FUNCTIONS +-- ====================================================================== + +-- Basic XSS prevention +function util.xss_clean(str) + if type(str) ~= "string" then + return str + end + + -- Convert problematic characters to entities + local result = util.html_special_chars(str) + + -- Remove JavaScript event handlers + result = result:gsub("on%w+%s*=", "") + + -- Remove JavaScript protocol + result = result:gsub("javascript:", "") + + -- Remove CSS expression + result = result:gsub("expression%s*%(", "") + + return result +end + +-- Base64 encode +function util.base64_encode(str) + if type(str) ~= "string" then + return str + end + + return __base64_encode(str) +end + +-- Base64 decode +function util.base64_decode(str) + if type(str) ~= "string" then + return str + end + + return __base64_decode(str) +end + +return util \ No newline at end of file