هوک wp_ajax_nopriv_ یکی از حساس‌ترین هوک‌های وردپرس برای پردازش درخواست‌های AJAX کاربران مهمان است. این هوک امکان ارائه قابلیت‌هایی مانند بارگذاری محتوا، فرم تماس و جستجوی عمومی را فراهم می‌کند اما نقطه ورود هر ربات و مهاجم خارجی نیز محسوب می‌شود. طراحی امن این هوک با بررسی nonce، Rate Limiting، sanitize و escape، پایه پیاده‌سازی AJAX عمومی محسوب می‌شود. اشتباهات رایجی مانند نبود nonce، نبود محدودیت نرخ، نبود escape و نبود تست می‌تواند به اسپم، DoS و نفوذ منجر شود. تسلط بر این هوک برای افزونه‌نویسی حرفه‌ای ضروری است و در امنیت AJAX عمومی کاربرد جدی دارد.

چرا AJAX عمومی یک نقطه حساس است؟

فایل admin-ajax.php یک نقطه ورودی عمومی است که هر کاربری — حتی بدون ورود به سایت — می‌تواند به آن درخواست ارسال کند. این ویژگی برای قابلیت‌هایی مانند فرم تماس، جستجوی زنده یا بارگذاری محتوا ضروری است اما نقطه جذابی برای ربات‌ها و مهاجمان محسوب می‌شود. اگر هندلر AJAX عمومی شما به‌درستی محافظت نشود، ربات‌ها می‌توانند به‌صورت خودکار درخواست ارسال کنند و به منابع سرور آسیب برسانند. این پدیده که به AJAX Flooding معروف است، می‌تواند سایت را از دسترس خارج کند.

هوک wp_ajax_nopriv_ چیست؟

هوک wp_ajax_nopriv_ یک اکشن هوک داینامیک در وردپرس است که توسط فایل admin-ajax.php فراخوانی می‌شود. این هوک برای پردازش درخواست‌های AJAX کاربران مهمان استفاده می‌شود. ساختار نام این هوک:
wp_ajax_nopriv_{$action}
که {$action} نام عملیات در درخواست AJAX است. این ساختار مشابه هوک wp_ajax_ است که در راهنمای هوک wp_ajax به تفصیل بررسی شده است. نکته مهم این است که این هوک تنها در نبود احراز هویت کاربر اجرا می‌شود. اگر کاربر وارد شده باشد، هوک wp_ajax_ اجرا می‌شود.

تفاوت با wp_ajax_

دو هوک جداگانه برای پردازش AJAX وجود دارد: - wp_ajax_{$action}: تنها برای کاربران وارد‌شده - wp_ajax_nopriv_{$action}: تنها برای کاربران مهمان برای پشتیبانی از هر دو گروه:
add_action( 'wp_ajax_myplugin_action', 'myplugin_handler' );
add_action( 'wp_ajax_nopriv_myplugin_action', 'myplugin_handler' );
function myplugin_handler() {
    // هندلر مشترک با بررسی وضعیت کاربر
}
نکته مهم: در هندلر مشترک، باید بر پایه وضعیت ورود کاربر رفتار متفاوتی پیاده کنید. برای اطلاعات بیشتر درباره بررسی وضعیت ورود، به راهنمای is_user_logged_in مراجعه کنید.

ساختار و پارامترها

ساختار پایه:
add_action( 'wp_ajax_nopriv_myplugin_public', 'myplugin_public_handler' );
function myplugin_public_handler() {
    // بررسی nonce
    // بررسی Rate Limit
    // پردازش درخواست
    // ارسال پاسخ
    wp_send_json_success( $data );
}
این هوک پارامتر ورودی نمی‌گیرد. تمام داده‌ها از طریق متغیرهای $_POST، $_GET و $_REQUEST قابل دسترسی هستند. نکته مهم: هرگز از پارامتر action به‌عنوان داده استفاده نکنید چرا که وردپرس از آن برای مسیریابی استفاده می‌کند.

بررسی nonce برای مهمان‌ها

یکی از چالش‌های AJAX عمومی، بررسی nonce است. برای کاربران وارد‌شده، nonce با نشست کاربر گره می‌خورد. برای مهمان‌ها، nonce بر پایه شناسه نشست و آدرس IP ساخته می‌شود. تولید nonce برای مهمان‌ها:
wp_localize_script(
    'myplugin-public',
    'mypluginPublic',
    array(
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_public_action' ),
    )
);
بررسی nonce در سمت سرور:
function myplugin_public_handler() {
    if ( ! check_ajax_referer( 'myplugin_public_action', 'nonce', false ) ) {
        wp_send_json_error( array(
            'code'    => 'invalid_nonce',
            'message' => 'درخواست نامعتبر است',
        ), 403 );
    }

    // ادامه پردازش
}
نکته مهم: nonce برای مهمان‌ها تنها ۲۴ ساعت معتبر است و پس از آن منقضی می‌شود. راهنمای این تابع در صفحه check_ajax_referer و صفحه wp_verify_nonce آمده است.

Rate Limiting و محدودیت نرخ

محدودیت نرخ، یکی از مهم‌ترین محافظت‌ها در AJAX عمومی است. بدون این محدودیت، یک ربات می‌تواند هزاران درخواست در دقیقه ارسال کند و منابع سرور را تمام کند. الگوی حرفه‌ای Rate Limiting با Transient:
function myplugin_check_rate_limit( $max_requests = 10, $window = 60 ) {
    $ip = myplugin_get_client_ip();

    if ( empty( $ip ) ) {
        return false;
    }

    $key = 'myplugin_rate_' . md5( $ip );
    $count = (int) get_transient( $key );

    if ( $count >= $max_requests ) {
        return false;
    }

    set_transient( $key, $count + 1, $window );

    return true;
}

function myplugin_get_client_ip() {
    if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
        return sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) );
    }

    if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
        $ips = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
        return trim( $ips[0] );
    }

    if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
        return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
    }

    return '';
}
نکته مهم: استفاده از Transient برای Rate Limiting به این معناست که پس از پنجره زمانی مشخص، شمارنده صفر می‌شود. راهنمای توابع Transient در صفحه set_transient و صفحه get_transient آمده است.

کاربردهای عملی در افزونه

پردازش فرم تماس برای مهمان‌ها:
add_action( 'wp_ajax_nopriv_myplugin_contact', 'myplugin_contact_public_handler' );
function myplugin_contact_public_handler() {
    if ( ! check_ajax_referer( 'myplugin_contact_nonce', 'nonce', false ) ) {
        wp_send_json_error( array( 'message' => 'درخواست نامعتبر' ), 403 );
    }

    if ( ! myplugin_check_rate_limit( 5, 300 ) ) {
        wp_send_json_error( array( 'message' => 'تعداد درخواست‌ها بیش از حد مجاز است' ), 429 );
    }

    $name    = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
    $email   = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
    $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';

    $errors = array();
    if ( empty( $name ) ) {
        $errors['name'] = 'نام الزامی است';
    }
    if ( ! is_email( $email ) ) {
        $errors['email'] = 'ایمیل نامعتبر است';
    }
    if ( empty( $message ) ) {
        $errors['message'] = 'پیام الزامی است';
    }

    if ( ! empty( $errors ) ) {
        wp_send_json_error( array(
            'message' => 'لطفاً خطاها را بررسی کنید',
            'errors'  => $errors,
        ), 422 );
    }

    wp_mail(
        get_option( 'admin_email' ),
        'پیام جدید از ' . $name,
        $message,
        array( 'Reply-To: ' . $email )
    );

    wp_send_json_success( array( 'message' => 'پیام شما ارسال شد' ) );
}
بارگذاری محتوای عمومی برای مهمان‌ها:
add_action( 'wp_ajax_nopriv_myplugin_load_more', 'myplugin_load_more_public' );
function myplugin_load_more_public() {
    if ( ! check_ajax_referer( 'myplugin_load_more_nonce', 'nonce', false ) ) {
        wp_send_json_error( array( 'message' => 'درخواست نامعتبر' ), 403 );
    }

    if ( ! myplugin_check_rate_limit( 30, 60 ) ) {
        wp_send_json_error( array( 'message' => 'محدودیت درخواست' ), 429 );
    }

    $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;

    if ( $page < 1 || $page > 100 ) {
        wp_send_json_error( array( 'message' => 'شماره صفحه نامعتبر' ), 400 );
    }

    $query = new WP_Query( array(
        'post_type'      => 'post',
        'posts_per_page' => 10,
        'paged'          => $page,
    ) );

    $posts = array();
    foreach ( $query->posts as $post ) {
        $posts[] = array(
            'id'    => $post->ID,
            'title' => esc_html( get_the_title( $post ) ),
            'url'   => esc_url( get_permalink( $post ) ),
        );
    }

    wp_send_json_success( array(
        'posts'    => $posts,
        'hasMore'  => $page < $query->max_num_pages,
    ) );
}
راهنمای توابع استفاده‌شده در این بخش: wp_send_json_success، wp_send_json_error، راهنمای WP_Query و راهنمای wp_localize_script.

Sanitize و Escape در پاسخ

داده‌هایی که از AJAX عمومی دریافت می‌شوند، از طرف کاربران ناشناس است و باید به‌دقت پاک‌سازی شوند. الگوی دوگانه: **پیش از ذخیره یا استفاده**:
$email   = sanitize_email( wp_unslash( $_POST['email'] ) );
$message = sanitize_textarea_field( wp_unslash( $_POST['message'] ) );
$post_id = absint( $_POST['post_id'] );
**پیش از ارسال به کلاینت**:
wp_send_json_success( array(
    'title'   => esc_html( $title ),
    'url'     => esc_url( $url ),
    'content' => wp_kses_post( $content ),
) );
نکته مهم: در سمت JavaScript، برای نمایش داده باید از textContent به‌جای innerHTML استفاده کنید. راهنمای توابع escape در صفحه esc_html آمده است.

نکات امنیتی و اشتباهات رایج

اشتباه اول، نبود nonce است. اگر nonce بررسی نشود، هر درخواستی از هر منبعی پذیرفته می‌شود. اشتباه دوم، نبود Rate Limiting است. بدون محدودیت نرخ، ربات‌ها می‌توانند سرور را از پا درآورند. اشتباه سوم، نبود escape در پاسخ است. اگر داده کاربر در HTML چاپ شود، حفره XSS ایجاد می‌شود. اشتباه چهارم، نبود sanitize در ورودی است. اگر داده خام ذخیره شود، ممکن است به SQL Injection یا XSS منجر شود. اشتباه پنجم، ثبت هندلر مشترک بدون بررسی is_user_logged_in است. اگر هندلر مشترک داشته باشید، باید رفتار متفاوتی برای مهمان و کاربر وارد‌شده پیاده کنید. اشتباه ششم، نبود محدودیت عملیات حساس است. عملیات حساس مانند حذف داده یا ارسال ایمیل انبوه نباید برای مهمان‌ها فعال باشد. اشتباه هفتم، نبود لاگ‌گیری است. درخواست‌های عمومی باید در لاگ سرور ثبت شوند تا حملات قابل ردیابی باشند. اشتباه هشتم، نبود تست است. باید در سناریوهای نبود nonce، محدودیت نرخ، داده خالی و درخواست سریع تست کنید.

تحلیل فنی پیشرفته

در نگاه مهندسی، هوک wp_ajax_nopriv_ یک نقطه معماری در لایه Public Gateway است که بر چند لایه سیستم اثر می‌گذارد. لایه اول لایه Routing است. فایل admin-ajax.php با دریافت پارامتر action و بررسی وضعیت احراز هویت، هوک مناسب را انتخاب می‌کند. لایه دوم لایه CSRF Protection است. برای کاربران مهمان، nonce به نشست کاربر گره نمی‌خورد اما برای یک پنجره زمانی معتبر است. این رویکرد، امنیت را کاهش می‌دهد اما امکان استفاده از AJAX عمومی را فراهم می‌کند. لایه سوم لایه Rate Limiting است. محدودیت نرخ بر پایه IP و Transient، از حملات DoS جلوگیری می‌کند. لایه چهارم لایه Input Validation است. هر داده ورودی باید sanitize شود. لایه پنجم لایه Output Escaping است. هر داده خروجی باید escape شود. لایه ششم لایه Cache Compatibility است. درخواست‌های AJAX نباید کش شوند. باید هدرهای Cache-Control: no-store ارسال شوند. لایه هفتم لایه Multisite است. در شبکه‌های Multisite، هندلرها در هر سایت مستقل اجرا می‌شوند. لایه هشتم لایه Testing است. تست‌های امنیتی باید همه سناریوها را پوشش دهند. مفاهیم پایه‌ای Rate Limiting در Rate limiting در ویکی‌پدیا توضیح داده شده است. برای مطالعه بیشتر روی توابع مرتبط، می‌توانید به راهنمای هوک wp_ajax، راهنمای wp_send_json_success، راهنمای wp_send_json_error، راهنمای check_ajax_referer، راهنمای wp_verify_nonce، راهنمای wp_localize_script، راهنمای is_user_logged_in و راهنمای set_transient مراجعه کنید.

پرسش‌های پرتکرار

تفاوت wp_ajax_ و wp_ajax_nopriv_ چیست؟ اولی برای کاربران وارد‌شده و دومی برای مهمان‌ها. چرا به nonce در AJAX عمومی نیاز داریم؟ برای جلوگیری از CSRF و اطمینان از اینکه درخواست از صفحه سایت شما ارسال شده است. چطور Rate Limiting را پیاده کنیم؟ با Transient و IP کاربر. آیا داده‌های مهمان قابل اعتماد هستند؟ خیر، باید همیشه sanitize شوند. آیا می‌توان از این هوک برای عملیات حساس استفاده کرد؟ توصیه نمی‌شود. عملیات حساس باید تنها برای کاربران وارد‌شده باشد.

ادامه مسیر

هوک wp_ajax_nopriv_ نقطه ورود عمومی درخواست‌های AJAX است. استفاده درست از آن یعنی بررسی nonce، Rate Limiting، sanitize ورودی، escape خروجی و لاگ‌گیری درخواست‌ها. اشتباه‌های کوچک در این هوک اغلب به اسپم، DoS یا حفره‌های امنیتی منجر می‌شوند. اگر این هوک را در پروژه‌ای واقعی به کار برده‌اید و رفتار غیرمنتظره‌ای دیده‌اید — به‌خصوص در سناریوهای پرترافیک یا در ترکیب با CDN — تجربه‌تان می‌تواند راهگشای دیگران باشد. کدام بخش بیشترین زمان را از شما گرفت؟ دیدگاه خود را بنویسید.