Исходные данные
Есть сайт на UMI.CMS. PHP шаблонизатор
Задача
Подключить Google reCAPTCHA для всех форм на сайте: для форм в контенте и модальных окнах. Форм на одной странице может быть несколько. Нужно защитить все.
Решение
Логика работы следующая:
- Загружается страница или модальное окно через AJAX;
- Для загруженной формы подключается reCAPTCHA;
- При попытке отправить форму происходит проверка на спамность;
- Если проверка пройдена – вызывается JS функция, которая AJAX-ом отправляет форму;
- Если пользователя заподозрили в спамности, ему предлагают пройти задание задание, например: найти что-то на картинке;
- Когда задание пройдено, вызывается JS функция отправки формы, указанная в качестве callback-функции.
Код приведен для отправки отзыва о товаре в модальном окне. Отзывы сохраняются в модуль Комментарии.
1. Добавляем новый сайт в консоль
Регистрируем сайт в консоле. Выбираем:
Тип reCAPTCHA:Версия 2 – невидимый значок
Этот тип рекапчи показывает задания только тогда, когда пользователя подозревают в спамности.
2. Подключаем скрипт reCAPTCHA на сайт
Добавляем в самом конце тега <body> (строго после других скриптов):
<script src="https://www.google.com/recaptcha/api.js?onload=recaptchaOnloadCallback&render=explicit&hl=ru" async defer></script>
3. Функция для добавления рекапчи в форму
Название функции – это параметр onload из пункта 2
sitekey – нужно подставить свой из пункта 1. Это: “КЛЮЧ САЙТА”
let _captchaTries = 0; // количество попыток добавить рекапчу function getTries() { _captchaTries++; return _captchaTries; } // функция добавляющая капчи в формы function recaptchaOnloadCallback(tries = getTries()) { if (tries > 3) return; // пробуем добавить 3 раза через 1 секунду (если форма сразу не прогрузились) let recaptchas = document.querySelectorAll('div[class=g-recaptcha]'); // все блоки рекапчи на странице let recaptchaId; for (let i = 0; i < recaptchas.length; i++) { // проходим все формы let element = recaptchas[i]; // текущий блок рекапчи let form = element.closest('form'); // текущая форма if (element.childNodes.length === 0) { // если блок рекапчи пустой recaptchaId = grecaptcha.render(element, { // добавляем рекапчу в форму и получаем ее ID sitekey: '6LdTunEUAAAAAKQxNE8m9VwPRpohPIDIDXyX9wBh', // публичный ключ size: 'invisible', // версия - невидимая badge: 'inline', // значок рекапчи в форме callback: function (token) { site.forms.checkRecaptcha(form); // функция которая вызовется при успешном выполнении задания }, }); element.dataset.widgetId = recaptchaId; // тегу рекапчи добавляем data атрибут с ID рекапчи } } window.setTimeout(recaptchaOnloadCallback, 1000); // пробуем еще раз через секунду } window.recaptchaOnloadCallback = recaptchaOnloadCallback;
4. Добавляем кнопку вызова формы
<button class="js_getajax_popup" data-module="comments" data-template="post" data-micromodal-trigger="add-review">Оставить отзыв</button>
5. Обработчик вызова формы
$(document).on('click', 'js_getajax_popup', function (e) { e.preventDefault(); const $this = $(this); const id = $this.data('micromodal-trigger'); // ID блока const $modal = $(`#${id}`); const module = $this.data('module'); // модуль const template = $this.data('template'); // шаблон const templateString = `${module}/${template}`; // путь к шаблону в папке директории ajax $modal.text(''); // очищаем блок на всякий случай $.ajax({ url: '/', dataType: 'html', data: { template: templateString, }, async: true, cache: true, type: 'GET', success(html) { // отправка удалась $modal.html(html); // пишем форму в контейнер _captchaTries = 0; // обнуляем переменную recaptchaOnloadCallback(); // подключаем рекапчу }, // конец успешной отправки error(xhr, ajaxOptions, thrownError) { console.log(`${thrownError}\r\n${xhr.statusText}\r\n${xhr.responseText}`); }, }); });
6. Обработчик отправки формы
$(document).on('submit', '.js_ajax_form', function (e) { e.preventDefault(); const $form = $(this); $form.find($('.errors')).html(''); // очищаем ошибки let recaptchaTag = $form.find($('.g-recaptcha')); // тег рекапчи let widgetId = recaptchaTag.data('widgetId'); // ID рекапчи grecaptcha.reset(widgetId); // обнуляем рекапчу if (recaptchaTag.is(':empty')) { // если капча не загружена console.log('Ошибка: капча не загружена'); return false; // прекращаем выполнение } // валидация формы let countInvalidElements = site.forms.validateForm($form); // количество невалидных полей if (countInvalidElements > 0) { // если есть невалидные - прекращаем let container = $('.modal__container'); // модальное окно let scrollTo = $form.find($('.is-invalid:first')); // первое невалидное поле // скролл к первому невалидному полю container.animate({scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop() - 70}); return false; // прекращаем выполнение } grecaptcha.execute(widgetId); // вызываем рекапчу. Если пользователь подозревается в спаме - появится проерка. // если проверка будет успешно пройдена или ее не будет - вызовется callback функция return false; });
7. Проверка рекапчи
Это callback функция из пункта 3
// функция проверяющая капчу checkRecaptcha: (form) => { const $form = $(form); // форма const recaptchaTag = $form.find($('.g-recaptcha')); // тег рекапчи const widgetId = recaptchaTag.data('widgetId'); // ID рекапчи const module = $form.data('module'); // модуль // отправка запроса на проверку рекапчи $.ajax({ url: '/', method: 'POST', dataType: 'json', data: { 'g-recaptcha-response': grecaptcha.getResponse(widgetId), 'template': 'get-recaptcha', // название шаблона в директории php/ajax/templates }, // отправляем только данные для капчи beforeSend: function (xhr) { // что делать перед отправкой // container.addClass('ajax-loader'); // Show our loader }, success: function (response) { // отправка успешна if (response.success === true) { // если капча не нужна if (module === 'comments') { // если форма отправки комментария site.forms.sendComments($form); // вызываем функцию отправки } } else { // если нужна Капча grecaptcha.reset(widgetId); // резетим капчю grecaptcha.execute(widgetId); // вызываем капчу $form.find($('.errors')).html('Ошибка при проверке на спам. Попробуйте отправить еще раз. Если не получится - перезагрузите страницу.'); // пишем ошибку } }, }); return false; }
8. PHP функция проверки рекапчи
В файл \templates\xxxxx\php\ajax\templates\get-recaptcha.phtml добавляем (заменить RECAPTCHA_SECRET_KEY на “СЕКРЕТНЫЙ КЛЮЧ” из пункта 1):
<?php /** @var umiTemplaterPHP|ViewPhpExtension|UpmixExtension $this */?> <?php /** @var array $variables */?> <?php use UmiCms\Service; define('RECAPTCHA_SECRET_KEY', '6LdTunEUAAAAAEjOIRevQSSyaHaqFYCM_BvEC5Vq'); // json response helper $json_response = function($data = []) { $buffer = Service::Response() ->getCurrentBuffer(); /* @var HTTPOutputBuffer $buffer*/ $buffer->contentType('application/x-www-form-urlencoded; charset=utf-8'); $buffer->option('generation-time', false); $buffer->push(json_encode($data, JSON_THROW_ON_ERROR)); $buffer->send(); exit; }; // handle post if ($_SERVER['REQUEST_METHOD'] === 'POST') { // define errors array $errors = []; // check g-recaptcha-response if (!isset($_POST['g-recaptcha-response'])) { $errors['recaptcha'] = 'Check the captcha form'; } // if all good call API else error out if (!empty($errors)) { $json_response(['errors' => $errors]); } // call recaptcha site verify $response = file_get_contents( 'https://www.google.com/recaptcha/api/siteverify?'.http_build_query([ 'secret' => RECAPTCHA_SECRET_KEY, 'response' => $_POST['g-recaptcha-response'], 'remoteip' => $_SERVER['REMOTE_ADDR'], ]) ); $response = json_decode($response, true, 512, JSON_THROW_ON_ERROR); // handle status and respond with json if ((int)$response['success'] !== 1) { $json_response(['errors' => ['recaptcha' => 'Captcha failed']]); } else { $json_response(['success' => true]); } }
9. Отправка формы, когда рекапча пройдена
// функция отправляющая ajax форму комментариев sendComments($form) { let formData = new FormData(); // данные формы $.each($('input[type=file]')[0].files, function(key, input) { formData.append('files[]', input); // добавляем данные из полей типа "Файл" }); let dataArray = $form.serializeArray(); // остальные поля for (let i = 0; i < dataArray.length; i++) { formData.append(dataArray[i].name, dataArray[i].value); // итоговый массив данных } formData.append('template', 'comments/postReview'); // шаблон формы formData.append('pageId', window.pageData.pageId); // ID текущей страницы из массива в <head> $.ajax({ url: '/', type: 'POST', data: formData, dataType: 'text', processData: false, contentType: false, cache: false, success: function (data) { // успешная отправка $form.find(':input').attr('disabled', false); $form.trigger('reset'); // сброс формы $('#overlay').fadeOut(); // убираем лоадер if (data === 'success') { $form.html('Отзыв отправлен.<br/>После проверки модератором он будет размещен на сайте'); let micromodalId = $form.data('micromodal-trigger'); setTimeout(function () { MicroModal.close(micromodalId); // закрываем модальное окно }, 3000); } else { $form.find($('.errors')).html('Произошла ошибка.<br/> Попробуйте еще раз. <br/> Если не получится - обновите страницу. <br/>Или свяжитесь с нами'); } }, beforeSend: function () { // перед отправкой $('#overlay').fadeIn(); // показываем прелоадер $form.find(':input').attr('disabled', true); // отключаем все поля }, }); return false; },
10. PHP шаблон отправки AJAX формы
<?php /** @var umiTemplaterPHP|ViewPhpExtension|UpmixExtension $this */?> <?php /** @var array $variables */?> <?php $pageId = getRequest('pageId'); $html = $this->macros('comments', 'postReview', array($pageId)); $this->sendAjaxResponse($html);
11. Опционные функции для валидации полей формы
/** * валидация формы * // @returns {} */ validateForm(form) { let count = 0; // количество невалидных полей form.find('input').filter('[required]').each((index, element) => { // поля с атрибутов required let $element = $(element); let phoneValue; let emailValue; $element.removeClass('is-invalid'); // убираем сообщения об ошибках switch ($element.attr('type')) { // для различных типов полей case 'text': // поля для ввода email type text if ($element.attr('name') === 'author_email') { emailValue = $element.val(); if (site.forms.validateEmail(emailValue) === false) { $element.addClass('is-invalid'); count++; } } else if ($element.val().length < 2) { // если введено меньше 2-х символов $element.addClass('is-invalid'); count++; } break; case 'tel': phoneValue = $element.inputmask('unmaskedvalue'); // телефон без маски if (typeof phoneValue === 'undefined' || phoneValue.length < 11) { $element.addClass('is-invalid'); count++; } break; case 'email': // поля для ввода email emailValue = $element.val(); if (site.forms.validateEmail(emailValue) === false) { $element.addClass('is-invalid'); count++; } break; case 'checkbox': // checkbox - согласие if ($element.prop('checked') === false) { $element.addClass('is-invalid'); count++; } break; case 'password': // пароль if ($element.val().length < 6) { // если меньше 6 символов $element.addClass('is-invalid'); count++; } break; default: break; } }); form.find('textarea').filter('[required]').each((index, element) => { const $element = $(element); if ($element.val().length < 2) { // если введено меньше 2-х символов $element.addClass('is-invalid'); count++; } }); form.find('select').filter('[required]').each((index, element) => { const $element = $(element); if (!$element.val().length) { // если введено меньше 2-х символов $element.addClass('is-invalid'); count++; } }); site.forms.registerOnFocusInputCallback(); // убираем ошибку при фокусе return count; }, /** * валидация email c поддержкой unicode * @returns {boolean} */ validateEmail(email) { const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i; return re.test(String(email).toLowerCase()); }, registerOnFocusInputCallback: () => { // при фокусе на поле убираем невалидность $('input.is-invalid').on('focus', function () { $(this).removeClass('is-invalid'); }); }, /** Добавляет маску формата телефонного номера в поля с номером телефона */ initInputmask: () => { $('input[type="tel"]').inputmask('+9-999-999-99-99'); },
Добавить комментарий