Исходные данные
Есть сайт на 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');
},

Добавить комментарий