CodeIgniterとSmarty懐かしい環境でのメールフォームの改修
<?php
/*
==============================================
ファイル完結システム(確認→登録→ダウンロード)
==============================================
*/
// Contact Controller(送信処理追加版)
class Contact extends CI_Controller {
public function __construct() {
parent::__construct();
$this->load->database();
$this->load->helper(['form', 'url', 'file', 'download']);
$this->load->library(['form_validation', 'upload', 'session', 'smarty', 'zip']);
$this->load->model('Contact_model');
}
/**
* メイン処理
*/
public function index() {
$data = [
'errors' => [],
'form_data' => [],
'success' => false
];
if ($this->input->method() === 'post') {
$action = $this->input->post('action');
switch ($action) {
case 'confirm':
$data = $this->_process_confirm();
if (empty($data['errors'])) {
$this->_show_confirm($data);
return;
}
break;
case 'submit':
$data = $this->_process_submit();
if ($data['success']) {
$this->_show_complete($data);
return;
}
break;
}
}
$this->_show_form($data);
}
/**
* 確認処理(前回と同じ)
*/
private function _process_confirm() {
$data = [
'errors' => [],
'form_data' => [],
'uploaded_files' => [],
'file_info' => []
];
// バリデーション設定
$this->form_validation->set_rules('name', '名前', 'required|trim|max_length[100]');
$this->form_validation->set_rules('email', 'メールアドレス', 'required|valid_email|max_length[255]');
$this->form_validation->set_rules('message', 'メッセージ', 'required|trim|max_length[1000]');
if (!$this->form_validation->run()) {
$data['errors'] = $this->form_validation->error_array();
}
$data['form_data'] = [
'name' => trim($this->input->post('name')),
'email' => trim($this->input->post('email')),
'message' => trim($this->input->post('message'))
];
$upload_result = $this->_handle_file_upload_temp();
if (!empty($upload_result['errors'])) {
$data['errors'] = array_merge($data['errors'], $upload_result['errors']);
} else {
$data['uploaded_files'] = $upload_result['files'];
$data['file_info'] = $upload_result['file_info'];
$this->session->set_userdata('temp_uploaded_files', $upload_result['files']);
$this->session->set_userdata('temp_file_info', $upload_result['file_info']);
}
return $data;
}
/**
* 送信処理(日時フォルダに整理&DB登録)
*/
private function _process_submit() {
$data = ['success' => false, 'errors' => [], 'submission_id' => '', 'contact_id' => 0];
// 送信日時でフォルダ作成
$submission_id = date('YmdHis') . '_' . uniqid(); // 例:20241209143022_abc123
$user_name = $this->input->post('name');
$safe_user_name = $this->_sanitize_filename($user_name);
// フォルダパス生成: uploads/2024/12/09/田中太郎_20241209143022_abc123/
$final_folder = './uploads/' . date('Y/m/d') . '/' . $safe_user_name . '_' . $submission_id . '/';
// フォルダ作成
if (!$this->_create_directory($final_folder)) {
$data['errors']['folder'] = 'フォルダ作成に失敗しました。';
return $data;
}
// セッションから一時ファイル情報を取得
$temp_files = $this->session->userdata('temp_uploaded_files') ?: [];
$file_info = $this->session->userdata('temp_file_info') ?: [];
// ファイルを最終フォルダに移動
$moved_files = $this->_move_files_to_final_folder($temp_files, $final_folder);
// お問い合わせ情報テキストファイル作成
$this->_create_contact_info_file($final_folder, [
'name' => $this->input->post('name'),
'email' => $this->input->post('email'),
'message' => $this->input->post('message'),
'submission_id' => $submission_id,
'created_at' => date('Y-m-d H:i:s'),
'file_info' => $file_info
]);
// データベース保存
$form_data = [
'name' => $this->input->post('name'),
'email' => $this->input->post('email'),
'message' => $this->input->post('message'),
'submission_id' => $submission_id,
'folder_path' => $final_folder,
'uploaded_files' => json_encode($moved_files),
'file_info' => json_encode($file_info)
];
$contact_id = $this->Contact_model->insert_contact($form_data);
if ($contact_id) {
$data['success'] = true;
$data['submission_id'] = $submission_id;
$data['contact_id'] = $contact_id;
$data['folder_path'] = $final_folder;
$data['file_count'] = count($moved_files);
// セッションクリア
$this->session->unset_userdata(['temp_uploaded_files', 'temp_file_info']);
// 一時フォルダのクリーンアップ
$this->_cleanup_temp_files();
} else {
$data['errors']['db'] = 'データベースエラーが発生しました。';
}
return $data;
}
/**
* ディレクトリ作成
*/
private function _create_directory($path) {
if (!is_dir($path)) {
return mkdir($path, 0755, true);
}
return true;
}
/**
* ファイルを最終フォルダに移動
*/
private function _move_files_to_final_folder($temp_files, $final_folder) {
$moved_files = [];
foreach ($temp_files as $temp_file) {
$temp_path = './uploads/temp/' . $temp_file;
$final_path = $final_folder . $temp_file;
if (file_exists($temp_path)) {
if (rename($temp_path, $final_path)) {
$moved_files[] = $temp_file;
} else {
log_message('error', 'ファイル移動失敗: ' . $temp_path . ' -> ' . $final_path);
}
}
}
return $moved_files;
}
/**
* お問い合わせ情報テキストファイル作成
*/
private function _create_contact_info_file($folder_path, $contact_data) {
$info_text = "=====================================\n";
$info_text .= "お問い合わせ情報\n";
$info_text .= "=====================================\n\n";
$info_text .= "送信日時: {$contact_data['created_at']}\n";
$info_text .= "送信ID: {$contact_data['submission_id']}\n";
$info_text .= "お名前: {$contact_data['name']}\n";
$info_text .= "メールアドレス: {$contact_data['email']}\n\n";
$info_text .= "お問い合わせ内容:\n";
$info_text .= "-------------------------------------\n";
$info_text .= $contact_data['message'] . "\n\n";
if (!empty($contact_data['file_info'])) {
$info_text .= "添付ファイル:\n";
$info_text .= "-------------------------------------\n";
foreach ($contact_data['file_info'] as $i => $file) {
$info_text .= ($i + 1) . ". {$file['input_display_name']}\n";
$info_text .= " 元ファイル名: {$file['original_name']}\n";
$info_text .= " 保存ファイル名: {$file['saved_name']}\n";
$info_text .= " サイズ: " . number_format($file['file_size']) . " bytes\n";
$info_text .= " 形式: {$file['file_type']}\n";
$info_text .= " アップロード: {$file['upload_time']}\n\n";
}
}
file_put_contents($folder_path . 'お問い合わせ内容.txt', $info_text);
}
/**
* ファイル名用の文字列サニタイズ
*/
private function _sanitize_filename($name) {
$name = trim($name);
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
$name = preg_replace('/\s+/', '_', $name);
if (mb_strlen($name) > 20) {
$name = mb_substr($name, 0, 20);
}
if (empty($name)) {
$name = 'user_' . date('mdHis');
}
return $name;
}
/**
* 一時ファイルのクリーンアップ
*/
private function _cleanup_temp_files() {
$temp_path = './uploads/temp/';
if (is_dir($temp_path)) {
$files = glob($temp_path . '*');
$now = time();
foreach ($files as $file) {
if (is_file($file) && ($now - filemtime($file)) > 3600) { // 1時間
unlink($file);
}
}
}
}
/**
* ファイルアップロード処理(一時保存)
*/
private function _handle_file_upload_temp() {
$result = [
'files' => [],
'file_info' => [],
'errors' => []
];
$user_name = trim($this->input->post('name'));
$safe_user_name = $this->_sanitize_filename($user_name);
$file_inputs = [
'application_form' => '申請書',
'resume' => '履歴書',
'portfolio' => 'ポートフォリオ',
'certificate' => '証明書',
'other_document' => 'その他資料'
];
if (isset($_FILES['upload_file']) && !empty($_FILES['upload_file']['name'])) {
$file_inputs['upload_file'] = '添付ファイル';
}
foreach ($file_inputs as $input_name => $display_name) {
if (!isset($_FILES[$input_name]) || empty($_FILES[$input_name]['name'])) {
continue;
}
$temp_path = './uploads/temp/';
if (!is_dir($temp_path)) {
mkdir($temp_path, 0755, true);
}
$original_name = $_FILES[$input_name]['name'];
$file_size = $_FILES[$input_name]['size'];
$file_type = $_FILES[$input_name]['type'];
$extension = pathinfo($original_name, PATHINFO_EXTENSION);
$timestamp = date('YmdHis');
$safe_filename = "{$safe_user_name}_{$display_name}_{$timestamp}." . strtolower($extension);
$config = [
'upload_path' => $temp_path,
'allowed_types' => 'jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|txt|zip|csv',
'max_size' => 10240,
'max_width' => 3000,
'max_height' => 3000,
'file_name' => $safe_filename,
'remove_spaces' => TRUE
];
$this->upload->initialize($config);
if ($this->upload->do_upload($input_name)) {
$upload_data = $this->upload->data();
$result['files'][] = $upload_data['file_name'];
$result['file_info'][] = [
'original_name' => $original_name,
'saved_name' => $upload_data['file_name'],
'input_name' => $input_name,
'input_display_name' => $display_name,
'file_size' => $file_size,
'file_type' => $file_type,
'extension' => $extension,
'upload_time' => date('Y-m-d H:i:s'),
'user_name' => $user_name
];
} else {
$result['errors'][$input_name] = strip_tags($this->upload->display_errors());
}
}
return $result;
}
/**
* フォーム表示
*/
private function _show_form($data) {
$this->smarty->assign('errors', $data['errors']);
$this->smarty->assign('form_data', $data['form_data']);
$this->smarty->display('contact/form.tpl');
}
/**
* 確認画面表示
*/
private function _show_confirm($data) {
$this->smarty->assign('form_data', $data['form_data']);
$this->smarty->assign('uploaded_files', $data['uploaded_files']);
$this->smarty->assign('file_info', $data['file_info']);
$this->smarty->display('contact/confirm.tpl');
}
/**
* 完了画面表示
*/
private function _show_complete($data) {
$this->smarty->assign('submission_id', $data['submission_id']);
$this->smarty->assign('contact_id', $data['contact_id']);
$this->smarty->assign('file_count', $data['file_count']);
$this->smarty->display('contact/complete.tpl');
}
}
/*
==============================================
管理画面Controller(Admin.php)
==============================================
*/
class Admin extends CI_Controller {
public function __construct() {
parent::__construct();
$this->load->database();
$this->load->helper(['url', 'download', 'file']);
$this->load->library(['pagination', 'smarty', 'zip']);
$this->load->model('Contact_model');
// 管理者認証チェック(必要に応じて)
// $this->_check_admin_auth();
}
/**
* 投稿一覧表示
*/
public function contact_list() {
// ページング設定
$config = [
'base_url' => site_url('admin/contact_list'),
'total_rows' => $this->Contact_model->count_contacts(),
'per_page' => 20,
'uri_segment' => 3,
'use_page_numbers' => TRUE,
'full_tag_open' => '<div class="pagination">',
'full_tag_close' => '</div>',
'first_link' => '最初',
'last_link' => '最後',
'next_link' => '次へ',
'prev_link' => '前へ',
'cur_tag_open' => '<span class="current">',
'cur_tag_close' => '</span>',
];
$this->pagination->initialize($config);
$page = ($this->uri->segment(3)) ? $this->uri->segment(3) : 1;
$offset = ($page - 1) * $config['per_page'];
$contacts = $this->Contact_model->get_all_contacts_with_files($config['per_page'], $offset);
// 各投稿のファイル情報を整理
foreach ($contacts as &$contact) {
$contact['file_count'] = 0;
$contact['total_size'] = 0;
$contact['has_files'] = false;
if (!empty($contact['file_info_array'])) {
$contact['file_count'] = count($contact['file_info_array']);
$contact['has_files'] = true;
foreach ($contact['file_info_array'] as $file) {
$contact['total_size'] += $file['file_size'];
}
}
$contact['zip_available'] = $this->_check_files_exist($contact);
}
$this->smarty->assign('contacts', $contacts);
$this->smarty->assign('pagination', $this->pagination->create_links());
$this->smarty->assign('total_rows', $config['total_rows']);
$this->smarty->display('admin/contact_list.tpl');
}
/**
* ZIPダウンロード
*/
public function download_zip($contact_id) {
$contact = $this->Contact_model->get_contact_with_files($contact_id);
if (!$contact) {
show_404('投稿が見つかりません');
return;
}
if (empty($contact['file_info_array'])) {
$this->session->set_flashdata('error', 'ダウンロードできるファイルがありません。');
redirect('admin/contact_list');
return;
}
// フォルダパス取得
$folder_path = $contact['folder_path'];
if (!is_dir($folder_path)) {
$this->session->set_flashdata('error', 'ファイルフォルダが見つかりません。');
redirect('admin/contact_list');
return;
}
// ZIP作成
$zip_result = $this->_create_contact_zip($contact, $folder_path);
if (!$zip_result['success']) {
$this->session->set_flashdata('error', $zip_result['error']);
redirect('admin/contact_list');
return;
}
// ダウンロード実行
$this->_download_and_cleanup($zip_result['zip_path'], $zip_result['zip_name']);
}
/**
* 個別ファイルダウンロード
*/
public function download_file($contact_id, $file_index) {
$contact = $this->Contact_model->get_contact_with_files($contact_id);
if (!$contact || empty($contact['file_info_array'])) {
show_404();
return;
}
if (!isset($contact['file_info_array'][$file_index])) {
show_404();
return;
}
$file_info = $contact['file_info_array'][$file_index];
$file_path = $contact['folder_path'] . $file_info['saved_name'];
if (!file_exists($file_path)) {
show_404('ファイルが見つかりません');
return;
}
// 元のファイル名でダウンロード
force_download($file_info['original_name'], file_get_contents($file_path));
}
/**
* 投稿詳細表示
*/
public function contact_detail($contact_id) {
$contact = $this->Contact_model->get_contact_with_files($contact_id);
if (!$contact) {
show_404();
return;
}
// ファイル存在チェック
if (!empty($contact['file_info_array'])) {
foreach ($contact['file_info_array'] as &$file) {
$file_path = $contact['folder_path'] . $file['saved_name'];
$file['file_exists'] = file_exists($file_path);
if ($file['file_exists']) {
$file['actual_size'] = filesize($file_path);
}
}
}
$this->smarty->assign('contact', $contact);
$this->smarty->display('admin/contact_detail.tpl');
}
/**
* ファイル存在チェック
*/
private function _check_files_exist($contact) {
if (empty($contact['file_info_array']) || empty($contact['folder_path'])) {
return false;
}
if (!is_dir($contact['folder_path'])) {
return false;
}
foreach ($contact['file_info_array'] as $file) {
$file_path = $contact['folder_path'] . $file['saved_name'];
if (file_exists($file_path)) {
return true;
}
}
return false;
}
/**
* ZIP作成
*/
private function _create_contact_zip($contact, $folder_path) {
$result = ['success' => false, 'error' => '', 'zip_path' => '', 'zip_name' => ''];
try {
$safe_name = preg_replace('/[^a-zA-Z0-9_-]/', '_', $contact['name']);
$zip_name = "contact_{$contact['id']}_{$safe_name}_{$contact['submission_id']}.zip";
$zip_path = "./uploads/temp/{$zip_name}";
$temp_dir = './uploads/temp/';
if (!is_dir($temp_dir)) {
mkdir($temp_dir, 0755, true);
}
$zip = new ZipArchive();
if ($zip->open($zip_path, ZipArchive::CREATE) !== TRUE) {
throw new Exception('ZIPファイルの作成に失敗しました。');
}
// フォルダ内の全ファイルを追加
$files = glob($folder_path . '*');
$added_files = 0;
foreach ($files as $file) {
if (is_file($file)) {
$filename = basename($file);
$zip->addFile($file, $filename);
$added_files++;
}
}
$zip->close();
if ($added_files === 0) {
unlink($zip_path);
throw new Exception('追加できるファイルがありませんでした。');
}
$result['success'] = true;
$result['zip_path'] = $zip_path;
$result['zip_name'] = $zip_name;
} catch (Exception $e) {
$result['error'] = $e->getMessage();
log_message('error', 'ZIP作成エラー: ' . $e->getMessage());
}
return $result;
}
/**
* ダウンロード実行&クリーンアップ
*/
private function _download_and_cleanup($zip_path, $zip_name) {
if (!file_exists($zip_path)) {
show_error('ZIPファイルが見つかりません。');
return;
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $zip_name . '"');
header('Content-Length: ' . filesize($zip_path));
header('Cache-Control: no-cache, must-revalidate');
header('Expires: 0');
readfile($zip_path);
unlink($zip_path);
exit;
}
}
/*
==============================================
Model拡張版
==============================================
*/
class Contact_model extends CI_Model {
private $table = 'contact_form';
public function __construct() {
parent::__construct();
$this->load->database();
}
/**
* お問い合わせデータ挿入(改良版)
*/
public function insert_contact($data) {
$insert_data = [
'name' => $data['name'],
'email' => $data['email'],
'message' => $data['message'],
'submission_id' => $data['submission_id'],
'folder_path' => $data['folder_path'],
'uploaded_files' => $data['uploaded_files'],
'file_info' => $data['file_info'],
'created_at' => date('Y-m-d H:i:s')
];
$this->db->insert($this->table, $insert_data);
return $this->db->insert_id(); // 挿入されたIDを返す
}
/**
* ファイル情報付きで全データ取得
*/
public function get_all_contacts_with_files($limit = 100, $offset = 0) {
$query = $this->db
->select('*')
->order_by('created_at', 'DESC')
->limit($limit, $offset)
->get($this->table);
$results = $query->result_array();
foreach ($results as &$result) {
$result['uploaded_files_array'] = !empty($result['uploaded_files'])
? json_decode($result['uploaded_files'], true)
: [];
$result['file_info_array'] = !empty($result['file_info'])
? json_decode($result['file_info'], true)
: [];
}
return $results;
}
/**
* ファイル情報付きでデータ取得
*/
public function get_contact_with_files($id) {
$query = $this->db->where('id', $id)->get($this->table);
$result = $query->row_array();
if ($result) {
$result['uploaded_files_array'] = !empty($result['uploaded_files'])
? json_decode($result['uploaded_files'], true)
: [];
$result['file_info_array'] = !empty($result['file_info'])
? json_decode($result['file_info'], true)
: [];
}
return $result;
}
/**
* データ件数取得
*/
public function count_contacts() {
return $this->db->count_all($this->table);
}
}
?>
<!--
==============================================
完了画面テンプレート: contact/complete.tpl
==============================================
-->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>送信完了</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; text-align: center; }
.container { max-width: 600px; margin: 50px auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.success-icon { font-size: 48px; color: #28a745; margin-bottom: 20px; }
h2 { color: #28a745; margin-bottom: 20px; }
.success-message { background: #d4edda; color: #155724; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #c3e6cb; }
.info-box { background: #e7f3ff; border: 1px solid #b8daff; border-radius: 4px; padding: 15px; margin: 20px 0; text-align: left; }
.submission-id { font-family: monospace; font-weight: bold; color: #0056b3; }
.btn { padding: 12px 30px; background: #007bff; color: white; border: none; border-radius: 4px; text-decoration: none; display: inline-block; font-size: 16px; margin: 10px; }
.btn:hover { background: #0056b3; }
.btn-secondary { background: #6c757d; }
.btn-secondary:hover { background: #545b62; }
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✓</div>
<h2>送信完了</h2>
<div class="success-message">
<p><strong>お問い合わせを受け付けました。</strong></p>
<p>ご連絡いただきありがとうございます。<br>
内容を確認の上、担当者よりご連絡させていただきます。</p>
</div>
<div class="info-box">
<h4>📋 送信情報</h4>