Files
xianyan/docs/ctc/index.php
Developer 016ad3cea1 feat: 新增CTC云端笔记仓库功能
- 新增多语言国际化文案支持笔记仓库模块
- 配置Universal Links与App Links支持ctc.s2ss.com域名跳转
- 实现CTS会话入口与会话时间更新逻辑
- 新增CTC笔记完整服务栈:API客户端、本地存储、同步服务
- 新增笔记编辑、预览、冲突解决、版本对比组件
- 新增二维码扫码/分享功能与路由配置
- 修复UrlAnalyzerService调用参数冗余问题
- 修复ProfileHeader组件样式问题
- 统一macOS部署目标版本为13.0
- 抑制liquid_glass_widgets高频调试日志
2026-06-11 08:46:46 +08:00

308 lines
8.5 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 笔记仓库 API - 服务端 v2
* 创建时间: 2026-06-11
* 更新时间: 2026-06-11
* 作用: 基于Minimalist Web Notepad扩展的云端笔记API
* 上次更新: 修复钥匙校验、JSON模式POST、CORS预检、超1MB响应
*/
$base_url = getenv('MWN_BASE_URL') ?: 'https://ctc.s2ss.com';
$save_path = getenv('MWN_SAVE_PATH') ?: '_notes';
$max_note_size = 1048576; // 1MB
$rate_limit_file = getenv('MWN_RATE_FILE') ?: '_rate_limits';
$rate_limit_per_minute = 120; // 每IP每分钟120次提高限制
// CORS 头(在所有响应前设置)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// CORS 预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
die;
}
/**
* 钥匙格式校验
* 规则: 仅数字和字母2-64位
*/
function validate_key($key) {
return preg_match('/^[a-zA-Z0-9]{2,64}$/', $key);
}
/**
* 速率限制检查基于IP文件存储
*/
function check_rate_limit() {
global $rate_limit_file, $rate_limit_per_minute;
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$now = time();
$file = $rate_limit_file . '/' . md5($ip);
if (!is_dir($rate_limit_file)) {
@mkdir($rate_limit_file, 0777, true);
}
$data = ['count' => 0, 'window' => $now];
if (is_file($file)) {
$raw = @file_get_contents($file);
if ($raw) $data = json_decode($raw, true) ?: $data;
}
// 新窗口
if ($now - ($data['window'] ?? 0) >= 60) {
$data = ['count' => 1, 'window' => $now];
} else {
$data['count'] = ($data['count'] ?? 0) + 1;
}
@file_put_contents($file, json_encode($data));
if ($data['count'] > $rate_limit_per_minute) {
json_resp(['code' => 0, 'msg' => '请求过于频繁,请稍后再试'], 429);
}
}
/**
* 内容审核(关键词过滤)
* 返回true表示通过false表示违规
*/
function content_audit($text) {
if (empty($text)) return true;
$blocked = [
'/赌博/i', '/色情/i', '/代开发票/i', '/办证/i',
'/贷款/i', '/信用卡套现/i', '/刷单/i',
];
foreach ($blocked as $pattern) {
if (preg_match($pattern, $text)) {
return false;
}
}
return true;
}
// 执行速率限制检查
check_rate_limit();
/**
* 保存笔记内容
* 返回: 'ok' | 'size_exceeded' | 'audit_failed'
*/
function save($path, $text) {
global $max_note_size;
if (strlen($text) > $max_note_size) {
return 'size_exceeded';
}
if (!content_audit($text)) {
return 'audit_failed';
}
file_put_contents($path, $text);
if (!strlen($text)) {
@unlink($path);
}
return 'ok';
}
/**
* 返回JSON响应
*/
function json_resp($data, $code = 200) {
header('Content-Type: application/json; charset=utf-8');
http_response_code($code);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
die;
}
/**
* 获取笔记信息(大小、修改时间)
*/
function get_note_info($path, $key) {
if (!is_file($path)) {
return null;
}
$stat = stat($path);
return [
'key' => $key,
'size' => $stat['size'],
'mtime' => $stat['mtime'],
'exists' => true,
];
}
/**
* 从REQUEST_URI解析钥匙
* 支持 /key?params 和 /index.php?key?params
*/
function parse_key_from_uri() {
$uri = $_SERVER['REQUEST_URI'] ?? '';
// 去掉query string
$path = parse_url($uri, PHP_URL_PATH);
// 去掉前导/和index.php
$path = preg_replace('#^/index\.php#', '', $path);
$path = ltrim($path, '/');
return $path ?: null;
}
// ========== API 路由 ==========
// 新建随机地址笔记
if (isset($_GET['new'])) {
$path_url = substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5);
$note_path = $save_path . '/' . $path_url;
$url = $base_url . '/' . $path_url;
$text = '';
if (isset($_GET['text'])) $text = $_GET['text'];
if (isset($_POST['text'])) $text = $_POST['text'];
$result = save($note_path, $text);
if ($result === 'size_exceeded') {
json_resp(['code' => 0, 'msg' => '内容超过1MB限制'], 400);
}
if ($result === 'audit_failed') {
json_resp(['code' => 0, 'msg' => '内容包含违规信息'], 400);
}
// JSON API模式
if (isset($_GET['json'])) {
json_resp([
'code' => 1,
'msg' => 'created',
'data' => [
'key' => $path_url,
'url' => $url,
'size' => strlen($text),
]
]);
}
echo($url);
die;
}
// 检查笔记是否存在及变更(轻量接口)
if (isset($_GET['check']) && isset($_GET['keys'])) {
$keys = explode(',', $_GET['keys']);
$results = [];
foreach ($keys as $key) {
$key = trim($key);
if (!validate_key($key)) continue;
$note_path = $save_path . '/' . $key;
$info = get_note_info($note_path, $key);
$results[$key] = $info;
}
json_resp(['code' => 1, 'data' => $results]);
}
// 获取笔记信息(单个)
if (isset($_GET['info']) && isset($_GET['note'])) {
$note = $_GET['note'];
if (!validate_key($note)) {
json_resp(['code' => 0, 'msg' => '无效的钥匙格式'], 400);
}
$note_path = $save_path . '/' . $note;
$info = get_note_info($note_path, $note);
if (!$info) {
json_resp(['code' => 0, 'msg' => '笔记不存在'], 404);
}
json_resp(['code' => 1, 'data' => $info]);
}
// 删除笔记
if (isset($_GET['delete']) && isset($_GET['note'])) {
$note = $_GET['note'];
if (!validate_key($note)) {
json_resp(['code' => 0, 'msg' => '无效的钥匙格式'], 400);
}
$note_path = $save_path . '/' . $note;
if (is_file($note_path)) {
@unlink($note_path);
json_resp(['code' => 1, 'msg' => 'deleted']);
}
json_resp(['code' => 0, 'msg' => '笔记不存在'], 404);
}
// ========== 笔记路由通过URI路径解析key ==========
// 从URI解析钥匙
$route_key = parse_key_from_uri();
// 如果没有有效钥匙,重定向到随机笔记
if (!$route_key || !validate_key($route_key)) {
// 无效钥匙格式 → 如果是API请求返回400否则重定向
$is_api = isset($_GET['json']) || isset($_GET['raw']) || isset($_GET['info']);
if ($is_api) {
json_resp(['code' => 0, 'msg' => '无效的钥匙格式'], 400);
}
header("Location: $base_url/" . substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5));
die;
}
$note_path = $save_path . '/' . $route_key;
// 写入/修改笔记
if (isset($_POST['text']) || isset($_GET['text'])) {
$text = isset($_POST['text']) ? $_POST['text'] : $_GET['text'];
$result = save($note_path, $text);
if ($result === 'size_exceeded') {
json_resp(['code' => 0, 'msg' => '内容超过1MB限制'], 400);
}
if ($result === 'audit_failed') {
json_resp(['code' => 0, 'msg' => '内容包含违规信息'], 400);
}
// JSON API模式
if (isset($_GET['json'])) {
$info = get_note_info($note_path, $route_key);
json_resp([
'code' => 1,
'msg' => 'saved',
'data' => $info
]);
}
echo("saved");
die;
}
// 获取笔记原始内容
if (isset($_GET['raw'])) {
if (is_file($note_path)) {
header('Content-Type: text/plain; charset=utf-8');
echo(file_get_contents($note_path));
} else {
http_response_code(404);
echo 'Not found';
}
die;
}
// ========== 默认渲染Web编辑页面 ==========
?><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Minimalist Web Notepad ">
<title><?php print htmlspecialchars($route_key, ENT_QUOTES, 'UTF-8'); ?></title>
<link rel="shortcut icon" href="<?php print $base_url; ?>/favicon.ico">
<link rel="stylesheet" href="<?php print $base_url; ?>/styles.css">
</head>
<body>
<div class="container">
<textarea id="content"><?php
if (is_file($note_path)) {
print htmlspecialchars(file_get_contents($note_path), ENT_QUOTES, 'UTF-8');
}
?></textarea>
</div>
<pre id="printable"></pre>
<script src="<?php print $base_url; ?>/script.js"></script>
</body>
</html>