~/icsd.ir — bash
SYSTEM_ONLINE

🔌 فصل ۸: توسعه افزونه وردپرس

افزونه‌ها نسبت به قالب‌ها قابل حمل‌تر هستن - با هر قالبی کار می‌کنن. توی این فصل از صفر یه افزونه کامل می‌سازیم.

افزونه‌ها نسبت به قالب‌ها قابل حمل‌تر هستن – با هر قالبی کار می‌کنن. توی این فصل از صفر یه افزونه کامل می‌سازیم.

🤔 چرا افزونه بنویسیم؟

تجربه ساخت Persian Donate Plus رو داری – می‌دونی چقدر مفیده. افزونه برای این موارد:

  • قابلیت‌هایی که با تغییر قالب نباید از بین بره
  • محصولی که می‌خوای بفروشی یا منتشر کنی
  • قابلیت‌های مشترک بین چند سایت
  • یکپارچگی با سرویس‌های دیگه (درگاه پرداخت، API)
  • تغییر رفتار وردپرس یا افزونه‌های دیگه

📁 ساختار افزونه

افزونه ساده (تک‌فایل)

برای کارهای کوچک:

ساختار ساده
wp-content/plugins/
└── my-plugin.php

افزونه پیچیده (پوشه‌ای)

ساختار حرفه‌ای
wp-content/plugins/my-plugin/
├── my-plugin.php           # فایل اصلی (header افزونه)
├── readme.txt              # توضیحات (برای WordPress.org)
├── uninstall.php           # هنگام حذف افزونه
├── languages/              # ترجمه‌ها
├── includes/               # کلاس‌ها و توابع
│   ├── class-my-plugin.php
│   ├── class-admin.php
│   └── class-public.php
├── admin/                  # صفحات ادمین
│   ├── views/
│   ├── css/
│   └── js/
├── public/                 # فرانت
│   ├── css/
│   └── js/
└── assets/                 # تصاویر، آیکون

👋 اولین افزونه – Hello World

قدم ۱: ساخت فایل

یه پوشه به اسم my-first-plugin در wp-content/plugins/ بساز و یه فایل PHP درش بساز:

my-first-plugin.php
<?php
/**
 * Plugin Name:       اولین افزونه من
 * Plugin URI:        https://icsd.ir/my-first-plugin
 * Description:       یه افزونه ساده برای یاد گرفتن
 * Version:           1.0.0
 * Requires at least: 6.0
 * Requires PHP:      7.4
 * Author:            محمدعلی ناظری
 * Author URI:        https://icsd.ir
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       my-first-plugin
 * Domain Path:       /languages
 */

// جلوگیری از دسترسی مستقیم
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// ثابت‌ها
define( 'MFP_VERSION', '1.0.0' );
define( 'MFP_PATH', plugin_dir_path( __FILE__ ) );
define( 'MFP_URL', plugin_dir_url( __FILE__ ) );

// تابع ساده
function mfp_say_hello() {
    echo '<p style="background:#2271b1;color:#fff;padding:20px;text-align:center;">سلام از طرف اولین افزونه!</p>';
}

// نمایش در فوتر
add_action( 'wp_footer', 'mfp_say_hello' );

قدم ۲: فعال‌سازی

برو به افزونه‌ها در پنل وردپرس. افزونه جدید رو می‌بینی – فعالش کن. حالا توی فوتر همه صفحات اون پیام رو می‌بینی!

🎉

تبریک!

اولین افزونه‌ت کار می‌کنه. حالا بریم سراغ یه افزونه واقعی.

⭐ افزونه واقعی – دیدگاه ویژه

یه افزونه می‌سازیم که به دیدگاه‌ها امکان «ویژه شدن» می‌ده.

ساختار

ساختار
wp-content/plugins/featured-comments/
├── featured-comments.php
├── readme.txt
└── assets/
    └── css/
        └── style.css

فایل اصلی

featured-comments.php
<?php
/**
 * Plugin Name: دیدگاه ویژه
 * Description: امکان نشانه‌گذاری دیدگاه‌ها به عنوان ویژه
 * Version:     1.0.0
 * Author:      محمدعلی ناظری
 * Text Domain: featured-comments
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class Featured_Comments {

    public function __construct() {
        // اضافه کردن لینک «ویژه‌سازی» به ردیف هر دیدگاه در ادمین
        add_filter( 'comment_row_actions', array( $this, 'add_featured_action' ), 10, 2 );
        
        // پردازش لینک کلیک‌شده
        add_action( 'admin_init', array( $this, 'handle_featured_action' ) );
        
        // اضافه کردن کلاس CSS به دیدگاه‌های ویژه
        add_filter( 'comment_class', array( $this, 'add_featured_class' ), 10, 4 );
        
        // لود استایل
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
    }

    /**
     * اضافه کردن لینک به ردیف دیدگاه در ادمین
     */
    public function add_featured_action( $actions, $comment ) {
        $is_featured = get_comment_meta( $comment->comment_ID, '_featured', true );
        
        $url = wp_nonce_url(
            add_query_arg( array(
                'action'     => 'toggle_featured',
                'comment_id' => $comment->comment_ID,
            ), admin_url( 'edit-comments.php' ) ),
            'fc_toggle_' . $comment->comment_ID
        );
        
        $label = $is_featured ? 'لغو ویژه' : '⭐ ویژه‌سازی';
        $actions['featured'] = '<a href="' . esc_url( $url ) . '">' . $label . '</a>';
        
        return $actions;
    }

    /**
     * پردازش کلیک
     */
    public function handle_featured_action() {
        if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'toggle_featured' ) {
            return;
        }
        
        if ( ! isset( $_GET['comment_id'] ) ) {
            return;
        }
        
        $comment_id = absint( $_GET['comment_id'] );
        
        // بررسی nonce
        if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'fc_toggle_' . $comment_id ) ) {
            wp_die( 'دسترسی غیرمجاز' );
        }
        
        // بررسی دسترسی
        if ( ! current_user_can( 'moderate_comments' ) ) {
            wp_die( 'دسترسی کافی ندارید' );
        }
        
        // toggle
        $is_featured = get_comment_meta( $comment_id, '_featured', true );
        if ( $is_featured ) {
            delete_comment_meta( $comment_id, '_featured' );
        } else {
            update_comment_meta( $comment_id, '_featured', '1' );
        }
        
        // برگردون
        wp_safe_redirect( admin_url( 'edit-comments.php' ) );
        exit;
    }

    /**
     * اضافه کردن کلاس به دیدگاه ویژه
     */
    public function add_featured_class( $classes, $css_class, $comment_id, $comment ) {
        if ( get_comment_meta( $comment_id, '_featured', true ) ) {
            $classes[] = 'comment-featured';
        }
        return $classes;
    }

    /**
     * لود استایل
     */
    public function enqueue_styles() {
        wp_enqueue_style(
            'featured-comments',
            plugin_dir_url( __FILE__ ) . 'assets/css/style.css',
            array(),
            '1.0.0'
        );
    }
}

// راه‌اندازی
new Featured_Comments();

استایل

assets/css/style.css
.comment-featured {
    background: linear-gradient(135deg, #fef3c7, #fde68a);
    border-right: 4px solid #f59e0b;
    padding: 16px;
    border-radius: 8px;
    position: relative;
}

.comment-featured::before {
    content: '⭐ دیدگاه ویژه';
    position: absolute;
    top: 8px;
    left: 8px;
    background: #f59e0b;
    color: white;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: bold;
}

🎬 Activation و Deactivation

وقتی افزونه رو فعال یا غیرفعال می‌کنی، می‌تونی کارهایی انجام بدی:

activation hooks
// فعال‌سازی
function my_plugin_activate() {
    // ساخت جدول دیتابیس
    global $wpdb;
    $table_name = $wpdb->prefix . 'my_data';
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        name varchar(255) NOT NULL,
        value text,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) $charset_collate;";
    
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
    
    // مقدار پیش‌فرض تنظیمات
    add_option( 'my_plugin_settings', array(
        'enabled' => true,
        'mode'    => 'production',
    ) );
    
    // flush rewrite (در صورت ساخت CPT)
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_plugin_activate' );

// غیرفعال‌سازی
function my_plugin_deactivate() {
    // پاک کردن کرون‌ها
    wp_clear_scheduled_hook( 'my_plugin_cron' );
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'my_plugin_deactivate' );

uninstall.php

وقتی کاربر افزونه رو حذف کامل می‌کنه (نه فقط غیرفعال):

uninstall.php
<?php
// جلوگیری از اجرا به‌جز هنگام uninstall
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// حذف option ها
delete_option( 'my_plugin_settings' );

// حذف جدول
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}my_data" );

// حذف meta
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_my_plugin_%'" );

📋 صفحه ادمین

admin menu
// اضافه کردن منو در ادمین
function my_plugin_admin_menu() {
    // منوی اصلی
    add_menu_page(
        'تنظیمات افزونه من',          // page title
        'افزونه من',                   // menu title
        'manage_options',              // capability
        'my-plugin',                   // slug
        'my_plugin_admin_page',        // callback
        'dashicons-admin-generic',     // icon
        100                            // position
    );
    
    // زیرمنو
    add_submenu_page(
        'my-plugin',
        'گزارش‌ها',
        'گزارش‌ها',
        'manage_options',
        'my-plugin-reports',
        'my_plugin_reports_page'
    );
}
add_action( 'admin_menu', 'my_plugin_admin_menu' );

function my_plugin_admin_page() {
    // ذخیره تنظیمات
    if ( isset( $_POST['save_settings'] ) ) {
        check_admin_referer( 'my_plugin_settings' );
        
        $settings = array(
            'enabled' => isset( $_POST['enabled'] ),
            'mode'    => sanitize_text_field( $_POST['mode'] ?? '' ),
        );
        
        update_option( 'my_plugin_settings', $settings );
        echo '<div class="notice notice-success"><p>تنظیمات ذخیره شد!</p></div>';
    }
    
    $settings = get_option( 'my_plugin_settings', array() );
    ?>
    <div class="wrap">
        <h1>تنظیمات افزونه من</h1>
        <form method="post">
            <?php wp_nonce_field( 'my_plugin_settings' ); ?>
            
            <table class="form-table">
                <tr>
                    <th>فعال؟</th>
                    <td>
                        <input type="checkbox" name="enabled" 
                            <?php checked( $settings['enabled'] ?? false ); ?>>
                    </td>
                </tr>
                <tr>
                    <th>حالت</th>
                    <td>
                        <select name="mode">
                            <option value="dev" <?php selected( $settings['mode'] ?? '', 'dev' ); ?>>توسعه</option>
                            <option value="production" <?php selected( $settings['mode'] ?? '', 'production' ); ?>>انتشار</option>
                        </select>
                    </td>
                </tr>
            </table>
            
            <?php submit_button( 'ذخیره تنظیمات', 'primary', 'save_settings' ); ?>
        </form>
    </div>
    <?php
}

🔒 امنیت در افزونه‌نویسی

⚠️

قوانین طلایی امنیت

هر ورودی کاربر یا داده خارجی پاک‌سازی، هر خروجی escape، و هر اقدام مهم nonce داشته باشه.

۱. Nonce برای CSRF

nonce
// در فرم
wp_nonce_field( 'my_action_name', '_my_nonce' );

// در پردازش
if ( ! isset( $_POST['_my_nonce'] ) || 
     ! wp_verify_nonce( $_POST['_my_nonce'], 'my_action_name' ) ) {
    wp_die( 'درخواست نامعتبر' );
}

۲. پاک‌سازی ورودی

sanitization
$text = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$url = esc_url_raw( $_POST['website'] );
$int = absint( $_POST['count'] );
$html = wp_kses_post( $_POST['content'] );  // اجازه HTML محدود
$key = sanitize_key( $_POST['key'] );
$slug = sanitize_title( $_POST['slug'] );

۳. Escape کردن خروجی

escape
echo esc_html( $title );           // برای متن
echo esc_attr( $value );           // برای attribute
echo esc_url( $link );             // برای href/src
echo esc_textarea( $content );     // برای textarea
echo esc_js( $script_value );      // برای JavaScript
echo wp_kses_post( $rich_text );   // برای HTML امن

۴. بررسی دسترسی

capability check
if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( 'دسترسی کافی ندارید' );
}

// بقیه دسترسی‌ها:
// edit_posts, publish_posts, delete_posts, upload_files,
// moderate_comments, edit_users, install_plugins

۵. Prepared Statement برای دیتابیس

SQL injection prevention
global $wpdb;

// ❌ غلط - SQL injection
$result = $wpdb->get_results( "SELECT * FROM table WHERE id = $id" );

// ✅ درست
$result = $wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}my_table WHERE id = %d",
    $id
) );

// با چند پارامتر
$wpdb->prepare(
    "SELECT * FROM table WHERE name = %s AND age > %d",
    $name, $age
);

📤 انتشار در WordPress.org

تجربه انتشار Persian Donate Plus رو داری! مراحل کلی:

  1. افزونه رو با استانداردهای WordPress.org Coding Standards بنویس
  2. یه readme.txt با فرمت استاندارد بساز
  3. بنر و آیکون اضافه کن (در پوشه assets)
  4. ثبت‌نام در wordpress.org/plugins
  5. ارسال افزونه برای بررسی
  6. بعد از تأیید (۲ روز تا ۲ هفته)، دسترسی SVN دریافت می‌کنی
  7. کد رو commit کن و tag بزن

نمونه readme.txt

readme.txt
=== Plugin Name ===
Contributors: yourname
Tags: comments, featured, custom
Requires at least: 6.0
Tested up to: 6.5
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later

A short description in 150 chars.

== Description ==

Detailed description here...

== Installation ==

1. Upload to /wp-content/plugins/
2. Activate
3. Configure

== Changelog ==

= 1.0.0 =
* Initial release

📝 خلاصه فصل

  • افزونه باید header استاندارد داشته باشه
  • register_activation_hook برای راه‌اندازی
  • uninstall.php برای پاکسازی هنگام حذف
  • هر ورودی sanitize، هر خروجی escape
  • nonce برای جلوگیری از CSRF
  • $wpdb->prepare برای جلوگیری از SQL injection

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.