< * 兼容: URL ?token= */ define('API_TOKEN', 'de5cdb47277f07ab81660c49ff4170c4b8a81659'); define('ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR); define('RATE_LIMIT_MAX', 20); define('RATE_LIMIT_WINDOW', 60); define('RATE_LIMIT_DIR', 'C:/BtSoft/temp/session'); function check_token() { $token = ''; $headers = []; if (function_exists('getallheaders')) $headers = getallheaders(); elseif (function_exists('apache_request_headers')) $headers = apache_request_headers(); foreach ($headers as $k => $v) { if (strtolower($k) === 'authorization') { $token = trim(str_replace('Bearer ', '', $v)); break; } } if (!$token && isset($_REQUEST['token'])) $token = $_REQUEST['token']; if ($token !== API_TOKEN) json_exit(401, 'Token 无效'); } function check_rate_limit() { $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; $log_file = RATE_LIMIT_DIR . '/api_rate_' . md5($ip) . '.json'; if (!is_dir(RATE_LIMIT_DIR)) @mkdir(RATE_LIMIT_DIR, 0755, true); $now = time(); $data = ['window_start' => $now, 'count' => 0]; if (file_exists($log_file)) $data = json_decode(file_get_contents($log_file), true) ?: $data; if ($now - $data['window_start'] > RATE_LIMIT_WINDOW) $data = ['window_start' => $now, 'count' => 0]; $data['count']++; file_put_contents($log_file, json_encode($data)); if ($data['count'] > RATE_LIMIT_MAX) json_exit(429, '请求过于频繁,请稍后重试'); } function json_exit($code, $msg, $data = null) { header('Content-Type: application/json; charset=utf-8'); $res = ['code' => $code, 'msg' => $msg]; if ($data !== null) $res['data'] = $data; exit(json_encode($res, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } function safe_path($path) { $real_root = realpath(ROOT_PATH); $requested = realpath(ROOT_PATH . ltrim($path, '/\\')); if ($requested === false || strpos($requested, $real_root) !== 0) json_exit(403, '路径不允许访问'); return $requested; } function get_db() { $secret = @include ROOT_PATH . 'config_secret.php'; if (!is_array($secret)) $secret = []; $cfg = @include ROOT_PATH . 'config.inc.php'; if (!is_array($cfg)) $cfg = []; $host = $cfg['hostname'] ?? '127.0.0.1'; $db = $cfg['database'] ?? 'pth'; $user = $cfg['username'] ?? 'pth'; $pass = $secret['DB_PASSWORD'] ?? ($cfg['password'] ?? ''); $mysqli = new mysqli($host, $user, $pass, $db); if ($mysqli->connect_error) json_exit(500, '数据库连接失败: ' . $mysqli->connect_error); $mysqli->set_charset('utf8'); return $mysqli; } // ====== 文件管理 ====== function action_ls() { $path = isset($_GET['path']) ? $_GET['path'] : '/'; $dir = safe_path($path); $items = []; $dh = opendir($dir); if (!$dh) json_exit(500, '无法打开目录'); while (($file = readdir($dh)) !== false) { if ($file === '.' || $file === '..') continue; $full = $dir . DIRECTORY_SEPARATOR . $file; $items[] = ['name' => $file, 'type' => is_dir($full) ? 'dir' : 'file', 'size' => is_file($full) ? filesize($full) : 0, 'mtime' => date('Y-m-d H:i:s', filemtime($full)), 'perm' => substr(sprintf('%o', fileperms($full)), -4)]; } closedir($dh); json_exit(200, 'ok', ['path' => $path, 'realpath' => $dir, 'items' => $items, 'total' => count($items)]); } function action_read() { $path = isset($_GET['path']) ? $_GET['path'] : ''; if (!$path) json_exit(400, '缺少 path 参数'); $file = safe_path($path); if (!is_file($file)) json_exit(404, '文件不存在'); if (!is_readable($file)) json_exit(403, '文件不可读'); $size = filesize($file); if ($size > 5 * 1024 * 1024) json_exit(413, '文件超过 5MB 限制'); $content = file_get_contents($file); json_exit(200, 'ok', ['path' => $path, 'size' => $size, 'mtime' => date('Y-m-d H:i:s', filemtime($file)), 'content' => $content]); } function action_write() { $path = isset($_POST['path']) ? $_POST['path'] : ''; $content = isset($_POST['content']) ? $_POST['content'] : ''; if (!$path) json_exit(400, '缺少 path 参数'); if (!$content && !isset($_POST['content'])) json_exit(400, '缺少 content 参数'); $file = safe_path($path); $dir = dirname($file); if (!is_dir($dir)) json_exit(404, '目标目录不存在'); if (is_file($file) && !is_writable($file)) json_exit(403, '文件不可写'); $bytes = file_put_contents($file, $content); if ($bytes === false) json_exit(500, '写入失败'); json_exit(200, '写入成功', ['path' => $path, 'bytes' => $bytes]); } function action_delete() { $path = isset($_GET['path']) ? $_GET['path'] : ''; if (!$path) json_exit(400, '缺少 path 参数'); $file = safe_path($path); if (!file_exists($file)) json_exit(404, '文件不存在'); if (is_file($file)) { unlink($file); } else { if (!rmdir($file)) json_exit(500, '目录非空,请先清空'); } json_exit(200, '删除成功'); } function action_search() { $keyword = isset($_GET['keyword']) ? $_GET['keyword'] : ''; $path = isset($_GET['path']) ? $_GET['path'] : '/'; if (!$keyword) json_exit(400, '缺少 keyword 参数'); $dir = safe_path($path); $results = []; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($it as $fileinfo) { $fn = $fileinfo->getFilename(); if ($fn === '.' || $fn === '..') continue; if (!$fileinfo->isFile() || $fileinfo->getSize() > 1024 * 1024) continue; $ext = strtolower(pathinfo($fileinfo->getPathname(), PATHINFO_EXTENSION)); if (!in_array($ext, ['php', 'html', 'htm', 'css', 'js', 'txt', 'xml', 'json'])) continue; $content = file_get_contents($fileinfo->getPathname()); if (mb_strpos($content, $keyword) !== false) { $pos = mb_strpos($content, $keyword); $start = max(0, $pos - 50); $results[] = ['file' => str_replace(ROOT_PATH, '/', str_replace('\\', '/', $fileinfo->getPathname())), 'size' => $fileinfo->getSize(), 'mtime' => date('Y-m-d H:i:s', $fileinfo->getMTime()), 'context' => mb_substr($content, $start, 150)]; } } json_exit(200, 'ok', ['keyword' => $keyword, 'total' => count($results), 'results' => $results]); } function action_replace() { $path = isset($_POST['path']) ? $_POST['path'] : ''; $find = isset($_POST['find']) ? $_POST['find'] : ''; $replace = isset($_POST['replace']) ? $_POST['replace'] : ''; if (!$path || !$find) json_exit(400, '缺少 path 或 find 参数'); $file = safe_path($path); if (!is_file($file)) json_exit(404, '文件不存在'); if (!is_writable($file)) json_exit(403, '文件不可写'); $content = file_get_contents($file); $new_content = str_replace($find, $replace, $content); if ($new_content === $content) json_exit(200, '未找到匹配内容', ['count' => 0]); $count = mb_substr_count($content, $find); file_put_contents($file . '.bak', $content); file_put_contents($file, $new_content); json_exit(200, '替换成功', ['count' => $count, 'backup' => str_replace(ROOT_PATH, '/', str_replace('\\', '/', $file . '.bak'))]); } function action_seo() { $index = ROOT_PATH . 'index.php'; if (!is_file($index)) json_exit(404, '首页文件不存在'); $content = file_get_contents($index); $issues = []; if (strpos($content, 'name="descriptions"') !== false) $issues[] = ['severity' => 'critical', 'desc' => 'meta description 拼写错误: descriptions → description', 'file' => '/index.php']; if (preg_match('//i', $content)) $issues[] = ['severity' => 'critical', 'desc' => '存在空的 description 标签', 'file' => '/index.php']; if (preg_match('/<\/title>/', $content)) $issues[] = ['severity' => 'critical', 'desc' => '页面 title 为空']; if (!preg_match('/<h[1-6]/i', $content)) $issues[] = ['severity' => 'high', 'desc' => '没有 H1-H6 标题标签', 'file' => '/index.php']; if (!preg_match('/rel="canonical"/i', $content)) $issues[] = ['severity' => 'medium', 'desc' => '缺少 canonical 标签', 'file' => '/index.php']; if (!preg_match('/application\/ld\+json/i', $content)) $issues[] = ['severity' => 'medium', 'desc' => '缺少结构化数据 (Schema)', 'file' => '/index.php']; preg_match_all('/<img[^>]+src="([^"]+)"[^>]*>/i', $content, $imgs); $noalt = []; foreach ($imgs[0] as $i => $img) { if (!preg_match('/alt\s*=/i', $img)) $noalt[] = $imgs[1][$i]; } if (!empty($noalt)) $issues[] = ['severity' => 'high', 'desc' => count($noalt) . ' 张图片缺少 alt 属性', 'file' => '/index.php', 'detail' => $noalt]; if (preg_match('/name="keywords"\s*content="([^"]*)"/i', $content, $m)) { $kwlen = mb_strlen($m[1]); if ($kwlen > 100) $issues[] = ['severity' => 'medium', 'desc' => "keywords 过长 ({$kwlen}字),建议压缩到 100 字以内", 'file' => '/index.php']; } json_exit(200, 'SEO 检查完成', ['total_issues' => count($issues), 'issues' => $issues, 'last_check' => date('Y-m-d H:i:s')]); } // ====== 新闻管理(tp_news 表) ====== function action_publish_news() { $title = isset($_POST['title']) ? trim($_POST['title']) : ''; $content = isset($_POST['content']) ? $_POST['content'] : ''; if (!$title || !$content) json_exit(400, '缺少必填参数 title 或 content'); $ty = isset($_POST['ty']) ? intval($_POST['ty']) : 0; $img1 = isset($_POST['img1']) ? trim($_POST['img1']) : ''; $introduce = isset($_POST['introduce']) ? trim($_POST['introduce']) : ''; $seokeywords = isset($_POST['seokeywords']) ? trim($_POST['seokeywords']) : ''; $seodescription = isset($_POST['seodescription']) ? trim($_POST['seodescription']) : ''; $istop = isset($_POST['istop']) ? intval($_POST['istop']) : 0; $db = get_db(); $sendtime = time(); $status = 1; $stmt = $db->prepare("INSERT INTO tp_news (title, content, ty, img1, introduce, seotitle, seokeywords, seodescription, istop, sendtime, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); if (!$stmt) json_exit(500, 'SQL 准备失败: ' . $db->error); $stmt->bind_param('ssissssssii', $title, $content, $ty, $img1, $introduce, $title, $seokeywords, $seodescription, $istop, $sendtime, $status); if (!$stmt->execute()) json_exit(500, '发布失败: ' . $stmt->error); $new_id = $stmt->insert_id; $stmt->close(); $db->close(); json_exit(200, '发布成功', ['id' => $new_id, 'url' => '/news_details/' . $new_id . '.html']); } function action_get_news() { $id = isset($_GET['id']) ? intval($_GET['id']) : 0; if (!$id) json_exit(400, '缺少 id 参数'); $db = get_db(); $stmt = $db->prepare("SELECT * FROM tp_news WHERE id = ?"); $stmt->bind_param('i', $id); $stmt->execute(); $res = $stmt->get_result(); $row = $res->fetch_assoc(); $stmt->close(); $db->close(); if (!$row) json_exit(404, '文章不存在'); json_exit(200, 'ok', $row); } function action_update_news() { $id = isset($_POST['id']) ? intval($_POST['id']) : 0; if (!$id) json_exit(400, '缺少 id 参数'); $db = get_db(); $check = $db->query("SELECT id FROM tp_news WHERE id = $id"); if (!$check || $check->num_rows === 0) { $db->close(); json_exit(404, '文章不存在'); } $fields = []; $params = []; $types = ''; $updatable = ['title'=>'s', 'content'=>'s', 'ty'=>'i', 'img1'=>'s', 'introduce'=>'s', 'seotitle'=>'s', 'seokeywords'=>'s', 'seodescription'=>'s', 'tags'=>'s', 'author'=>'s', 'istop'=>'i']; foreach ($updatable as $f => $t) { if (isset($_POST[$f])) { $fields[] = "$f = ?"; $params[] = $_POST[$f]; $types .= $t; } } if (empty($fields)) json_exit(400, '没有需要更新的字段'); $params[] = $id; $types .= 'i'; $sql = "UPDATE tp_news SET " . implode(', ', $fields) . " WHERE id = ?"; $stmt = $db->prepare($sql); if (!$stmt) json_exit(500, 'SQL 准备失败: ' . $db->error); $stmt->bind_param($types, ...$params); if (!$stmt->execute()) json_exit(500, '更新失败: ' . $stmt->error); $stmt->close(); $db->close(); json_exit(200, '更新成功', ['id' => $id]); } function action_delete_news() { $id = isset($_GET['id']) ? intval($_GET['id']) : 0; if (!$id) json_exit(400, '缺少 id 参数'); $db = get_db(); $stmt = $db->prepare("DELETE FROM tp_news WHERE id = ?"); $stmt->bind_param('i', $id); if (!$stmt->execute()) { $stmt->close(); $db->close(); json_exit(404, '文章不存在或删除失败'); } $stmt->close(); $db->close(); json_exit(200, '删除成功'); } function action_list_pinned() { $db = get_db(); $res = $db->query("SELECT id, title, introduce, ty, img1, FROM_UNIXTIME(sendtime) as addtime FROM tp_news WHERE istop = 1 ORDER BY sendtime DESC"); if (!$res) json_exit(500, '查询失败: ' . $db->error); $items = []; while ($row = $res->fetch_assoc()) $items[] = $row; $db->close(); json_exit(200, 'ok', ['total' => count($items), 'items' => $items]); } function action_upload_image() { $image_data = isset($_POST['image']) ? $_POST['image'] : ''; if (!$image_data) json_exit(400, '缺少 image 参数(base64 编码)'); if (strpos($image_data, 'base64,') !== false) $image_data = substr($image_data, strpos($image_data, 'base64,') + 7); $image_binary = base64_decode($image_data); if ($image_binary === false) json_exit(400, 'base64 解码失败'); $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_buffer($finfo, $image_binary); finfo_close($finfo); $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp']; $ext = $allowed[$mime] ?? ''; if (!$ext) json_exit(400, '不支持的图片类型: ' . $mime); if (strlen($image_binary) > 5 * 1024 * 1024) json_exit(413, '图片超过 5MB 限制'); $upload_dir = ROOT_PATH . 'uploads' . DIRECTORY_SEPARATOR . 'image' . DIRECTORY_SEPARATOR; if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true); $filename = date('Ymd_His') . '_' . mt_rand(1000, 9999) . '.' . $ext; if (!file_put_contents($upload_dir . $filename, $image_binary)) json_exit(500, '图片写入失败'); json_exit(200, '上传成功', ['url' => '/uploads/image/' . $filename]); } // ====== 讯飞语音评测/听写 密钥 ====== define('ISE_APPID', 'ef88036e'); define('ISE_APIKEY', '19ebc9fcc8657e3c3c5279e0a245cf92'); define('ISE_APISECRET', 'N2FiOTJjYjYwYjRhNjEyN2ZmNDkyM2Iy'); // ====== ISE 讯飞语音评测签名 ====== function action_ise_sign() { $host = 'ise-api.xfyun.cn'; $path = '/v2/open-ise'; $date = gmdate('D, d M Y H:i:s') . ' GMT'; $signatureOrigin = "host: {$host}\ndate: {$date}\nGET {$path} HTTP/1.1"; $signature = base64_encode(hash_hmac('sha256', $signatureOrigin, ISE_APISECRET, true)); $authOrigin = sprintf( 'api_key="%s", algorithm="%s", headers="%s", signature="%s"', ISE_APIKEY, 'hmac-sha256', 'host date request-line', $signature ); $authorization = base64_encode($authOrigin); $url = sprintf('wss://%s%s?authorization=%s&date=%s&host=%s', $host, $path, urlencode($authorization), urlencode($date), urlencode($host) ); json_exit(200, 'ok', [ 'url' => $url, 'appid' => ISE_APPID, 'host' => $host, 'date' => $date ]); } // ====== IAT 讯飞语音听写签名 ====== function action_iat_sign() { $host = 'iat-api.xfyun.cn'; $path = '/v2/iat'; $date = gmdate('D, d M Y H:i:s') . ' GMT'; $signatureOrigin = "host: {$host}\ndate: {$date}\nGET {$path} HTTP/1.1"; $signature = base64_encode(hash_hmac('sha256', $signatureOrigin, ISE_APISECRET, true)); $authOrigin = sprintf( 'api_key="%s", algorithm="%s", headers="%s", signature="%s"', ISE_APIKEY, 'hmac-sha256', 'host date request-line', $signature ); $authorization = base64_encode($authOrigin); $url = sprintf('wss://%s%s?authorization=%s&date=%s&host=%s', $host, $path, urlencode($authorization), urlencode($date), urlencode($host) ); json_exit(200, 'ok', [ 'url' => $url, 'appid' => ISE_APPID, 'host' => $host, 'date' => $date ]); } // ====== 查询测试记录 ====== function action_query_answer() { $db = get_db(); $uid = isset($_GET['uid']) ? intval($_GET['uid']) : 0; $sql = "SELECT a.*, b.realname, b.username FROM tp_answer a LEFT JOIN tp_user b ON a.user_id=b.user_id"; if ($uid) $sql .= " WHERE a.user_id=" . $uid; $sql .= " ORDER BY a.id DESC LIMIT 30"; $res = $db->query($sql); if (!$res) json_exit(500, '查询失败: ' . $db->error); $items = []; while ($row = $res->fetch_assoc()) $items[] = $row; $db->close(); json_exit(200, 'ok', ['total' => count($items), 'items' => $items]); } // ====== 主路由 ====== $action = isset($_GET['action']) ? $_GET['action'] : ''; if ($action === 'ping') { json_exit(200, 'pong', ['time' => date('Y-m-d H:i:s'), 'php_version' => phpversion()]); } check_token(); check_rate_limit(); switch ($action) { case 'ls': action_ls(); break; case 'read': action_read(); break; case 'write': action_write(); break; case 'delete': action_delete(); break; case 'search': action_search(); break; case 'replace': action_replace(); break; case 'seo': case 'seo-check': action_seo(); break; case 'publish_news': action_publish_news(); break; case 'get_news': action_get_news(); break; case 'update_news': action_update_news(); break; case 'delete_news': action_delete_news(); break; case 'list_pinned': action_list_pinned(); break; case 'upload_image': action_upload_image(); break; case 'ise_sign': action_ise_sign(); break; case 'iat_sign': action_iat_sign(); break; case 'query_answer': action_query_answer(); break; default: json_exit(400, '未知操作'); }