🔌 Plugin Development Guide

Learn how to extend Xiuno BBS with custom plugins

Getting Started

Plugin Architecture

Xiuno BBS uses a hook-based plugin system. Plugins can:

Directory Structure

plugin/
└── myplugin/
    ├── myplugin.php          # Main plugin file (required)
    ├── conf.json             # Plugin metadata (required)
    ├── install.php           # Installation script
    ├── uninstall.php         # Cleanup script
    ├── admin_setting.htm     # Admin settings page
    ├── lang/
    │   ├── en-us.php        # English translations
    │   └── zh-cn.php        # Chinese translations
    ├── css/
    │   └── myplugin.css     # Plugin styles
    ├── js/
    │   └── myplugin.js      # Plugin scripts
    └── README.md            # Documentation

Creating Your First Plugin

Step 1: Plugin Metadata (conf.json)

Create plugin/hello-world/conf.json:

{
    "name": "Hello World",
    "brief": "A simple example plugin",
    "version": "1.0.0",
    "bbs_version": "4.0.0",
    "installed": 0,
    "enable": 1,
    "hook": 1,
    "modules": {
        "hello_world": {
            "name": "Hello World Module",
            "enable": 1
        }
    },
    "nav": "Hello",
    "setting": 1,
    "copyright": "Your Name"
}

Metadata Fields

Field Type Description
name string Plugin display name
brief string Short description
version string Plugin version (semantic versioning)
bbs_version string Minimum Xiuno BBS version required
hook int 1 = uses hooks, 0 = standalone
setting int 1 = has settings page, 0 = no settings

Step 2: Main Plugin File

Create plugin/hello-world/hello_world.php:

<?php
!defined('DEBUG') AND exit('Access Denied.');

class hello_world {
    
    // Constructor - runs when plugin loads
    public function __construct() {
        // Initialize plugin
    }
    
    // Hook into index page
    public function route_index_start() {
        global $conf, $gid;
        
        // Add custom message to homepage
        echo '<div class="alert alert-info">Hello from plugin!</div>';
    }
    
    // Hook into user registration
    public function user_create_after($uid) {
        // Send welcome email to new user
        $user = user_read($uid);
        
        // Your custom code here
        error_log("New user registered: {$user['username']}");
    }
    
    // Add custom route
    public function route_hello() {
        global $conf;
        
        include _include(APP_PATH.'plugin/hello-world/hello_page.htm');
        exit;
    }
}

?>

Step 3: Installation Script

Create plugin/hello-world/install.php:

<?php
!defined('DEBUG') AND exit('Access Denied.');

// Create custom database table
$sql = "
CREATE TABLE IF NOT EXISTS `bbs_hello_world` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) NOT NULL,
  `message` varchar(255) NOT NULL,
  `created` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `uid` (`uid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
";

$db->query($sql);

// Set default configuration
kv_set('hello_world_config', array(
    'enabled' => 1,
    'message' => 'Welcome to Xiuno BBS!'
));

return TRUE;
?>

Hook System

Available Hooks

Hook Name When It Fires Use Case
user_create_before Before user registration Validate registration data
user_create_after After user registration Send welcome email
thread_create_before Before creating thread Content filtering, validation
thread_create_after After creating thread Notifications, logging
post_create_before Before creating post Spam detection
post_create_after After creating post Update statistics
route_index_start Homepage loading Add custom content
route_thread_start Thread page loading Modify thread display

Hook Parameters

Most hooks pass relevant data as parameters:

// Example: Modify thread data before saving
public function thread_create_before(&$thread) {
    // $thread is passed by reference - you can modify it
    
    // Auto-tag threads
    if(strpos($thread['subject'], '[Bug]') !== false) {
        $thread['tag'] = 'bug';
    }
    
    // Add custom field
    $thread['custom_field'] = 'value';
}

Database Operations

Reading Data

// Get single user
$user = user_read($uid);

// Get thread
$thread = thread_read($tid);

// Custom query
global $db, $tablepre;
$sql = "SELECT * FROM {$tablepre}hello_world WHERE uid = ?";
$results = $db->query($sql, $uid);

Writing Data

// Create user
$uid = user_create($user_array);

// Update user
user_update($uid, $update_array);

// Delete user
user_delete($uid);

// Custom insert
global $db, $tablepre;
$db->insert("{$tablepre}hello_world", array(
    'uid' => $uid,
    'message' => 'Hello',
    'created' => time()
));

Using Prepared Statements

global $db;

// Safe query with placeholders
$sql = "SELECT * FROM bbs_user WHERE username = ? AND email = ?";
$user = $db->query_first($sql, $username, $email);

// Prevent SQL injection - DO NOT concatenate user input
// ❌ BAD: $sql = "SELECT * FROM bbs_user WHERE uid = $uid";
// ✓ GOOD: $sql = "SELECT * FROM bbs_user WHERE uid = ?";
//         $user = $db->query_first($sql, $uid);

Template Integration

Adding HTML Content

// In your plugin class
public function route_thread_end() {
    global $thread;
    
    // Include template file
    include _include(APP_PATH.'plugin/hello-world/thread_addon.htm');
}

// thread_addon.htm
<div class="plugin-content">
    <h3>Related Threads</h3>
    <ul>
        <?php foreach($related as $item): ?>
            <li><a href="?thread-<?php echo $item['tid']; ?>.htm"><?php echo $item['subject']; ?></a></li>
        <?php endforeach; ?>
    </ul>
</div>

Injecting CSS & JavaScript

public function route_index_start() {
    global $conf;
    
    // Add CSS
    echo '<link rel="stylesheet" href="'.$conf['siteurl'].'plugin/hello-world/css/style.css">';
    
    // Add JavaScript
    echo '<script src="'.$conf['siteurl'].'plugin/hello-world/js/script.js"></script>';
}

Admin Settings Panel

Creating Settings Page

Create plugin/hello-world/admin_setting.htm:

<?php !defined('DEBUG') AND exit('Access Denied.'); ?>

<form method="post" class="form-horizontal">
    <div class="form-group">
        <label class="col-sm-3 control-label">Enable Plugin</label>
        <div class="col-sm-9">
            <input type="checkbox" name="enabled" value="1" 
                <?php echo $conf['hello_world_enabled'] ? 'checked' : ''; ?>>
        </div>
    </div>
    
    <div class="form-group">
        <label class="col-sm-3 control-label">Welcome Message</label>
        <div class="col-sm-9">
            <input type="text" name="message" class="form-control" 
                value="<?php echo htmlspecialchars($conf['hello_world_message']); ?>">
        </div>
    </div>
    
    <div class="form-group">
        <div class="col-sm-offset-3 col-sm-9">
            <button type="submit" class="btn btn-primary">Save Settings</button>
        </div>
    </div>
</form>

Processing Settings

// In your plugin class
public function admin_setting() {
    global $conf;
    
    if($_POST) {
        // Save settings
        $config = array(
            'hello_world_enabled' => (int)$_POST['enabled'],
            'hello_world_message' => xn_filter($_POST['message'])
        );
        
        setting_set($config);
        
        message(1, 'Settings saved successfully');
    }
    
    include _include(APP_PATH.'plugin/hello-world/admin_setting.htm');
}

Internationalization

Language Files

Create plugin/hello-world/lang/en-us.php:

<?php
return array(
    'hello_world' => 'Hello World',
    'welcome_message' => 'Welcome to our forum!',
    'settings_saved' => 'Settings saved successfully',
);
?>

Using Translations

$lang = _include(APP_PATH.'plugin/hello-world/lang/'.$conf['lang'].'.php');

echo $lang['welcome_message'];

Example Plugins

View Counter Plugin

<?php
!defined('DEBUG') AND exit('Access Denied.');

class view_counter {
    
    // Track thread views
    public function thread_read_start($tid) {
        global $db, $tablepre;
        
        // Increment view count
        $db->query("UPDATE {$tablepre}thread SET views = views + 1 WHERE tid = ?", $tid);
    }
    
    // Display popular threads widget
    public function route_index_end() {
        global $db, $tablepre;
        
        // Get top 10 popular threads
        $threads = $db->query("
            SELECT * FROM {$tablepre}thread 
            ORDER BY views DESC 
            LIMIT 10
        ");
        
        echo '<div class="popular-widget">';
        echo '<h3>🔥 Popular Threads</h3><ul>';
        
        foreach($threads as $thread) {
            echo '<li><a href="?thread-'.$thread['tid'].'.htm">';
            echo htmlspecialchars($thread['subject']);
            echo '</a> <small>('.$thread['views'].' views)</small></li>';
        }
        
        echo '</ul></div>';
    }
}
?>

Spam Filter Plugin

<?php
!defined('DEBUG') AND exit('Access Denied.');

class spam_filter {
    
    private $spam_keywords = array('viagra', 'casino', 'lottery');
    
    // Check post content before saving
    public function post_create_before(&$post) {
        global $conf;
        
        $content = strtolower($post['message']);
        
        // Check for spam keywords
        foreach($this->spam_keywords as $keyword) {
            if(strpos($content, $keyword) !== false) {
                message(-1, 'Your post contains prohibited content');
            }
        }
        
        // Check for excessive links
        if(substr_count($content, 'http') > 3) {
            message(-1, 'Too many links in post');
        }
        
        // Rate limiting - max 10 posts per hour
        $user_posts = post_count_by_user($_SESSION['uid'], 3600);
        if($user_posts > 10) {
            message(-1, 'You are posting too quickly. Please wait.');
        }
    }
}
?>

Best Practices

✅ Plugin Development Guidelines:
• Use descriptive, unique plugin names
• Prefix database tables with bbs_pluginname_
• Always validate and sanitize user input
• Use prepared statements for database queries
• Clean up resources in uninstall.php
• Test with different PHP versions (7.4-8.4)
• Document your code and include README
• Follow Xiuno coding standards
• Handle errors gracefully

Security Considerations

// ✓ Filter user input
$username = xn_filter($_POST['username']);

// ✓ Escape output
echo htmlspecialchars($user['username']);

// ✓ Check permissions
if($gid != 1) {
    message(-1, 'Admin only');
}

// ✓ Validate CSRF token
if(!token_check($_POST['token'])) {
    message(-1, 'Invalid request');
}

Publishing Your Plugin

  1. Test thoroughly on clean Xiuno installation
  2. Create comprehensive README.md with:
    • Description and features
    • Installation instructions
    • Configuration options
    • Screenshots
    • Changelog
  3. Package as ZIP file: pluginname-1.0.0.zip
  4. Submit to Xiuno Plugin Directory
  5. Share on forum: Community Forum
📦 Package Structure:
ZIP should contain plugin folder directly, not nested:
✓ myplugin/myplugin.php
❌ myplugin-1.0/myplugin/myplugin.php

Further Resources