نموذج الاتصال موجود في كل موقع، وهو يستقبل كل يوم دعوات الروبوتات ومرسلي الرسائل المزعجة، وأحيانًا من يريد إساءة استخدام خادمك لإرسال البريد. والنموذج الذي يضع المُدخلات في البريد دون فحص له خطران: أحدهما أن يملأ صندوقك بالرسائل المزعجة، والآخر أن يترك الباب مفتوحًا لحقن ترويسات البريد أو السكربتات.

والخبر الجيد أن النموذج الآمن ليس معقدًا. تكفي أربع طبقات بسيطة: nonce وحقل فخ وتنقية المُدخلات والتحقق منها وتحديد عدد مرات الإرسال. وسنبني كلًّا منها فيما يلي.

ماذا يحتاج النموذج الآمن؟#

الطبقةوظيفتهاما الذي تصده
nonceتثبت أن الطلب يأتي من نموذج الموقع نفسهالإرسال من مكان آخر، وتكرار الطلب
honeypotحقل مخفي لا يملؤه إلا الروبوتالروبوتات البسيطة
sanitize وvalidateتنقية المُدخلات وفحصهاحقن ترويسات البريد والسكربتات
تحديد العددمنع الإرسال المتتاليسيل الرسائل وإساءة الاستخدام

وبدون أي واحدة منها يعمل النموذج لكنه بلا دفاع.

بنية الكود#

نعرض النموذج بوسم قصير (shortcode) ونُسلّم الإرسال إلى admin-post.php. وهذه هي الطريقة المعتادة في ووردبريس لمعالجة النماذج ولا تحتاج إلى ملف منفصل. ضع كل شيء في إضافة صغيرة لا في القالب. وإذا لم تكن على دراية بالخطافات فإن مقال الأكشن والفلتر وبناء إضافة من الصفر يعطيانك الأساس.

عرض النموذج#

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 يحفظ وقت عرض النموذج لنعرف إن مُلئ في أقل من بضع ثوانٍ.

معالجة الإرسال#

PHP
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 الطريقة الأصح، ولا ينبغي قبول ترويسة يحددها المستخدم دون قيد.

كابتشا أم لا؟#

الطبقات أعلاه تصد جزءًا كبيرًا من الرسائل المزعجة. وإذا بقيت تصلك فأضف كابتشا غير مرئية أو خدمة للتحقق من أنك إنسان؛ وكثير منها يتحقق من الإجابة على جهة الخادم. وانتبه إلى أنك بسبب الخصوصية يجب أن تعلن السكربت الخارجي في صفحة الخصوصية. وبعض الخدمات تقيّد الوصول بحسب البلد، فاختبر التوافق قبل الاختيار.

الاختبار قبل النشر#

  1. املأ النموذج بشكل عادي واستلم البريد.
  2. أفرغ حقل nonce بأداة المطور؛ ينبغي أن تحصل على خطأ 403.
  3. املأ الحقل المخفي وتحقق من أن البريد لا يصل.
  4. أرسل مرتين متتاليتين؛ ينبغي أن تُرفض الثانية.
  5. جرّب الاسم والبريد بحرف سطر جديد وتحقق من أن الترويسة لا تُضاف.

وهناك عمل آخر هو المراجعة العامة لـأمان الموقع: فالنموذج الآمن دون ووردبريس وإضافات محدّثة ليس إلا بابًا متينًا في جدار قصير.

أخطاء شائعة في النماذج المصنوعة يدويًا#

النماذج التي نكتبها بأنفسنا تضعف عادةً من عدة جهات. أولها استخدام $_POST دون تنقية؛ فلا تضع القيمة الخام في البريد أو قاعدة البيانات أبدًا. والثاني وضع بريد العميل في ترويسة From وهو يفسد التسليم ويفتح باب حقن الترويسات. والثالث عرض رسائل الخطأ التقنية للمستخدم؛ قل للمستخدم رسالة بسيطة فقط واكتب التفاصيل في السجل. والرابع عدم توفير بديل: ضع دائمًا عنوان بريد أو رقم تواصل بجانب النموذج حتى لا يضيع العميل إذا تعطل النموذج. وأخيرًا اختبر نص النموذج ورسائله على الجوال أيضًا؛ فالنموذج الذي يصعب ملؤه على الجوال يستقبل رسائل أقل حتى بدون سبام.

الخلاصة#

  • نموذج الاتصال غير المحمي يستقبل السبام ويمكن أن يتيح حقن ترويسات البريد.
  • أربع طبقات أساسية: nonce، وحقل فخ، والتنقية والتحقق، وتحديد العدد.
  • نفّذ المعالجة بـ admin-post.php ودائمًا استخدم wp_unslash وsanitize_*.
  • ضع عنوان العميل في Reply-To وأبقِ المرسل من نطاقك.
  • إذا لم تصل الرسائل فالمشكلة عادةً في SMTP وSPF/DKIM لا في النموذج.
  • قبل النشر اكسر كل طبقة عمدًا لتتأكد أنها تعمل.