0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(覚書)メールフォーム最後まで

Posted at

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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?