Intermediaire 1 min de lecture · 164 mots

WordPress REST API : Créer des endpoints personnalisés

Introduction

L’API REST de WordPress est devenue un composant essentiel pour créer des applications modernes, des interfaces découplées et des intégrations tierces. En 2025, la sécurité des endpoints personnalisés est plus critique que jamais, avec une attention particulière portée à l’authentification, la validation des données et la limitation du débit. Ce guide complet vous accompagnera dans la création d’endpoints REST API sécurisés et performants.

Comprendre l’architecture de l’API REST WordPress

Anatomie d’une requête REST

L’API REST WordPress suit les conventions RESTful standards :

  • GET : Récupérer des données
  • POST : Créer de nouvelles données
  • PUT/PATCH : Mettre à jour des données existantes
  • DELETE : Supprimer des données
  • Les endpoints WordPress suivent cette structure :

https://example.com/wp-json/namespace/v1/resource

Structure de base d’un endpoint

Voici les composants essentiels d’un endpoint REST :

namespace, '/items', array(
            'methods'             => WP_REST_Server::READABLE, // GET
            'callback'            => array( $this, 'get_items' ),
            'permission_callback' => array( $this, 'get_items_permissions_check' ),
            'args'                => $this->get_collection_params(),
        ) );

        // Route POST
        register_rest_route( $this->namespace, '/items', array(
            'methods'             => WP_REST_Server::CREATABLE, // POST
            'callback'            => array( $this, 'create_item' ),
            'permission_callback' => array( $this, 'create_item_permissions_check' ),
            'args'                => $this->get_endpoint_args_for_item_schema(),
        ) );

        // Route pour un item spécifique (GET, PUT, DELETE)
        register_rest_route( $this->namespace, '/items/(?Pd+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE, // GET
                'callback'            => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
                'args'                => array(
                    'id' => array(
                        'description' => **( 'Identifiant unique de l'item.', 'mon-plugin' ),
                        'type'        => 'integer',
                        'required'    => true,
                    ),
                ),
            ),
            array(
                'methods'             => WP_REST_Server::EDITABLE, // PUT/PATCH
                'callback'            => array( $this, 'update_item' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( false ),
            ),
            array(
                'methods'             => WP_REST_Server::DELETABLE, // DELETE
                'callback'            => array( $this, 'delete_item' ),
                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
            ),
            'schema' => array( $this, 'get_public_item_schema' ),
        ) );
    }

    /**
     * Récupérer une collection d'items
     */
    public function get_items( $request ) {
        $items = array();
        $data  = array();

        // Récupérer les paramètres de pagination
        $per_page = $request->get_param( 'per_page' );
        $page     = $request->get_param( 'page' );
        $search   = $request->get_param( 'search' );

        // Requête à la base de données
        global $wpdb;
        $table_name = $wpdb->prefix . 'mon_plugin_items';

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

        $where = '';
        if ( ! empty( $search ) ) {
            $where = $wpdb->prepare( " WHERE title LIKE %s", '%' . $wpdb->esc_like( $search ) . '%' );
        }

        $items = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM $table_name $where ORDER BY created_at DESC LIMIT %d OFFSET %d",
                $per_page,
                $offset
            )
        );

        // Préparer les données pour la réponse
        foreach ( $items as $item ) {
            $itemdata = $this->prepare_item_for_response( $item, $request );
            $data[]   = $this->prepare_response_for_collection( $itemdata );
        }

        // Compter le total pour les en-têtes de pagination
        $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table_name $where" );
        $max_pages   = ceil( $total_items / $per_page );

        // Créer la réponse avec en-têtes
        $response = rest_ensure_response( $data );
        $response->header( 'X-WP-Total', (int) $total_items );
        $response->header( 'X-WP-TotalPages', (int) $max_pages );

        return $response;
    }

    /**
     * Récupérer un seul item
     */
    public function get_item( $request ) {
        $id = $request->get_param( 'id' );

        global $wpdb;
        $table_name = $wpdb->prefix . 'mon_plugin_items';

        $item = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
        );

        if ( empty( $item ) ) {
            return new WP_Error(
                'rest_item_not_found',
                **( 'Item non trouvé.', 'mon-plugin' ),
                array( 'status' => 404 )
            );
        }

        $data = $this->prepare_item_for_response( $item, $request );

        return rest_ensure_response( $data );
    }

    /**
     * Créer un nouvel item
     */
    public function create_item( $request ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'mon_plugin_items';

        // Récupérer et valider les données
        $title       = sanitize_text_field( $request->get_param( 'title' ) );
        $description = wp_kses_post( $request->get_param( 'description' ) );
        $status      = sanitize_text_field( $request->get_param( 'status' ) );
        $user_id     = get_current_user_id();

        // Insérer dans la base de données
        $inserted = $wpdb->insert(
            $table_name,
            array(
                'title'       => $title,
                'description' => $description,
                'status'      => $status,
                'user_id'     => $user_id,
                'created_at'  => current_time( 'mysql' ),
            ),
            array( '%s', '%s', '%s', '%d', '%s' )
        );

        if ( ! $inserted ) {
            return new WP_Error(
                'rest_item_create_failed',
                **( 'Impossible de créer l'item.', 'mon-plugin' ),
                array( 'status' => 500 )
            );
        }

        $item_id = $wpdb->insert_id;

        // Récupérer l'item créé
        $item = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $item_id )
        );

        $data = $this->prepare_item_for_response( $item, $request );

        $response = rest_ensure_response( $data );
        $response->set_status( 201 );
        $response->header( 'Location', rest_url( sprintf( '%s/items/%d', $this->namespace, $item_id ) ) );

        return $response;
    }

    /**
     * Mettre à jour un item
     */
    public function update_item( $request ) {
        $id = $request->get_param( 'id' );

        global $wpdb;
        $table_name = $wpdb->prefix . 'mon_plugin_items';

        // Vérifier que l'item existe
        $item = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
        );

        if ( empty( $item ) ) {
            return new WP_Error(
                'rest_item_not_found',
                **( 'Item non trouvé.', 'mon-plugin' ),
                array( 'status' => 404 )
            );
        }

        // Préparer les données à mettre à jour
        $update_data = array();
        $update_format = array();

        if ( $request->has_param( 'title' ) ) {
            $update_data['title'] = sanitize_text_field( $request->get_param( 'title' ) );
            $update_format[] = '%s';
        }

        if ( $request->has_param( 'description' ) ) {
            $update_data['description'] = wp_kses_post( $request->get_param( 'description' ) );
            $update_format[] = '%s';
        }

        if ( $request->has_param( 'status' ) ) {
            $update_data['status'] = sanitize_text_field( $request->get_param( 'status' ) );
            $update_format[] = '%s';
        }

        $update_data['updated_at'] = current_time( 'mysql' );
        $update_format[] = '%s';

        // Mettre à jour dans la base de données
        $updated = $wpdb->update(
            $table_name,
            $update_data,
            array( 'id' => $id ),
            $update_format,
            array( '%d' )
        );

        if ( false === $updated ) {
            return new WP_Error(
                'rest_item_update_failed',
                **( 'Impossible de mettre à jour l'item.', 'mon-plugin' ),
                array( 'status' => 500 )
            );
        }

        // Récupérer l'item mis à jour
        $item = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
        );

        $data = $this->prepare_item_for_response( $item, $request );

        return rest_ensure_response( $data );
    }

    /**
     * Supprimer un item
     */
    public function delete_item( $request ) {
        $id = $request->get_param( 'id' );

        global $wpdb;
        $table_name = $wpdb->prefix . 'mon_plugin_items';

        // Vérifier que l'item existe
        $item = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
        );

        if ( empty( $item ) ) {
            return new WP_Error(
                'rest_item_not_found',
                **( 'Item non trouvé.', 'mon-plugin' ),
                array( 'status' => 404 )
            );
        }

        // Préparer la réponse avant suppression
        $previous = $this->prepare_item_for_response( $item, $request );

        // Supprimer de la base de données
        $deleted = $wpdb->delete(
            $table_name,
            array( 'id' => $id ),
            array( '%d' )
        );

        if ( ! $deleted ) {
            return new WP_Error(
                'rest_item_delete_failed',
                **( 'Impossible de supprimer l'item.', 'mon-plugin' ),
                array( 'status' => 500 )
            );
        }

        $response = new WP_REST_Response();
        $response->set_data( array(
            'deleted'  => true,
            'previous' => $previous->get_data(),
        ) );

        return $response;
    }

    /**
     * Préparer un item pour la réponse
     */
    public function prepare_item_for_response( $item, $request ) {
        $data = array(
            'id'          => (int) $item->id,
            'title'       => $item->title,
            'description' => $item->description,
            'status'      => $item->status,
            'user_id'     => (int) $item->user_id,
            'created_at'  => mysql2date( 'c', $item->created_at ),
            'updated_at'  => $item->updated_at ? mysql2date( 'c', $item->updated_at ) : null,
        );

        // Ajouter des liens
        $data['*links'] = array(
            'self'       => array(
                'href' => rest_url( sprintf( '%s/items/%d', $this->namespace, $item->id ) ),
            ),
            'collection' => array(
                'href' => rest_url( sprintf( '%s/items', $this->namespace ) ),
            ),
        );

        if ( ! empty( $item->user_id ) ) {
            $data['*links']['author'] = array(
                'href'       => rest_url( sprintf( 'wp/v2/users/%d', $item->user_id ) ),
                'embeddable' => true,
            );
        }

        return rest_ensure_response( $data );
    }

    /**
     * Préparer la réponse pour une collection
     */
    public function prepare_response_for_collection( $response ) {
        if ( ! ( $response instanceof WP_REST_Response ) ) {
            return $response;
        }

        $data = (array) $response->get_data();
        $server = rest_get_server();
        $links = $server::get_compact_response_links( $response );

        if ( ! empty( $links ) ) {
            $data['*links'] = $links;
        }

        return $data;
    }

    /**
     * Paramètres de collection (pagination, recherche)
     */
    public function get_collection_params() {
        return array(
            'page'     => array(
                'description'       => **( 'Page actuelle de la collection.', 'mon-plugin' ),
                'type'              => 'integer',
                'default'           => 1,
                'sanitize_callback' => 'absint',
                'validate_callback' => 'rest_validate_request_arg',
                'minimum'           => 1,
            ),
            'per_page' => array(
                'description'       => **( 'Nombre maximum d'items retournés.', 'mon-plugin' ),
                'type'              => 'integer',
                'default'           => 10,
                'minimum'           => 1,
                'maximum'           => 100,
                'sanitize_callback' => 'absint',
                'validate_callback' => 'rest_validate_request_arg',
            ),
            'search'   => array(
                'description'       => **( 'Rechercher dans les items.', 'mon-plugin' ),
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
                'validate_callback' => 'rest_validate_request_arg',
            ),
        );
    }

    /**
     * Arguments pour créer/modifier un item
     */
    public function get_endpoint_args_for_item_schema( $method_create = true ) {
        return array(
            'title'       => array(
                'description'       => **( 'Titre de l'item.', 'mon-plugin' ),
                'type'              => 'string',
                'required'          => $method_create,
                'sanitize_callback' => 'sanitize_text_field',
                'validate_callback' => 'rest_validate_request_arg',
            ),
            'description' => array(
                'description'       => **( 'Description de l'item.', 'mon-plugin' ),
                'type'              => 'string',
                'sanitize_callback' => 'wp_kses_post',
                'validate_callback' => 'rest_validate_request_arg',
            ),
            'status'      => array(
                'description'       => **( 'Statut de l'item.', 'mon-plugin' ),
                'type'              => 'string',
                'enum'              => array( 'draft', 'published', 'archived' ),
                'default'           => 'draft',
                'sanitize_callback' => 'sanitize_text_field',
                'validate_callback' => 'rest_validate_request_arg',
            ),
        );
    }

    /**
     * Schéma de l'item
     */
    public function get_public_item_schema() {
        if ( $this->schema ) {
            return $this->add_additional_fields_schema( $this->schema );
        }

        $schema = array(
            '$schema'    => 'http://json-schema.org/draft-04/schema#',
            'title'      => 'item',
            'type'       => 'object',
            'properties' => array(
                'id'          => array(
                    'description' => **( 'Identifiant unique de l'item.', 'mon-plugin' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit', 'embed' ),
                    'readonly'    => true,
                ),
                'title'       => array(
                    'description' => **( 'Titre de l'item.', 'mon-plugin' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit', 'embed' ),
                ),
                'description' => array(
                    'description' => **( 'Description de l'item.', 'mon-plugin' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit' ),
                ),
                'status'      => array(
                    'description' => **( 'Statut de l'item.', 'mon-plugin' ),
                    'type'        => 'string',
                    'enum'        => array( 'draft', 'published', 'archived' ),
                    'context'     => array( 'view', 'edit' ),
                ),
                'user_id'     => array(
                    'description' => **( 'ID de l'auteur.', 'mon-plugin' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                ),
                'created_at'  => array(
                    'description' => **( 'Date de création (ISO8601).', 'mon-plugin' ),
                    'type'        => 'string',
                    'format'      => 'date-time',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'updated_at'  => array(
                    'description' => **( 'Date de dernière modification (ISO8601).', 'mon-plugin' ),
                    'type'        => array( 'string', 'null' ),
                    'format'      => 'date-time',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
            ),
        );

        $this->schema = $schema;

        return $this->add_additional_fields_schema( $this->schema );
    }

    /**
     * Vérifications de permissions - Récupérer items
     */
    public function get_items_permissions_check( $request ) {
        // Accessible publiquement
        return true;
    }

    /**
     * Vérifications de permissions - Récupérer un item
     */
    public function get_item_permissions_check( $request ) {
        // Accessible publiquement
        return true;
    }

    /**
     * Vérifications de permissions - Créer un item
     */
    public function create_item_permissions_check( $request ) {
        // Seuls les utilisateurs connectés peuvent créer
        if ( ! is_user_logged_in() ) {
            return new WP_Error(
                'rest_forbidden',
                **( 'Vous devez être connecté pour créer un item.', 'mon-plugin' ),
                array( 'status' => rest_authorization_required_code() )
            );
        }

        return true;
    }

    /**
     * Vérifications de permissions - Mettre à jour un item
     */
    public function update_item_permissions_check( $request ) {
        if ( ! is_user_logged_in() ) {
            return new WP_Error(
                'rest_forbidden',
                **( 'Vous devez être connecté pour modifier un item.', 'mon-plugin' ),
                array( 'status' => rest_authorization_required_code() )
            );
        }

        $id = $request->get_param( 'id' );

        global $wpdb;
        $table_name = $wpdb->prefix . 'mon_plugin_items';

        $item = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id )
        );

        if ( empty( $item ) ) {
            return new WP_Error(
                'rest_item_not_found',
                **( 'Item non trouvé.', 'mon-plugin' ),
                array( 'status' => 404 )
            );
        }

        // Vérifier que l'utilisateur est propriétaire ou admin
        $current_user_id = get_current_user_id();
        if ( (int) $item->user_id !== $current_user_id && ! current_user_can( 'manage_options' ) ) {
            return new WP_Error(
                'rest_forbidden',
                **( 'Vous n'avez pas la permission de modifier cet item.', 'mon-plugin' ),
                array( 'status' => rest_authorization_required_code() )
            );
        }

        return true;
    }

    /**
     * Vérifications de permissions - Supprimer un item
     */
    public function delete_item_permissions_check( $request ) {
        return $this->update_item_permissions_check( $request );
    }
}

// Initialiser l'endpoint
new EndpointExample();

Authentification sécurisée

Méthodes d’authentification en 2025

WordPress offre plusieurs méthodes d’authentification :

1. Authentification par cookie (sessions WordPress)

Utilisée automatiquement pour les requêtes depuis le site WordPress :

get_header( 'X-WP-Nonce' );

    if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
        return new WP_Error(
            'rest_cookie_invalid_nonce',
            **( 'Nonce invalide.', 'mon-plugin' ),
            array( 'status' => 403 )
        );
    }

    return true;
}

2. Authentification par clé API personnalisée

get_api_key_from_request();

        if ( empty( $api_key ) ) {
            return $user_id;
        }

        // Valider la clé API
        $user_id = $this->validate_api_key( $api_key );

        return $user_id;
    }

    /**
     * Récupérer la clé API depuis la requête
     */
    private function get_api_key_from_request() {
        // Vérifier dans les en-têtes
        $api_key = null;

        if ( isset( $*SERVER['HTTP_X_API_KEY'] ) ) {
            $api_key = sanitize_text_field( wp_unslash( $*SERVER['HTTP_X_API_KEY'] ) );
        } elseif ( isset( $*GET['api_key'] ) ) {
            // Fallback pour query parameter (moins sécurisé)
            $api_key = sanitize_text_field( wp_unslash( $*GET['api_key'] ) );
        }

        return $api_key;
    }

    /**
     * Valider la clé API et retourner l'ID utilisateur
     */
    private function validate_api_key( $api_key ) {
        global $wpdb;

        // Hasher la clé pour comparaison
        $api_key_hash = hash( 'sha256', $api_key );

        $user_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT user_id FROM {$wpdb->usermeta}
                WHERE meta_key = 'mon_plugin_api_key_hash'
                AND meta_value = %s
                LIMIT 1",
                $api_key_hash
            )
        );

        if ( $user_id ) {
            // Logger l'utilisation de la clé
            $this->log_api_key_usage( $user_id, $api_key_hash );

            return (int) $user_id;
        }

        return 0;
    }

    /**
     * Logger l'utilisation de la clé API
     */
    private function log_api_key_usage( $user_id, $api_key_hash ) {
        global $wpdb;

        $wpdb->insert(
            $wpdb->prefix . 'mon_plugin_api_logs',
            array(
                'user_id'      => $user_id,
                'api_key_hash' => $api_key_hash,
                'endpoint'     => sanitize_text_field( $*SERVER['REQUEST_URI'] ),
                'ip_address'   => $this->get_client_ip(),
                'user_agent'   => sanitize_text_field( $*SERVER['HTTP_USER_AGENT'] ),
                'timestamp'    => current_time( 'mysql' ),
            ),
            array( '%d', '%s', '%s', '%s', '%s', '%s' )
        );
    }

    /**
     * Récupérer l'IP du client
     */
    private function get_client_ip() {
        $ip = '';

        if ( isset( $*SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
            // Cloudflare
            $ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_CF_CONNECTING_IP'] ) );
        } elseif ( isset( $*SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
            $ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_X_FORWARDED_FOR'] ) );
        } elseif ( isset( $*SERVER['REMOTE_ADDR'] ) ) {
            $ip = sanitize_text_field( wp_unslash( $*SERVER['REMOTE_ADDR'] ) );
        }

        return $ip;
    }

    /**
     * Gérer les erreurs d'authentification
     */
    public function rest_authentication_errors( $error ) {
        // Ne pas vérifier sur les routes publiques
        if ( ! empty( $error ) ) {
            return $error;
        }

        // Vérifier si l'authentification par API key est requise
        if ( ! is_user_logged_in() ) {
            $api_key = $this->get_api_key_from_request();

            if ( $api_key && ! $this->validate_api_key( $api_key ) ) {
                return new WP_Error(
                    'rest_api_key_invalid',
                    **( 'Clé API invalide.', 'mon-plugin' ),
                    array( 'status' => 401 )
                );
            }
        }

        return $error;
    }

    /**
     * Générer une nouvelle clé API pour un utilisateur
     */
    public static function generate_api_key( $user_id ) {
        // Générer une clé aléatoire sécurisée
        $api_key = bin2hex( random_bytes( 32 ) );

        // Hasher et stocker
        $api_key_hash = hash( 'sha256', $api_key );

        update_user_meta( $user_id, 'mon_plugin_api_key_hash', $api_key_hash );
        update_user_meta( $user_id, 'mon_plugin_api_key_created', current_time( 'mysql' ) );

        // Retourner la clé non hashée (à afficher une seule fois)
        return $api_key;
    }

    /**
     * Révoquer la clé API d'un utilisateur
     */
    public static function revoke_api_key( $user_id ) {
        delete_user_meta( $user_id, 'mon_plugin_api_key_hash' );
        delete_user_meta( $user_id, 'mon_plugin_api_key_created' );
    }
}

new APIKeyAuth();

Limitation du débit (Rate Limiting)

Implémentez un rate limiting pour prévenir les abus :

get_route();

        if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
            return $result;
        }

        $identifier = $this->get_identifier();
        $current_count = $this->get_request_count( $identifier );

        if ( $current_count >= $this->max_requests ) {
            return new WP_Error(
                'rest_rate_limit_exceeded',
                sprintf(
                    **( 'Limite de débit dépassée. Maximum %d requêtes par heure.', 'mon-plugin' ),
                    $this->max_requests
                ),
                array(
                    'status' => 429,
                    'headers' => array(
                        'X-RateLimit-Limit'     => $this->max_requests,
                        'X-RateLimit-Remaining' => 0,
                        'X-RateLimit-Reset'     => $this->get_reset_time( $identifier ),
                    ),
                )
            );
        }

        // Incrémenter le compteur
        $this->increment_request_count( $identifier );

        // Ajouter les en-têtes de rate limit
        add_filter( 'rest_post_dispatch', function( $response ) use ( $current_count ) {
            $response->header( 'X-RateLimit-Limit', $this->max_requests );
            $response->header( 'X-RateLimit-Remaining', max( 0, $this->max_requests - $current_count - 1 ) );

            return $response;
        } );

        return $result;
    }

    /**
     * Obtenir un identifiant unique (IP ou User ID)
     */
    private function get_identifier() {
        $user_id = get_current_user_id();

        if ( $user_id ) {
            return 'user*' . $user_id;
        }

        // Utiliser l'IP pour les utilisateurs non connectés
        $ip = $this->get_client_ip();

        return 'ip*' . md5( $ip );
    }

    /**
     * Obtenir le nombre de requêtes
     */
    private function get_request_count( $identifier ) {
        $transient_key = 'mon_plugin_rate_limit*' . $identifier;

        $count = get_transient( $transient_key );

        return $count ? (int) $count : 0;
    }

    /**
     * Incrémenter le compteur de requêtes
     */
    private function increment_request_count( $identifier ) {
        $transient_key = 'mon_plugin_rate_limit*' . $identifier;

        $count = $this->get_request_count( $identifier );

        set_transient( $transient_key, $count + 1, $this->time_window );
    }

    /**
     * Obtenir le timestamp de réinitialisation
     */
    private function get_reset_time( $identifier ) {
        $transient_key = 'mon_plugin_rate_limit*' . $identifier;

        $timeout = get_option( '*transient_timeout*' . $transient_key );

        return $timeout ? (int) $timeout : time() + $this->time_window;
    }

    /**
     * Récupérer l'IP du client
     */
    private function get_client_ip() {
        $ip = '';

        if ( isset( $*SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
            $ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_CF_CONNECTING_IP'] ) );
        } elseif ( isset( $*SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
            $ip = sanitize_text_field( wp_unslash( $*SERVER['HTTP_X_FORWARDED_FOR'] ) );
        } elseif ( isset( $*SERVER['REMOTE_ADDR'] ) ) {
            $ip = sanitize_text_field( wp_unslash( $*SERVER['REMOTE_ADDR'] ) );
        }

        return $ip;
    }
}

new RateLimiter();

Validation et sanitization avancées

Validateurs personnalisés

 400 )
            );
        }

        // Vérifier le domaine
        $allowed_domains = array( 'example.com', 'test.com' );
        $email_parts = explode( '@', $value );
        $domain = end( $email_parts );

        if ( ! in_array( $domain, $allowed_domains, true ) ) {
            return new WP_Error(
                'rest_invalid_email_domain',
                sprintf(
                    **( 'Le domaine email doit être l'un des suivants : %s', 'mon-plugin' ),
                    implode( ', ', $allowed_domains )
                ),
                array( 'status' => 400 )
            );
        }

        return true;
    }

    /**
     * Valider un numéro de téléphone
     */
    public static function validate_phone( $value, $request, $param ) {
        // Format : +33612345678 ou 0612345678
        $pattern = '/^(+33|0)1-9$/';

        if ( ! preg_match( $pattern, $value ) ) {
            return new WP_Error(
                'rest_invalid_phone',
                **( 'Le numéro de téléphone n'est pas valide.', 'mon-plugin' ),
                array( 'status' => 400 )
            );
        }

        return true;
    }

    /**
     * Valider une URL avec domaines autorisés
     */
    public static function validate_url_domain( $value, $request, $param ) {
        if ( ! filter_var( $value, FILTER_VALIDATE_URL ) ) {
            return new WP_Error(
                'rest_invalid_url',
                **( 'L'URL n'est pas valide.', 'mon-plugin' ),
                array( 'status' => 400 )
            );
        }

        $allowed_domains = array( 'example.com', 'cdn.example.com' );
        $parsed_url = wp_parse_url( $value );
        $host = $parsed_url['host'] ?? '';

        if ( ! in_array( $host, $allowed_domains, true ) ) {
            return new WP_Error(
                'rest_invalid_url_domain',
                **( 'Le domaine de l'URL n'est pas autorisé.', 'mon-plugin' ),
                array( 'status' => 400 )
            );
        }

        return true;
    }

    /**
     * Valider une date au format ISO 8601
     */
    public static function validate_iso8601_date( $value, $request, $param ) {
        $date = DateTime::createFromFormat( DateTime::ISO8601, $value );

        if ( ! $date || $date->format( DateTime::ISO8601 ) !== $value ) {
            return new WP_Error(
                'rest_invalid_date',
                **( 'La date doit être au format ISO 8601.', 'mon-plugin' ),
                array( 'status' => 400 )
            );
        }

        return true;
    }

    /**
     * Valider un JSON
     */
    public static function validate_json( $value, $request, $param ) {
        json_decode( $value );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            return new WP_Error(
                'rest_invalid_json',
                sprintf(
                    **( 'JSON invalide : %s', 'mon-plugin' ),
                    json_last_error_msg()
                ),
                array( 'status' => 400 )
            );
        }

        return true;
    }

    /**
     * Sanitizer pour JSON
     */
    public static function sanitize_json( $value ) {
        $decoded = json_decode( $value, true );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            return '';
        }

        // Re-encoder pour garantir un JSON propre
        return wp_json_encode( $decoded );
    }
}

Utilisation depuis JavaScript

Exemple avec Fetch API

/**
 * Client JavaScript pour l'API REST personnalisée
 */

class MonPluginAPIClient {
    constructor(baseUrl = '/wp-json/mon-plugin/v1') {
        this.baseUrl = baseUrl;
        this.nonce = wpApiSettings?.nonce || '';
    }

    /**
     * Effectuer une requête HTTP
     */
    async request(endpoint, options = {}) {
        const url = ${this.baseUrl}${endpoint};

        const defaultOptions = {
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': this.nonce,
            },
        };

        const mergedOptions = {
            ...defaultOptions,
            ...options,
            headers: {
                ...defaultOptions.headers,
                ...options.headers,
            },
        };

        try {
            const response = await fetch(url, mergedOptions);

            // Vérifier les en-têtes de rate limiting
            const rateLimit = {
                limit: response.headers.get('X-RateLimit-Limit'),
                remaining: response.headers.get('X-RateLimit-Remaining'),
                reset: response.headers.get('X-RateLimit-Reset'),
            };

            if (rateLimit.limit) {
                console.log('Rate Limit:', rateLimit);
            }

            const data = await response.json();

            if (!response.ok) {
                throw new Error(data.message || 'Erreur API');
            }

            return {
                success: true,
                data: data,
                status: response.status,
                headers: response.headers,
            };
        } catch (error) {
            console.error('Erreur API:', error);
            return {
                success: false,
                error: error.message,
            };
        }
    }

    /**
     * GET - Récupérer tous les items
     */
    async getItems(params = {}) {
        const queryString = new URLSearchParams(params).toString();
        const endpoint = queryString ? /items?${queryString} : '/items';

        return this.request(endpoint, {
            method: 'GET',
        });
    }

    /**
     * GET - Récupérer un item
     */
    async getItem(id) {
        return this.request(/items/${id}, {
            method: 'GET',
        });
    }

    /**
     * POST - Créer un item
     */
    async createItem(data) {
        return this.request('/items', {
            method: 'POST',
            body: JSON.stringify(data),
        });
    }

    /**
     * PUT - Mettre à jour un item
     */
    async updateItem(id, data) {
        return this.request(/items/${id}, {
            method: 'PUT',
            body: JSON.stringify(data),
        });
    }

    /**
     * DELETE - Supprimer un item
     */
    async deleteItem(id) {
        return this.request(/items/${id}, {
            method: 'DELETE',
        });
    }
}

// Utilisation
(async () => {
    const api = new MonPluginAPIClient();

    // Récupérer tous les items avec pagination
    const items = await api.getItems({ page: 1, per_page: 10, search: 'test' });
    console.log('Items:', items);

    // Créer un nouvel item
    const newItem = await api.createItem({
        title: 'Mon nouvel item',
        description: 'Description de l'item',
        status: 'published',
    });
    console.log('Item créé:', newItem);

    // Mettre à jour un item
    if (newItem.success) {
        const updated = await api.updateItem(newItem.data.id, {
            title: 'Titre modifié',
        });
        console.log('Item mis à jour:', updated);
    }

    // Supprimer un item
    if (newItem.success) {
        const deleted = await api.deleteItem(newItem.data.id);
        console.log('Item supprimé:', deleted);
    }
})();

Tests et debugging

Tester avec WP-CLI

# Lister toutes les routes
wp rest list

# Tester un endpoint GET
wp rest get /mon-plugin/v1/items

# Tester un endpoint POST
wp rest post /mon-plugin/v1/items --title="Test Item" --status="published"

# Tester avec un utilisateur spécifique
wp rest get /mon-plugin/v1/items --user=admin

Logger les requêtes pour debugging

get_route();

        if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
            return $result;
        }

        error_log( sprintf(
            '[API Request] %s %s | Params: %s',
            $request->get_method(),
            $route,
            wp_json_encode( $request->get_params() )
        ) );

        return $result;
    }

    /**
     * Logger la réponse
     */
    public function log_response( $response, $server, $request ) {
        if ( ! WP_DEBUG || ! WP_DEBUG_LOG ) {
            return $response;
        }

        $route = $request->get_route();

        if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
            return $response;
        }

        $status = $response->get_status();
        $data = $response->get_data();

        error_log( sprintf(
            '[API Response] %s %s | Status: %d | Data: %s',
            $request->get_method(),
            $route,
            $status,
            wp_json_encode( $data )
        ) );

        return $response;
    }
}

if ( WP_DEBUG ) {
    new APILogger();
}

Erreurs courantes à éviter

1. Ne pas valider les permissions

MAUVAIS :

'permission_callback' => '**return_true'

BON :

'permission_callback' => array( $this, 'check_permissions' )

2. Oublier la sanitization

MAUVAIS :

$title = $*POST['title'];

BON :

$title = sanitize_text_field( $request->get_param( 'title' ) );

3. Ne pas gérer les erreurs

MAUVAIS :

return $data;

BON :

if ( ! $data ) {
    return new WP_Error( 'rest_not_found', **( 'Données non trouvées.' ), array( 'status' => 404 ) );
}
return rest_ensure_response( $data );

Conclusion

La création d’endpoints REST API personnalisés en 2025 nécessite une attention particulière à la sécurité, avec l’implémentation systématique de l’authentification, de la validation des données, du rate limiting et du logging. En suivant ces pratiques, vous créerez des APIs robustes et sécurisées.

Points clés :

  • Toujours valider et sanitize les entrées
  • Implémenter une authentification appropriée (cookies, API keys, JWT)
  • Utiliser le rate limiting pour prévenir les abus
  • Retourner des erreurs WP_Error appropriées
  • Logger les requêtes pour le debugging
  • Documenter vos endpoints avec des schémas JSON
  • Tester avec WP-CLI et des outils comme Postman
  • Ressources supplémentaires

  • REST API Handbook
  • REST API Security Guide
  • Adding Custom Endpoints

  • Mots-clés:* WordPress REST API, endpoints personnalisés WordPress, API REST sécurité, authentification WordPress API, rate limiting WordPress, validation REST API, WordPress API 2025, WP REST API custom, WordPress API développement

    Une remarque, un retour ?

    Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.

    Laisser un commentaire