feat: 新增CTC云端笔记仓库功能
- 新增多语言国际化文案支持笔记仓库模块 - 配置Universal Links与App Links支持ctc.s2ss.com域名跳转 - 实现CTS会话入口与会话时间更新逻辑 - 新增CTC笔记完整服务栈:API客户端、本地存储、同步服务 - 新增笔记编辑、预览、冲突解决、版本对比组件 - 新增二维码扫码/分享功能与路由配置 - 修复UrlAnalyzerService调用参数冗余问题 - 修复ProfileHeader组件样式问题 - 统一macOS部署目标版本为13.0 - 抑制liquid_glass_widgets高频调试日志
This commit is contained in:
@@ -1,83 +1,307 @@
|
||||
<?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');
|
||||
function save($path, $text)
|
||||
{
|
||||
file_put_contents($path, $text);
|
||||
if (!strlen($text)) {
|
||||
unlink($path);
|
||||
}
|
||||
return true;
|
||||
|
||||
// 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);
|
||||
$path = $save_path . '/' . $path_url;
|
||||
$url = $base_url . '/' . $path_url;
|
||||
if (isset($_GET['text'])) {
|
||||
$text = $_GET['text'];
|
||||
if (save($path, $text)) {
|
||||
echo("$url");
|
||||
}
|
||||
die;
|
||||
}
|
||||
if (isset($_POST['text'])) {
|
||||
$text = $_POST['text'];
|
||||
if (save($path, $text)) {
|
||||
echo("$url");
|
||||
}
|
||||
die;
|
||||
}
|
||||
$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['note']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['note']) || strlen($_GET['note']) > 64) {
|
||||
header("Location: $base_url/" . substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5));
|
||||
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]);
|
||||
}
|
||||
$path = $save_path . '/' . $_GET['note'];
|
||||
if (isset($_POST['text'])) {
|
||||
$text = $_POST['text'];
|
||||
if (save($path, $text)) {
|
||||
echo("saved");
|
||||
}
|
||||
die;
|
||||
|
||||
// 获取笔记信息(单个)
|
||||
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['text'])) {
|
||||
$text = $_GET['text'];
|
||||
if (save($path, $text)) {
|
||||
echo("saved");
|
||||
}
|
||||
die;
|
||||
|
||||
// 删除笔记
|
||||
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($path)) {
|
||||
echo(file_get_contents($path));
|
||||
} else {
|
||||
header('HTTP/1.0 404 Not Found');
|
||||
}
|
||||
die;
|
||||
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 $_GET['note'];
|
||||
?></title>
|
||||
<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($path)) {
|
||||
print htmlspecialchars(file_get_contents($path), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
?></textarea>
|
||||
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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user