Для защиты формы от спама и ботов при отправке через AJAX на JavaScript и PHP, можно использовать несколько методов. Вот пример реализации:
Использование капчи — это один из самых надежных способов защиты от ботов.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Форма с защитой от ботов</title> <script src="https://www.google.com/recaptcha/api.js" async defer></script> </head> <body> <form id="myForm"> <input type="text" name="name" placeholder="Ваше имя" required> <input type="email" name="email" placeholder="Ваш email" required> <div class="g-recaptcha" data-sitekey="ВАШ_SITE_KEY"></div> <button type="submit">Отправить</button> </form> <script> document.getElementById('myForm').addEventListener('submit', function(event) { event.preventDefault(); const formData = new FormData(this); const captchaResponse = grecaptcha.getResponse(); if (!captchaResponse) { alert('Пожалуйста, подтвердите, что вы не робот.'); return; } formData.append('g-recaptcha-response', captchaResponse); fetch('submit.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { alert('Форма успешно отправлена!'); } else { alert('Ошибка: ' + data.message); } }) .catch(error => { console.error('Ошибка:', error); }); }); </script> </body> </html>
<?php
$secretKey = "ВАШ_SECRET_KEY";
$captcha = $_POST['g-recaptcha-response'];
if (!$captcha) {
echo json_encode(['success' => false, 'message' => 'Капча не была отправлена.']);
exit;
}
$response = file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=$secretKey&response=$captcha");
$responseKeys = json_decode($response, true);
if (intval($responseKeys["success"]) !== 1) {
echo json_encode(['success' => false, 'message' => 'Ошибка проверки капчи.']);
exit;
}
// Обработка данных формы
$name = $_POST['name'];
$email = $_POST['email'];
// Здесь можно добавить код для сохранения данных в базу данных или отправки email
echo json_encode(['success' => true, 'message' => 'Данные успешно обработаны.']);
?>Добавьте скрытое поле в форму, которое будет заполняться только ботами. Если это поле заполнено, форма не будет обработана.
<form id="myForm"> <input type="text" name="name" placeholder="Ваше имя" required> <input type="email" name="email" placeholder="Ваш email" required> <input type="text" name="honeypot" style="display:none;"> <button type="submit">Отправить</button> </form> <script> document.getElementById('myForm').addEventListener('submit', function(event) { event.preventDefault(); const formData = new FormData(this); if (formData.get('honeypot')) { alert('Обнаружен бот!'); return; } fetch('submit.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { alert('Форма успешно отправлена!'); } else { alert('Ошибка: ' + data.message); } }) .catch(error => { console.error('Ошибка:', error); }); }); </script>
<?php
if (!empty($_POST['honeypot'])) {
echo json_encode(['success' => false, 'message' => 'Обнаружен бот!']);
exit;
}
// Обработка данных формы
$name = $_POST['name'];
$email = $_POST['email'];
// Здесь можно добавить код для сохранения данных в базу данных или отправки email
echo json_encode(['success' => true, 'message' => 'Данные успешно обработаны.']);
?>Использование CSRF-токена для защиты от межсайтовой подделки запросов.
<form id="myForm"> <input type="text" name="name" placeholder="Ваше имя" required> <input type="email" name="email" placeholder="Ваш email" required> <input type="hidden" name="csrf_token" value="СГЕНЕРИРОВАННЫЙ_ТОКЕН"> <button type="submit">Отправить</button> </form> <script> document.getElementById('myForm').addEventListener('submit', function(event) { event.preventDefault(); const formData = new FormData(this); fetch('submit.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { alert('Форма успешно отправлена!'); } else { alert('Ошибка: ' + data.message); } }) .catch(error => { console.error('Ошибка:', error); }); }); </script>
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
echo json_encode(['success' => false, 'message' => 'Неверный CSRF-токен.']);
exit;
}
// Обработка данных формы
$name = $_POST['name'];
$email = $_POST['email'];
// Здесь можно добавить код для сохранения данных в базу данных или отправки email
echo json_encode(['success' => true, 'message' => 'Данные успешно обработаны.']);
} else {
// Генерация CSRF-токена
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>Можно ограничить количество отправок формы с одного IP-адреса за определенный промежуток времени.
<?php
session_start();
$ip = $_SERVER['REMOTE_ADDR'];
$currentTime = time();
$timeLimit = 60; // 1 минута
$maxRequests = 3;
if (!isset($_SESSION['requests'])) {
$_SESSION['requests'] = [];
}
$_SESSION['requests'] = array_filter($_SESSION['requests'], function($time) use ($currentTime, $timeLimit) {
return $time > $currentTime - $timeLimit;
});
if (count($_SESSION['requests']) >= $maxRequests) {
echo json_encode(['success' => false, 'message' => 'Слишком много запросов. Попробуйте позже.']);
exit;
}
$_SESSION['requests'][] = $currentTime;
// Обработка данных формы
$name = $_POST['name'];
$email = $_POST['email'];
// Здесь можно добавить код для сохранения данных в базу данных или отправки email
echo json_encode(['success' => true, 'message' => 'Данные успешно обработаны.']);
?>Комбинируя эти методы, можно значительно повысить защиту формы от спама и ботов. Например, можно использовать капчу вместе с CSRF-токеном и ограничением частоты отправки.
Для реализации функционала создания и редактирования событий в календаре с использованием FullCalendar, jQuery, Bootstrap и PHP, а также отправки данных через AJAX, выполним следующие шаги:
Включим необходимые библиотеки в HTML-файл:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>FullCalendar Example</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- FullCalendar CSS --> <link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css" rel="stylesheet"> </head> <body> <div class="container mt-5"> <div id="calendar"></div> </div> <!-- Modal для создания/редактирования события --> <div class="modal fade" id="eventModal" tabindex="-1" aria-labelledby="eventModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="eventModalLabel">Создать событие</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <form id="eventForm"> <input type="hidden" id="eventId"> <div class="mb-3"> <label for="eventTitle" class="form-label">Заголовок</label> <input type="text" class="form-control" id="eventTitle" required> </div> <div class="mb-3"> <label for="eventDescription" class="form-label">Описание</label> <textarea class="form-control" id="eventDescription" rows="3"></textarea> </div> <div class="mb-3"> <label for="eventStart" class="form-label">Начало</label> <input type="datetime-local" class="form-control" id="eventStart" required> </div> <div class="mb-3"> <label for="eventEnd" class="form-label">Конец</label> <input type="datetime-local" class="form-control" id="eventEnd" required> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button> <button type="button" class="btn btn-primary" id="saveEvent">Сохранить</button> </div> </div> </div> </div> <!-- jQuery --> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- FullCalendar JS --> <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js"></script> <!-- Ваш скрипт --> <script src="script.js"></script> </body> </html>
Создадим файл script.js, в котором настроим календарь и обработку событий:
document.addEventListener('DOMContentLoaded', function () { const calendarEl = document.getElementById('calendar'); const calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'dayGridMonth', selectable: true, editable: true, eventClick: function (info) { // Редактирование события openModal(info.event); }, select: function (info) { // Создание нового события openModal({ start: info.start, end: info.end }); } }); calendar.render(); const eventModal = new bootstrap.Modal(document.getElementById('eventModal')); const eventForm = document.getElementById('eventForm'); // Открытие модального окна function openModal(event) { document.getElementById('eventId').value = event.id || ''; document.getElementById('eventTitle').value = event.title || ''; document.getElementById('eventDescription').value = event.extendedProps?.description || ''; document.getElementById('eventStart').value = event.start ? event.start.toISOString().slice(0, 16) : ''; document.getElementById('eventEnd').value = event.end ? event.end.toISOString().slice(0, 16) : ''; eventModal.show(); } // Сохранение события document.getElementById('saveEvent').addEventListener('click', function () { const eventId = document.getElementById('eventId').value; const eventTitle = document.getElementById('eventTitle').value; const eventDescription = document.getElementById('eventDescription').value; const eventStart = document.getElementById('eventStart').value; const eventEnd = document.getElementById('eventEnd').value; const eventData = { id: eventId, title: eventTitle, description: eventDescription, start: eventStart, end: eventEnd }; // Отправка данных через AJAX $.ajax({ url: 'save_event.php', method: 'POST', data: eventData, success: function (response) { if (response.success) { calendar.refetchEvents(); // Обновить календарь eventModal.hide(); } else { alert('Ошибка: ' + response.message); } }, error: function () { alert('Ошибка при отправке данных.'); } }); }); });
Создадим файл save_event.php, который будет обрабатывать данные события:
<?php
header('Content-Type: application/json');
// Подключение к базе данных (пример для MySQL)
$host = 'localhost';
$db = 'calendar';
$user = 'root';
$pass = '';
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die(json_encode(['success' => false, 'message' => 'Ошибка подключения к базе данных.']));
}
// Получение данных из запроса
$id = $_POST['id'] ?? null;
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$start = $_POST['start'] ?? '';
$end = $_POST['end'] ?? '';
// Валидация данных
if (empty($title) || empty($start)) {
echo json_encode(['success' => false, 'message' => 'Заголовок и дата начала обязательны.']);
exit;
}
// Сохранение или обновление события
if ($id) {
// Редактирование существующего события
$stmt = $conn->prepare("UPDATE events SET title = ?, description = ?, start = ?, end = ? WHERE id = ?");
$stmt->bind_param('ssssi', $title, $description, $start, $end, $id);
} else {
// Создание нового события
$stmt = $conn->prepare("INSERT INTO events (title, description, start, end) VALUES (?, ?, ?, ?)");
$stmt->bind_param('ssss', $title, $description, $start, $end);
}
if ($stmt->execute()) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Ошибка при сохранении события.']);
}
$stmt->close();
$conn->close();
?>Создайте таблицу events в базе данных:
CREATE TABLE events ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, start DATETIME NOT NULL, end DATETIME );
Добавьте в script.js загрузку событий из базы данных:
document.addEventListener('DOMContentLoaded', function () { const calendarEl = document.getElementById('calendar'); const calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'dayGridMonth', selectable: true, editable: true, events: 'load_events.php', // Загрузка событий eventClick: function (info) { openModal(info.event); }, select: function (info) { openModal({ start: info.start, end: info.end }); } }); calendar.render(); });
Создайте файл load_events.php для загрузки событий:
<?php
header('Content-Type: application/json');
$host = 'localhost';
$db = 'calendar';
$user = 'root';
$pass = '';
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die(json_encode([]));
}
$result = $conn->query("SELECT id, title, description, start, end FROM events");
$events = [];
while ($row = $result->fetch_assoc()) {
$events[] = [
'id' => $row['id'],
'title' => $row['title'],
'description' => $row['description'],
'start' => $row['start'],
'end' => $row['end']
];
}
echo json_encode($events);
$conn->close();
?>Теперь у вас есть полнофункциональный календарь с возможностью создания, редактирования и сохранения событий через AJAX. Данные сохраняются в базе данных и загружаются при открытии календаря.
Для добавления функционала повторяющихся событий в календарь (ежедневно, еженедельно, ежемесячно, ежегодно), нужно расширить логику как на стороне клиента (JavaScript), так и на стороне сервера (PHP). Вот пошаговая реализация:
Добавим в модальное окно выпадающий список (<select>) для выбора типа повторения:
<div class="modal-body"> <form id="eventForm"> <input type="hidden" id="eventId"> <div class="mb-3"> <label for="eventTitle" class="form-label">Заголовок</label> <input type="text" class="form-control" id="eventTitle" required> </div> <div class="mb-3"> <label for="eventDescription" class="form-label">Описание</label> <textarea class="form-control" id="eventDescription" rows="3"></textarea> </div> <div class="mb-3"> <label for="eventStart" class="form-label">Начало</label> <input type="datetime-local" class="form-control" id="eventStart" required> </div> <div class="mb-3"> <label for="eventEnd" class="form-label">Конец</label> <input type="datetime-local" class="form-control" id="eventEnd" required> </div> <div class="mb-3"> <label for="eventRepeat" class="form-label">Повторение</label> <select class="form-select" id="eventRepeat"> <option value="none">Не повторять</option> <option value="daily">Каждый день</option> <option value="weekly">Каждую неделю</option> <option value="monthly">Каждый месяц</option> <option value="yearly">Каждый год</option> </select> </div> </form> </div>
В файле script.js добавим обработку выбора повторения и отправку данных на сервер:
document.getElementById('saveEvent').addEventListener('click', function () { const eventId = document.getElementById('eventId').value; const eventTitle = document.getElementById('eventTitle').value; const eventDescription = document.getElementById('eventDescription').value; const eventStart = document.getElementById('eventStart').value; const eventEnd = document.getElementById('eventEnd').value; const eventRepeat = document.getElementById('eventRepeat').value; const eventData = { id: eventId, title: eventTitle, description: eventDescription, start: eventStart, end: eventEnd, repeat: eventRepeat }; // Отправка данных через AJAX $.ajax({ url: 'save_event.php', method: 'POST', data: eventData, success: function (response) { if (response.success) { calendar.refetchEvents(); // Обновить календарь eventModal.hide(); } else { alert('Ошибка: ' + response.message); } }, error: function () { alert('Ошибка при отправке данных.'); } }); });
В файле save_event.php добавим логику для создания повторяющихся событий:
<?php
header('Content-Type: application/json');
// Подключение к базе данных
$host = 'localhost';
$db = 'calendar';
$user = 'root';
$pass = '';
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die(json_encode(['success' => false, 'message' => 'Ошибка подключения к базе данных.']));
}
// Получение данных из запроса
$id = $_POST['id'] ?? null;
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$start = $_POST['start'] ?? '';
$end = $_POST['end'] ?? '';
$repeat = $_POST['repeat'] ?? 'none';
// Валидация данных
if (empty($title) || empty($start)) {
echo json_encode(['success' => false, 'message' => 'Заголовок и дата начала обязательны.']);
exit;
}
// Функция для сохранения события
function saveEvent($conn, $title, $description, $start, $end) {
$stmt = $conn->prepare("INSERT INTO events (title, description, start, end) VALUES (?, ?, ?, ?)");
$stmt->bind_param('ssss', $title, $description, $start, $end);
return $stmt->execute();
}
// Сохранение события
if ($repeat === 'none') {
// Одиночное событие
if (saveEvent($conn, $title, $description, $start, $end)) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Ошибка при сохранении события.']);
}
} else {
// Повторяющееся событие
$startDate = new DateTime($start);
$endDate = new DateTime($end);
$interval = null;
switch ($repeat) {
case 'daily':
$interval = new DateInterval('P1D'); // Каждый день
break;
case 'weekly':
$interval = new DateInterval('P1W'); // Каждую неделю
break;
case 'monthly':
$interval = new DateInterval('P1M'); // Каждый месяц
break;
case 'yearly':
$interval = new DateInterval('P1Y'); // Каждый год
break;
}
// Сохраняем события на год вперед
$maxDate = (new DateTime())->modify('+1 year');
while ($startDate <= $maxDate) {
if (!saveEvent($conn, $title, $description, $startDate->format('Y-m-d H:i:s'), $endDate->format('Y-m-d H:i:s'))) {
echo json_encode(['success' => false, 'message' => 'Ошибка при сохранении повторяющегося события.']);
exit;
}
$startDate->add($interval);
$endDate->add($interval);
}
echo json_encode(['success' => true]);
}
$conn->close();
?>В файле load_events.php изменений не требуется, так как все события уже сохраняются в базе данных как отдельные записи. FullCalendar автоматически отобразит их.
Таблица events должна содержать поля для хранения данных о событиях:
CREATE TABLE events ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, start DATETIME NOT NULL, end DATETIME );
Теперь пользователь может создавать повторяющиеся события:
Ежедневно: Событие будет повторяться каждый день.
Еженедельно: Событие будет повторяться каждую неделю.
Ежемесячно: Событие будет повторяться каждый месяц.
Ежегодно: Событие будет повторяться каждый год.
Все события сохраняются в базе данных и отображаются в календаре.
Ошибка Fatal error: Uncaught TypeError: DateTime::add(): Argument #1 ($interval) must be of type DateInterval, null given возникает, потому что переменная $interval не была инициализирована для какого-то значения $repeat. Это может произойти, если значение $repeat не соответствует ни одному из случаев в switch.
Чтобы исправить эту ошибку, нужно:
Убедиться, что значение $repeat всегда соответствует одному из ожидаемых вариантов (none, daily, weekly, monthly, yearly).
Добавить обработку по умолчанию в switch, чтобы избежать null в $interval.
Вот исправленный код:
save_event.php<?php
header('Content-Type: application/json');
// Подключение к базе данных
$host = 'localhost';
$db = 'calendar';
$user = 'root';
$pass = '';
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die(json_encode(['success' => false, 'message' => 'Ошибка подключения к базе данных.']));
}
// Получение данных из запроса
$id = $_POST['id'] ?? null;
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$start = $_POST['start'] ?? '';
$end = $_POST['end'] ?? '';
$repeat = $_POST['repeat'] ?? 'none';
// Валидация данных
if (empty($title) || empty($start)) {
echo json_encode(['success' => false, 'message' => 'Заголовок и дата начала обязательны.']);
exit;
}
// Функция для сохранения события
function saveEvent($conn, $title, $description, $start, $end) {
$stmt = $conn->prepare("INSERT INTO events (title, description, start, end) VALUES (?, ?, ?, ?)");
$stmt->bind_param('ssss', $title, $description, $start, $end);
return $stmt->execute();
}
// Сохранение события
if ($repeat === 'none') {
// Одиночное событие
if (saveEvent($conn, $title, $description, $start, $end)) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Ошибка при сохранении события.']);
}
} else {
// Повторяющееся событие
$startDate = new DateTime($start);
$endDate = new DateTime($end);
$interval = null;
switch ($repeat) {
case 'daily':
$interval = new DateInterval('P1D'); // Каждый день
break;
case 'weekly':
$interval = new DateInterval('P1W'); // Каждую неделю
break;
case 'monthly':
$interval = new DateInterval('P1M'); // Каждый месяц
break;
case 'yearly':
$interval = new DateInterval('P1Y'); // Каждый год
break;
default:
// Если значение $repeat не распознано, завершаем с ошибкой
echo json_encode(['success' => false, 'message' => 'Неверный тип повторения.']);
exit;
}
// Сохраняем события на год вперед
$maxDate = (new DateTime())->modify('+1 year');
while ($startDate <= $maxDate) {
if (!saveEvent($conn, $title, $description, $startDate->format('Y-m-d H:i:s'), $endDate->format('Y-m-d H:i:s'))) {
echo json_encode(['success' => false, 'message' => 'Ошибка при сохранении повторяющегося события.']);
exit;
}
$startDate->add($interval);
$endDate->add($interval);
}
echo json_encode(['success' => true]);
}
$conn->close();
?>Добавлен default в switch:
Если значение $repeat не соответствует ни одному из ожидаемых вариантов (daily, weekly, monthly, yearly), выполнится блок default.
В этом случае скрипт завершится с ошибкой: Неверный тип повторения.
Проверка значения $repeat:
Если $repeat не передано или передано неверное значение, скрипт не попытается создать DateInterval с null.
repeatЕсли в запросе передано неверное значение repeat, например invalid, сервер вернет ошибку:
{ "success": false, "message": "Неверный тип повторения." }
repeatЕсли в запросе передано корректное значение, например weekly, сервер создаст повторяющиеся события:
{ "success": true }
Валидация на стороне клиента:
Убедитесь, что в <select> для выбора повторения передаются только допустимые значения (none, daily, weekly, monthly, yearly).
Валидация на стороне сервера:
Всегда проверяйте входные данные перед использованием.
Логирование:
Добавьте логирование ошибок, чтобы отслеживать неожиданные ситуации.
Теперь ошибка DateTime::add(): Argument #1 ($interval) must be of type DateInterval, null given больше не возникнет, так как $interval всегда будет корректным объектом DateInterval или выполнение скрипта завершится с ошибкой.