بیشتر سایت‌های وردپرسی دیر یا زود به کدی نیاز پیدا می‌کنند که هیچ افزونه‌ی آماده‌ای دقیقاً انجامش نمی‌دهد. راه معمول چسباندن تکه‌کد در functions.php قالب است، که با اولین تعویض قالب از بین می‌رود.

راه درست یک افزونه‌ی کوچک است. در این راهنما یک افزونه‌ی واقعی می‌سازیم که پیامی کوتاه را بالای همه‌ی صفحه‌های سایت نشان می‌دهد و از پیشخوان تنظیم می‌شود، و در همین مسیر قاعده‌های امنیتی را که هر افزونه‌ای باید رعایت کند می‌بینیم.

افزونه‌ی وردپرس از چه فایل‌هایی ساخته می‌شود؟#

دست‌کم یک فایل PHP با یک توضیح سربرگ، داخل یک پوشه در wp-content/plugins. افزونه در وردپرس پوشه‌ای است که فایل اصلی آن سربرگ استاندارد دارد و وردپرس از روی همین سربرگ آن را در فهرست افزونه‌ها نشان می‌دهد.

  • di9-notice-bar/di9-notice-bar.php فایل اصلی.
  • di9-notice-bar/uninstall.php کارهای هنگام حذف.
  • پوشه‌های includes، assets و languages وقتی افزونه بزرگ‌تر شد.

همه‌ی نام‌ها، از تابع تا تنظیم، یک پیشوند یکتا دارند؛ اینجا di9nb_. پیشوند جلوی تداخل نام با افزونه‌ها و قالب‌های دیگر را می‌گیرد.

سربرگ افزونه چطور نوشته می‌شود؟#

با یک توضیح PHP در ابتدای فایل اصلی که دست‌کم نام افزونه را دارد. خط بعد جلوی اجرای مستقیم فایل از بیرون وردپرس را می‌گیرد:

PHP
<?php
/**
 * Plugin Name: Di9 Notice Bar
 * Description: نمایش یک پیام کوتاه بالای همه‌ی صفحه‌های سایت.
 * Version: 0.1.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: di9-notice-bar
 */

defined( 'ABSPATH' ) || exit;

همین حالا افزونه در افزونه‌ها ← افزونه‌های نصب‌شده دیده می‌شود و قابل فعال‌کردن است، هرچند هنوز کاری نمی‌کند.

قلاب در وردپرس چیست؟#

قلاب (Hook) نقطه‌ای در اجرای وردپرس است که افزونه می‌تواند کد خودش را به آن وصل کند. دو نوع دارد: اکشن که در لحظه‌ی مشخصی کاری انجام می‌دهد، و فیلتر که مقداری را می‌گیرد، تغییر می‌دهد و برمی‌گرداند. تقریباً همه‌ی کار یک افزونه با add_action و add_filter انجام می‌شود.

نمونه‌ی فیلتر: این کد به انتهای متن هر نوشته یک جمله اضافه می‌کند. تابع مقدار را می‌گیرد و حتماً باید آن را برگرداند؛ اگر فراموش کنید، متن همه‌ی نوشته‌ها خالی نمایش داده می‌شود.

PHP
add_filter( 'the_content', 'di9nb_append_note' );
function di9nb_append_note( $content ) {
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }
    return $content . '<p class="di9nb-note">' . esc_html__( 'Thanks for reading.', 'di9-notice-bar' ) . '</p>';
}

شرط اول باعث می‌شود جمله فقط در صفحه‌ی خود نوشته و فقط یک بار اضافه شود، نه در ابزارک‌ها یا فهرست نوشته‌ها. تابع esc_html__ هم متن را قابل ترجمه می‌کند و هم امن. رشته‌های افزونه را انگلیسی و با تابع‌های ترجمه‌ی وردپرس بنویسید و همان Text Domain سربرگ را به آن‌ها بدهید؛ ترجمه‌ی فارسی بعداً در پوشه‌ی languages جا می‌گیرد و کد دست نمی‌خورد.

صفحه‌ی تنظیمات امن را چطور بسازیم؟#

با Settings API وردپرس، که nonce و بررسی دسترسی ذخیره را خودش انجام می‌دهد. Settings API مجموعه تابع‌هایی است که ثبت، ذخیره و پاک‌سازی تنظیمات افزونه را استاندارد می‌کند.

PHP
add_action( 'admin_init', 'di9nb_register_settings' );
function di9nb_register_settings() {
    register_setting( 'di9nb_group', 'di9nb_message', array(
        'type'              => 'string',
        'sanitize_callback' => 'sanitize_text_field',
        'default'           => '',
    ) );
}

add_action( 'admin_menu', 'di9nb_add_page' );
function di9nb_add_page() {
    add_options_page( 'Notice Bar', 'Notice Bar', 'manage_options', 'di9nb', 'di9nb_render_page' );
}

function di9nb_render_page() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    ?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
        <form action="options.php" method="post">
            <?php settings_fields( 'di9nb_group' ); ?>
            <input type="text" class="regular-text" name="di9nb_message"
                value="<?php echo esc_attr( get_option( 'di9nb_message', '' ) ); ?>">
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}

سه نکته در این کد مهم است:

  • settings_fields فیلدهای پنهان و nonce فرم را می‌سازد و options.php پیش از ذخیره، nonce و دسترسی manage_options را بررسی می‌کند.
  • sanitize_callback هر مقدار ورودی را پیش از ذخیره تمیز می‌کند.
  • مقدار ذخیره‌شده هنگام نمایش در فیلد با esc_attr امن می‌شود.

صفحه حالا در تنظیمات ← Notice Bar دیده می‌شود.

پیام را چطور در سایت نمایش دهیم؟#

با اکشن wp_body_open، که قالب‌های امروزی درست بعد از شروع بدنه‌ی صفحه اجرا می‌کنند:

PHP
add_action( 'wp_body_open', 'di9nb_render_bar' );
function di9nb_render_bar() {
    $message = get_option( 'di9nb_message', '' );
    if ( '' === $message ) {
        return;
    }
    printf( '<div class="di9nb-bar">%s</div>', esc_html( $message ) );
}

قاعده‌ی کلی: ورودی را هنگام ذخیره پاک‌سازی کنید و خروجی را هنگام نمایش escape کنید. Escape یعنی تبدیل نویسه‌های خاص به شکل بی‌خطر، تا متن ذخیره‌شده هرگز به‌عنوان کد در مرورگر اجرا نشود.

فرم اختصاصی را چطور امن کنیم؟#

هر فرمی که خارج از Settings API ذخیره می‌کند، باید چهار ایستگاه را خودش پیاده کند: nonce، بررسی دسترسی، پاک‌سازی و escape خروجی.

مسیر امن داده از ورودی فرم تا بررسی توکن، بررسی دسترسی، پاک‌سازی و خروجی امن

نمونه: یک یادداشت داخلی که فقط مدیر ثبت می‌کند. فرم در همان صفحه‌ی تنظیمات:

PHP
<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
    <input type="hidden" name="action" value="di9nb_save_note">
    <?php wp_nonce_field( 'di9nb_save_note' ); ?>
    <textarea name="di9nb_note" rows="4" class="large-text"><?php echo esc_textarea( get_option( 'di9nb_note', '' ) ); ?></textarea>
    <?php submit_button( 'Save note' ); ?>
</form>

و تابعی که آن را دریافت می‌کند:

PHP
add_action( 'admin_post_di9nb_save_note', 'di9nb_save_note' );
function di9nb_save_note() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Access denied.', '', array( 'response' => 403 ) );
    }
    check_admin_referer( 'di9nb_save_note' );

    $note = isset( $_POST['di9nb_note'] )
        ? sanitize_textarea_field( wp_unslash( $_POST['di9nb_note'] ) )
        : '';

    update_option( 'di9nb_note', $note, false );

    wp_safe_redirect( admin_url( 'options-general.php?page=di9nb' ) );
    exit;
}

اگر به دیتابیس پرس‌وجوی مستقیم می‌زنید، ورودی را هرگز به متن SQL نچسبانید؛ از $wpdb->prepare استفاده کنید. نمونه‌ی زیر فقط الگو را نشان می‌دهد و برای همین کار تابع آماده‌ی count_user_posts هم وجود دارد:

PHP
global $wpdb;

$count = (int) $wpdb->get_var(
    $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_author = %d AND post_status = %s",
        $user_id,
        'publish'
    )
);

هنگام فعال‌سازی و حذف افزونه چه کنیم؟#

هنگام فعال‌سازی مقدارهای پیش‌فرض را بسازید و هنگام حذف، هر چه ساخته‌اید پاک کنید. در فایل اصلی:

PHP
register_activation_hook( __FILE__, 'di9nb_activate' );
function di9nb_activate() {
    add_option( 'di9nb_message', '' );
}

و در uninstall.php، که فقط هنگام حذف افزونه از پیشخوان اجرا می‌شود:

PHP
<?php
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

delete_option( 'di9nb_message' );
delete_option( 'di9nb_note' );

افزونه را چطور آزمایش و خطایابی کنیم؟#

روی نصب آزمایشی، با گزارش خطای روشن. گزارش خطا را روی سایت زنده روشن نکنید و روش امن آن را در رفع خطاهای رایج وردپرس ببینید.

توسعه‌ی افزونه‌ی وردپرس به کجا می‌رود#

جهت‌ها جاافتاده‌اند: رابط کاربری افزونه‌ها بیشتر به شکل بلوک و بر پایه‌ی REST API ساخته می‌شود، حداقل نسخه‌ی PHP مرتب بالا می‌رود و مخزن رسمی ابزار بررسی خودکار افزونه را جدی‌تر به کار می‌گیرد. دستیارهای هوش مصنوعی هم نوشتن کد را سریع‌تر کرده‌اند، که بازبینی امنیتی را از همیشه مهم‌تر می‌کند.

برای آماده‌بودن:

  • کد را برای نسخه‌های امروزی PHP بنویسید و از رابط‌های رسمی وردپرس، مثل Settings API و REST API، استفاده کنید.
  • چهار قاعده‌ی nonce، دسترسی، پاک‌سازی و escape را در هر فرم و هر خروجی رعایت کنید.
  • پیشوند یا فضای نام یکتا به کار ببرید و افزونه را پیش از انتشار با ابزار بررسی افزونه‌ی مخزن رسمی آزمایش کنید.

خلاصه#

  • افزونه یک پوشه با یک فایل PHP و سربرگ استاندارد است.
  • اکشن کاری انجام می‌دهد؛ فیلتر مقداری را تغییر می‌دهد و برمی‌گرداند.
  • برای تنظیمات از Settings API استفاده کنید تا nonce و دسترسی خودکار بررسی شوند.
  • هر فرم اختصاصی: nonce، دسترسی، پاک‌سازی، و escape هنگام نمایش.
  • هنگام حذف، هر چه ساخته‌اید پاک کنید.