نموذج الاتصال موجود في كل موقع، وهو يستقبل كل يوم دعوات الروبوتات ومرسلي الرسائل المزعجة، وأحيانًا من يريد إساءة استخدام خادمك لإرسال البريد. والنموذج الذي يضع المُدخلات في البريد دون فحص له خطران: أحدهما أن يملأ صندوقك بالرسائل المزعجة، والآخر أن يترك الباب مفتوحًا لحقن ترويسات البريد أو السكربتات.
والخبر الجيد أن النموذج الآمن ليس معقدًا. تكفي أربع طبقات بسيطة: nonce وحقل فخ وتنقية المُدخلات والتحقق منها وتحديد عدد مرات الإرسال. وسنبني كلًّا منها فيما يلي.
ماذا يحتاج النموذج الآمن؟#
| الطبقة | وظيفتها | ما الذي تصده |
|---|---|---|
| nonce | تثبت أن الطلب يأتي من نموذج الموقع نفسه | الإرسال من مكان آخر، وتكرار الطلب |
| honeypot | حقل مخفي لا يملؤه إلا الروبوت | الروبوتات البسيطة |
| sanitize وvalidate | تنقية المُدخلات وفحصها | حقن ترويسات البريد والسكربتات |
| تحديد العدد | منع الإرسال المتتالي | سيل الرسائل وإساءة الاستخدام |
وبدون أي واحدة منها يعمل النموذج لكنه بلا دفاع.
بنية الكود#
نعرض النموذج بوسم قصير (shortcode) ونُسلّم الإرسال إلى admin-post.php. وهذه هي الطريقة المعتادة في ووردبريس لمعالجة النماذج ولا تحتاج إلى ملف منفصل. ضع كل شيء في إضافة صغيرة لا في القالب. وإذا لم تكن على دراية بالخطافات فإن مقال الأكشن والفلتر وبناء إضافة من الصفر يعطيانك الأساس.
عرض النموذج#
<?php
/**
* Plugin Name: Safe Contact Form
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
add_shortcode( 'safe_contact_form', function () {
$status = isset( $_GET['sc_status'] ) ? sanitize_key( wp_unslash( $_GET['sc_status'] ) ) : '';
$messages = array(
'ok' => __( 'تم إرسال رسالتك.', 'safe-contact' ),
'invalid' => __( 'يرجى إدخال الاسم وبريد إلكتروني صالح والرسالة كاملة.', 'safe-contact' ),
'wait' => __( 'انتظر لحظات ثم حاول مرة أخرى.', 'safe-contact' ),
'error' => __( 'تعذّر الإرسال. حاول لاحقًا.', 'safe-contact' ),
);
ob_start();
if ( isset( $messages[ $status ] ) ) {
printf( '<p role="status">%s</p>', esc_html( $messages[ $status ] ) );
}
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="safe_contact_submit">
<?php wp_nonce_field( 'safe_contact_submit', 'sc_nonce' ); ?>
<input type="hidden" name="sc_time" value="<?php echo esc_attr( time() ); ?>">
<p>
<label for="sc_name"><?php esc_html_e( 'الاسم', 'safe-contact' ); ?></label>
<input type="text" id="sc_name" name="sc_name" required maxlength="100">
</p>
<p>
<label for="sc_email"><?php esc_html_e( 'البريد الإلكتروني', 'safe-contact' ); ?></label>
<input type="email" id="sc_email" name="sc_email" required maxlength="150">
</p>
<p>
<label for="sc_message"><?php esc_html_e( 'الرسالة', 'safe-contact' ); ?></label>
<textarea id="sc_message" name="sc_message" rows="6" required maxlength="3000"></textarea>
</p>
<p style="position:absolute;left:-9999px;" aria-hidden="true">
<label for="sc_website">Website</label>
<input type="text" id="sc_website" name="sc_website" tabindex="-1" autocomplete="off">
</p>
<p><button type="submit"><?php esc_html_e( 'إرسال', 'safe-contact' ); ?></button></p>
</form>
<?php
return ob_get_clean();
} );الحقل sc_website هو الفخ. لا يراه المستخدم الحقيقي. والحقل sc_time يحفظ وقت عرض النموذج لنعرف إن مُلئ في أقل من بضع ثوانٍ.
معالجة الإرسال#
add_action( 'admin_post_nopriv_safe_contact_submit', 'sc_handle_submit' );
add_action( 'admin_post_safe_contact_submit', 'sc_handle_submit' );
function sc_redirect( string $status ): void {
$back = wp_get_referer();
$back = $back ? $back : home_url( '/' );
wp_safe_redirect( add_query_arg( 'sc_status', $status, $back ) );
exit;
}
function sc_handle_submit(): void {
if ( ! isset( $_POST['sc_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['sc_nonce'] ) ), 'safe_contact_submit' ) ) {
wp_die( esc_html__( 'الطلب غير صالح.', 'safe-contact' ), '', array( 'response' => 403 ) );
}
// الفخ: إذا كان ممتلئًا فتظاهر بالنجاح بصمت.
if ( ! empty( $_POST['sc_website'] ) ) {
sc_redirect( 'ok' );
}
// النموذج المرسل في أقل من 3 ثوانٍ غالبًا من روبوت.
$shown = isset( $_POST['sc_time'] ) ? (int) $_POST['sc_time'] : 0;
if ( $shown <= 0 || ( time() - $shown ) < 3 ) {
sc_redirect( 'wait' );
}
// التحديد: إرسال واحد كل 60 ثانية لكل عنوان IP.
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
$key = 'sc_rate_' . md5( $ip );
if ( get_transient( $key ) ) {
sc_redirect( 'wait' );
}
$name = isset( $_POST['sc_name'] ) ? sanitize_text_field( wp_unslash( $_POST['sc_name'] ) ) : '';
$email = isset( $_POST['sc_email'] ) ? sanitize_email( wp_unslash( $_POST['sc_email'] ) ) : '';
$message = isset( $_POST['sc_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['sc_message'] ) ) : '';
if ( '' === $name || ! is_email( $email ) || '' === $message ) {
sc_redirect( 'invalid' );
}
set_transient( $key, 1, MINUTE_IN_SECONDS );
$to = get_option( 'admin_email' );
$subject = sprintf( '[%s] رسالة جديدة من نموذج الاتصال', wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
$body = "الاسم: {$name}\nالبريد: {$email}\n\n{$message}";
$headers = array(
'Content-Type: text/plain; charset=UTF-8',
'Reply-To: ' . $name . ' <' . $email . '>',
);
$sent = wp_mail( $to, $subject, $body, $headers );
sc_redirect( $sent ? 'ok' : 'error' );
}عدة نقاط في هذا الكود مهمة:
wp_verify_nonceلا يسمح بإرسال النموذج من خارج الموقع. وبجانبهwp_unslashوsanitize_*، لأن ووردبريس يمرر قيم$_POSTبشرطات مائلة عكسية.sanitize_emailوis_emailيمنعان حقن ترويسات البريد. ولأنsanitize_text_fieldيحذف السطر الجديد فلا يمكن للاسم أن يضيف ترويسة أيضًا.- المرسل يذهب في
Reply-Toلا فيFrom. فالرسالة تأتي من عنوان الموقع، وعندما تضغط «رد» يذهب الرد إلى العميل. وإذا كانت الرسائل لا تصل فراجع إعداد SMTP. - تحديد العدد بـ
transientبسيط. وإذا كنت خلف وكيل (proxy) أو CDN فسيصبحREMOTE_ADDRعنوان الوكيل؛ وفي تلك الحالة يحدد إعداد الخادم أو الـ CDN الطريقة الأصح، ولا ينبغي قبول ترويسة يحددها المستخدم دون قيد.
كابتشا أم لا؟#
الطبقات أعلاه تصد جزءًا كبيرًا من الرسائل المزعجة. وإذا بقيت تصلك فأضف كابتشا غير مرئية أو خدمة للتحقق من أنك إنسان؛ وكثير منها يتحقق من الإجابة على جهة الخادم. وانتبه إلى أنك بسبب الخصوصية يجب أن تعلن السكربت الخارجي في صفحة الخصوصية. وبعض الخدمات تقيّد الوصول بحسب البلد، فاختبر التوافق قبل الاختيار.
الاختبار قبل النشر#
- املأ النموذج بشكل عادي واستلم البريد.
- أفرغ حقل nonce بأداة المطور؛ ينبغي أن تحصل على خطأ 403.
- املأ الحقل المخفي وتحقق من أن البريد لا يصل.
- أرسل مرتين متتاليتين؛ ينبغي أن تُرفض الثانية.
- جرّب الاسم والبريد بحرف سطر جديد وتحقق من أن الترويسة لا تُضاف.
وهناك عمل آخر هو المراجعة العامة لـأمان الموقع: فالنموذج الآمن دون ووردبريس وإضافات محدّثة ليس إلا بابًا متينًا في جدار قصير.
أخطاء شائعة في النماذج المصنوعة يدويًا#
النماذج التي نكتبها بأنفسنا تضعف عادةً من عدة جهات. أولها استخدام $_POST دون تنقية؛ فلا تضع القيمة الخام في البريد أو قاعدة البيانات أبدًا. والثاني وضع بريد العميل في ترويسة From وهو يفسد التسليم ويفتح باب حقن الترويسات. والثالث عرض رسائل الخطأ التقنية للمستخدم؛ قل للمستخدم رسالة بسيطة فقط واكتب التفاصيل في السجل. والرابع عدم توفير بديل: ضع دائمًا عنوان بريد أو رقم تواصل بجانب النموذج حتى لا يضيع العميل إذا تعطل النموذج. وأخيرًا اختبر نص النموذج ورسائله على الجوال أيضًا؛ فالنموذج الذي يصعب ملؤه على الجوال يستقبل رسائل أقل حتى بدون سبام.
الخلاصة#
- نموذج الاتصال غير المحمي يستقبل السبام ويمكن أن يتيح حقن ترويسات البريد.
- أربع طبقات أساسية: nonce، وحقل فخ، والتنقية والتحقق، وتحديد العدد.
- نفّذ المعالجة بـ
admin-post.phpودائمًا استخدمwp_unslashوsanitize_*. - ضع عنوان العميل في
Reply-Toوأبقِ المرسل من نطاقك. - إذا لم تصل الرسائل فالمشكلة عادةً في SMTP وSPF/DKIM لا في النموذج.
- قبل النشر اكسر كل طبقة عمدًا لتتأكد أنها تعمل.