إذا سبق لك أن رأيت "401 غير مصرح به" في برنامج Postman أثناء تسجيل دخولك WordPressلقد أصبت كبد الحقيقة: إنشاء نقطة نهاية REST لا يقتصر فقط على "إرجاع JSON"، بل يتعلق بإدارة الأذونات والتحقق من الصحة. مخبأ والتوافق.

المشكلة / الحاجة

تريد عرض (أو استهلاك) البيانات ووردبريس عبر واجهة برمجة التطبيقات (API): إرسال نموذج إلى ووردبريس، تشغيل تطبيق جوال، مزامنة نظام إدارة علاقات العملاء (CRM)، تشغيل إجراء من جانب الخادم، أو ببساطة توفير موجز JSON نظيف لواجهة أمامية بدون رأس.

يتضمن ووردبريس 6.9.4 واجهة برمجة تطبيقات REST ناضجة، ولكن بمجرد انحرافك عن المسارات الأصلية (/wp/v2/posts, /wp/v2/users...)، يجب عليك كتابة نقطة النهاية الخاصة بك: تحديد مسار، والتحقق من الشخص الذي لديه الحق في الوصول إليه، والتحقق من صحة المعلمات، وإرجاع استجابات متسقة.

بنهاية هذا المقال، ستتعلم كيفية إنشاء نقطة نهاية REST مخصصة وقوية (للقراءة والكتابة)، وتأمينها (من حيث الصلاحيات، وكلمة مرور التطبيق/الرقم العشوائي)، واختبارها بشكل صحيح باستخدام curl أو Postman، دون تعطيل موقعك.

ملخص سريع

  • ستقوم بتسجيل مسار REST باستخدام register_rest_route() على الخطاف rest_api_init.
  • ستقوم بتحديد رد الاتصال الخاص بالصلاحيات واقعية (القدرات، أو الوصول العام الخاضع للرقابة).
  • ستقوم بالتحقق من صحة الإعدادات وتنظيفها (التنظيف + التحقق) عبر args و/أو رمز الخادم.
  • سترسل ردودًا موحدة مع WP_REST_Response et WP_Error.
  • ستتعامل مع حالة ملموسة: إنشاء وعرض "طلبات الاتصال" المخزنة في قاعدة البيانات (جدول مخصص)، مع ترقيم الصفحات.
  • ستقوم بالاختبار في بيئة محلية/تجريبية وتجنب الأخطاء الشائعة (الخطاف السيئ، ذاكرة التخزين المؤقت، الروابط الدائمة، المصادقة).

متى يستخدم هذا الحل

  • نموذج مخصص (الأمامي) الذي يجب أن يسجل البيانات على الجانب ووردبريس بدون أعد تحميل الصفحة.
  • التكامل الخارجي (مثل Zapier، CRM، ERP) الذي يدفع البيانات إلى WordPress.
  • مكتب خلفي مخصص (React/Vue) الذي يستخدم مسارًا مخصصًا بدلاً من إساءة الاستخدام admin-ajax.php.
  • بدون رأس / منفصل : واجهة المستخدم الخاصة بك (Next.js، Astro، تطبيق الهاتف المحمول) تستهلك مسارات محددة.
  • الأمثل : أنت تريد استجابة أخف من نقاط النهاية الأصلية (عدد أقل من الحقول، وعدد أقل من عمليات الربط).

متى لا يجب استخدام هذا الحل

  • إذا كانت نقطة النهاية الأصلية تؤدي المهمة بالفعل: ابدأ بـ /wp/v2 ومعاييرها (_fields, per_pageإلخ). الوثائق: دليل واجهة برمجة تطبيقات REST.
  • إذا كنت ترغب فقط في "تشغيل إجراء" من المسؤول شاشة إدارة + admin-post.php أحيانًا يكون الأمر أبسط وأكثر أمانًا.
  • إذا كشفت بيانات حساسة دون داعٍ - عبر مسار REST عام ضعيف التصفية - فهذا يُعد تسريبًا للبيانات في بيئة الإنتاج. لقد رأيتُ مرارًا مواقع إلكترونية تكشف عن طريق الخطأ عناوين البريد الإلكتروني/أرقام الهواتف عبر مسار "مؤقت".
  • إذا كنت تبحث عن استجابات فورية (دفع): REST = طلب/استجابة. للحصول على استجابات فورية حقيقية، ضع في اعتبارك استخدام روابط الويب الصادرة بالإضافة إلى خادم، أو الاستقصاء المُتحكم به.

المتطلبات الأساسية / قبل البدء

إصدارات مستهدفة : WordPress 6.9.4، PHP 8.1+.

  • العمل على انطلاق أو محليًا (باستخدام WP-CLI وقاعدة بيانات مخصصة). تجنب اختبار هذا في بيئة الإنتاج بدون نسخة احتياطية.
  • مكن WP_DEBUG et WP_DEBUG_LOG في بيئة الاختبار.
  • خطط لاستخدام إضافة "mu-plugin" أو إضافة قياسية. تجنب لصق هذا الكود في functions.php استخدام سمة معينة إذا كنت تريد الاستقرار (تغيير السمة = اختفاء واجهة برمجة التطبيقات).
  • أدوات الاختبار: curl، Postman/Insomnia، أو عميل REST الخاص ببيئة التطوير المتكاملة (IDE) الخاصة بك.
  • المصادقة: لاختبار نقاط النهاية المحمية، استخدم إما ملف تعريف ارتباط الجلسة (من المتصفح + قيمة عشوائية)، أو... كلمات مرور التطبيق. مرجع : مصادقة REST API.

مصادر (رسمية) مفيدة يُنصح بالاحتفاظ بها:

النهج الساذج (ولماذا يجب تجنبه)

الكود الذي أراه في أغلب الأحيان من المطورين الذين "يريدون فقط JSON سريعًا": مسار REST لا يتحقق من أي شيء، ويأخذ المعلمات بكميات كبيرة، ويكتب إلى قاعدة البيانات بدون تنظيف.

<?php
// Anti-pattern : exemple volontairement mauvais (ne pas utiliser).
add_action('rest_api_init', function () {
	register_rest_route('demo/v1', '/contact', [
		'methods'  => 'POST',
		'callback' => function ($request) {
			// 1) Pas de permission_callback => endpoint potentiellement public.
			// 2) Pas de validation/sanitization => injection de contenu, spam, données cassées.
			// 3) Réponse non standard => difficile à déboguer côté client.

			global $wpdb;
			$name  = $_POST['name'] ?? '';
			$email = $_POST['email'] ?? '';

			$wpdb->query("INSERT INTO {$wpdb->prefix}demo_contacts (name,email) VALUES ('$name','$email')");

			return ['ok' => true];
		},
	]);
});

لماذا تُعدّ هذه مشكلة؟

  • أمن : نقطة نهاية عامة + الكتابة إلى قاعدة البيانات = البريد العشوائي، والفيضان، وربما حقن SQL (هنا، إنها صورة كاريكاتورية).
  • معطيات رسائل بريد إلكتروني غير صالحة، حقول طويلة جدًا، ترميز غريب، إلخ.
  • الدورية لا يوجد رسم تخطيطي، ولا رموز خطأ، ولا ترقيم صفحات، ولا سجلات.
  • إمكانية التشغيل المتداخل من جانب العميل، أنت لا تعرف كيف تميز بين النجاح والفشل (HTTP 200 في كل مكان).

النهج الصحيح - دليل خطوة بخطوة

هدف ملموس

نقوم بإنشاء "خدمة" REST مصغرة bpcab/v1 بنقطتي نهاية:

  • سأعين /wp-json/bpcab/v1/contact : إنشاء طلب اتصال (عام، مع الحد الأدنى من إجراءات مكافحة إساءة الاستخدام).
  • للحصول على /wp-json/bpcab/v1/contact : قم بإدراج الطلبات (محجوزة للمسؤولين/المحررين حسب اختيارك).

يتم تخزين الطلبات في طاولة مصممة حسب الطلبلماذا لا نستخدم نوع منشور مخصص؟ لأن الجداول المخصصة غالبًا ما تكون أسرع وأكثر تنظيمًا في حالة المشاركات الكبيرة (التي تحتوي على أسطر كثيرة ولا تحتاج إلى تحرير مكثف)، كما أنها تتجنب ازدحام النظام. wp_posts.

الخطوة 1 - إنشاء إضافة (مستحسن)

إنشاء ملف: wp-content/plugins/bpcab-rest-contact/bpcab-rest-contact.php.

فعّله في لوحة التحكم. أصر: الصق هذا في functions.php يعمل... حتى يأتي اليوم الذي يتسبب فيه تغيير في القالب في تعطيل واجهة برمجة التطبيقات (API) الخاصة بك.

الخطوة 2 - إنشاء الجدول عند التفعيل

نحن نستخدم dbDelta() لإنشاء/تحديث الجدول. المرجع: dbDelta().

الخطوة 3 - حفظ مسار REST

خطاف: rest_api_initهذا هو الوقت المناسب: يقوم ووردبريس بإعداد خادم REST، وأنت تقوم بتحديد مساراتك.

الخطوة الرابعة - تحديد صلاحيات واقعية

  • ل بوست بابليك نسمح للجميع، لكننا نضيف إجراءً وقائيًا بسيطًا (حدٌّ بسيطٌ لمعدل الطلبات عبر مؤقت + نظام خداع اختياري). إنه ليس جدار حماية تطبيقات الويب، ولكنه يمنع 80% من برامج الروبوت الأساسية.
  • ل للحصول على : هناك حاجة إلى قدرة معينة (على سبيل المثال: edit_posts ou manage_options).

الخطوة 5 - التحقق من الإعدادات وتنظيفها

نحن نحدد args في register_rest_route() : تنظيف + التحقق. ثم نعيد التحقق من جانب رد الاتصال للحالات الشاذة (الأطوال، المحتوى، إلخ).

الخطوة 6 - إجاباتك الخاصة

نشير إلى:

  • HTTP 201 لعملية إنشاء، مع الكائن الذي تم إنشاؤه (أو مجموعة فرعية منه).
  • HTTP 400/422 لأخطاء التحقق.
  • HTTP 401/403 للمصادقة/الأذونات.

الرمز الكامل

انسخ هذا الملف والصقه كما هو في الملحق. إنه مصمم ليكون مكتفياً ذاتياً عن قصد.

<?php
/**
 * Plugin Name: BPCAB - REST Contact Endpoint
 * Description: Endpoint REST personnalisé (WP 6.9.4+) pour créer et lister des demandes de contact.
 * Version: 1.0.0
 * Author: BPCAB
 * Requires at least: 6.9
 * Requires PHP: 8.1
 */

defined('ABSPATH') || exit;

final class BPCAB_REST_Contact_Endpoint {
	private const DB_VERSION_OPTION = 'bpcab_rest_contact_db_version';
	private const DB_VERSION = '1.0.0';
	private const TABLE_SLUG = 'bpcab_contacts';

	public static function init(): void {
		add_action('rest_api_init', [__CLASS__, 'register_routes']);
		register_activation_hook(__FILE__, [__CLASS__, 'activate']);
	}

	public static function activate(): void {
		self::maybe_create_table();
	}

	private static function table_name(): string {
		global $wpdb;
		return $wpdb->prefix . self::TABLE_SLUG;
	}

	private static function maybe_create_table(): void {
		$installed = get_option(self::DB_VERSION_OPTION);

		// Évite de relancer dbDelta à chaque chargement.
		if ($installed === self::DB_VERSION) {
			return;
		}

		global $wpdb;
		$table = self::table_name();

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$charset_collate = $wpdb->get_charset_collate();

		// Table simple : index sur created_at pour la pagination/tri.
		$sql = "CREATE TABLE {$table} (
			id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
			name VARCHAR(190) NOT NULL,
			email VARCHAR(190) NOT NULL,
			message TEXT NOT NULL,
			ip_hash CHAR(64) NULL,
			user_agent VARCHAR(255) NULL,
			created_at DATETIME NOT NULL,
			PRIMARY KEY  (id),
			KEY created_at (created_at)
		) {$charset_collate};";

		dbDelta($sql);

		update_option(self::DB_VERSION_OPTION, self::DB_VERSION, true);
	}

	public static function register_routes(): void {
		register_rest_route('bpcab/v1', '/contact', [
			[
				'methods'             => WP_REST_Server::CREATABLE, // POST
				'callback'            => [__CLASS__, 'handle_create_contact'],
				'permission_callback' => [__CLASS__, 'permissions_create_contact'],
				'args'                => self::get_create_args(),
			],
			[
				'methods'             => WP_REST_Server::READABLE, // GET
				'callback'            => [__CLASS__, 'handle_list_contacts'],
				'permission_callback' => [__CLASS__, 'permissions_list_contacts'],
				'args'                => self::get_list_args(),
			],
		]);
	}

	private static function get_create_args(): array {
		return [
			'name' => [
				'type'              => 'string',
				'required'          => true,
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => function ($param) {
					return is_string($param) && mb_strlen(trim($param)) >= 2 && mb_strlen($param) <= 190;
				},
				'description'       => 'Nom de la personne.',
			],
			'email' => [
				'type'              => 'string',
				'required'          => true,
				'sanitize_callback' => 'sanitize_email',
				'validate_callback' => function ($param) {
					return is_string($param) && is_email($param);
				},
				'description'       => 'Adresse email.',
			],
			'message' => [
				'type'              => 'string',
				'required'          => true,
				// sanitize_textarea_field garde les retours à la ligne, retire les balises.
				'sanitize_callback' => 'sanitize_textarea_field',
				'validate_callback' => function ($param) {
					return is_string($param) && mb_strlen(trim($param)) >= 10 && mb_strlen($param) <= 5000;
				},
				'description'       => 'Message.',
			],
			// Honeypot optionnel : un champ que les humains laissent vide.
			'website' => [
				'type'              => 'string',
				'required'          => false,
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => function ($param) {
					// Doit rester vide. Si rempli => bot probable.
					return is_string($param);
				},
				'description'       => 'Champ anti-spam (doit rester vide).',
			],
		];
	}

	private static function get_list_args(): array {
		return [
			'page' => [
				'type'              => 'integer',
				'required'          => false,
				'default'           => 1,
				'sanitize_callback' => 'absint',
				'validate_callback' => function ($param) {
					return is_numeric($param) && (int) $param >= 1 && (int) $param <= 9999;
				},
			],
			'per_page' => [
				'type'              => 'integer',
				'required'          => false,
				'default'           => 20,
				'sanitize_callback' => 'absint',
				'validate_callback' => function ($param) {
					// Limite volontairement basse pour éviter de sortir 50k lignes par erreur.
					return is_numeric($param) && (int) $param >= 1 && (int) $param <= 100;
				},
			],
			'search' => [
				'type'              => 'string',
				'required'          => false,
				'sanitize_callback' => 'sanitize_text_field',
			],
		];
	}

	public static function permissions_create_contact(WP_REST_Request $request): bool|WP_Error {
		// Endpoint public : on autorise, mais on applique un rate-limit simple (IP hash).
		$ip = self::get_client_ip();

		// Si on ne peut pas déterminer l'IP, on ne bloque pas, mais on reste prudent.
		if (!$ip) {
			return true;
		}

		$ip_hash = hash('sha256', $ip);
		$key = 'bpcab_contact_rl_' . $ip_hash;

		$count = (int) get_transient($key);

		// Exemple : max 10 soumissions / 10 minutes par IP.
		if ($count >= 10) {
			return new WP_Error(
				'bpcab_rate_limited',
				'Trop de requêtes. Réessayez dans quelques minutes.',
				['status' => 429]
			);
		}

		set_transient($key, $count + 1, 10 * MINUTE_IN_SECONDS);

		return true;
	}

	public static function permissions_list_contacts(WP_REST_Request $request): bool|WP_Error {
		// Ajustez selon votre besoin.
		if (current_user_can('manage_options')) {
			return true;
		}

		return new WP_Error(
			'bpcab_forbidden',
			'Accès refusé.',
			['status' => 403]
		);
	}

	public static function handle_create_contact(WP_REST_Request $request): WP_REST_Response|WP_Error {
		self::maybe_create_table();

		// Anti-spam : honeypot. Si rempli => on accepte mais on ne stocke pas (ou on renvoie 400).
		$website = (string) $request->get_param('website');
		if (trim($website) !== '') {
			return new WP_Error(
				'bpcab_spam_detected',
				'Requête refusée.',
				['status' => 400]
			);
		}

		$name    = (string) $request->get_param('name');
		$email   = (string) $request->get_param('email');
		$message = (string) $request->get_param('message');

		// Double validation côté serveur (défense en profondeur).
		if (!is_email($email)) {
			return new WP_Error('bpcab_invalid_email', 'Email invalide.', ['status' => 422]);
		}
		if (mb_strlen(trim($message)) < 10) {
			return new WP_Error('bpcab_invalid_message', 'Message trop court.', ['status' => 422]);
		}

		global $wpdb;
		$table = self::table_name();

		$ip = self::get_client_ip();
		$ip_hash = $ip ? hash('sha256', $ip) : null;

		$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? substr((string) $_SERVER['HTTP_USER_AGENT'], 0, 255) : null;

		$inserted = $wpdb->insert(
			$table,
			[
				'name'       => $name,
				'email'      => $email,
				'message'    => $message,
				'ip_hash'    => $ip_hash,
				'user_agent' => $user_agent,
				'created_at' => current_time('mysql'),
			],
			[
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
			]
		);

		if (!$inserted) {
			return new WP_Error(
				'bpcab_db_insert_failed',
				'Impossible d’enregistrer la demande.',
				['status' => 500]
			);
		}

		$id = (int) $wpdb->insert_id;

		$response = new WP_REST_Response(
			[
				'id'         => $id,
				'name'       => $name,
				'email'      => $email,
				'message'    => $message,
				'created_at' => current_time('mysql'),
			],
			201
		);

		// Location header utile pour une création REST.
		$response->header('Location', rest_url('bpcab/v1/contact/' . $id));

		return $response;
	}

	public static function handle_list_contacts(WP_REST_Request $request): WP_REST_Response|WP_Error {
		self::maybe_create_table();

		global $wpdb;
		$table = self::table_name();

		$page     = max(1, (int) $request->get_param('page'));
		$per_page = min(100, max(1, (int) $request->get_param('per_page')));
		$search   = (string) $request->get_param('search');

		$offset = ($page - 1) * $per_page;

		$where = '1=1';
		$params = [];

		if ($search !== '') {
			// Recherche simple sur nom/email/message.
			$like = '%' . $wpdb->esc_like($search) . '%';
			$where .= " AND (name LIKE %s OR email LIKE %s OR message LIKE %s)";
			$params[] = $like;
			$params[] = $like;
			$params[] = $like;
		}

		// Total pour pagination.
		$sql_count = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
		$total = (int) $wpdb->get_var($wpdb->prepare($sql_count, $params));

		// Résultats paginés.
		$sql = "SELECT id, name, email, message, created_at
				FROM {$table}
				WHERE {$where}
				ORDER BY created_at DESC
				LIMIT %d OFFSET %d";

		$params_with_limit = array_merge($params, [$per_page, $offset]);

		$rows = $wpdb->get_results($wpdb->prepare($sql, $params_with_limit), ARRAY_A);

		// Headers de pagination façon REST WordPress.
		$total_pages = (int) ceil($total / $per_page);

		$response = new WP_REST_Response(
			[
				'items' => $rows,
				'meta'  => [
					'page'        => $page,
					'per_page'    => $per_page,
					'total'       => $total,
					'total_pages' => $total_pages,
				],
			],
			200
		);

		$response->header('X-WP-Total', (string) $total);
		$response->header('X-WP-TotalPages', (string) $total_pages);

		return $response;
	}

	private static function get_client_ip(): ?string {
		// Attention : derrière un proxy/CDN, cette valeur peut être trompeuse.
		// Ne l'utilisez pas comme preuve d'identité. Ici c'est seulement du rate-limit “best effort”.
		$keys = [
			'HTTP_CF_CONNECTING_IP', // Cloudflare
			'HTTP_X_FORWARDED_FOR',
			'REMOTE_ADDR',
		];

		foreach ($keys as $key) {
			if (empty($_SERVER[$key])) {
				continue;
			}

			$value = (string) $_SERVER[$key];

			// X-Forwarded-For peut contenir une liste.
			if ($key === 'HTTP_X_FORWARDED_FOR') {
				$parts = array_map('trim', explode(',', $value));
				$value = $parts[0] ?? '';
			}

			if (filter_var($value, FILTER_VALIDATE_IP)) {
				return $value;
			}
		}

		return null;
	}
}

BPCAB_REST_Contact_Endpoint::init();

شرح الكود

نظرة عامة (بسيطة)

  • يقوم الملحق بإنشاء جدول wp_bpcab_contacts عند التفعيل (وإذا لزم الأمر في حالة عدم وجود الجدول).
  • يُعلن عن مسار REST /wp-json/bpcab/v1/contact باستخدام طريقتين: GET و POST.
  • يتم إرسال طلب POST بشكل عام ولكن يتم إبطاؤه (تحديد المعدل + مصيدة العسل).
  • يتطلب GET manage_options (لذا يكون المدير افتراضيًا).

تسجيل المسار

register_rest_route() يأخذ مساحة اسم (bpcab/v1), مسار (/contact)، وقائمة بالتعريفات حسب الطريقة. المرجع الرسمي: register_rest_route().

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

التحقق من الصحة / التطهير عبر الوسائط

في args، نُعرّف:

  • دالة استدعاء التعقيم : يحول المعلمة إلى قيمة ذاتية (على سبيل المثال، sanitize_email).
  • دالة التحقق من الصحة : يحدد ما إذا كانت القيمة مقبولة (على سبيل المثال، الحد الأدنى للطول).

وهذا له ميزتان: أنت تقوم بمركزة القواعد، ووردبريس يعيد خطأ REST متسقًا إذا كانت المعلمة غير صالحة.

استجابات REST نظيفة

نحن نستخدم WP_REST_Response للتحقق من حالة HTTP وعناوينها. المرجع: WP_REST_Response.

فيما يتعلق بالأخطاء، WP_Error مع status في dataيقوم ووردبريس بتحويل هذا إلى JSON صحيح للأخطاء.

كتابة استعلامات SQL دون إلحاق الضرر بنفسك

بالنسبة للإدراج، نستخدم $wpdb->insert() مع التنسيقات. للاستعلامات، نستخدم $wpdb->prepare() + esc_like()يمنع هذا هجمات حقن SQL وأخطاء الهروب من الرموز % في عبارات LIKE.

حد معدل "بذل قصارى الجهد"

يُخزَّن عداد مؤقت لكل تجزئة لعنوان IP. صحيحٌ أنه ليس حماية مثالية (بسبب NAT، والخوادم الوكيلة، والروبوتات الموزعة)، ولكنه يقلل الضرر. والأهم من ذلك كله، أنه سهل الصيانة.

ملاحظة: لا تتعامل مع عنوان IP كهوية. هنا، هو مجرد إشارة للحد من التشويش.

المتغيرات وحالات الاستخدام

الخيار 1 - نقطة نهاية عامة للقراءة فقط (كتالوج، قائمة)

إذا كنت تعرض بيانات عامة (مثل قائمة موارد)، فيمكنك وضع permission_callback à __return_trueلكن احتفظ بالتحكم في حقول الإخراج (_fields (جانب ووردبريس الأصلي، أو مخطط بسيط على الجانب المخصص).

كثيراً ما رأيتُ نقطة نهاية عامة تُعيد عن طريق الخطأ حقولاً داخلية (معرّفات، عناوين بريد إلكتروني، بيانات وصفية حساسة). أفضل حل: إنشاء مصفوفة الإخراج بشكل صريح.

الخيار الثاني - المصادقة عبر رقم عشوائي (واجهة أمامية متصلة)

بالنسبة لواجهة ووردبريس الكلاسيكية (لمستخدم مسجل الدخول)، يمكنك استدعاء نقطة النهاية الخاصة بك باستخدام رمز REST nonce (X-WP-Nonce). مرجع : التحقق من صحة ملفات تعريف الارتباط.

في هذه الحالة، أنت permission_callback يمكن التحقق من القدرة (read, edit_postsإلخ). هذا أنظف عموماً من جعل المنشور عاماً.

مثال جافا سكريبت (جلب) مع nonce

// Exemple : à exécuter dans un contexte WordPress où wpApiSettings.nonce est disponible.
async function sendContact(data) {
  const res = await fetch('/wp-json/bpcab/v1/contact', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-WP-Nonce': wpApiSettings.nonce
    },
    body: JSON.stringify(data)
  });

  const json = await res.json();
  if (!res.ok) throw json;
  return json;
}

للحقن wpApiSettings.nonce، ويمكنك wp_localize_script() أو استخدم الآليات التي يوفرها ووردبريس بالفعل حسب سياقك. المرجع: wp_create_nonce().

الخيار الثالث - أضف نقطة نهاية "GET /contact/{id}"

يتضمن الكود الكامل بالفعل رأسًا Location يشير إلى /contact/{id}لكن المسار غير مُعلن. إذا كنت بحاجة إليه، فأضف مسارًا مع مُعامل. (?P<id>d+) وإذن صارم.

مقتطف لإضافته إلى register_routes()

// Exemple : route supplémentaire (lecture d'une demande).
register_rest_route('bpcab/v1', '/contact/(?P<id>d+)', [
	'methods'             => WP_REST_Server::READABLE,
	'callback'            => [__CLASS__, 'handle_get_contact'],
	'permission_callback' => [__CLASS__, 'permissions_list_contacts'],
	'args'                => [
		'id' => [
			'type'              => 'integer',
			'required'          => true,
			'sanitize_callback' => 'absint',
		],
	],
]);

التوافق مع Divi 5 / Elementor / Avada

توجد نقطة نهاية REST على جانب الخادم، لذا فهي مستقلة عمومًا عن أداة الإنشاء. تظهر الاختلافات عند استدعاء نقطة النهاية من واجهة المستخدم (النموذج، الأداة، الوحدة).

ديفي 5

يُتيح Divi 5 إنشاء نماذج ووحدات نمطية مُخصصة. وهناك طريقتان شائعتان:

  • نموذج ديفي غالباً ما يكون استخدام نظام البريد الإلكتروني أسهل من استخدام رابط ويب خارجي. ولكن إذا كنت ترغب في تخزين البيانات في قاعدة بيانات عبر REST، فستحتاج إلى إضافة برنامج نصي يعترض عملية الإرسال ويرسلها إلى الخادم المناسب. /wp-json/bpcab/v1/contact.
  • وحدة Divi 5 المخصصة : اعرض الحقول (الاسم/البريد الإلكتروني/الرسالة) وقم بـ fetch عند نقطة النهاية. حسب تجربتي، يكمن الخلل في التخزين المؤقت/التصغير: تأكد من أن ملفات جافا سكريبت الخاصة بك مُدرجة بشكل صحيح في قائمة الانتظار وليست محظورة بسبب إعدادات الأداء.

Elementor

يمكن لنماذج Elementor Pro إرسال البيانات إلى رابط ويب، ولكن لاستدعاء مسار REST داخلي، ستحتاج عادةً إلى نص برمجي صغير. هناك نقطتان يجب الانتباه إليهما:

  • إذا كنت تستخدم المصادقة القائمة على nonce، فتأكد من تحميل البرنامج النصي الخاص بك للمستخدمين المسجلين وإدخال nonce.
  • في الأماكن العامة، حافظ على حد المعدل (أو ضع حلاً أكثر قوة لمكافحة البريد العشوائي على جانب النموذج).

أفادا (منشئ الاندماج)

توفر Avada أيضًا نماذج هجومية وخيارات تحسين متقدمة. أما المشاكل التي أواجهها غالبًا فهي:

  • جافا سكريبت مدمجة/مصغرة تتسبب في حدوث خلل fetch (خطأ صامت): تم تعطيل التحسين أثناء الاختبار.
  • إذا كانت ذاكرة التخزين المؤقت للخادم/شبكة توصيل المحتوى (CDN) تعرض صفحة بدون رمز التحقق الصحيح (إذا كنت مسجلاً الدخول): قم بإبطال ذاكرة التخزين المؤقت أو استبعاد الصفحات المتأثرة.

فحوصات ما بعد التثبيت

1) تأكد من وجود الطريق

يفتح: /wp-json/ وانظر bpcab/v1 في الفهرس.

2) اختبر طلب POST باستخدام curl

curl -i -X POST "https://example.com/wp-json/bpcab/v1/contact" 
  -H "Content-Type: application/json" 
  --data '{"name":"Alice","email":"[email protected]","message":"Bonjour, je souhaite un devis."}'

أنت بحاجة إلى الحصول على 201 وملف JSON يحتوي على id.

3) اختبار طلب GET (محمي)

أسهل طريقة للاختبار السريع: كلمات مرور التطبيقات. المرجع: كلمات مرور التطبيق.

curl -i "https://example.com/wp-json/bpcab/v1/contact?per_page=5" 
  -u "admin:APPLICATION_PASSWORD_ICI"

أنت بحاجة إلى الحصول على 200 + العناوين X-WP-Total et X-WP-TotalPages.

مخطط التشخيص السريع

عرض السبب المحتمل التحقق الحلول
404 في /wp-json/bpcab/v1/contact إضافة غير نشطة أو كود في مكان خاطئ الإدارة > الإضافات، أو /wp-json/ لا يدرج bpcab/v1 قم بتفعيل الإضافة، وتجنب functions.phpتحقق من وجود أخطاء في لغة PHP
403 "تم رفض الوصول" على GET قدرة غير كافية اختبر مع مسؤول النظام محول permissions_list_contacts() (على سبيل المثال edit_posts)
429 "طلبات كثيرة جداً" تم تفعيل حد المعدل إعادة تشغيل عدة منشورات متتالية قم بزيادة الحد الأدنى، أو تقليل حجم النافذة، أو تعطيله في وضع الاختبار.
500 "تعذر التسجيل" الجدول مفقود أو خطأ في قاعدة البيانات Regarder wp-content/debug.logتحقق من الجدول في phpMyAdmin أعد تنشيط الملحق، وتحقق من أذونات قاعدة البيانات، وأعد تشغيل عملية إنشاء الجدول.

إذا لم ينجح الأمر

  1. انظر إلى السجلات : wp-content/debug.logيحدث نسيان قوس أو فقدان فاصلة منقوطة أسرع مما تتخيل.
  2. تحقق من مكان لصق الكود : يمكن تعطيل جزء من التعليمات البرمجية في إضافة أجزاء التعليمات البرمجية عن طريق الخطأ، أو تحميله متأخرًا جدًا.
  3. اختبار فهرس REST : /wp-json/إذا لم يظهر نطاق الاسم الخاص بك، فإن الخطاف rest_api_init فشل التشغيل (خطأ فادح، المكون الإضافي غير نشط).
  4. قم بتعطيل ذاكرة التخزين المؤقت مؤقتًا (ذاكرة التخزين المؤقت للملحقات + ذاكرة التخزين المؤقت للخادم/شبكة توصيل المحتوى). لقد رأيت بالفعل شبكات توصيل المحتوى تُعيد بيانات قديمة. /wp-json/ بدون الطريق الجديد.
  5. تأكد من إصدار PHP إذا كنت تستخدم PHP 7.4/8.0 عن طريق الخطأ، فقد تتعطل بعض أنواع البيانات/القيم المُعادة. يعمل WordPress 6.9.4، ولكن الكود الخاص بك مصمم للعمل مع PHP 8.1 أو أحدث.
  6. المصادقة إذا كنت تختبر طلب GET، فاستخدم كلمات مرور التطبيق أو ملف تعريف ارتباط مع قيمة عشوائية (nonce). لا يكفي مجرد "تسجيل الدخول في علامة تبويب أخرى". curl.
  7. DB : التحقق من وجود الجدول وأن مستخدم قاعدة البيانات لديه صلاحيات الإنشاء/الإدراج.

الأخطاء الشائعة والمزالق

خطأ سبب الحلول
لم يتم العثور على المسار (404) تم وضع الكود في ملف غير محمل، أو لم يتم تفعيل الملحق. أنشئ إضافة حقيقية، وتحقق من تفعيلها، ثم افتحها. /wp-json/
rest_no_route مساحة اسم/مسار غير صحيح، أو روابط دائمة/ذاكرة تخزين مؤقتة مربكة تحقق من عنوان URL الدقيق، وامسح ذاكرة التخزين المؤقت، ثم أعد الاختبار.
401 / 403 inattendu permission_callback المصادقة المفرطة في الصرامة أو سيئة التنفيذ اختبر باستخدام كلمات مرور التطبيقات، واضبط الإمكانيات.
معلمات فارغة في دالة الاستدعاء تقراون $_POST بدلا من $request استعمال $request->get_param() et args
إرسال رسائل بريد إلكتروني جماعية عبر نقطة نهاية عامة منشور عام مكافحة الإساءة تحديد معدل الطلبات، أو استخدام أنظمة الخداع، أو اختبارات التحقق من المستخدم (CAPTCHA) في واجهة المستخدم، أو اشتراط المصادقة.
خطأ في استعلام SQL أثناء البحث مثل الهروب بشكل سيئ (نسيان esc_like) استعمال $wpdb->esc_like() + prepare()
خطأ فادح بعد التحديث كود من درس تعليمي قديم (واجهة برمجة تطبيقات قديمة، لغة PHP قديمة) استهدف ووردبريس 6.9.4 والإصدارات الأحدث، وPHP 8.1 والإصدارات الأحدث، وتجنب استخدام الأجزاء البرمجية القديمة.
لا يتم تنفيذ جافا سكريبت (الواجهة الأمامية) سوء إدارة قوائم الانتظار، والتصغير، وتعارض البناء قم بتعطيل خاصية التصغير، وتحقق من وحدة التحكم، وقم بتحميل البرنامج النصي بشكل صحيح.

نصائح السلامة والأداء والصيانة

  • لا تجعل نقطة نهاية الكتابة عامة "افتراضياً".إذا قمت بذلك، فقم بتطبيق استراتيجية لمكافحة البريد العشوائي (تحديد المعدل، أو نظام الخداع، أو CAPTCHA، أو المصادقة).
  • القدرات : لنقاط نهاية الإدارة، manage_options آمن ولكنه مقيد. في كثير من الأحيان، edit_posts هذا يكفي (افتتاحية). عدّل حسب الحاجة.
  • التحقق الصارم حدد أحجام الخزانات (190 خزانًا، 5000 خزان). بدون تحديد حجم، ستواجه طوابير طويلة وسرعات بطيئة.
  • الترقيم مطلوب لا تُرجع القيمة "كل شيء" افتراضيًا أبدًا، حتى لو كنت مسؤول النظام. ففي يوم من الأيام، سيدخل أحدهم 100000 سطر، وسيتعطل نظامك.
  • تجنب تخزين عنوان IP كنص عادي. إذا لم تكن بحاجة إليه. هنا، نخزن قيمة تجزئة للاستخدام المحدود (التشخيص/تحديد معدل الطلبات). راجع التزاماتك بموجب اللائحة العامة لحماية البيانات (GDPR) وفقًا لسياقك.
  • مراقبة في مرحلة الإنتاج، خطط لاستراتيجية تسجيل البيانات (بدون بيانات شخصية في نص عادي) والتنبيهات على المنافذ 429/500.
  • التوافق المستقبلي : احتفظ بمساحة اسم ذات إصدارات (v1) ولا تكسره. أضف v2 إذا كنت بحاجة إلى تغيير العقد.

الموارد

الأسئلة الشائعة

لماذا نستخدم REST بدلاً من admin-ajax.php؟

يوفر لك REST حالات HTTP، وبنية خطأ قياسية، وتكاملًا أفضل مع العملاء الخارجيين، ونموذجًا واضحًا (المسارات، والأساليب). admin-ajax.php لا يزال هذا الأمر مفيداً في الحالات التاريخية، لكنني أحتفظ به لاحتياجات محددة للغاية.

هل يجب عليّ إنشاء جدول مخصص أم نوع منشور مخصص؟

إذا كنت بحاجة إلى التعديل/المراجعة في لوحة تحكم ووردبريس باستخدام واجهة المستخدم، فغالبًا ما يكون إنشاء جدول مخصص أسرع. أما إذا كان لديك عدد كبير من الطلبات وتحتاج إلى إدارة البيانات (عرضها، تصديرها، حذفها)، فإن الجدول المخصص هو الخيار الأنسب.

كيفية تأمين نقطة نهاية الكتابة العامة بشكل صحيح؟

الحد الأدنى: تحديد معدل الطلبات + نظام خداعي. الأفضل: اشتراط المصادقة (باستخدام قيمة عشوائية في حال تسجيل دخول المستخدم، أو كلمات مرور التطبيقات للتكاملات)، أو إضافة نظام مكافحة البريد العشوائي في واجهة المستخدم (CAPTCHA/hCaptcha) والتحقق من جانب الخادم.

لماذا استخدام args بدلاً من التحقق من الصحة فقط في رد الاتصال؟

args يُوحّد هذا الأسلوب عملية التحقق من الصحة ويتجنب إعادة كتابة البنية التحتية. مع ذلك، لا يزال بإمكانك الاحتفاظ بعملية التحقق من الصحة "التجارية" في جانب الاستدعاء للقواعد الأكثر تعقيدًا.

أستقبل rest_cookie_invalid_nonceماذا علي أن أفعل؟

يمكنك استدعاء واجهة برمجة التطبيقات باستخدام ملف تعريف ارتباط المصادقة (جلسة) دون إرسال رأس الطلب. X-WP-Nonce (أو باستخدام قيمة عشوائية منتهية الصلاحية). أدخل قيمة عشوائية عبر wp_create_nonce('wp_rest') ثم أرسلها. أو بدلاً من ذلك، استخدم كلمات مرور التطبيقات لاختبار سطر الأوامر.

لماذا لا تظهر نقطة النهاية الخاصة بي في /wp-json/ ?

فشل تحميل الملحق (خطأ فادح)، أو لم يتم تنفيذ التعليمات البرمجية الخاصة بك. تحقق من ذلك. debug.logوتأكد من أنك متورط rest_api_initلست متأكداً init نأمل أن يكون ذلك كافياً.

هل أحتاج إلى "إعادة إنشاء الروابط الدائمة" لنقطة نهاية REST؟

عادةً لا. ولكن إذا كان موقعك قد خالف قواعد إعادة الكتابة أو كان يستخدم إضافة أمان تقوم بالتصفية /wp-json/قد تظهر لك أخطاء 404. في هذه الحالة، قد يساعدك الانتقال إلى الإعدادات > الروابط الدائمة، ولكن الأهم من ذلك كله هو البحث عن أي حظر من الخادم (جدار حماية تطبيقات الويب، قواعد nginx/Apache).

كيف يمكنني تحديد إصدارات واجهة برمجة التطبيقات (API) الخاصة بي؟

احتفظ bpcab/v1 مستقر. إذا كنت بحاجة إلى تغيير العقد (إعادة تسمية الحقول، سلوك مختلف)، فأنشئ bpcab/v2تجنب الاستراحة v1 "على انفراد".

كيف يمكنني منع قائمة (GET) من أن تكون بطيئة للغاية؟

قاعدة بيانات الفهرس (هنا) created_atترقيم الصفحات، وقيود صارمة (per_page <= 100) وحقل search معقول. إذا أصبح البحث بالغ الأهمية، فستضيف في النهاية فهرسًا للنصوص الكاملة أو استراتيجية بحث مخصصة.

هل يمكنني استدعاء نقطة النهاية هذه من موقع خارجي (CORS)؟

نعم، ولكن تتم إدارة CORS من جانب الخادم (عن طريق الرؤوس). لا تفتحها بشكل عام دون سبب وجيه. إذا كانت لديك حاجة حقيقية للوصول عبر النطاقات، فقم بتطبيق سياسة CORS صارمة (المصادر المصرح بها) واختبرها بعناية، لأنها نقطة دخول شائعة للاستغلال.