true, 'captcha' => $captcha]); exit; } // 授权链接访问检查 $auth_view_id = null; $auth_view_token = null; $is_auth_view = false; $can_view_private = false; // 是否可以查看私密留言 if (isset($_GET['id']) && isset($_GET['token'])) { $auth_view_id = sanitize_text_field($_GET['id']); $auth_view_token = sanitize_text_field($_GET['token']); if (argon_verify_feedback_token($auth_view_id, $auth_view_token)) { $is_auth_view = true; $can_view_private = true; // 授权链接可以查看私密留言 } } // 管理员也可以查看私密留言 if ($is_admin) { $can_view_private = true; } // AJAX 处理图片上传 if (isset($_POST['feedback_action']) && $_POST['feedback_action'] === 'upload_image') { header('Content-Type: application/json; charset=utf-8'); // 验证 nonce(防止 CSRF 攻击) if (!isset($_POST['feedback_upload_nonce']) || !wp_verify_nonce($_POST['feedback_upload_nonce'], 'argon_feedback_upload')) { echo json_encode(['success' => false, 'message' => '安全验证失败']); exit; } // IP 黑名单检查 if (argon_is_ip_blocked_global()) { echo json_encode(['success' => false, 'message' => '您的 IP 已被限制访问']); exit; } // 频率限制(防止滥用) $user_identifier = argon_get_feedback_user_identifier(); $rate_limit_key = 'feedback_upload_' . $user_identifier; $upload_count = get_transient($rate_limit_key); $max_uploads = intval(get_option('argon_feedback_upload_limit', 20)); if ($upload_count !== false && $upload_count >= $max_uploads) { echo json_encode(['success' => false, 'message' => '上传过于频繁,请稍后再试']); exit; } $image_data = $_POST['image_data'] ?? ''; if (empty($image_data)) { echo json_encode(['success' => false, 'message' => '图片数据为空']); exit; } $saved_url = argon_save_feedback_image($image_data); if ($saved_url) { // 更新频率限制计数 $limit_period = intval(get_option('argon_feedback_upload_period', 3600)); if ($upload_count === false) { set_transient($rate_limit_key, 1, $limit_period); } else { set_transient($rate_limit_key, $upload_count + 1, $limit_period); } echo json_encode(['success' => true, 'url' => $saved_url]); } else { echo json_encode(['success' => false, 'message' => '图片保存失败']); } exit; } // AJAX 处理反馈管理 if (isset($_POST['feedback_action'])) { header('Content-Type: application/json; charset=utf-8'); $response = ['success' => false, 'message' => '']; $action = sanitize_text_field($_POST['feedback_action']); // 管理员操作 if (in_array($action, ['delete', 'toggle_public', 'reply', 'update_status', 'get', 'reply_and_resolve'])) { if (!$is_admin) { echo json_encode(['success' => false, 'message' => '权限不足']); exit; } if (!isset($_POST['feedback_nonce']) || !wp_verify_nonce($_POST['feedback_nonce'], 'argon_feedback_manage')) { echo json_encode(['success' => false, 'message' => '安全验证失败']); exit; } } switch ($action) { case 'submit': $response = argon_handle_feedback_submit($_POST); break; case 'delete': $id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : ''; $response = ['success' => argon_delete_feedback($id)]; break; case 'toggle_public': $id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : ''; $response = argon_toggle_feedback_public($id); break; case 'reply': $id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : ''; $reply = isset($_POST['reply']) ? sanitize_textarea_field($_POST['reply']) : ''; $is_private = !empty($_POST['is_private']); $response = argon_reply_feedback($id, $reply, false, $is_private); break; case 'reply_and_resolve': $id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : ''; $reply = isset($_POST['reply']) ? sanitize_textarea_field($_POST['reply']) : ''; $is_private = !empty($_POST['is_private']); $response = argon_reply_feedback($id, $reply, true, $is_private); break; case 'update_status': $id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : ''; $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : ''; $response = argon_update_feedback_status($id, $status, false); break; case 'get': $id = isset($_POST['id']) ? sanitize_text_field($_POST['id']) : ''; $feedback = argon_get_feedback($id); if ($feedback) { // 如果是未查看状态,自动改为已查看 if ($feedback['status'] === 'pending') { argon_update_feedback_status($id, 'viewed', false); $feedback['status'] = 'viewed'; } // 确保 images 字段存在(兼容旧数据) if (!isset($feedback['images'])) { $feedback['images'] = []; } $response = ['success' => true, 'data' => $feedback]; } else { $response = ['success' => false, 'message' => '反馈不存在']; } break; } echo json_encode($response); exit; } // ==================== 反馈数据操作函数 ==================== function argon_get_feedback_user_identifier() { if (is_user_logged_in()) { return 'user_' . get_current_user_id(); } $ip = $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip)[0]); $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; return substr(hash('sha256', $ip . '|' . $user_agent), 0, 16); } function argon_get_feedbacks($filter = 'all') { $feedbacks = get_option('argon_feedbacks', []); if (!is_array($feedbacks)) $feedbacks = []; usort($feedbacks, function($a, $b) { return ($b['created_at'] ?? 0) - ($a['created_at'] ?? 0); }); if ($filter === 'public') { return array_values(array_filter($feedbacks, function($f) { return !empty($f['is_public']); })); } return $feedbacks; } function argon_get_feedback($id) { foreach (argon_get_feedbacks() as $f) { if ($f['id'] === $id) return $f; } return null; } function argon_get_feedback_stats() { $feedbacks = argon_get_feedbacks(); $stats = ['total' => count($feedbacks), 'pending' => 0, 'viewed' => 0, 'processing' => 0, 'resolved' => 0, 'closed' => 0]; foreach ($feedbacks as $f) { $status = $f['status'] ?? 'pending'; if (isset($stats[$status])) $stats[$status]++; } return $stats; } function argon_add_feedback($data) { $feedbacks = get_option('argon_feedbacks', []); if (!is_array($feedbacks)) $feedbacks = []; $id = 'fb_' . uniqid(); // 处理图片上传 $images = []; if (!empty($data['feedback_images']) && is_array($data['feedback_images'])) { $upload_dir = wp_upload_dir(); $feedback_url_prefix = $upload_dir['baseurl'] . '/feedback-images/'; foreach ($data['feedback_images'] as $image_url) { if (!empty($image_url) && count($images) < 10) { // 验证是否是本站上传的图片 URL(已经通过 upload_image 接口上传) if (strpos($image_url, $feedback_url_prefix) === 0) { $images[] = $image_url; } } } } $feedback = [ 'id' => $id, 'title' => sanitize_text_field($data['title'] ?? ''), 'content' => sanitize_textarea_field($data['content'] ?? ''), 'type' => sanitize_text_field($data['type'] ?? 'suggestion'), 'name' => sanitize_text_field($data['name'] ?? ''), 'email' => sanitize_email($data['email'] ?? ''), 'url' => esc_url_raw($data['url'] ?? ''), 'is_public' => !empty($data['is_public']), 'status' => 'pending', 'user_identifier' => argon_get_feedback_user_identifier(), 'user_id' => is_user_logged_in() ? get_current_user_id() : 0, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'created_at' => time(), 'replies' => [], 'images' => $images, ]; $feedbacks[] = $feedback; update_option('argon_feedbacks', $feedbacks); argon_send_feedback_new_notify($feedback); return $id; } function argon_delete_feedback($id) { $feedbacks = get_option('argon_feedbacks', []); if (!is_array($feedbacks)) return false; foreach ($feedbacks as $key => $f) { if ($f['id'] === $id) { unset($feedbacks[$key]); update_option('argon_feedbacks', array_values($feedbacks)); return true; } } return false; } function argon_toggle_feedback_public($id) { $feedbacks = get_option('argon_feedbacks', []); if (!is_array($feedbacks)) return ['success' => false]; foreach ($feedbacks as &$f) { if ($f['id'] === $id) { $f['is_public'] = !$f['is_public']; update_option('argon_feedbacks', $feedbacks); return ['success' => true, 'is_public' => $f['is_public']]; } } return ['success' => false]; } function argon_reply_feedback($id, $reply, $resolve = false, $is_private = false) { $feedbacks = get_option('argon_feedbacks', []); if (!is_array($feedbacks)) return ['success' => false]; foreach ($feedbacks as &$f) { if ($f['id'] === $id) { // 检查是否已完结,已完结的反馈不允许修改状态,只能追加留言 $is_archived = in_array($f['status'], ['resolved', 'closed']); // 初始化 replies 数组(兼容旧数据) if (!isset($f['replies'])) { $f['replies'] = []; // 迁移旧的 reply 字段 if (!empty($f['reply'])) { $f['replies'][] = [ 'content' => $f['reply'], 'time' => $f['reply_at'] ?? time(), 'is_private' => false, ]; } } // 添加新留言 if (!empty($reply)) { $new_reply = [ 'content' => $reply, 'time' => time(), 'is_private' => $is_private, ]; $f['replies'][] = $new_reply; // 只有未完结的反馈才能修改状态 if (!$is_archived) { if ($resolve) { $f['status'] = 'resolved'; } elseif ($f['status'] === 'pending' || $f['status'] === 'viewed') { $f['status'] = 'processing'; } } update_option('argon_feedbacks', $feedbacks); // 发送邮件通知 if ($resolve) { argon_send_feedback_resolved_notify($f); } elseif (!$is_private) { // 只有非私密留言才发送通知 if ($is_archived) { // 已完结的反馈发送留言追加通知 argon_send_feedback_reply_append_notify($f, $reply); } else { // 未完结的反馈发送普通回复通知 argon_send_feedback_reply_notify($f, $reply); } } } else { // 没有留言内容,只修改状态 if (!$is_archived && $resolve) { $f['status'] = 'resolved'; update_option('argon_feedbacks', $feedbacks); argon_send_feedback_resolved_notify($f); } } return ['success' => true, 'is_archived' => $is_archived]; } } return ['success' => false]; } function argon_update_feedback_status($id, $status, $send_notify = true) { $valid_statuses = ['pending', 'viewed', 'processing', 'resolved', 'closed']; if (!in_array($status, $valid_statuses)) return ['success' => false, 'message' => '无效状态']; $feedbacks = get_option('argon_feedbacks', []); if (!is_array($feedbacks)) return ['success' => false]; foreach ($feedbacks as &$f) { if ($f['id'] === $id) { // 检查是否已完结 $is_archived = in_array($f['status'], ['resolved', 'closed']); if ($is_archived) { return ['success' => false, 'message' => '已完结的反馈不允许修改状态']; } $old_status = $f['status']; $f['status'] = $status; update_option('argon_feedbacks', $feedbacks); // 只有明确要求发送通知且状态变为 resolved 时才发送 if ($send_notify && $status === 'resolved' && $old_status !== 'resolved') { argon_send_feedback_resolved_notify($f); } return ['success' => true]; } } return ['success' => false]; } function argon_is_feedback_captcha_enabled() { $mode = get_option('argon_feedback_captcha_mode', 'global'); if ($mode === 'enabled') return true; if ($mode === 'disabled') return false; return function_exists('argon_is_captcha_enabled') && argon_is_captcha_enabled(); } function argon_save_feedback_image($base64_data) { // 验证 base64 数据 if (strpos($base64_data, 'data:image/') !== 0) { return false; } // 解析 base64 $data_parts = explode(',', $base64_data); if (count($data_parts) !== 2) { return false; } $image_data = base64_decode($data_parts[1]); if ($image_data === false) { return false; } // 检查大小(250KB) if (strlen($image_data) > 250 * 1024) { return false; } // 获取 MIME 类型 preg_match('/data:image\/(\w+);/', $data_parts[0], $matches); $ext = $matches[1] ?? 'jpg'; if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) { $ext = 'jpg'; } // 生成文件名 $upload_dir = wp_upload_dir(); $feedback_dir = $upload_dir['basedir'] . '/feedback-images'; if (!file_exists($feedback_dir)) { wp_mkdir_p($feedback_dir); } $filename = 'fb_' . uniqid() . '_' . time() . '.' . $ext; $filepath = $feedback_dir . '/' . $filename; // 保存文件 if (file_put_contents($filepath, $image_data) === false) { return false; } // 返回相对 URL return $upload_dir['baseurl'] . '/feedback-images/' . $filename; } function argon_handle_feedback_submit($data) { // IP 黑名单检查 if (argon_is_ip_blocked_global()) { return ['success' => false, 'message' => __('您的 IP 已被限制访问', 'argon')]; } // 提交频率限制 $user_identifier = argon_get_feedback_user_identifier(); $submit_limit_key = 'feedback_submit_' . $user_identifier; $submit_count = get_transient($submit_limit_key); $max_submits = intval(get_option('argon_feedback_submit_limit', 5)); $limit_period = intval(get_option('argon_feedback_submit_period', 3600)); if ($submit_count !== false && $submit_count >= $max_submits) { return ['success' => false, 'message' => __('提交过于频繁,请稍后再试', 'argon')]; } if (empty($data['feedback_email'])) return ['success' => false, 'message' => __('请填写邮箱', 'argon')]; if (empty($data['feedback_name'])) return ['success' => false, 'message' => __('请填写昵称', 'argon')]; if (empty($data['feedback_content'])) return ['success' => false, 'message' => __('请填写反馈内容', 'argon')]; if (!is_email($data['feedback_email'])) return ['success' => false, 'message' => __('邮箱格式错误', 'argon')]; // 验证码检查 if (argon_is_feedback_captcha_enabled()) { $captcha_type = get_option('argon_captcha_type', 'math'); if ($captcha_type === 'geetest') { if (function_exists('geetest_validate')) { $lot_number = $data['geetest_lot_number'] ?? ''; $captcha_output = $data['geetest_captcha_output'] ?? ''; $pass_token = $data['geetest_pass_token'] ?? ''; $gen_time = $data['geetest_gen_time'] ?? ''; if (empty($lot_number) || empty($captcha_output)) { return ['success' => false, 'message' => __('请完成验证码验证', 'argon')]; } $result = geetest_validate($lot_number, $captcha_output, $pass_token, $gen_time); if ($result !== true) return ['success' => false, 'message' => __('验证码验证失败', 'argon')]; } } else { $captcha_input = $data['feedback_captcha'] ?? ''; if (empty($captcha_input)) { return ['success' => false, 'message' => __('请输入验证码', 'argon')]; } if (function_exists('get_comment_captcha_answer')) { $correct_answer = get_comment_captcha_answer(); if ($captcha_input != $correct_answer) { if (function_exists('get_comment_captcha_seed')) get_comment_captcha_seed(true); return ['success' => false, 'message' => __('验证码错误', 'argon')]; } if (function_exists('get_comment_captcha_seed')) get_comment_captcha_seed(true); } } } $id = argon_add_feedback([ 'title' => $data['feedback_title'] ?? '', 'content' => $data['feedback_content'], 'type' => $data['feedback_type'] ?? 'suggestion', 'name' => $data['feedback_name'], 'email' => $data['feedback_email'], 'url' => $data['feedback_url'] ?? '', 'is_public' => !empty($data['feedback_public']), 'feedback_images' => $data['feedback_images'] ?? [], ]); if ($id) { // 更新提交频率计数 if ($submit_count === false) { set_transient($submit_limit_key, 1, $limit_period); } else { set_transient($submit_limit_key, $submit_count + 1, $limit_period); } return ['success' => true, 'message' => __('反馈提交成功,感谢您的建议!', 'argon')]; } return ['success' => false, 'message' => __('提交失败,请稍后重试', 'argon')]; } // ==================== 页面渲染 ==================== $all_feedbacks = argon_get_feedbacks(); $public_feedbacks = argon_get_feedbacks('public'); $stats = argon_get_feedback_stats(); $captcha_enabled = argon_is_feedback_captcha_enabled(); $captcha_type = get_option('argon_captcha_type', 'math'); $saved_name = $_COOKIE['comment_author_' . COOKIEHASH] ?? ''; $saved_email = $_COOKIE['comment_author_email_' . COOKIEHASH] ?? ''; $saved_url = $_COOKIE['comment_author_url_' . COOKIEHASH] ?? ''; if (is_user_logged_in()) { $current_user = wp_get_current_user(); $saved_name = $current_user->display_name; $saved_email = $current_user->user_email; $saved_url = $current_user->user_url; } $single_feedback = null; if ($is_auth_view && $auth_view_id) { $single_feedback = argon_get_feedback($auth_view_id); } get_header(); $type_labels = ['bug' => __('Bug', 'argon'), 'suggestion' => __('建议', 'argon'), 'question' => __('问题', 'argon'), 'other' => __('其他', 'argon')]; $status_labels = ['pending' => __('未查看', 'argon'), 'viewed' => __('已查看', 'argon'), 'processing' => __('进行中', 'argon'), 'resolved' => __('已完成', 'argon'), 'closed' => __('已关闭', 'argon')]; ?>

80]); ?>
$single_feedback['reply'], 'time' => $single_feedback['reply_at'] ?? time(), 'is_private' => false, ]]; } if (!empty($replies)) : foreach ($replies as $reply_item) : if (!empty($reply_item['is_private']) && !$can_view_private) : ?>
·
·
·

0) : ?>

80]); ?>
onclick="openFeedbackDetail('')">
$fb['reply'], 'time' => $fb['reply_at'] ?? time(), 'is_private' => false, ]]; } if (!empty($replies)) : foreach ($replies as $reply_item) : if (!empty($reply_item['is_private']) && !$can_view_private) : ?>
·
·
·

()

80]); ?>
100) echo '...'; ?>