この記事では、Vue.js を使用したお問い合わせフォームに CSV 保存機能 を追加する方法を説明します。さらに、reCAPTCHA
や CSRFトークン
を導入し、セキュアなフォームを実現します。
プロジェクトのファイル構成
以下のような構成でプロジェクトを作成します:
project/
│
├── api/
│ ├── contact.php // フォームデータを受け取り CSV に保存
│ ├── csrf_token.php // CSRF トークンを発行
│ ├── recaptcha_key.php // reCAPTCHA サイトキーを提供
│ ├── config.php // 各種設定
│
├── index.html // フロントエンドのフォーム
├── submissions.csv // 保存されるフォームデータ(初回実行時に自動生成)
├── .htaccess // URL リダイレクト設定
1. index.html
index.html
は Vue.js を使用してフォームの入力、確認、送信を実現します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue.js お問い合わせフォーム</title>
<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 サイトキーの取得中にエラーが発生しました。');
});
</script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div id="app" class="bg-white shadow-md rounded p-6 w-full max-w-md mx-auto">
<router-view></router-view>
</div>
<script>
const { createRouter, createWebHistory } = VueRouter;
const { createApp } = Vue;
// 入力画面
const InputForm = {
template: `
<div>
<h1 class="text-2xl font-bold mb-4">お問い合わせフォーム</h1>
<form @submit.prevent="goToConfirm">
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700">名前:</label>
<input v-model="formData.name" type="text" id="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 v-model="formData.email" type="email" id="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 v-model="formData.message" id="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="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">確認画面へ</button>
</form>
</div>
`,
data() {
return {
formData: JSON.parse(localStorage.getItem('formData')) || { name: '', email: '', message: '' },
};
},
methods: {
goToConfirm() {
localStorage.setItem('formData', JSON.stringify(this.formData));
this.$router.push({ name: 'confirm' });
},
},
};
// 確認画面
const ConfirmForm = {
template: `
<div>
<h1 class="text-2xl font-bold mb-4">内容の確認</h1>
<p class="mb-2"><strong>名前:</strong> {{ formData.name }}</p>
<p class="mb-2"><strong>メールアドレス:</strong> {{ formData.email }}</p>
<p class="mb-4"><strong>お問い合わせ内容:</strong> {{ formData.message }}</p>
<div class="flex space-x-4">
<button @click="goBack" class="w-1/2 bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700">戻る</button>
<button @click="sendForm" class="w-1/2 bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">送信する</button>
</div>
</div>
`,
data() {
return {
formData: JSON.parse(localStorage.getItem('formData')) || { name: '', email: '', message: '' },
};
},
methods: {
goBack() {
this.$router.push({ name: 'input' });
},
async sendForm() {
try {
const recaptchaToken = await grecaptcha.execute(window.recaptchaSiteKey, { action: 'submit' });
const csrfToken = localStorage.getItem('csrfToken');
if (!csrfToken) {
throw new Error('CSRFトークンが取得できませんでした。');
}
const response = await fetch(`${API_URL}/contact.php`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify({ ...this.formData, recaptcha_token: recaptchaToken }),
});
const data = await response.json();
if (data.success) {
localStorage.removeItem('formData');
this.$router.push({ name: 'complete' });
} else {
alert('送信に失敗しました: ' + data.error);
}
} catch (error) {
alert('エラーが発生しました: ' + error.message);
}
},
},
created() {
if (!localStorage.getItem('csrfToken')) {
fetch(`${API_URL}/csrf_token.php`)
.then((response) => response.json())
.then((data) => {
localStorage.setItem('csrfToken', data.csrf_token);
})
.catch(() => alert('CSRFトークンの取得に失敗しました。'));
}
},
};
// 完了画面
const CompleteForm = {
template: `
<div>
<h1 class="text-2xl font-bold mb-4">送信が完了しました</h1>
<p>お問い合わせいただきありがとうございます。</p>
<button @click="goToInput" class="mt-4 w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">フォームに戻る</button>
</div>
`,
methods: {
goToInput() {
this.$router.push({ name: 'input' });
},
},
};
const routes = [
{ path: '/', name: 'input', component: InputForm },
{ path: '/confirm', name: 'confirm', component: ConfirmForm },
{ path: '/complete', name: 'complete', component: CompleteForm },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
const App = {
template: '<router-view></router-view>',
};
const app = createApp(App);
app.use(router);
app.mount('#app');
</script>
</body>
</html>
2. config.php
このファイルはプロジェクト全体の設定を一元管理します。メールアドレスや reCAPTCHA キー、CSRFトークンの有効期限などをここに記載します。
<?php
return [
'recaptcha' => [
'site_key' => '1234XXXXXXXXXXXXXXXXXXXXXXXXXX', // reCAPTCHAサイトキー(ダミー)
'secret_key' => '5678XXXXXXXXXXXXXXXXXXXXXXXXXX', // reCAPTCHAシークレットキー(ダミー)
],
'email' => [
'admin' => 'admin@example.com', // 管理者のメールアドレス(ダミー)
'from' => 'no-reply@example.com', // メール送信元のアドレス(ダミー)
],
'csrf' => [
'token_expiry' => 3600, // CSRFトークンの有効期限(秒)
],
];
3. contact.php
contact.php
は API のエンドポイントとして動作し、フォームデータを処理して保存します。CSV への保存や reCAPTCHA の検証、メール送信も行います。
<?php
session_start();
header('Content-Type: application/json');
// 設定の読み込み
$config = include __DIR__ . '/config.php';
// リクエストデータの取得
$data = json_decode(file_get_contents('php://input'), true);
// CSRFトークンの検証
if (empty($_SESSION['csrf_token']) || empty($_SERVER['HTTP_X_CSRF_TOKEN']) || $_SESSION['csrf_token'] !== $_SERVER['HTTP_X_CSRF_TOKEN']) {
echo json_encode(['success' => false, 'error' => 'CSRFトークンが無効です。']);
exit;
}
// reCAPTCHA の検証
$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
$recaptcha_secret = $config['recaptcha']['secret_key'];
$recaptcha_response = $data['recaptcha_token'];
$response = file_get_contents($recaptcha_url . '?secret=' . $recaptcha_secret . '&response=' . $recaptcha_response);
$recaptcha = json_decode($response, true);
if (!$recaptcha['success']) {
echo json_encode(['success' => false, 'error' => 'reCAPTCHA 検証に失敗しました。']);
exit;
}
// 入力データのサニタイズとバリデーション
$name = htmlspecialchars($data['name'], ENT_QUOTES, 'UTF-8');
$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
$message = htmlspecialchars($data['message'], ENT_QUOTES, 'UTF-8');
if (!$email) {
echo json_encode(['success' => false, 'error' => 'メールアドレスが無効です。']);
exit;
}
// 保存先のCSVファイルパス
$csvFile = __DIR__ . '/submissions.csv';
// CSV への保存
try {
$fileHandle = fopen($csvFile, 'a');
if (!$fileHandle) {
throw new Exception('CSVファイルのオープンに失敗しました。');
}
$timestamp = date('Y-m-d H:i:s');
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '不明';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '不明';
// CSVデータの構築
$csvData = [
$timestamp,
$name,
$email,
$message,
$ipAddress,
$userAgent,
];
if (fputcsv($fileHandle, $csvData) === false) {
throw new Exception('CSVファイルへの書き込みに失敗しました。');
}
fclose($fileHandle);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => 'CSV保存エラー: ' . $e->getMessage()]);
exit;
}
// メール送信処理
$toAdmin = $config['email']['admin'];
$subjectAdmin = 'お問い合わせフォームからのメッセージ';
$messageAdmin = "名前: $name\nメールアドレス: $email\nメッセージ: $message";
$headersAdmin = 'From: ' . $config['email']['from'];
$mailToAdmin = mail($toAdmin, $subjectAdmin, $messageAdmin, $headersAdmin);
// 問い合わせ者への自動返信メール
$toUser = $email;
$subjectUser = 'お問い合わせありがとうございます';
$messageUser = "$name 様\n\nお問い合わせありがとうございます。\n以下の内容で受け付けました。\n\n----------------------\nお名前: $name\nメールアドレス: $email\nお問い合わせ内容:\n$message\n----------------------\n\n担当者より折り返しご連絡いたします。\n\nよろしくお願いいたします。\n";
$headersUser = 'From: ' . $config['email']['from'];
$mailToUser = mail($toUser, $subjectUser, $messageUser, $headersUser);
// レスポンス
if ($mailToAdmin && $mailToUser) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'メール送信に失敗しました。']);
}
4. csrf_token.php
CSRF トークンを発行します。
<?php
session_start();
// 設定を読み込む
$config = include __DIR__ . '/config.php';
// CSRFトークンを生成
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$_SESSION['csrf_token_expiry'] = time() + $config['csrf']['token_expiry'];
}
// トークンが期限切れの場合、新しいトークンを生成
if (time() > $_SESSION['csrf_token_expiry']) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$_SESSION['csrf_token_expiry'] = time() + $config['csrf']['token_expiry'];
}
// レスポンスに CSRF トークンを含める
echo json_encode(['csrf_token' => $_SESSION['csrf_token']]);
5. recaptcha_key.php
reCAPTCHA サイトキーを提供します。
<?php
header('Content-Type: application/json');
// 設定を読み込む
$config = include __DIR__ . '/config.php';
// reCAPTCHA サイトキーを返す
echo json_encode(['site_key' => $config['recaptcha']['site_key']]);
6. .htaccess
この設定により、URLのパスが適切に処理されます。
# URLのパスをindex.htmlにリダイレクト
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
まとめ
このプロジェクトでは以下のことを実現しました:
- Vue.js を使用したフォームの作成。
- CSV への保存 機能。
- CSRF 対策 と reCAPTCHA 検証 の導入。
- メール送信 機能。
- プロジェクト全体の設定を
config.php
で一元管理。
コメント