Plugin Development Guide

FearlessCMS Plugin Development Guide

FearlessCMS features a powerful plugin system that allows you to extend the functionality of your website. This guide will walk you through creating your own plugins.

Table of Contents

  1. Plugin Structure
  2. Basic Plugin Example
  3. Plugin Configuration
  4. Admin Interface Integration
  5. Hooks and Filters
  6. Route Handling
  7. Content Processing
  8. Database Integration
  9. Security Considerations
  10. Testing Your Plugin
  11. Distribution

Plugin Structure

A FearlessCMS plugin consists of the following structure:

plugins/
└── your-plugin-name/
    ├── plugin.json          # Plugin metadata
    ├── your-plugin-name.php # Main plugin file
    ├── includes/            # Additional PHP files
    ├── templates/           # HTML templates
    ├── assets/              # CSS, JS, images
    └── README.md            # Documentation

Required Files

plugin.json

This file contains metadata about your plugin:

{
    "name": "Your Plugin Name",
    "description": "A brief description of what your plugin does",
    "version": "1.0.0",
    "author": "Your Name",
    "main": "your-plugin-name.php",
    "features": [
        "Feature 1",
        "Feature 2"
    ]
}

Main Plugin File

The main PHP file should follow this naming convention: your-plugin-name.php (where your-plugin-name matches the directory name).

Basic Plugin Example

Here's a simple plugin that adds a custom function:

<?php
/*
Plugin Name: Example Plugin
Description: A simple example plugin for FearlessCMS
Version: 1.0
Author: Your Name
*/

// Define constants for your plugin
define('EXAMPLE_PLUGIN_DIR', PLUGIN_DIR . '/example-plugin');
define('EXAMPLE_PLUGIN_DATA_DIR', CONTENT_DIR . '/example-plugin-data');

// Initialize the plugin
function example_plugin_init() {
    // Create necessary directories
    if (!file_exists(EXAMPLE_PLUGIN_DATA_DIR)) {
        mkdir(EXAMPLE_PLUGIN_DATA_DIR, 0755, true);
    }

    // Register admin section
    fcms_register_admin_section('example', [
        'label' => 'Example Plugin',
        'menu_order' => 50,
        'parent' => 'plugins',
        'render_callback' => 'example_admin_page'
    ]);

    // Register hooks
    fcms_add_hook('content', 'example_process_content');
}

// Admin page callback
function example_admin_page() {
    ob_start();
    ?>
    <h2 class="text-2xl font-bold mb-6">Example Plugin Settings</h2>
    <p>This is your plugin's admin interface.</p>
    <?php
    return ob_get_clean();
}

// Content processing hook
function example_process_content($content) {
    // Process the content here
    return $content;
}

// Initialize the plugin
example_plugin_init();

Plugin Configuration

Constants

Define constants at the top of your main plugin file:

// Plugin directory
define('YOUR_PLUGIN_DIR', PLUGIN_DIR . '/your-plugin-name');

// Data directory (for storing plugin data)
define('YOUR_PLUGIN_DATA_DIR', CONTENT_DIR . '/your-plugin-data');

// Configuration file
define('YOUR_PLUGIN_CONFIG', YOUR_PLUGIN_DATA_DIR . '/config.json');

Settings Management

function your_plugin_get_settings() {
    $defaults = [
        'option1' => 'default_value',
        'option2' => true,
        'option3' => []
    ];

    if (file_exists(YOUR_PLUGIN_CONFIG)) {
        $settings = json_decode(file_get_contents(YOUR_PLUGIN_CONFIG), true);
        if (is_array($settings)) {
            return array_merge($defaults, $settings);
        }
    }

    return $defaults;
}

function your_plugin_save_settings($settings) {
    return file_put_contents(YOUR_PLUGIN_CONFIG, json_encode($settings, JSON_PRETTY_PRINT));
}

Admin Interface Integration

Registering Admin Sections

fcms_register_admin_section('your_plugin', [
    'label' => 'Your Plugin',
    'menu_order' => 50, // Lower numbers appear first
    'parent' => 'plugins', // Optional: makes it a submenu
    'render_callback' => 'your_plugin_admin_page'
]);

Admin Page Implementation

function your_plugin_admin_page() {
    $success_message = '';
    $error_message = '';

    // Handle form submissions
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
        switch ($_POST['action']) {
            case 'save_settings':
                $settings = [
                    'option1' => trim($_POST['option1'] ?? ''),
                    'option2' => isset($_POST['option2'])
                ];
                if (your_plugin_save_settings($settings)) {
                    $success_message = 'Settings saved successfully!';
                } else {
                    $error_message = 'Failed to save settings.';
                }
                break;
        }
    }

    // Load current settings
    $settings = your_plugin_get_settings();

    // Start output buffer
    ob_start();
    ?>

    <?php if ($success_message): ?>
        <div class="bg-green-100 text-green-700 p-4 rounded mb-4">
            <?= htmlspecialchars($success_message) ?>
        </div>
    <?php endif; ?>

    <?php if ($error_message): ?>
        <div class="bg-red-100 text-red-700 p-4 rounded mb-4">
            <?= htmlspecialchars($error_message) ?>
        </div>
    <?php endif; ?>

    <h2 class="text-2xl font-bold mb-6">Your Plugin Settings</h2>

    <form method="POST" class="space-y-4">
        <input type="hidden" name="action" value="save_settings">

        <div>
            <label class="block font-medium mb-1">Option 1</label>
            <input type="text" name="option1" 
                   value="<?= htmlspecialchars($settings['option1']) ?>" 
                   class="w-full border rounded px-3 py-2">
        </div>

        <div>
            <label class="flex items-center">
                <input type="checkbox" name="option2" 
                       <?= $settings['option2'] ? 'checked' : '' ?> 
                       class="mr-2">
                Option 2
            </label>
        </div>

        <button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
            Save Settings
        </button>
    </form>

    <?php
    return ob_get_clean();
}

Hooks and Filters

FearlessCMS uses a hook system to allow plugins to extend functionality. There are three types of hooks:

1. Action Hooks (By-Value)

Use fcms_do_hook() for hooks that return values:

// Register a hook
fcms_add_hook('my_custom_hook', 'my_callback_function');

// Call the hook
$result = fcms_do_hook('my_custom_hook', $param1, $param2);

2. Action Hooks (By-Reference)

Use fcms_do_hook_ref() for hooks that need to modify variables by reference:

// Register a hook
fcms_add_hook('before_render', 'my_render_callback');

// Call the hook (in core code)
fcms_do_hook_ref('before_render', $content, $page_data);

3. Filter Hooks

Use fcms_apply_filter() for value filters:

// Register a filter
fcms_add_hook('content', 'my_content_filter');

// Apply the filter
$filtered_content = fcms_apply_filter('content', $original_content);

Available Core Hooks

Route Handling

Plugins can register custom routes:

function my_plugin_route_handler(&$route, &$params) {
    if ($route === 'my-plugin') {
        // Handle the route
        $content = my_plugin_render_page();
        echo $content;
        return true; // Route handled
    }
    return false; // Route not handled
}

fcms_add_hook('route', 'my_plugin_route_handler');

Content Processing

Plugins can modify content before it's displayed:

function my_content_processor($content) {
    // Add custom processing here
    $content = str_replace('[custom_tag]', 'Custom Content', $content);
    return $content;
}

fcms_add_hook('content', 'my_content_processor');

Database Integration

FearlessCMS supports database-backed plugins through the MariaDB Connector plugin.

Using the MariaDB Connector

First, ensure the MariaDB Connector plugin is enabled, then use the provided hooks:

// Get a database connection
$pdo = fcms_do_hook('database_connect');
if ($pdo) {
    // Use the connection
    $stmt = $pdo->query('SELECT * FROM my_table');
    $rows = $stmt->fetchAll();
}

// Execute a parameterized query
$stmt = fcms_do_hook('database_query', 'SELECT * FROM users WHERE id = ?', [1]);
if ($stmt) {
    $user = $stmt->fetch();
}

Creating Database Tables

function my_plugin_create_tables() {
    $pdo = fcms_do_hook('database_connect');
    if (!$pdo) return false;

    $sql = "CREATE TABLE IF NOT EXISTS my_plugin_data (
        id INT AUTO_INCREMENT PRIMARY KEY,
        title VARCHAR(255) NOT NULL,
        content TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )";

    try {
        $pdo->exec($sql);
        return true;
    } catch (PDOException $e) {
        error_log("Failed to create table: " . $e->getMessage());
        return false;
    }
}

Security Considerations

Input Validation

Always validate and sanitize user input:

function my_plugin_save_data($input) {
    // Validate input
    $title = trim($input['title'] ?? '');
    if (empty($title) || strlen($title) > 255) {
        return false;
    }

    // Sanitize content
    $content = htmlspecialchars($input['content'] ?? '', ENT_QUOTES, 'UTF-8');

    // Save to database
    $pdo = fcms_do_hook('database_connect');
    if ($pdo) {
        $stmt = $pdo->prepare('INSERT INTO my_table (title, content) VALUES (?, ?)');
        return $stmt->execute([$title, $content]);
    }

    return false;
}

File Upload Security

If your plugin handles file uploads:

function my_plugin_handle_upload($file) {
    // Check file type
    $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
    if (!in_array($file['type'], $allowed_types)) {
        return false;
    }

    // Check file size (e.g., 5MB limit)
    if ($file['size'] > 5 * 1024 * 1024) {
        return false;
    }

    // Generate safe filename
    $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
    $filename = uniqid() . '.' . $extension;

    // Move to safe location
    $upload_dir = YOUR_PLUGIN_DATA_DIR . '/uploads/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }

    return move_uploaded_file($file['tmp_name'], $upload_dir . $filename);
}

Configuration Security

Never hardcode sensitive information like database credentials:

// ❌ Bad - hardcoded credentials
$config = [
    'host' => 'localhost',
    'username' => 'myuser',
    'password' => 'mypassword'
];

// ✅ Good - load from configuration
$config = your_plugin_get_settings();

Testing Your Plugin

Development Testing

  1. Enable your plugin in the admin interface
  2. Test all functionality in a development environment
  3. Check for PHP errors in the error log
  4. Test with different CMS modes

Error Handling

Always include proper error handling:

function my_plugin_function() {
    try {
        // Your plugin logic here
        return true;
    } catch (Exception $e) {
        error_log("My Plugin Error: " . $e->getMessage());
        return false;
    }
}

Debugging

Use error logging for debugging:

error_log("My Plugin Debug: " . print_r($data, true));

Distribution

Plugin Package Structure

When distributing your plugin, include:

your-plugin-name/
├── plugin.json
├── your-plugin-name.php
├── README.md
├── CHANGELOG.md
└── LICENSE

README.md Template

# Your Plugin Name

Brief description of what your plugin does.

## Installation

1. Upload the plugin to `/plugins/your-plugin-name/`
2. Enable the plugin in the admin interface
3. Configure settings in the plugin admin page

## Features

- Feature 1
- Feature 2
- Feature 3

## Configuration

Describe configuration options and how to use them.

## Changelog

### 1.0.0
- Initial release
- Basic functionality

## Support

How users can get help or report issues.

MariaDB Connector Plugin: Database Integration

The MariaDB Connector plugin enables FearlessCMS plugins to use a shared MariaDB/MySQL database connection, extending the flat-file CMS with robust database-backed features.

What It Does

Installation & Configuration

  1. Enable the plugin in the admin Plugins section.
  2. Go to MariaDB Connector under Plugins in the admin menu.
  3. Enter your database credentials and save.
  4. Use the 'Test Connection' button to verify connectivity.

Configuration is stored in content/mariadb-connector/config.json.

How Plugins Use the Database

Other plugins should NOT create their own DB connections. Instead, use the provided hooks:

// Get a PDO connection
$pdo = fcms_do_hook('database_connect');
if ($pdo) {
    // Use $pdo as usual
    $stmt = $pdo->query('SELECT * FROM my_table');
    $rows = $stmt->fetchAll();
}

// Execute a parameterized query
$stmt = fcms_do_hook('database_query', 'SELECT * FROM users WHERE id = ?', [1]);
if ($stmt) {
    $user = $stmt->fetch();
}

Best Practices

Admin Integration

Hybrid Content Model

Conclusion

This guide covers the essential aspects of plugin development for FearlessCMS. Remember to:

For more advanced examples, study the existing plugins in the /plugins/ directory of your FearlessCMS installation.