はじめに
本記事では、jQuery を使ったお問い合わせフォームに、サーバーサイドでCSV保存機能を実装する方法を解説します。
修正後のファイル構成
今回の実装で使用するファイル構成は以下の通りです:
project/
│
├── index.html # フロントエンドのフォームページ
├── api/
│ ├── contact.php # フォーム送信処理 (CSV保存含む)
│ ├── csrf_token.php # CSRFトークン生成API
│ ├── recaptcha_key.php # reCAPTCHAサイトキー取得API
│ ├── config.php # 設定ファイル
└── .htaccess # ルートディレクトリのリダイレクト設定
使用技術とバージョン
- jQuery: 3.6.4
フォーム送信やAJAXリクエスト処理に使用します。 - PHP: 8.2.x
サーバーサイドの処理で使用。 - Tailwind CSS: 3.4.x
シンプルなデザインを作成するために利用。 - reCAPTCHA v3
スパム防止用。
コードの実装
1. フロントエンド: フォームページ
以下は、CSV保存機能に対応した index.html
のコードです。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jQuery お問い合わせフォーム</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script>
// API URLを設定
const API_URL = "/api";
// reCAPTCHA サイトキーをAPIから取得しスクリプトを動的にロード
fetch(`${API_URL}/recaptcha_key.php`)
.then((response) => response.json())
.then((data) => {
if (data.site_key) {
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js?render=${data.site_key}`;
document.head.appendChild(script);
window.recaptchaSiteKey = data.site_key; // グローバル変数に保存
} else {
console.error('reCAPTCHA サイトキーが取得できませんでした。');
}
})
.catch(() => {
console.error('reCAPTCHA サイトキーの取得中にエラーが発生しました。');
});
// CSRFトークンの取得
fetch(`${API_URL}/csrf_token.php`)
.then((response) => response.json())
.then((data) => {
localStorage.setItem('csrfToken', data.csrf_token); // CSRFトークンをローカルストレージに保存
})
.catch(() => alert('CSRFトークンの取得に失敗しました。'));
</script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div id="form-wrapper" class="container mx-auto p-4 max-w-screen-md">
<!-- 入力画面 -->
<div id="input-screen" class="page bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
<h1 class="text-2xl font-bold mb-4">お問い合わせフォーム</h1>
<form id="contact-form">
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700">名前:</label>
<input type="text" id="name" name="name" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="mb-4">
<label for="email" class="block text-sm font-medium text-gray-700">メールアドレス:</label>
<input type="email" id="email" name="email" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="mb-4">
<label for="message" class="block text-sm font-medium text-gray-700">お問い合わせ内容:</label>
<textarea id="message" name="message" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"></textarea>
</div>
<button type="button" id="to-confirm" class="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">
確認画面へ
</button>
</form>
</div>
<!-- 確認画面 -->
<div id="confirm-screen" class="page hidden bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
<h1 class="text-2xl font-bold mb-4">内容の確認</h1>
<p class="mb-2"><strong>名前:</strong> <span id="confirm-name"></span></p>
<p class="mb-2"><strong>メールアドレス:</strong> <span id="confirm-email"></span></p>
<p class="mb-4"><strong>お問い合わせ内容:</strong> <span id="confirm-message"></span></p>
<div class="flex space-x-4">
<button id="back-to-input" class="w-1/2 bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700">
戻る
</button>
<button id="submit-form" class="w-1/2 bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">
送信する
</button>
</div>
</div>
<!-- 完了画面 -->
<div id="complete-screen" class="page hidden bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
<h1 class="text-2xl font-bold mb-4">送信が完了しました</h1>
<p>お問い合わせいただきありがとうございます。</p>
<button id="reset-form" class="mt-4 w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">
フォームに戻る
</button>
</div>
</div>
<script>
$(document).ready(function () {
const routes = {
"/": "#input-screen",
"/confirm": "#confirm-screen",
"/complete": "#complete-screen",
};
function navigate(path) {
$(".page").addClass("hidden");
const target = routes[path] || "#input-screen";
$(target).removeClass("hidden");
history.pushState({}, "", path);
}
navigate(window.location.pathname);
$("#to-confirm").click(function () {
const name = $("#name").val().trim();
const email = $("#email").val().trim();
const message = $("#message").val().trim();
if (!name || !email || !message) {
alert("全ての項目を入力してください。");
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert("有効なメールアドレスを入力してください。");
return;
}
$("#confirm-name").text(name);
$("#confirm-email").text(email);
$("#confirm-message").text(message);
navigate("/confirm");
});
$("#back-to-input").click(function () {
navigate("/");
});
$("#submit-form").click(function () {
grecaptcha.ready(() => {
grecaptcha.execute(window.recaptchaSiteKey, { action: "submit" }).then((token) => {
const csrfToken = localStorage.getItem("csrfToken");
const formData = {
name: $("#name").val(),
email: $("#email").val(),
message: $("#message").val(),
recaptcha_token: token,
};
$.ajax({
url: `${API_URL}/contact.php`,
type: "POST",
headers: {
"X-CSRF-TOKEN": csrfToken,
},
contentType: "application/json",
data: JSON.stringify(formData),
success: function (response) {
if (response.success) {
navigate("/complete");
} else {
alert("送信に失敗しました: " + (response.error || "不明なエラー"));
}
},
error: function () {
alert("エラーが発生しました。");
},
});
});
});
});
$("#reset-form").click(function () {
$("#name").val("");
$("#email").val("");
$("#message").val("");
navigate("/");
});
});
</script>
</body>
</html>
2. サーバーサイド: フォームデータをCSVに保存
api/contact.php
<?php
$config = include __DIR__ . '/config.php';
session_start();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
// CSRFトークン検証
if (empty($_SERVER['HTTP_X_CSRF_TOKEN']) || $_SERVER['HTTP_X_CSRF_TOKEN'] !== $_SESSION['csrf_token']) {
echo json_encode(['success' => false, 'error' => 'CSRFトークンが無効です。']);
exit;
}
// CSV保存処理
$csvFile = __DIR__ . '/data.csv';
$csvData = [
$data['name'],
$data['email'],
$data['message'],
date('Y-m-d H:i:s'),
];
$fileHandle = fopen($csvFile, 'a');
fputcsv($fileHandle, $csvData);
fclose($fileHandle);
echo json_encode(['success' => true]);
3. CSRFトークン生成
api/csrf_token.php
<?php
session_start();
$config = include __DIR__ . '/config.php';
if (empty($_SESSION['csrf_token']) || time() > $_SESSION['csrf_token_expiry']) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$_SESSION['csrf_token_expiry'] = time() + $config['csrf']['token_expiry'];
}
header('Content-Type: application/json');
echo json_encode(['csrf_token' => $_SESSION['csrf_token']]);
4. reCAPTCHAキーの取得
api/recaptcha_key.php
<?php
$config = include __DIR__ . '/config.php';
header('Content-Type: application/json');
echo json_encode(['site_key' => $config['recaptcha']['site_key']]);
5. 設定ファイル
api/config.php
<?php
return [
'recaptcha' => [
'site_key' => 'your_recaptcha_site_key',
'secret_key' => 'your_recaptcha_secret_key',
],
'csrf' => [
'token_expiry' => 3600, // CSRFトークンの有効期限(秒)
],
'admin_email' => 'admin@example.com',
'from_email' => 'no-reply@example.com',
];
まとめ
今回の記事では、以下のステップを通じてjQueryフォームにCSV保存機能を実装しました:
- フォーム送信とCSRFトークンの検証。
- reCAPTCHAでのスパム対策。
- フォームデータをCSVファイルに保存。
コメント