🔌 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:
- Hook into core functions (before/after execution)
- Override default behavior
- Add new routes and pages
- Modify templates and output
- Access database directly
- Create admin panel settings
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
• 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
• 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
- Test thoroughly on clean Xiuno installation
- Create comprehensive README.md with:
- Description and features
- Installation instructions
- Configuration options
- Screenshots
- Changelog
- Package as ZIP file:
pluginname-1.0.0.zip - Submit to Xiuno Plugin Directory
- Share on forum: Community Forum
📦 Package Structure:
ZIP should contain plugin folder directly, not nested:
ZIP should contain plugin folder directly, not nested:
✓ myplugin/myplugin.php❌ myplugin-1.0/myplugin/myplugin.php
Further Resources
- API Reference - Complete function documentation
- Plugin Examples - Download sample plugins
- Developer Forum - Get help from community
- GitHub Repository - Contribute to core