Защита ajax-формы WordPress от СПАМа

  1. Главная
  2. Блог
  3. WordPress
  4. Защита ajax-формы WordPress от СПАМа

Вступление

Эта статья — продолжение статьи «Как создать ajax-форму в WordPress без плагинов» — там мы научились создавать простую форму ajax для WordPress, но не научились защищать ее от СПАМ-ботов.

Защита от СПАМа важна, иначе вы рискуете в один момент обнаружить полнейший завал почты бесполезными письмами, который будет трудно разгрести. Благо, есть эффективные и доступные всем способы. Рассмотрим вариант с Google reCaptcha, и вариант с Yandex Smart Captcha.

Yandex Smart Captcha

Для начала необходимо создать саму капчу. О том, как это сделать я написал в этой статье, в разделе «Регистрация и подготовка».

Далее я предполагаю, что у вас уже есть ключ клиента и ключ сервера.

Далее нам нужно подключить JavaScript яндекс капчи. Добавим в functions.php следующий код:

<?php

//Добавим тег defer для подключаемых JS-скриптов - без этого не будет работать
function add_defer_tag_script( $tag, $handle, $source ) {
    $path = parse_url( $_SERVER['REQUEST_URI'] );
    if ( strripos( $path['path'], 'wp-admin/' ) ) {
        return $tag;
    }

    return str_replace( "\">", "\" defer>", $tag );
}
add_filter( 'script_loader_tag', 'add_defer_tag_script', 1, 3 );

//Подключим JS-скрипт яндекс капчи
function feodoraxis_enqueue_scripts() {
    wp_enqueue_script( 
        'ya-smart-captcha', 
        'https://smartcaptcha.yandexcloud.net/captcha.js', 
        [], 
        '1.0' 
    );
}
add_action( 'wp_enqueue_scripts', 'feodoraxis_enqueue_scripts', 100 );

Далее в нашу форму нужно добавить такой код:

<div id="captcha-container" class="smart-captcha" data-sitekey="<ключ_клиента>" ></div>

Должно получиться так:

<?php

//Зафиксируем публичный ключ в константе
define( "CLIENT_KEY", "ysc1_YIrsc28ZIwCNtdzCF6WbOax3XTMtHl1S8MZsbrXf30e89098" );

function feodoraxis_recall_form() {
    return '<form method="POST" data-admin-ajax="' . admin_url( "admin-ajax.php" ) . '" action="' . $_SERVER['REQUEST_URI'] . '" id="recall_form">
        <input type="text" name="your-name" value="">
        <input type="email" name="your-email" value="">
        <button>Отправить</button>
        <div id="captcha-container" class="smart-captcha" data-sitekey="' . CLIENT_KEY . '" ></div>
    </form>';
}
add_shortcode( 'feodoraxis_recall_form', 'feodoraxis_recall_form' );

Если вы всё сделали правильно, то капча должна отобразиться под кнопкой «Отправить».

Но пока капча не работает полноценно, и форма отправляет любые запросы — независимо от того, прошел юзер капчу или нет.

Чтобы капча заработала полноценно, необходимо добавить функцию проверки входящего токена капчи, а также модифицировать функцию отправки сообщения feodoraxis_recall_form(). В этом листинге я оставлю комментарии только в тех местах, которые мы изменили:

<?php
//Добавим секретный ключ
define( "SERVER_KEY", "ysc2_YIrsc28ZIwCNtdzCF6WbijyIbqngEOt0gUnavHEYfdcdc897" );

//Опишем функцию проверки токена
function token_validate( string $token ):bool {
    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL => 'https://smartcaptcha.yandexcloud.net/validate?secret=' . SERVER_KEY . '&ip=' . $_SERVER['REMOTE_ADDR'] . '&token=' . $token,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => 'GET',
    ] );
    $json_validation = curl_exec( $curl );
    curl_close( $curl );
    $response = json_decode( $json_validation, true );

    if ( json_last_error() || $response['status'] !== 'ok' ) {
        return false;
    }

    return true;
}

//Модифицируем эту функцию
function feodoraxis_recall_form_request() {
    if ( $_SERVER['REQUEST_METHOD'] != 'POST' || ! wp_doing_ajax() )
        wp_send_json_error();
    }

    //Если токен капчи не передан, то сообщение не отправляем
    if ( ! isset( $_POST['smart-token'] ) || empty( trim( $_POST['smart-token'] ) ) ) {
        wp_send_json_error( [
            'error' => "Необходимо пройти капчу"
        ] );
    }

    //Если токен есть - то проведем его валидацию
    $smart_token = $_POST['smart-token'];
    if ( ! token_validate( $smart_token ) ) {
        wp_send_json_error( [
            'error' => "Ошибка: необходимо пройти капчу"
        ] );
    }

    if ( ! isset( $_POST['your-name'] ) || ! isset( $_POST['your-email'] ) ||  empty( trim( $_POST['your-name'] ) ) || empty( trim( $_POST['your-email'] ) ) ) {
        wp_send_json_error( [
            'error' => "Необходимо заполнить все поля формы"
        ] );
    }

    if ( ! is_email( $_POST['your-email'] ) ) {
        wp_send_json_error( [
            'error' => "Указан некорректный email"
        ] );
    }

    $your_name = sanitize_text_field( $_POST['your-name'] );
    $your_email = sanitize_text_field( $_POST['your-email'] );

    $headers = "MIME-Version: 1.0\r\n";
    $headers .= "Content-type: text/html; charset=utf-8\r\n";
    $headers .= "From: Андрей Смородин <no-reply@" . $_SERVER["HTTP_HOST"] . ">\r\n";

    $message = '<b>Имя:</b> ' . $your_name . '<br>';
    $message .= '<b>Email:</b> ' . $your_email . '<br>';

    $result = wp_mail( 'info@feodoraxis.ru', 'Заявка из формы обратной связи', $message, $headers )

    if ( $result ) {
        wp_send_json_success( [
            'message' => "Сообщение отправлено. Мы свяжемся с вами в ближайшее время."
        ] );
    }

    wp_send_json_error( [
        'error' => "Письмо не было отправлено"
    ] );
}
add_action( 'wp_ajax_' . 'recall_form', 'feodoraxis_recall_form_request' );
add_action( 'wp_ajax_nopriv_' . 'recall_form', 'feodoraxis_recall_form_request' );

Отлично! Если вы все сделали правильно, то теперь капча будет работать и защищать вашу форму от спама!

Еще у яндекс капчи есть невидимая версия. Но у меня так и не вышло ее внедрить.

Листинг кода для Yandex Smart Captcha

<?php

//Зафиксируем публичный ключ в константе
const CLIENT_KEY = "ysc1_YIrsc28ZIwCNtdzCF6WbOax3XTMtHl1S8MZsbrXf30e89098";
//Добавим секретный ключ
const SERVER_KEY = "ysc2_YIrsc28ZIwCNtdzCF6WbijyIbqngEOt0gUnavHEYfdcdc897";

//Добавим тег defer для подключаемых JS-скриптов - без этого не будет работать
function add_defer_tag_script( $tag, $handle, $source ) {
    $path = parse_url( $_SERVER['REQUEST_URI'] );
    if ( strripos( $path['path'], 'wp-admin/' ) ) {
        return $tag;
    }

    return str_replace( "\">", "\" defer>", $tag );
}
add_filter( 'script_loader_tag', 'add_defer_tag_script', 1, 3 );

//Подключим JS-скрипт яндекс капчи
function feodoraxis_enqueue_scripts() {
    wp_enqueue_script( 
        'ya-smart-captcha', 
        'https://smartcaptcha.yandexcloud.net/captcha.js', 
        [], 
        '1.0' 
    );
}
add_action( 'wp_enqueue_scripts', 'feodoraxis_enqueue_scripts', 100 );

function feodoraxis_recall_form() {
    return '<form method="POST" data-admin-ajax="' . admin_url( "admin-ajax.php" ) . '" action="' . $_SERVER['REQUEST_URI'] . '" id="recall_form">
        <input type="text" name="your-name" value="">
        <input type="email" name="your-email" value="">
        <button>Отправить</button>
        <div id="captcha-container" class="smart-captcha" data-sitekey="' . CLIENT_KEY . '" ></div>
    </form>';
}
add_shortcode( 'feodoraxis_recall_form', 'feodoraxis_recall_form' );

function feodoraxis_show_js_for_form() {
    ?>
    <script>
        
        document.addEventListener("DOMContentLoaded", () => { //Исполняем скрипт только когда загрузится страница, заодно оборачиваем скрипт в свое пространство
            const recallForm = document.querySelector('#recall_form');

            //Если формы нет - то дальше ничего не делаем
            if (!recallForm) {
                return;
            }

            recallForm.addEventListener('submit', (event) => {

                //Отменим стандартное поведение формы
                event.preventDefault();

                // Получим данные формы
                const formData = new FormData(recallForm);

                //Обязательно добавляем значение action, которое мы будем использовать для обработки запроса на стороне сервера
                formData.append('action', 'recall_form');

                //Создадим объект запроса
                const request = new XMLHttpRequest();

                //Откроем запрос, определим метод и адрес куда его отправить, из атрибута data-admin-ajax формы
                request.open("POST", recallForm.getAttribute("data-admin-ajax"));
            
                //Отпрвим запрос
                request.send(formData);

                //При получении ответа от сервера обработаем его
                request.onload = () => {

                    //Получим ответ в виде JSON и преобразуем его в объект для удобства
                    const resultJson = request.responseText;
                    const result = JSON.parse(resultJson);

                    //При успешной обработке запроса выведем сообщение сервера
                    if (result.success === true) {
                        alert(result.data.message);
                    } else { //В случае неудачи выведем информацию об ошибке
                        alert(result.data.error);
                    }
                    
                };
            });

        });
    </script>
    <?php
}

add_action( "wp_footer", "feodoraxis_show_js_for_form", 1000 );

//Опишем функцию проверки токена
function token_validate( string $token ):bool {
    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL => 'https://smartcaptcha.yandexcloud.net/validate?secret=' . SERVER_KEY . '&ip=' . $_SERVER['REMOTE_ADDR'] . '&token=' . $token,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => 'GET',
    ] );
    $json_validation = curl_exec( $curl );
    curl_close( $curl );
    $response = json_decode( $json_validation, true );

    if ( json_last_error() || $response['status'] !== 'ok' ) {
        return false;
    }

    return true;
}

//Модифицируем эту функцию
function feodoraxis_recall_form_request() {
    if ( $_SERVER['REQUEST_METHOD'] != 'POST' || ! wp_doing_ajax() )
        wp_send_json_error();
    }

    //Если токен капчи не передан, то сообщение не отправляем
    if ( ! isset( $_POST['smart-token'] ) || empty( trim( $_POST['smart-token'] ) ) ) {
        wp_send_json_error( [
            'error' => "Необходимо пройти капчу"
        ] );
    }

    //Если токен есть - то проведем его валидацию
    $smart_token = $_POST['smart-token'];
    if ( ! token_validate( $smart_token ) ) {
        wp_send_json_error( [
            'error' => "Ошибка: необходимо пройти капчу"
        ] );
    }

    if ( ! isset( $_POST['your-name'] ) || ! isset( $_POST['your-email'] ) ||  empty( trim( $_POST['your-name'] ) ) || empty( trim( $_POST['your-email'] ) ) ) {
        wp_send_json_error( [
            'error' => "Необходимо заполнить все поля формы"
        ] );
    }

    if ( ! is_email( $_POST['your-email'] ) ) {
        wp_send_json_error( [
            'error' => "Указан некорректный email"
        ] );
    }

    $your_name = sanitize_text_field( $_POST['your-name'] );
    $your_email = sanitize_text_field( $_POST['your-email'] );

    $headers = "MIME-Version: 1.0\r\n";
    $headers .= "Content-type: text/html; charset=utf-8\r\n";
    $headers .= "From: Андрей Смородин <no-reply@" . $_SERVER["HTTP_HOST"] . ">\r\n";

    $message = '<b>Имя:</b> ' . $your_name . '<br>';
    $message .= '<b>Email:</b> ' . $your_email . '<br>';

    $result = wp_mail( 'info@feodoraxis.ru', 'Заявка из формы обратной связи', $message, $headers )

    if ( $result ) {
        wp_send_json_success( [
            'message' => "Сообщение отправлено. Мы свяжемся с вами в ближайшее время."
        ] );
    }

    wp_send_json_error( [
        'error' => "Письмо не было отправлено"
    ] );
}
add_action( 'wp_ajax_' . 'recall_form', 'feodoraxis_recall_form_request' );
add_action( 'wp_ajax_nopriv_' . 'recall_form', 'feodoraxis_recall_form_request' );

Google reCaptcha

Google reCaptcha — аналогичный сервис как вариант от яндекса. Рассмотрим его интеграцию, хотя в целом процесс похож на интеграцию капчи яндекса. Но здесь мы будем интегрировать невидимую версию — т.е. reCaptcha v3.

Сначала нужно получить публичный и серверный ключ. Для этого нужно авторизоваться в гугле, а потом перейти Админ-панель Google reCaptcha.

Дальше выбираем reCAPTCHA V3 и указываем домен

После регистрации вы получите ключ сайта и секретный ключ для сайта. Скопируйте и сохраните их.

Теперь подключим JavaScript reCaptcha:

<?php

const SITEKEY = '6KepjAsTFFFFRvMEDcY0Z3fqc3TEd3YVxo8cHsGX';

//Подключим JS-скрипт
add_action( 'wp_enqueue_scripts', 'feodoraxis_enqueue_scripts', 100 );
function feodoraxis_enqueue_scripts() {
    wp_enqueue_script( 
        'recaptcha', 
        'https://www.google.com/recaptcha/api.js?render=' . SITE_KEY, 
        [], 
        '1.0' 
    );
}

Теперь нам нужно подключить PHP-библиотеку капчи гугл из этого репозитория — https://github.com/google/recaptcha

Для этого в активной теме или плагине подключаем ее через composer:

composer require google/recaptcha "^1.3"

О том, как пользоваться composer можно прочитать в этой статье

Далее нужно подключить автолоадер Composer. Если он у вас подключен, то этот пункт можно пропустить. Вариант подключения для темы:

<?php
add_action( 'after_setup_theme', 'feodoraxis_load' );
function feodoraxis_load() {
    if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
        require_once( __DIR__ . '/vendor/autoload.php' );
    }
}

Вариант подключения для плагина:

<?php
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) {
    require_once( __DIR__ . '/vendor/autoload.php' );
}

Доработаем скрипт отправки:

<?php
function feodoraxis_show_js_for_form() {
    ?>
    <script>
        
        document.addEventListener("DOMContentLoaded", () => {
            const recallForm = document.querySelector('#recall_form');
            if (!recallForm) {
                return;
            }

            recallForm.addEventListener('submit', (event) => {
                event.preventDefault();

                //Отправляем запрос только когда капча готова
                grecaptcha.ready(function() {

                    //Получаем токен
                    grecaptcha.execute('<?php echo SITE_KEY; ?>', {action: 'submit'}).then(function(token) {

                        //Если токен пустой - значит пользователь не прошел тест
                        if (!token.length) {
                            alert("Вы не прошли капчу, форма не отправлена");
                            return;
                        }

                        const formData = new FormData(recallForm);
                        formData.append('action', 'recall_form');

                        //Добавим полученную строку капчи в форму, которую отправляем
                        formData.append('g-recaptcha-response', token);

                        const request = new XMLHttpRequest();
                        request.open("POST", recallForm.getAttribute("data-admin-ajax"));
                        request.send(formData);
                        request.onload = () => {
                            const resultJson = request.responseText;
                            const result = JSON.parse(resultJson);

                            if (result.success === true) {
                                alert(result.data.message);
                            } else {
                                alert(result.data.error);
                            }
                            
                        };
                    });
                });
            });

        });
    </script>
    <?php
}

add_action( "wp_footer", "feodoraxis_show_js_for_form", 1000 );

Доработаем обработку запроса на бекенде.

<?php
const SECRET_KEY = '6Le_LvwqAAAAAJLdFdNjaDeXkRYa9FHMsfQl0nWb';

add_action( 'wp_ajax_' . 'recall_form', 'feodoraxis_recall_form_request' );
add_action( 'wp_ajax_nopriv_' . 'recall_form', 'feodoraxis_recall_form_request' );
function feodoraxis_recall_form_request() {
    if ( $_SERVER['REQUEST_METHOD'] != 'POST' || ! wp_doing_ajax() ) {
        wp_send_json_error();
    }

    //Если нет токена капчи, то сообщение не отправляем
    if ( ! isset( $_POST['g-recaptcha-response'] ) ) {
    	wp_send_json_error( [
            'error' => "Вы не прошли капчу"
        ] );
    }

    //Если токен есть - то проверяем его
    $recaptcha = new \ReCaptcha\ReCaptcha( SECRET_KEY );
	$result = $recaptcha->verify( $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'] );
	if ( !$result->isSuccess() ) {
		wp_send_json_error( [
            'error' => "Вы не прошли капчу"
        ] );
	} 

    if ( ! isset( $_POST['your-name'] ) || ! isset( $_POST['your-email'] ) ||  empty( trim( $_POST['your-name'] ) ) || empty( trim( $_POST['your-email'] ) ) ) {
        wp_send_json_error( [
            'error' => "Необходимо заполнить все поля формы"
        ] );
    }

    if ( ! is_email( $_POST['your-email'] ) ) {
        wp_send_json_error( [
            'error' => "Указан некорректный email"
        ] );
    }

    $your_name = sanitize_text_field( $_POST['your-name'] );
    $your_email = sanitize_text_field( $_POST['your-email'] );

    $headers = "MIME-Version: 1.0\r\n";
    $headers .= "Content-type: text/html; charset=utf-8\r\n";
    $headers .= "From: Андрей Смородин <no-reply@" . $_SERVER["HTTP_HOST"] . ">\r\n";

    $message = '<b>Имя:</b> ' . $your_name . '<br>';
    $message .= '<b>Email:</b> ' . $your_email . '<br>';

    $result = wp_mail( 'info@feodoraxis.ru', 'Заявка из формы обратной связи', $message, $headers );

    if ( $result ) {
        wp_send_json_success( [
            'message' => "Сообщение отправлено. Мы свяжемся с вами в ближайшее время."
        ] );
    }

    wp_send_json_error( [
        'error' => "Письмо не было отправлено"
    ] );
}

Готово! Если вы все сделали правильно — то форма должна работать. По моему личному опыту, капча гугла версии 3 работает очень точно. Хотя в идеале ее лучше совмещать со второй версией, т.к. ошибки все-таки случаются и нужно давать пользователю возможность доказать, что он реальный человек.

Листинг кода для reCaptcha

<?php
add_action( 'after_setup_theme', 'feodoraxis_load' );
function feodoraxis_load() {
    if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
        require_once( __DIR__ . '/vendor/autoload.php' );
    }
}

const SITE_KEY = '6Le_LvwqAAAAABqqkpyOqZuuDL1JmeFygdPlc_VA';
const SECRET_KEY = '6Le_LvwqAAAAAJLdFdNjaDeXkRYa9FHMsfQl0nWb';

//Подключим JS-скрипт
function feodoraxis_enqueue_scripts() {
    wp_enqueue_script( 
        'recaptcha', 
        'https://www.google.com/recaptcha/api.js?render=' . SITE_KEY, 
        [], 
        '1.0' 
    );
}
add_action( 'wp_enqueue_scripts', 'feodoraxis_enqueue_scripts', 100 );

function feodoraxis_recall_form() {
    return '<form method="POST" data-admin-ajax="' . admin_url( "admin-ajax.php" ) . '" action="' . $_SERVER['REQUEST_URI'] . '" id="recall_form">
        <input type="text" name="your-name" value="">
        <input type="email" name="your-email" value="">
        <button>Отправить</button>
    </form>';
}
add_shortcode( 'feodoraxis_recall_form', 'feodoraxis_recall_form' );

function feodoraxis_show_js_for_form() {
    ?>
    <script>
        
        document.addEventListener("DOMContentLoaded", () => {
            const recallForm = document.querySelector('#recall_form');
            if (!recallForm) {
                return;
            }

            recallForm.addEventListener('submit', (event) => {
                event.preventDefault();

                //Отправляем запрос только когда капча готова
                grecaptcha.ready(function() {

                    //Получаем токен
                    grecaptcha.execute('<?php echo SITE_KEY; ?>', {action: 'submit'}).then(function(token) {

                        //Если токен пустой - значит пользователь не прошел тест
                        if (!token.length) {
                            alert("Вы не прошли капчу, форма не отправлена");
                            return;
                        }

                        const formData = new FormData(recallForm);
                        formData.append('action', 'recall_form');

                        //Добавим полученную строку капчи в форму, которую отправляем
                        formData.append('g-recaptcha-response', token);

                        const request = new XMLHttpRequest();
                        request.open("POST", recallForm.getAttribute("data-admin-ajax"));
                        request.send(formData);
                        request.onload = () => {
                            const resultJson = request.responseText;
                            const result = JSON.parse(resultJson);

                            if (result.success === true) {
                                alert(result.data.message);
                            } else {
                                alert(result.data.error);
                            }
                            
                        };
                    });
                });
            });

        });
    </script>
    <?php
}

add_action( "wp_footer", "feodoraxis_show_js_for_form", 1000 );

function feodoraxis_recall_form_request() {
    if ( $_SERVER['REQUEST_METHOD'] != 'POST' || ! wp_doing_ajax() ) {
        wp_send_json_error();
    }

    //Если нет токена капчи, то сообщение не отправляем
    if ( ! isset( $_POST['g-recaptcha-response'] ) ) {
    	wp_send_json_error( [
            'error' => "Вы не прошли капчу"
        ] );
    }

    //Если токен есть - то проверяем его
    $recaptcha = new \ReCaptcha\ReCaptcha( SECRET_KEY );
	$result = $recaptcha->verify( $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'] );
	if ( ! $result->isSuccess() ) {
		wp_send_json_error( [
            'error' => "Вы не прошли капчу"
        ] );
	} 

    if ( ! isset( $_POST['your-name'] ) || ! isset( $_POST['your-email'] ) ||  empty( trim( $_POST['your-name'] ) ) || empty( trim( $_POST['your-email'] ) ) ) {
        wp_send_json_error( [
            'error' => "Необходимо заполнить все поля формы"
        ] );
    }

    if ( ! is_email( $_POST['your-email'] ) ) {
        wp_send_json_error( [
            'error' => "Указан некорректный email"
        ] );
    }

    $your_name = sanitize_text_field( $_POST['your-name'] );
    $your_email = sanitize_text_field( $_POST['your-email'] );

    $headers = "MIME-Version: 1.0\r\n";
    $headers .= "Content-type: text/html; charset=utf-8\r\n";
    $headers .= "From: Андрей Смородин <no-reply@" . $_SERVER["HTTP_HOST"] . ">\r\n";

    $message = '<b>Имя:</b> ' . $your_name . '<br>';
    $message .= '<b>Email:</b> ' . $your_email . '<br>';

    $result = wp_mail( 'info@feodoraxis.ru', 'Заявка из формы обратной связи', $message, $headers );

    if ( $result ) {
        wp_send_json_success( [
            'message' => "Сообщение отправлено. Мы свяжемся с вами в ближайшее время."
        ] );
    }

    wp_send_json_error( [
        'error' => "Письмо не было отправлено"
    ] );
}
add_action( 'wp_ajax_' . 'recall_form', 'feodoraxis_recall_form_request' );
add_action( 'wp_ajax_nopriv_' . 'recall_form', 'feodoraxis_recall_form_request' );