文章详情

返回首页

阅后即焚-index.php

分享文章 作者: Ws01 创建时间: 2025-11-24 📝 字数: 27,402 字 👁️ 阅读: 8 次
<?php // 确保在文件最开头启动会话 if (session_status() === PHP_SESSION_NONE) { session_start(); } // 设置默认时区 date_default_timezone_set('Asia/Shanghai'); // 错误报告设置 error_reporting(E_ALL); ini_set('display_errors', 0); ini_set('log_errors', 1); ini_set('error_log', __DIR__ . '/php_errors.log'); // 配置 $CONFIG = [ 'db_path' => __DIR__ . '/messages.db', 'max_size' => 10000, 'max_lifetime' => 86400, 'cleanup_chance' => 20, 'encrypt_key' => 'your-secret-key-here' // 请更改为您的密钥 ]; // 初始化数据库 function initDatabase($dbPath) { try { $db = new SQLite3($dbPath); $db->enableExceptions(true); // 创建消息表 $db->exec(" CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, content TEXT NOT NULL, password TEXT, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, viewed INTEGER DEFAULT 0 ) "); // 创建索引 $db->exec("CREATE INDEX IF NOT EXISTS idx_expire_at ON messages(expire_at)"); $db->exec("CREATE INDEX IF NOT EXISTS idx_viewed ON messages(viewed)"); return $db; } catch (Exception $e) { error_log('Database initialization error: ' . $e->getMessage()); throw new Exception('无法初始化数据库'); } } // 获取数据库连接 function getDatabase() { global $CONFIG; static $db = null; if ($db === null) { $db = initDatabase($CONFIG['db_path']); } return $db; } // 统一JSON响应函数 function sendJsonResponse($success, $message = '', $data = []) { header('Content-Type: application/json'); $response = [ 'success' => $success, 'message' => $message, 'data' => $data ]; echo json_encode($response, JSON_UNESCAPED_UNICODE); exit; } // 加密函数 function encrypt($data, $key) { $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc')); $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv); return base64_encode($iv . $encrypted); } // 解密函数 function decrypt($data, $key) { $data = base64_decode($data); $iv = substr($data, 0, openssl_cipher_iv_length('aes-256-cbc')); $data = substr($data, openssl_cipher_iv_length('aes-256-cbc')); return openssl_decrypt($data, 'aes-256-cbc', $key, 0, $iv); } // 获取当前URL function getCurrentUrl() { $protocol = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; return $protocol . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?'); } // 清理过期消息 function cleanupOldMessages() { global $CONFIG; if (rand(1, 100) > $CONFIG['cleanup_chance']) { return; } try { $db = getDatabase(); $stmt = $db->prepare("DELETE FROM messages WHERE expire_at < ?"); $stmt->bindValue(1, time(), SQLITE3_INTEGER); $stmt->execute(); // 清理已查看的消息(超过1小时) $stmt = $db->prepare("DELETE FROM messages WHERE viewed = 1 AND created_at < ?"); $stmt->bindValue(1, time() - 3600, SQLITE3_INTEGER); $stmt->execute(); } catch (Exception $e) { error_log('Cleanup error: ' . $e->getMessage()); } } // 处理AJAX请求 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { try { // 获取并清理输入 $content = trim($_POST['content'] ?? ''); $password = trim($_POST['msg_password'] ?? ''); $expire_hours = max(1, min(720, intval($_POST['expire_hours'] ?? 24))); // 验证输入 if (empty($content)) { sendJsonResponse(false, '消息内容不能为空'); } if (strlen($content) > $CONFIG['max_size']) { sendJsonResponse(false, '消息过长,最多允许' . $CONFIG['max_size'] . '个字符'); } // 处理特殊字符 $content = htmlspecialchars_decode($content); // 生成唯一ID $id = md5(uniqid(mt_rand(), true)); // 准备存储数据 $encryptedContent = encrypt($content, $CONFIG['encrypt_key']); $createdAt = time(); $expireAt = $createdAt + ($expire_hours * 3600); $hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : null; // 存储消息到数据库 $db = getDatabase(); $stmt = $db->prepare(" INSERT INTO messages (id, content, password, created_at, expire_at, viewed) VALUES (?, ?, ?, ?, ?, 0) "); $stmt->bindValue(1, $id, SQLITE3_TEXT); $stmt->bindValue(2, $encryptedContent, SQLITE3_TEXT); $stmt->bindValue(3, $hashedPassword, SQLITE3_TEXT); $stmt->bindValue(4, $createdAt, SQLITE3_INTEGER); $stmt->bindValue(5, $expireAt, SQLITE3_INTEGER); if (!$stmt->execute()) { throw new Exception('无法保存消息到数据库'); } // 生成访问链接 $url = getCurrentUrl() . '?id=' . $id; // 返回成功响应 sendJsonResponse(true, '安全链接已生成', [ 'url' => $url, 'expire_time' => date('Y-m-d H:i:s', $expireAt), 'generation_time' => date('Y-m-d H:i:s', $createdAt) ]); } catch (Exception $e) { error_log('Error: ' . $e->getMessage()); sendJsonResponse(false, '服务器内部错误: ' . $e->getMessage()); } } // 处理消息查看请求 if (isset($_GET['id'])) { handleViewRequest(); exit; } // 处理表单提交 if ($_SERVER['REQUEST_METHOD'] === 'POST') { handlePostRequest(); } // 显示主页面 displayHomePage(); // 处理消息查看请求 function handleViewRequest() { global $CONFIG; // 安全过滤ID $id = preg_replace('/[^a-f0-9]/', '', $_GET['id']); if (strlen($id) !== 32) { displayError('无效的消息ID'); return; } try { $db = getDatabase(); $stmt = $db->prepare("SELECT * FROM messages WHERE id = ? AND viewed = 0"); $stmt->bindValue(1, $id, SQLITE3_TEXT); $result = $stmt->execute(); $message = $result->fetchArray(SQLITE3_ASSOC); if (!$message) { displayError('消息不存在或已被查看'); return; } // 检查是否过期 if ($message['expire_at'] < time()) { // 删除过期消息 $stmt = $db->prepare("DELETE FROM messages WHERE id = ?"); $stmt->bindValue(1, $id, SQLITE3_TEXT); $stmt->execute(); displayError('消息已过期'); return; } // 检查密码 if (!empty($message['password']) && !isset($_POST['password'])) { displayPasswordForm($id); return; } if (!empty($message['password']) && !password_verify($_POST['password'], $message['password'])) { displayError('密码错误'); displayPasswordForm($id); return; } // 解密内容 $content = decrypt($message['content'], $CONFIG['encrypt_key']); // 标记消息为已查看 $stmt = $db->prepare("UPDATE messages SET viewed = 1 WHERE id = ?"); $stmt->bindValue(1, $id, SQLITE3_TEXT); $stmt->execute(); // 显示消息 displayMessage($content); // 随机清理旧消息 cleanupOldMessages(); } catch (Exception $e) { error_log('View message error: ' . $e->getMessage()); displayError('服务器内部错误'); } } // 显示主页面 function displayHomePage() { ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>阅后即焚</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script> <style> .status-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; } .status-valid { background-color: #10B981; } .status-expired { background-color: #EF4444; } .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 20px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; animation: fadeInOut 3s forwards; } .toast-success { background-color: #10B981; color: white; } .toast-error { background-color: #EF4444; color: white; } @keyframes fadeInOut { 0% { opacity: 0; transform: translateX(-50%) translateY(-20px); } 10% { opacity: 1; transform: translateX(-50%) translateY(0); } 90% { opacity: 1; transform: translateX(-50%) translateY(0); } 100% { opacity: 0; transform: translateX(-50%) translateY(-20px); } } .fade-in { animation: fadeIn 0.5s; } @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .copied { background-color: #bbf7d0 !important; transition: background 0.3s; } .url-display { word-break: break-all; overflow-wrap: break-word; white-space: pre-wrap; padding: 8px; font-size: 14px; line-height: 1.4; } </style> </head> <body class="bg-gray-100 min-h-screen font-sans text-gray-800 py-4 px-4 sm:px-6"> <div class="max-w-md mx-auto"> <!-- 表单卡片 --> <div class="bg-white rounded-lg shadow p-4 mb-4"> <h1 class="text-lg font-semibold text-center mb-4">阅后即焚</h1> <form id="message-form" class="space-y-3"> <div> <label class="block text-sm text-gray-700 mb-1"> 消息内容 <span class="text-red-500">*</span> </label> <textarea name="content" id="message_content" class="w-full px-3 py-2 border rounded-md focus:ring-1 focus:ring-green-500 focus:border-green-500" placeholder="输入您要分享的秘密消息" required rows="5" ></textarea> <p class="text-xs text-gray-500 mt-1">最多允许10000个字符</p> </div> <div> <label class="block text-sm text-gray-700 mb-1"> 过期时间(小时) <span class="text-red-500">*</span> </label> <input type="number" name="expire_hours" id="expire_hours" class="w-full px-3 py-2 border rounded-md focus:ring-1 focus:ring-green-500 focus:border-green-500" value="24" min="1" max="720" required > <p class="text-xs text-gray-500 mt-1">范围:1-720小时(30天)</p> </div> <div> <label class="block text-sm text-gray-700 mb-1"> 密码保护(可选) </label> <input type="password" name="msg_password" id="msg_password" class="w-full px-3 py-2 border rounded-md focus:ring-1 focus:ring-green-500 focus:border-green-500" placeholder="设置查看密码" > </div> <button type="submit" id="submit-btn" class="w-full bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center justify-center transition-colors" > <div id="button-content"> 生成安全链接 </div> </button> </form> </div> <!-- 结果卡片 --> <div id="result-card" class="bg-white rounded-lg shadow p-4 hidden"> <div class="mb-4"> <p class="text-xs text-gray-500 text-center mb-1">安全链接</p> <div class="bg-gray-50 p-2 rounded-md"> <div onclick="copyToClipboard(this, '安全链接')" id="message-url" class="font-mono text-center cursor-pointer hover:bg-gray-100 transition-colors url-display" > 请先输入消息内容 </div> </div> <p class="text-xs text-center mt-1 text-green-500"> <span class="status-dot status-valid"></span> 有效期至: <span id="expiration-date"></span> UTC+8 </p> </div> <div id="generation-time" class="text-xs text-gray-500 mt-3 text-center"> 生成于: <span id="generation-time-value"></span> UTC+8 </div> </div> <div class="text-center text-gray-500 text-xs mt-4"> <p>阅后即焚 &copy; <?= date('Y') ?></p> <p class="mt-1"><?= $_SERVER['HTTP_HOST']; ?></p> </div> </div> <script> // 显示消息提示 function showToast(message, isSuccess = true) { const toast = $('<div>').addClass(`toast ${isSuccess ? 'toast-success' : 'toast-error'}`) .text(message); $('body').append(toast); setTimeout(() => { toast.remove(); }, 3000); } // 复制到剪贴板并高亮 function copyToClipboard(element, type) { const $el = $(element); const text = $el.text().trim(); if (text.includes('请先输入')) return; navigator.clipboard.writeText(text).then(() => { showToast(`${type}已复制`); $el.addClass('copied'); setTimeout(() => $el.removeClass('copied'), 600); }).catch(err => { showToast(`复制失败: ${err}`, false); }); } // 处理表单提交 $('#message-form').on('submit', function(e) { e.preventDefault(); const $form = $(this); const $submitBtn = $('#submit-btn'); // 禁用按钮并显示加载状态 $submitBtn.prop('disabled', true).addClass('opacity-60 cursor-not-allowed'); $submitBtn.find('#button-content').html(` <svg class="animate-spin h-5 w-5 mr-2 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path> </svg>处理中... `); $.ajax({ url: '', method: 'POST', data: $form.serialize(), dataType: 'json', beforeSend: function(xhr) { xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); }, success: function(data) { // 恢复按钮状态 $submitBtn.prop('disabled', false).removeClass('opacity-60 cursor-not-allowed'); $submitBtn.find('#button-content').html('生成安全链接'); if (data.success) { // 显示结果卡片 $('#result-card').removeClass('hidden').addClass('fade-in'); setTimeout(() => $('#result-card').removeClass('fade-in'), 600); $('#message-url').text(data.data.url); $('#expiration-date').text(data.data.expire_time); $('#generation-time-value').text(data.data.generation_time); showToast(data.message); } else { showToast(data.message || '操作失败', false); } }, error: function(xhr, status, error) { // 恢复按钮状态 $submitBtn.prop('disabled', false).removeClass('opacity-60 cursor-not-allowed'); $submitBtn.find('#button-content').html('生成安全链接'); let errorMsg = '请求失败'; try { const response = JSON.parse(xhr.responseText); errorMsg = response.message || errorMsg; } catch (e) { if (xhr.responseText.includes('<!DOCTYPE')) { errorMsg = '服务器返回了错误页面,请检查服务器日志'; } else { errorMsg = xhr.responseText || errorMsg; } } showToast(errorMsg, false); console.error('AJAX Error:', status, error, xhr.responseText); } }); }); // 自动聚焦消息内容输入框 $('#message_content').focus(); </script> </body> </html> <?php } function displayMessage($content) { ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>秘密消息 - 阅后即焚</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <style> .message-box { background-color: #f3f4f6; border-radius: 0.375rem; padding: 1rem; margin-bottom: 1rem; min-height: 4rem; word-break: break-word; white-space: pre-wrap; font-family: monospace; } .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 20px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; animation: fadeInOut 3s forwards; } .toast-success { background-color: #10B981; color: white; } .toast-error { background-color: #EF4444; color: white; } </style> </head> <body class="bg-gray-100 min-h-screen font-sans text-gray-800 py-4 px-4 sm:px-6"> <div class="max-w-md mx-auto"> <div class="bg-white rounded-lg shadow p-4 mb-4"> <h1 class="text-lg font-semibold text-center mb-4">秘密消息</h1> <div id="message-content" class="message-box"><?= htmlspecialchars($content) ?></div> <button onclick="copyMessage()" class="w-full bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md transition-colors" > 复制内容 </button> <div class="mt-4 text-center text-sm text-red-500"> <span class="inline-block w-2 h-2 rounded-full bg-red-500 mr-1"></span> 此消息已被销毁,刷新页面将无法再次查看 </div> </div> </div> <script> function showToast(message, isSuccess = true) { const toast = $('<div>').addClass(`toast ${isSuccess ? 'toast-success' : 'toast-error'}`) .text(message); $('body').append(toast); setTimeout(() => { toast.remove(); }, 3000); } function copyMessage() { const content = $('#message-content').text(); navigator.clipboard.writeText(content).then(() => { showToast('内容已复制'); }).catch(err => { showToast('复制失败: ' + err, false); }); } </script> </body> </html> <?php } // 显示错误页面 function displayError($message) { ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>错误 - 阅后即焚</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script> </head> <body class="bg-gray-100 min-h-screen font-sans text-gray-800 py-4 px-4 sm:px-6"> <div class="max-w-md mx-auto"> <div class="bg-white rounded-lg shadow p-4 mb-4"> <h1 class="text-lg font-semibold text-center mb-4">错误</h1> <div class="bg-red-50 text-red-700 p-4 rounded-md mb-4"> <?= htmlspecialchars($message) ?> </div> <a href="<?= htmlspecialchars(getCurrentUrl()) ?>" class="w-full block bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded-md text-center transition-colors" > 返回 </a> </div> <div class="text-center text-gray-500 text-xs mt-4"> <p>阅后即焚 &copy; <?= date('Y') ?></p> <p class="mt-1"><?= $_SERVER['HTTP_HOST']; ?></p> </div> </div> </body> </html> <?php } // 显示密码表单 function displayPasswordForm($id) { ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>需要密码 - 阅后即焚</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script> </head> <body class="bg-gray-100 min-h-screen font-sans text-gray-800 py-4 px-4 sm:px-6"> <div class="max-w-md mx-auto"> <div class="bg-white rounded-lg shadow p-4 mb-4"> <h1 class="text-lg font-semibold text-center mb-4">需要密码</h1> <div class="bg-blue-50 text-blue-700 p-4 rounded-md mb-4"> 此消息受密码保护,请输入密码查看 </div> <form method="POST" action="?id=<?= htmlspecialchars($id) ?>" class="space-y-3"> <div> <label class="block text-sm text-gray-700 mb-1"> 密码 <span class="text-red-500">*</span> </label> <input type="password" name="password" class="w-full px-3 py-2 border rounded-md focus:ring-1 focus:ring-green-500 focus:border-green-500" required > </div> <button type="submit" class="w-full bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md transition-colors" > 查看消息 </button> </form> </div> <div class="text-center text-gray-500 text-xs mt-4"> <p>阅后即焚 &copy; <?= date('Y') ?></p> <p class="mt-1"><?= $_SERVER['HTTP_HOST']; ?></p> </div> </div> </body> </html> <?php } // 处理表单提交(非AJAX) function handlePostRequest() { global $CONFIG; $content = trim($_POST['content'] ?? ''); $password = trim($_POST['msg_password'] ?? ''); $expire_hours = max(1, min(720, intval($_POST['expire_hours'] ?? 24))); if (empty($content)) { displayError('消息内容不能为空'); return; } if (strlen($content) > $CONFIG['max_size']) { displayError('消息过长,最多允许' . $CONFIG['max_size'] . '个字符'); return; } try { // 生成唯一ID $id = md5(uniqid(mt_rand(), true)); // 准备存储数据 $encryptedContent = encrypt($content, $CONFIG['encrypt_key']); $createdAt = time(); $expireAt = $createdAt + ($expire_hours * 3600); $hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : null; // 存储消息到数据库 $db = getDatabase(); $stmt = $db->prepare(" INSERT INTO messages (id, content, password, created_at, expire_at, viewed) VALUES (?, ?, ?, ?, ?, 0) "); $stmt->bindValue(1, $id, SQLITE3_TEXT); $stmt->bindValue(2, $encryptedContent, SQLITE3_TEXT); $stmt->bindValue(3, $hashedPassword, SQLITE3_TEXT); $stmt->bindValue(4, $createdAt, SQLITE3_INTEGER); $stmt->bindValue(5, $expireAt, SQLITE3_INTEGER); if (!$stmt->execute()) { throw new Exception('无法保存消息到数据库'); } // 生成访问链接 $url = getCurrentUrl() . '?id=' . $id; // 显示成功页面 displaySuccess($url, $expire_hours); } catch (Exception $e) { error_log('Post request error: ' . $e->getMessage()); displayError('服务器内部错误'); } } // 显示成功页面 function displaySuccess($url, $expire_hours) { $_SESSION['result'] = [ 'status' => 'success', 'message' => '安全链接已生成', 'url' => $url, 'expire_time' => date('Y-m-d H:i:s', time() + ($expire_hours * 3600)), 'generation_time' => date('Y-m-d H:i:s') ]; header('Location: ' . getCurrentUrl()); exit; }