feat(api): 优化获取评论可能导致的DB500问题;兼容url中包含中文、空格、多编码等的评论处理
- 在 getComments、getPagePv、like、postComment 和 trackVisit 等 API 中统一使用 decodePostSlug 函数处理文章别名 - 实现更精确的文章别名匹配,支持多种别名格式的查询 - 修复了原始别名处理中的潜在安全问题 - 移除了过时的 LIKE 查询模式,提高查询效率 - 统一了所有涉及文章别名的参数验证逻辑 (cherry picked from commit b9b944d7aff3f2e6008e52ba8f515bc451cfc32d)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -175,3 +175,7 @@ wrangler.jsonc
|
||||
.trae
|
||||
|
||||
.npmrc
|
||||
|
||||
# idea
|
||||
|
||||
.idea
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Context } from 'hono'
|
||||
import { Bindings } from '../../bindings'
|
||||
import { getCravatar } from '../../utils/getAvatar'
|
||||
import { decodePostSlug, getAllSlugFormats, escapeLikePattern } from '../../utils/decodePostSlug'
|
||||
|
||||
export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
const rawPostSlug = c.req.query('post_slug') || ''
|
||||
const postSlug = rawPostSlug.trim()
|
||||
const postSlug = decodePostSlug(rawPostSlug)
|
||||
const page = parseInt(c.req.query('page') || '1')
|
||||
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50)
|
||||
const nested = c.req.query('nested') !== 'false'
|
||||
@@ -42,13 +43,14 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const equalSlugs = Array.from(new Set(slugList))
|
||||
const likePatternsSet = new Set<string>()
|
||||
for (const s of equalSlugs) {
|
||||
likePatternsSet.add(`${s}#%`)
|
||||
likePatternsSet.add(`${s}?%`)
|
||||
const allSlugFormats = new Set<string>()
|
||||
for (const s of slugList) {
|
||||
for (const format of getAllSlugFormats(s)) {
|
||||
allSlugFormats.add(format)
|
||||
}
|
||||
}
|
||||
const likePatterns = Array.from(likePatternsSet)
|
||||
const equalSlugs = Array.from(allSlugFormats)
|
||||
|
||||
const whereParts: string[] = []
|
||||
if (equalSlugs.length === 1) {
|
||||
whereParts.push('post_slug = ?')
|
||||
@@ -56,16 +58,13 @@ export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
const placeholders = equalSlugs.map(() => '?').join(', ')
|
||||
whereParts.push(`post_slug IN (${placeholders})`)
|
||||
}
|
||||
for (let i = 0; i < likePatterns.length; i += 1) {
|
||||
whereParts.push('post_slug LIKE ?')
|
||||
}
|
||||
const whereClause =
|
||||
whereParts.length > 0
|
||||
? `status = "approved" AND (${whereParts.join(' OR ')})`
|
||||
: 'status = "approved"'
|
||||
? `(${whereParts.join(' OR ')})`
|
||||
: '1=1'
|
||||
|
||||
let finalWhereClause = whereClause
|
||||
const bindParams: unknown[] = [...equalSlugs, ...likePatterns]
|
||||
let finalWhereClause = `status = "approved" AND ${whereClause}`
|
||||
const bindParams: unknown[] = [...equalSlugs]
|
||||
|
||||
if (siteId) {
|
||||
finalWhereClause += ' AND site_id = ?'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { Bindings } from '../../bindings';
|
||||
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||
|
||||
export const getPagePv = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
try {
|
||||
const rawPostSlug = c.req.query('post_slug') || '';
|
||||
const postSlug = rawPostSlug.trim();
|
||||
const postSlug = decodePostSlug(rawPostSlug);
|
||||
const rawSiteId = c.req.query('siteId') || '';
|
||||
const siteId = rawSiteId && rawSiteId !== 'default' ? rawSiteId : '';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { Bindings } from '../../bindings';
|
||||
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||
|
||||
type LikeStatusResponse = {
|
||||
liked: boolean;
|
||||
@@ -36,7 +37,7 @@ export const getLikeStatus = async (
|
||||
): Promise<Response> => {
|
||||
try {
|
||||
const rawPostSlug = c.req.query('post_slug') || '';
|
||||
const postSlug = rawPostSlug.trim();
|
||||
const postSlug = decodePostSlug(rawPostSlug);
|
||||
const siteId = c.req.query('siteId') || '';
|
||||
|
||||
if (!postSlug) {
|
||||
@@ -92,13 +93,14 @@ export const likePage = async (
|
||||
|
||||
const rawPostSlug =
|
||||
typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
||||
const postSlug = decodePostSlug(rawPostSlug);
|
||||
const rawPostTitle =
|
||||
typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||
const rawPostUrl =
|
||||
typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||
const siteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
||||
|
||||
if (!rawPostSlug) {
|
||||
if (!postSlug) {
|
||||
return c.json({ message: 'postSlug is required' }, 400);
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export const likePage = async (
|
||||
const existingLike = await c.env.CWD_DB.prepare(
|
||||
'SELECT id FROM Likes WHERE page_slug = ? AND user_id = ? AND site_id = ?'
|
||||
)
|
||||
.bind(rawPostSlug, userId, siteId)
|
||||
.bind(postSlug, userId, siteId)
|
||||
.first<{ id: number }>();
|
||||
|
||||
let alreadyLiked = false;
|
||||
@@ -118,7 +120,7 @@ export const likePage = async (
|
||||
await c.env.CWD_DB.prepare(
|
||||
'INSERT INTO Likes (page_slug, user_id, created_at, site_id) VALUES (?, ?, ?, ?)'
|
||||
)
|
||||
.bind(rawPostSlug, userId, now, siteId)
|
||||
.bind(postSlug, userId, now, siteId)
|
||||
.run();
|
||||
} else {
|
||||
alreadyLiked = true;
|
||||
@@ -127,7 +129,7 @@ export const likePage = async (
|
||||
const pageStatsRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT id FROM page_stats WHERE post_slug = ? AND site_id = ?'
|
||||
)
|
||||
.bind(rawPostSlug, siteId)
|
||||
.bind(postSlug, siteId)
|
||||
.first<{ id: number }>();
|
||||
|
||||
if (!pageStatsRow) {
|
||||
@@ -135,7 +137,7 @@ export const likePage = async (
|
||||
'INSERT INTO page_stats (post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at, site_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
rawPostSlug,
|
||||
postSlug,
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
0,
|
||||
@@ -161,7 +163,7 @@ export const likePage = async (
|
||||
const totalRow = await c.env.CWD_DB.prepare(
|
||||
'SELECT COUNT(*) AS count FROM Likes WHERE page_slug = ? AND site_id = ?'
|
||||
)
|
||||
.bind(rawPostSlug, siteId)
|
||||
.bind(postSlug, siteId)
|
||||
.first<{ count: number }>();
|
||||
|
||||
const totalLikes = totalRow?.count || 0;
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
EmailNotificationSettings
|
||||
} from '../../utils/email';
|
||||
import { loadTelegramSettings, sendTelegramMessage } from '../../utils/telegram';
|
||||
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||
|
||||
// 检查内容,将<script>标签之间的内容删除
|
||||
export function checkContent(content: string): string {
|
||||
return content.replace(/<script[\s\S]*?<\/script>/g, "");
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return c.json({ message: '无效的请求体' }, 400);
|
||||
}
|
||||
const { post_slug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data;
|
||||
const { post_slug: rawPostSlug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data;
|
||||
const post_slug = decodePostSlug(rawPostSlug || '');
|
||||
const site_id = data.site_id ? String(data.site_id).trim() : "";
|
||||
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
|
||||
if (!post_slug || typeof post_slug !== 'string') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Context } from 'hono';
|
||||
import type { Bindings } from '../../bindings';
|
||||
import { decodePostSlug } from '../../utils/decodePostSlug';
|
||||
|
||||
type TrackVisitBody = {
|
||||
postSlug?: string;
|
||||
@@ -32,11 +33,12 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
const body = (await c.req.json().catch(() => ({}))) as TrackVisitBody;
|
||||
|
||||
const rawPostSlug = typeof body.postSlug === 'string' ? body.postSlug.trim() : '';
|
||||
const postSlug = decodePostSlug(rawPostSlug);
|
||||
const rawPostTitle = typeof body.postTitle === 'string' ? body.postTitle.trim() : '';
|
||||
const rawPostUrl = typeof body.postUrl === 'string' ? body.postUrl.trim() : '';
|
||||
const rawSiteId = typeof body.siteId === 'string' ? body.siteId.trim() : '';
|
||||
|
||||
if (!rawPostSlug) {
|
||||
if (!postSlug) {
|
||||
return c.json({ message: 'postSlug is required' }, 400);
|
||||
}
|
||||
|
||||
@@ -47,10 +49,9 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
|
||||
const domain =
|
||||
extractDomain(rawPostUrl) ||
|
||||
extractDomain(rawPostSlug) ||
|
||||
extractDomain(postSlug) ||
|
||||
null;
|
||||
|
||||
// Upsert page_stats using ON CONFLICT (site_id, post_slug)
|
||||
await c.env.CWD_DB.prepare(
|
||||
`INSERT INTO page_stats (site_id, post_slug, post_title, post_url, pv, last_visit_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 1, ?, ?, ?)
|
||||
@@ -63,7 +64,7 @@ export const trackVisit = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
)
|
||||
.bind(
|
||||
rawSiteId,
|
||||
rawPostSlug,
|
||||
postSlug,
|
||||
rawPostTitle || null,
|
||||
rawPostUrl || null,
|
||||
nowTs,
|
||||
|
||||
37
cwd-api/src/utils/decodePostSlug.ts
Normal file
37
cwd-api/src/utils/decodePostSlug.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export function decodePostSlug(slug: string): string {
|
||||
if (!slug) return slug;
|
||||
let decoded = slug.trim();
|
||||
let prev = '';
|
||||
while (prev !== decoded) {
|
||||
prev = decoded;
|
||||
try {
|
||||
decoded = decodeURIComponent(decoded);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function getAllSlugFormats(slug: string): string[] {
|
||||
if (!slug) return [];
|
||||
const decoded = decodePostSlug(slug);
|
||||
const formats = new Set<string>();
|
||||
|
||||
formats.add(decoded);
|
||||
|
||||
if (decoded !== slug) {
|
||||
formats.add(slug);
|
||||
}
|
||||
|
||||
const encoded = encodeURI(decoded);
|
||||
if (encoded !== decoded && !formats.has(encoded)) {
|
||||
formats.add(encoded);
|
||||
}
|
||||
|
||||
return Array.from(formats);
|
||||
}
|
||||
|
||||
export function escapeLikePattern(str: string): string {
|
||||
return str.replace(/[%_]/g, '|$&');
|
||||
}
|
||||
Reference in New Issue
Block a user