Files
PyPost/lua

Plugin Manager Documentation

Table of Contents

  1. Overview
  2. Architecture
  3. Getting Started
  4. Core Concepts
  5. API Reference
  6. Guardrails & Security
  7. 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

pip install lupa watchdog

Creating Your First Plugin

  1. Create a file plugins/hello.lua:
-- Register a simple route
add_route("/hello", function(req)
    return "<h1>Hello from Lua Plugin!</h1>"
end)

log("Hello plugin loaded")
  1. Start the plugin manager:
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"}, "<h1>Hello from Lua Plugin!</h1>")
  1. 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:

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, Examples - 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, Examples - 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:

function(request) -> response

Request Object:

{
    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):

return "<html>...</html>"

Option 2 - Full response tuple:

return 200, {["Content-Type"] = "application/json"}, '{"status":"ok"}'

Example:

add_route("/api/status", function(req)
    return 200, {["Content-Type"] = "application/json"}, '{"status": "online"}'
end)

See also: Hooks, 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:

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:

-- Content transformation hook
register_hook("transform_html", function(html)
    -- Add analytics script
    return html_insert_before(html, "</body>", "<script>analytics.js</script>")
end)

-- Logging hook
register_hook("request_complete", function(path, status)
    log("Request to " .. path .. " returned " .. status)
end)

Python Side - Triggering Hooks:

# 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, Examples - Hooks


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:

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:

write_file("output.txt", "Generated content")

See also: HTML Operations, Markdown Operations


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:

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:

local modified = html_insert_after(html, "<head>", "<meta name='theme' content='dark'>")
write_html("index.html", modified)

list_html_files()

Get list of all HTML files in the html/ directory.

Returns: Array of filenames

Example:

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:

local title_tag = html_find_tag(html, "title")
-- Result: "<title>My Page</title>"

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:

-- Replace title content
local html = "<title>Old Title</title>"
local modified = html_replace_tag(html, "title", "New Title")
-- Result: "<title>New Title</title>"

See also: html_wrap_content()

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:

-- Insert meta tag before closing head
local html = "<head></head><body></body>"
local modified = html_insert_before(html, "</head>", "<meta charset='utf-8'>")
-- Result: "<head><meta charset='utf-8'></head><body></body>"

See also: html_insert_after()

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:

-- Insert script before closing body
local modified = html_insert_after(html, "<body>", "<script>init();</script>")

See also: html_insert_before()

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:

local html = "<div>Hello World</div>"
local modified = html_wrap_content(html, "div", "span", 'class="highlight"')
-- Result: "<div><span class="highlight">Hello World</span></div>"

See also: html_replace_tag()


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:

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:

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:

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:

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_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:

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_append_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:

local md = "# Document\n\nExisting content"
local modified = md_append_content(md, "## Appendix\n\nAdditional information")

See also: md_add_header()


Logging Functions

log(message)

Log informational message.

Parameters:

  • message (string): Message to log

Example:

log("Plugin initialized successfully")

See also: log_warn(), log_error()

log_warn(message)

Log warning message.

Parameters:

  • message (string): Warning message

Example:

if not config then
    log_warn("Configuration file not found, using defaults")
end

See also: log(), log_error()

log_error(message)

Log error message.

Parameters:

  • message (string): Error message

Example:

local html = read_html("template.html")
if not html then
    log_error("Failed to load template.html")
    return
end

See also: log(), log_warn()


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:

local data = {name = "John", age = 30}
local json = table_to_json(data)
-- Result: '{"name":"John","age":30}'

See also: json_to_table()

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:

local json = '{"status":"ok","count":5}'
local data = json_to_table(json)
log("Status: " .. data.status)  -- Status: ok

See also: table_to_json()


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:

-- 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:

local msg = safe_concat("User ", nil, " logged in at ", os.time())
-- Result: "User  logged in at 1609459200"

See also: escape_html()

escape_html(str)

Escape HTML special characters to prevent XSS attacks.

Escapes:

  • &&amp;
  • <&lt;
  • >&gt;
  • "&quot;
  • '&#39;

Example:

local user_input = '<script>alert("XSS")</script>'
local safe = escape_html(user_input)
-- Result: "&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;"

return "<div>" .. safe .. "</div>"

Use Case: Always escape user input before including in HTML responses.

See also: Security Best Practices

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:

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

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:

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()

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:

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()

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:

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

Table Utilities

table_contains(tbl, value)

Check if table contains a value.

Example:

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:

local data = {name = "John", age = 30}
local keys = table_keys(data)
-- Result: {"name", "age"}

table_values(tbl)

Get all values from a table.

Example:

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:

-- 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:

result = manager.handle_request("/api/hello", {
    "query": {"name": "Alice"}
})
# Returns: (200, {...}, '{"message": "Hello Alice"}')

See also: Route Registration


Example 2: Hook System

Create plugins/analytics.lua:

-- Hook into HTML transformation
register_hook("transform_html", function(html, page_name)
    log("Adding analytics to " .. page_name)
    
    local analytics_script = [[
        <script>
        window.analytics = {
            page: "]] .. page_name .. [[",
            timestamp: ]] .. os.time() .. [[
        };
        </script>
    ]]
    
    return html_insert_before(html, "</head>", analytics_script)
end)

Python Side:

# Trigger hook
original_html = "<html><head></head><body>...</body></html>"
modified_html = manager.run_hook("transform_html", original_html, "home")

See also: Hook Registration


Example 3: HTML Template Processor

Create plugins/templates.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, Route Registration


Example 4: Markdown Documentation Generator

Create plugins/docs_generator.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


Example 5: Content Security System

Create plugins/security.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([[
<!DOCTYPE html>
<html>
<head>
    <title>%s</title>
</head>
<body>
    <h1>%s</h1>
    <p>By: %s</p>
    <div>%s</div>
</body>
</html>
    ]], 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


Example 6: Multi-File HTML Processor

Create plugins/html_batch.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 = [[
    <footer style="text-align:center; padding:20px; background:#f0f0f0;">
        <p>&copy; 2025 My Website. All rights reserved.</p>
    </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, "</body>", 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, Error Handling


Best Practices

Plugin Design

  1. Single Responsibility: Each plugin should have one clear purpose
  2. Error Handling: Always use try_catch() for operations that might fail
  3. Logging: Use appropriate log levels (log(), log_warn(), log_error())
  4. Documentation: Add comments explaining what your plugin does
-- 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


Security

  1. Always Escape User Input: Use escape_html() for any user-provided content
  2. Validate Filenames: Use is_valid_filename() before file operations
  3. Validate Requests: Use validate_request() for required fields
  4. Implement Rate Limiting: Use check_rate_limit() for public APIs
  5. Never Trust Input: Sanitize and validate all external data
-- 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, Path Traversal Protection