# Plugin Manager Documentation ## Table of Contents 1. [Overview](#overview) 2. [Architecture](#architecture) 3. [Getting Started](#getting-started) 4. [Core Concepts](#core-concepts) - [Plugins](#plugins) - [Routes](#routes) - [Hooks](#hooks) 5. [API Reference](#api-reference) - [Route Registration](#route-registration) - [Hook Registration](#hook-registration) - [File Operations](#file-operations) - [HTML Manipulation](#html-manipulation) - [Markdown Manipulation](#markdown-manipulation) - [Logging Functions](#logging-functions) - [Utility Functions](#utility-functions) 6. [Guardrails & Security](#guardrails--security) 7. [Examples](#examples) --- ## Overview The Plugin Manager is a Python-based system that enables dynamic Lua plugin loading for extending application functionality. It provides a secure, sandboxed environment for plugins to register HTTP routes, hook into application events, and manipulate HTML/Markdown content. **Key Features:** - Hot-reload plugins without restarting the application - Secure file operations with path traversal protection - Built-in HTML and Markdown manipulation tools - Event-based hook system - HTTP route registration - Comprehensive logging - Rate limiting and validation helpers --- ## Architecture ### Directory Structure ``` project/ ├── plugin_manager.py # Main plugin manager ├── plugins/ # Lua plugin files (*.lua) ├── html/ # HTML files for manipulation └── markdown/ # Markdown files for manipulation ``` ### Component Overview **PluginManager**: Core class that manages plugin lifecycle, Lua runtime, and API exposure **PluginFSHandler**: Watchdog-based file system handler for hot-reloading **LuaRuntime**: Embedded Lua interpreter with Python bindings via `lupa` --- ## Getting Started ### Installation Requirements ```bash pip install lupa watchdog ``` ### Creating Your First Plugin 1. Create a file `plugins/hello.lua`: ```lua -- Register a simple route add_route("/hello", function(req) return "

Hello from Lua Plugin!

" end) log("Hello plugin loaded") ``` 2. Start the plugin manager: ```python from plugin_manager import PluginManager manager = PluginManager() manager.load_all() # Handle a request result = manager.handle_request("/hello") print(result) # (200, {"Content-Type": "text/html"}, "

Hello from Lua Plugin!

") ``` 3. The plugin will automatically reload when you modify `hello.lua` --- ## Core Concepts ### Plugins Plugins are Lua scripts (`.lua` files) placed in the `plugins/` directory. Each plugin can: - Register HTTP routes via [`add_route()`](#route-registration) - Hook into events via [`register_hook()`](#hook-registration) - Access [file operations](#file-operations) - Use [HTML](#html-manipulation) and [Markdown manipulation](#markdown-manipulation) tools **Plugin Lifecycle:** 1. **Load**: Plugin file is read and executed in Lua runtime 2. **Active**: Plugin routes/hooks are registered and callable 3. **Reload**: File change detected → unload → load 4. **Unload**: Plugin deleted → routes/hooks removed ### Routes Routes map URL paths to Lua handler functions. When a request matches a registered route, the corresponding Lua function is called. **See:** [Route Registration](#route-registration), [Examples - Route Handler](#example-1-simple-route-handler) ### Hooks Hooks are named events where plugins can register callback functions. Multiple plugins can hook into the same event, and they'll be called in registration order. **Common Use Cases:** - Pre/post-processing request data - Modifying responses before they're sent - Content transformation pipelines - Event notifications **See:** [Hook Registration](#hook-registration), [Examples - Hook System](#example-2-hook-system) --- ## API Reference ### Route Registration #### `add_route(path, handler_function)` Register a URL path handler. **Parameters:** - `path` (string): URL path to handle (e.g., `/api/users`) - `handler_function` (function): Lua function to handle requests **Handler Function Signature:** ```lua function(request) -> response ``` **Request Object:** ```lua { method = "GET", -- HTTP method path = "/api/users", -- Request path query = {...}, -- Query parameters body = "...", -- Request body headers = {...} -- Request headers } ``` **Response Format:** Option 1 - Simple string (returns 200 with text/html): ```lua return "..." ``` Option 2 - Full response tuple: ```lua return 200, {["Content-Type"] = "application/json"}, '{"status":"ok"}' ``` **Example:** ```lua add_route("/api/status", function(req) return 200, {["Content-Type"] = "application/json"}, '{"status": "online"}' end) ``` **See also:** [Hooks](#hook-registration), [Examples](#examples) --- ### Hook Registration #### `register_hook(hook_name, handler_function)` Register a callback for a named hook event. **Parameters:** - `hook_name` (string): Name of the hook to register for - `handler_function` (function): Callback function **Handler Function Signature:** ```lua function(...args) -> result ``` **Behavior:** - All registered hooks for an event are called in order - Return values can modify data (last non-nil value is used) - Hooks receive arguments passed by `run_hook()` from Python **Example:** ```lua -- Content transformation hook register_hook("transform_html", function(html) -- Add analytics script return html_insert_before(html, "", "") end) -- Logging hook register_hook("request_complete", function(path, status) log("Request to " .. path .. " returned " .. status) end) ``` **Python Side - Triggering Hooks:** ```python # Transform HTML through all registered hooks modified_html = manager.run_hook("transform_html", original_html) # Notify all hooks of event manager.run_hook("request_complete", "/api/users", 200) ``` **See also:** [Routes](#route-registration), [Examples - Hooks](#example-2-hook-system) --- ### File Operations #### `read_file(path)` Read any file with path validation. **Parameters:** - `path` (string): File path to read **Returns:** File content as string, or `nil` on error **Security:** Path traversal (`..`) is blocked **Example:** ```lua local config = read_file("config.json") if config then log("Config loaded") end ``` #### `write_file(path, content)` Write content to file with path validation. **Parameters:** - `path` (string): File path to write - `content` (string): Content to write **Returns:** `true` on success, `false` on error **Example:** ```lua write_file("output.txt", "Generated content") ``` **See also:** [HTML Operations](#html-manipulation), [Markdown Operations](#markdown-manipulation) --- ### HTML Manipulation #### `read_html(filename)` Read HTML file from the `html/` directory. **Parameters:** - `filename` (string): Name of HTML file (e.g., `"index.html"`) **Returns:** HTML content as string, or `nil` if not found **Example:** ```lua local html = read_html("index.html") if html then -- Process HTML end ``` #### `write_html(filename, content)` Write HTML content to the `html/` directory. **Parameters:** - `filename` (string): Name of HTML file - `content` (string): HTML content to write **Returns:** `true` on success, `false` on error **Example:** ```lua local modified = html_insert_after(html, "", "") write_html("index.html", modified) ``` #### `list_html_files()` Get list of all HTML files in the `html/` directory. **Returns:** Array of filenames **Example:** ```lua local files = list_html_files() for _, file in ipairs(files) do log("Found HTML file: " .. file) end ``` #### `html_find_tag(html, tag)` Find the first occurrence of an HTML tag. **Parameters:** - `html` (string): HTML content to search - `tag` (string): Tag name (e.g., `"div"`, `"title"`) **Returns:** Matched tag with content, or `nil` if not found **Example:** ```lua local title_tag = html_find_tag(html, "title") -- Result: "My Page" ``` #### `html_replace_tag(html, tag, new_content)` Replace the content inside an HTML tag. **Parameters:** - `html` (string): HTML content - `tag` (string): Tag name to replace content within - `new_content` (string): New content (tag itself is preserved) **Returns:** Modified HTML **Example:** ```lua -- Replace title content local html = "Old Title" local modified = html_replace_tag(html, "title", "New Title") -- Result: "New Title" ``` **See also:** [`html_wrap_content()`](#html_wrap_contenthtml-tag-wrapper_tag-attrs) #### `html_insert_before(html, marker, content)` Insert content before a marker string. **Parameters:** - `html` (string): HTML content - `marker` (string): String to insert before - `content` (string): Content to insert **Returns:** Modified HTML **Example:** ```lua -- Insert meta tag before closing head local html = "" local modified = html_insert_before(html, "", "") -- Result: "" ``` **See also:** [`html_insert_after()`](#html_insert_afterhtml-marker-content) #### `html_insert_after(html, marker, content)` Insert content after a marker string. **Parameters:** - `html` (string): HTML content - `marker` (string): String to insert after - `content` (string): Content to insert **Returns:** Modified HTML **Example:** ```lua -- Insert script before closing body local modified = html_insert_after(html, "", "") ``` **See also:** [`html_insert_before()`](#html_insert_beforehtml-marker-content) #### `html_wrap_content(html, tag, wrapper_tag, attrs)` Wrap the content of a tag with another tag. **Parameters:** - `html` (string): HTML content - `tag` (string): Tag whose content to wrap - `wrapper_tag` (string): Tag to wrap with - `attrs` (string): Attributes for wrapper tag (optional) **Returns:** Modified HTML **Example:** ```lua local html = "
Hello World
" local modified = html_wrap_content(html, "div", "span", 'class="highlight"') -- Result: "
Hello World
" ``` **See also:** [`html_replace_tag()`](#html_replace_taghtml-tag-new_content) --- ### Markdown Manipulation #### `read_markdown(filename)` Read Markdown file from the `markdown/` directory. **Parameters:** - `filename` (string): Name of Markdown file (e.g., `"README.md"`) **Returns:** Markdown content as string, or `nil` if not found **Example:** ```lua local md = read_markdown("README.md") ``` #### `write_markdown(filename, content)` Write Markdown content to the `markdown/` directory. **Parameters:** - `filename` (string): Name of Markdown file - `content` (string): Markdown content to write **Returns:** `true` on success, `false` on error **Example:** ```lua write_markdown("output.md", "# Generated Document\n\nContent here...") ``` #### `list_markdown_files()` Get list of all Markdown files in the `markdown/` directory. **Returns:** Array of filenames **Example:** ```lua local files = list_markdown_files() for _, file in ipairs(files) do local content = read_markdown(file) -- Process each file end ``` #### `md_add_header(markdown, level, text)` Add a header at the beginning of Markdown content. **Parameters:** - `markdown` (string): Existing Markdown content - `level` (number): Header level (1-6, where 1 is `#` and 6 is `######`) - `text` (string): Header text **Returns:** Modified Markdown with header prepended **Example:** ```lua local md = "Some content here" local modified = md_add_header(md, 1, "Introduction") -- Result: -- # Introduction -- -- Some content here ``` **See also:** [`md_replace_section()`](#md_replace_sectionmarkdown-header-new_content) #### `md_replace_section(markdown, header, new_content)` Replace an entire Markdown section (from header to next header or end). **Parameters:** - `markdown` (string): Markdown content - `header` (string): Header text to find (without `#` symbols) - `new_content` (string): New content for the section **Returns:** Modified Markdown **Example:** ```lua local md = [[ # Introduction Old intro text # Features Feature list here ]] local modified = md_replace_section(md, "Introduction", "New introduction paragraph") -- Result: -- ## Introduction -- -- New introduction paragraph -- -- # Features -- Feature list here ``` **See also:** [`md_add_header()`](#md_add_headermarkdown-level-text), [`md_append_content()`](#md_append_contentmarkdown-content) #### `md_append_content(markdown, content)` Append content to the end of Markdown document. **Parameters:** - `markdown` (string): Existing Markdown content - `content` (string): Content to append **Returns:** Modified Markdown **Example:** ```lua local md = "# Document\n\nExisting content" local modified = md_append_content(md, "## Appendix\n\nAdditional information") ``` **See also:** [`md_add_header()`](#md_add_headermarkdown-level-text) --- ### Logging Functions #### `log(message)` Log informational message. **Parameters:** - `message` (string): Message to log **Example:** ```lua log("Plugin initialized successfully") ``` **See also:** [`log_warn()`](#log_warnmessage), [`log_error()`](#log_errormessage) #### `log_warn(message)` Log warning message. **Parameters:** - `message` (string): Warning message **Example:** ```lua if not config then log_warn("Configuration file not found, using defaults") end ``` **See also:** [`log()`](#logmessage), [`log_error()`](#log_errormessage) #### `log_error(message)` Log error message. **Parameters:** - `message` (string): Error message **Example:** ```lua local html = read_html("template.html") if not html then log_error("Failed to load template.html") return end ``` **See also:** [`log()`](#logmessage), [`log_warn()`](#log_warnmessage) --- ### Utility Functions #### `table_to_json(lua_table)` Convert Lua table to JSON string. **Parameters:** - `lua_table` (table): Lua table to convert **Returns:** JSON string **Example:** ```lua local data = {name = "John", age = 30} local json = table_to_json(data) -- Result: '{"name":"John","age":30}' ``` **See also:** [`json_to_table()`](#json_to_tablejson_str) #### `json_to_table(json_str)` Parse JSON string into Lua table. **Parameters:** - `json_str` (string): JSON string **Returns:** Lua table, or empty table on error **Example:** ```lua local json = '{"status":"ok","count":5}' local data = json_to_table(json) log("Status: " .. data.status) -- Status: ok ``` **See also:** [`table_to_json()`](#table_to_jsonlua_table) --- ## Guardrails & Security The plugin system includes built-in security features and helper functions to prevent common vulnerabilities. ### Path Traversal Protection All file operations automatically block path traversal attempts: ```lua -- BLOCKED - Will fail safely read_file("../../../etc/passwd") read_html("../../config.php") -- ALLOWED - Sandboxed to designated directories read_html("index.html") read_markdown("docs.md") ``` **Implementation:** Any path containing `..` is rejected before file access. ### Safe String Operations #### `safe_concat(...)` Safely concatenate multiple values, converting to strings and handling `nil`. **Example:** ```lua local msg = safe_concat("User ", nil, " logged in at ", os.time()) -- Result: "User logged in at 1609459200" ``` **See also:** [`escape_html()`](#escape_htmlstr) #### `escape_html(str)` Escape HTML special characters to prevent XSS attacks. **Escapes:** - `&` → `&` - `<` → `<` - `>` → `>` - `"` → `"` - `'` → `'` **Example:** ```lua local user_input = '' local safe = escape_html(user_input) -- Result: "<script>alert("XSS")</script>" return "
" .. safe .. "
" ``` **Use Case:** Always escape user input before including in HTML responses. **See also:** [Security Best Practices](#security) ### Filename Validation #### `is_valid_filename(name)` Validate filename for safety. **Checks:** - Not nil or empty - No `..` (path traversal) - No `/` or `\` (directory separators) **Returns:** `true` if valid, `false` otherwise **Example:** ```lua local filename = req.query.file if not is_valid_filename(filename) then return 400, {}, "Invalid filename" end local content = read_html(filename) ``` **See also:** [Path Traversal Protection](#path-traversal-protection) ### Error Handling #### `try_catch(fn, catch_fn)` Safe error handling wrapper using `pcall`. **Parameters:** - `fn` (function): Function to execute - `catch_fn` (function): Error handler (receives error message) **Returns:** `true` if successful, `false` if error occurred **Example:** ```lua local success = try_catch( function() local html = read_html("template.html") local modified = html_replace_tag(html, "title", "New Title") write_html("output.html", modified) end, function(err) log_error("HTML processing failed: " .. tostring(err)) end ) if not success then return 500, {}, "Internal error" end ``` **See also:** [`validate_request()`](#validate_requestreq-required_fields) ### Request Validation #### `validate_request(req, required_fields)` Validate request object has required fields. **Parameters:** - `req` (table): Request object to validate - `required_fields` (array): List of required field names **Returns:** `(true, nil)` if valid, or `(false, error_message)` if invalid **Example:** ```lua add_route("/api/create", function(req) local valid, err = validate_request(req, {"name", "email", "body"}) if not valid then return 400, {}, "Bad request: " .. err end -- Process valid request return 200, {}, "Created" end) ``` **See also:** [`try_catch()`](#try_catchfn-catch_fn) ### Rate Limiting #### `check_rate_limit(key, max_calls, window_seconds)` Simple in-memory rate limiting. **Parameters:** - `key` (string): Unique identifier for rate limit (e.g., IP address, user ID) - `max_calls` (number): Maximum calls allowed in window - `window_seconds` (number): Time window in seconds **Returns:** `true` if within limit, `false` if exceeded **Example:** ```lua add_route("/api/search", function(req) local client_ip = req.headers["X-Forwarded-For"] or "unknown" if not check_rate_limit(client_ip, 10, 60) then return 429, {}, "Rate limit exceeded. Try again later." end -- Process search return 200, {}, "Search results" end) ``` **Note:** Rate limits are stored in memory and reset when plugin reloads. **See also:** [Security Best Practices](#security) ### Table Utilities #### `table_contains(tbl, value)` Check if table contains a value. **Example:** ```lua local allowed = {"admin", "moderator", "user"} if table_contains(allowed, req.role) then -- Process request end ``` #### `table_keys(tbl)` Get all keys from a table. **Example:** ```lua local data = {name = "John", age = 30} local keys = table_keys(data) -- Result: {"name", "age"} ``` #### `table_values(tbl)` Get all values from a table. **Example:** ```lua local data = {a = 1, b = 2, c = 3} local values = table_values(data) -- Result: {1, 2, 3} ``` --- ## Examples ### Example 1: Simple Route Handler Create `plugins/api.lua`: ```lua -- Simple API route add_route("/api/hello", function(req) local name = req.query.name or "World" local safe_name = escape_html(name) return 200, {["Content-Type"] = "application/json"}, '{"message": "Hello ' .. safe_name .. '"}' end) log("API plugin loaded") ``` **Usage:** ```python result = manager.handle_request("/api/hello", { "query": {"name": "Alice"} }) # Returns: (200, {...}, '{"message": "Hello Alice"}') ``` **See also:** [Route Registration](#route-registration) --- ### Example 2: Hook System Create `plugins/analytics.lua`: ```lua -- Hook into HTML transformation register_hook("transform_html", function(html, page_name) log("Adding analytics to " .. page_name) local analytics_script = [[ ]] return html_insert_before(html, "", analytics_script) end) ``` **Python Side:** ```python # Trigger hook original_html = "..." modified_html = manager.run_hook("transform_html", original_html, "home") ``` **See also:** [Hook Registration](#hook-registration) --- ### Example 3: HTML Template Processor Create `plugins/templates.lua`: ```lua -- Process HTML templates with variable substitution add_route("/render/:template", function(req) local template_name = req.params.template .. ".html" -- Validate filename if not is_valid_filename(template_name) then return 400, {}, "Invalid template name" end -- Read template local html = read_html(template_name) if not html then return 404, {}, "Template not found" end -- Replace variables local title = req.query.title or "Untitled" local content = req.query.content or "No content" html = html_replace_tag(html, "title", escape_html(title)) html = html_replace_tag(html, "main", escape_html(content)) return html end) ``` **See also:** [HTML Manipulation](#html-manipulation), [Route Registration](#route-registration) --- ### Example 4: Markdown Documentation Generator Create `plugins/docs_generator.lua`: ```lua -- Generate documentation from code files add_route("/docs/generate", function(req) local doc_content = [[ # API Documentation Generated on: ]] .. os.date() .. [[ ## Overview This documentation is auto-generated from source code. ]] -- Add sections for each endpoint local endpoints = { {path = "/api/users", method = "GET", desc = "Get all users"}, {path = "/api/users/:id", method = "GET", desc = "Get user by ID"}, {path = "/api/users", method = "POST", desc = "Create new user"} } for _, endpoint in ipairs(endpoints) do local section = string.format([[ ## %s %s %s **Example:** ``` curl -X %s http://localhost%s ``` ]], endpoint.method, endpoint.path, endpoint.desc, endpoint.method, endpoint.path) doc_content = md_append_content(doc_content, section) end -- Save to file write_markdown("api_docs.md", doc_content) return 200, {}, "Documentation generated successfully" end) ``` **See also:** [Markdown Manipulation](#markdown-manipulation) --- ### Example 5: Content Security System Create `plugins/security.lua`: ```lua -- Rate-limited API with validation add_route("/api/submit", function(req) -- Rate limiting local client_ip = req.headers["X-Real-IP"] or "unknown" if not check_rate_limit(client_ip, 5, 60) then log_warn("Rate limit exceeded for " .. client_ip) return 429, {}, "Too many requests" end -- Request validation local valid, err = validate_request(req, {"title", "content", "author"}) if not valid then return 400, {}, err end -- Content sanitization local title = escape_html(req.body.title) local content = escape_html(req.body.content) local author = escape_html(req.body.author) -- Save as HTML local html = string.format([[ %s

%s

By: %s

%s
]], title, title, author, content) local filename = "post_" .. os.time() .. ".html" write_html(filename, html) log("Created post: " .. filename) return 200, {}, "Post created successfully" end) ``` **See also:** [Guardrails & Security](#guardrails--security) --- ### Example 6: Multi-File HTML Processor Create `plugins/html_batch.lua`: ```lua -- Process all HTML files in a directory add_route("/admin/add_footer", function(req) local files = list_html_files() local processed = 0 local footer = [[ ]] for _, filename in ipairs(files) do try_catch( function() local html = read_html(filename) if html then -- Add footer before closing body tag local modified = html_insert_before(html, "", footer) write_html(filename, modified) processed = processed + 1 end end, function(err) log_error("Failed to process " .. filename .. ": " .. tostring(err)) end ) end return 200, {}, "Processed " .. processed .. " files" end) ``` **See also:** [HTML Manipulation](#html-manipulation), [Error Handling](#error-handling) --- ## Best Practices ### Plugin Design 1. **Single Responsibility**: Each plugin should have one clear purpose 2. **Error Handling**: Always use [`try_catch()`](#try_catchfn-catch_fn) for operations that might fail 3. **Logging**: Use appropriate log levels ([`log()`](#logmessage), [`log_warn()`](#log_warnmessage), [`log_error()`](#log_errormessage)) 4. **Documentation**: Add comments explaining what your plugin does ```lua -- Good: Clear purpose, error handling, logging add_route("/api/config", function(req) try_catch( function() local config = read_file("config.json") return 200, {}, config end, function(err) log_error("Config read failed: " .. tostring(err)) return 500, {}, "Internal error" end ) end) ``` **See also:** [Examples](#examples) --- ### Security 1. **Always Escape User Input**: Use [`escape_html()`](#escape_htmlstr) for any user-provided content 2. **Validate Filenames**: Use [`is_valid_filename()`](#is_valid_filenamename) before file operations 3. **Validate Requests**: Use [`validate_request()`](#validate_requestreq-required_fields) for required fields 4. **Implement Rate Limiting**: Use [`check_rate_limit()`](#check_rate_limitkey-max_calls-window_seconds) for public APIs 5. **Never Trust Input**: Sanitize and validate all external data ```lua -- Good: Comprehensive security add_route("/api/read", function(req) local filename = req.query.file -- Validate filename if not is_valid_filename(filename) then return 400, {}, "Invalid filename" end -- Rate limit if not check_rate_limit(req.ip, 10, 60) then return 429, {}, "Rate limited" end -- Safe read local content = read_html(filename) if content then return escape_html(content) end return 404, {}, "Not found" end) ``` **See also:** [Guardrails & Security](#guardrails--security), [Path Traversal Protection](#path-traversal-protection)