jQueryフォームにCSV保存機能を追加する方法

はじめに

本記事では、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保存機能を実装しました:

  1. フォーム送信とCSRFトークンの検証
  2. reCAPTCHAでのスパム対策
  3. フォームデータをCSVファイルに保存

コメント

タイトルとURLをコピーしました