Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/wiki-shared/src/i18n/en.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions packages/wiki-shared/src/i18n/source.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions packages/wiki-shared/src/i18n/zh-CN.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/client/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function escapeHtml(str) {
// ── 미디어 태그 입력 위젯 (카테고리 입력과 동일한 UX) ──
// 컨테이너에 버블 UI + 자동완성(/api/media/search-tags)을 장착한다.
// 업로드 모달, 이미지 검색 모달, 이미지 문서 편집 모달에서 공통 사용한다.
const MEDIA_TAG_VALID_RE = /^[가-힣a-zA-Z0-9 _.-]+$/;
const MEDIA_TAG_VALID_RE = /^[\p{Script=Han}가-힣a-zA-Z0-9 _.-]+$/u;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include supplementary Han in tag prefix searches

For an accepted tag whose next character after the current query is supplementary Han, such as tag 中𠀀 with query 中, the autocomplete endpoint's upper bound ${q}\uffff in src/routes/media.ts:308-310 excludes it under SQLite's binary UTF-8 ordering because supplementary code points sort after U+FFFF. The newly allowed tag can therefore disappear from autocomplete while the user types its prefix; use a prefix query that spans the full Unicode range.

Useful? React with 👍 / 👎.

function mountMediaTagInput({ container, input, initial }) {
const tags = Array.isArray(initial) ? initial.slice() : [];

Expand Down
2 changes: 1 addition & 1 deletion src/client/edit/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1580,7 +1580,7 @@ async function addCategoryTag(tag: string): Promise<void> {
if (!window.categoryTags) window.categoryTags = [];
if (!window.categoryAclChoices) window.categoryAclChoices = {};

if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(cleanTag)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(cleanTag)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the exported shared autocomplete validator

When an application consumes the public @eoeoe2/wiki-shared/editor/autocomplete export, Chinese categories are still rejected client-side because the duplicate validator in packages/wiki-shared/src/editor/autocomplete.ts:1558 retains the Hangul/ASCII-only regex. Updating only this app-specific copy therefore leaves shared-package consumers unable to use the newly supported categories even though the server accepts them; keep the exported implementation in sync.

Useful? React with 👍 / 👎.

window.Swal?.fire({
icon: 'warning',
title: ui("m_63d43c7f9b539745"),
Expand Down
2 changes: 1 addition & 1 deletion src/client/edit/bulk-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function hookRuleDeleteButtons(container: HTMLElement, relatedTo: string) {
// 카테고리 칩 한 항목의 형식 검증 — 백엔드 CATEGORY_PATTERN 이 콤마 포함 문자열
// 전체에 한글/영문/숫자/공백/쉼표만 허용하므로, 칩 단위로는 콤마를 제외한
// 한글/영문/숫자/공백만 허용한다.
const BULK_CAT_TAG_RE = /^[가-힣a-zA-Z0-9\s]+$/;
const BULK_CAT_TAG_RE = /^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u;

interface BulkCatAcState {
visible: boolean;
Expand Down
6 changes: 3 additions & 3 deletions src/client/edit/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2871,7 +2871,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const _autoSlugCat = (() => {
if (!slug?.startsWith('카테고리:')) return null;
const n = slug.slice('카테고리:'.length).trim();
return (n && /^[가-힣a-zA-Z0-9\s]+$/.test(n)) ? n : null;
return (n && /^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u.test(n)) ? n : null;
})();
if (_autoSlugCat && !categoryTags.includes(_autoSlugCat)) {
categoryTags.unshift(_autoSlugCat);
Expand Down Expand Up @@ -3041,7 +3041,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// 카테고리 문서(카테고리:이름)는 신규 생성 시에도 해당 카테고리를 미리 주입
if (slug?.startsWith('카테고리:')) {
const _autoNewCat = slug.slice('카테고리:'.length).trim();
if (_autoNewCat && /^[가-힣a-zA-Z0-9\s]+$/.test(_autoNewCat) && !categoryTags.includes(_autoNewCat)) {
if (_autoNewCat && /^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u.test(_autoNewCat) && !categoryTags.includes(_autoNewCat)) {
categoryTags.unshift(_autoNewCat);
document.getElementById('categoryInput').value = categoryTags.join(',');
syncStateToWindow();
Expand Down Expand Up @@ -3954,7 +3954,7 @@ async function savePage() {
return;
}

if (category && !/^[가-힣a-zA-Z0-9\s,]+$/.test(category)) {
if (category && !/^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u.test(category)) {
window.Swal.fire(ui("m_0bc1fb72ae1be5c5"), ui("m_a21df18dbb6dce0c"), 'warning');
return;
}
Expand Down
4 changes: 2 additions & 2 deletions src/client/permissions/permissions-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ export async function openPermissionsModal(rawSlug: string): Promise<void> {
const addTag = (tag: string) => {
const cleanTag = tag.trim();
if (!cleanTag) return;
if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(cleanTag)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(cleanTag)) {
swal.fire({
icon: 'warning',
title: ui("m_4907df8dbe60e7cc"),
Expand Down Expand Up @@ -791,7 +791,7 @@ export async function openPermissionsModal(rawSlug: string): Promise<void> {
}
if (categoriesAction === 'add' || categoriesAction === 'set') {
const raw = readBulkCatInput();
if (!/^[가-힣a-zA-Z0-9\s_.,-]+$/.test(raw)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.,-]+$/u.test(raw)) {
swal.showValidationMessage(ui("m_35926634b2949979"));
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/admin-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ export async function dispatchAdminEditTool(c: Context<Env>, user: User, toolNam
if (slug.startsWith('이미지:')) return asTextResult(ui("m_e02821f6bffe9ca0"), true);
if (slug.startsWith('map:')) return asTextResult(ui("m_51f6566a2dc68dac"), true); if (typeof args.content !== 'string') return asTextResult(ui("m_b3104adc425b92b3"), true);
if (args.category && typeof args.category === 'string') {
if (!/^[가-힣a-zA-Z0-9\s,]+$/.test(args.category)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u.test(args.category)) {
return asTextResult(ui("m_ecc6b4ed43d3fa11"), true);
}
}
Expand Down Expand Up @@ -2178,7 +2178,7 @@ export async function dispatchAdminEditTool(c: Context<Env>, user: User, toolNam

const trimmedCategory = (args.category as string).trim();
const newCategory = trimmedCategory ? trimmedCategory : null;
if (newCategory && !/^[가-힣a-zA-Z0-9\s,]+$/.test(newCategory)) {
if (newCategory && !/^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u.test(newCategory)) {
return asTextResult(ui("m_ecc6b4ed43d3fa11"), true);
}

Expand Down
6 changes: 3 additions & 3 deletions src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ adminRoutes.put('/category-acl/:name', async (c) => {
const currentUser = c.get('user')!;
const name = c.req.param('name');
if (!name) return c.json({ error: ui("m_fd04e6fd149684a1") }, 400);
if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(name)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(name)) {
return c.json({ error: ui("m_592c3fe662fb1c68") }, 400);
}

Expand Down Expand Up @@ -1304,7 +1304,7 @@ adminRoutes.post('/category-acl/:name/bulk-apply', async (c) => {
const currentUser = c.get('user')!;
const name = c.req.param('name');
if (!name) return c.json({ error: ui("m_fd04e6fd149684a1") }, 400);
if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(name)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(name)) {
return c.json({ error: ui("m_592c3fe662fb1c68") }, 400);
}

Expand Down Expand Up @@ -2095,7 +2095,7 @@ adminRoutes.get('/pages/deleted', async (c) => {
// 관리자 전용 (이 라우터 자체가 requireAdmin 미들웨어로 보호됨).

const PREFIX_FORBIDDEN_CHARS = /[\x00-\x1F\x7F]/;
const CATEGORY_PATTERN = /^[가-힣a-zA-Z0-9\s,]+$/;
const CATEGORY_PATTERN = /^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u;

function normalizeCategoryString(s: string): string {
return s
Expand Down
6 changes: 3 additions & 3 deletions src/routes/wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export function getCategoryDocAutoCategory(slug: string): string | null {
if (!slug.startsWith(prefix)) return null;
const name = slug.slice(prefix.length).trim();
if (!name) return null;
if (!/^[가-힣a-zA-Z0-9\s]+$/.test(name)) return null;
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s]+$/u.test(name)) return null;
return name;
}

Expand Down Expand Up @@ -1094,7 +1094,7 @@ wiki.post('/w/check-category', requireAuth, async (c) => {
if (!raw) {
return c.json({ ok: false, reason: 'invalid' as const });
}
if (!/^[가-힣a-zA-Z0-9\s_.-]+$/.test(raw)) {
if (!/^[\p{Script=Han}가-힣a-zA-Z0-9\s_.-]+$/u.test(raw)) {
return c.json({ ok: false, reason: 'invalid' as const });
}

Expand Down Expand Up @@ -2189,7 +2189,7 @@ wiki.put('/w/:slug', requireAuth, async (c) => {

// 보안: 카테고리 특수문자 금지 (한글, 영문, 숫자, 공백, 쉼표만 허용)
if (body.category) {
const categoryPattern = /^[가-힣a-zA-Z0-9\s,]+$/;
const categoryPattern = /^[\p{Script=Han}가-힣a-zA-Z0-9\s,]+$/u;
if (!categoryPattern.test(body.category)) {
return c.json({ error: ui("m_a21df18dbb6dce0c") }, 400);
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/mediaTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
*/

/**
* 태그 문자열 규칙: 카테고리와 동일 (한글/영숫자/공백/_/./-).
* 태그 문자열 규칙: 중국어/한글/영숫자/공백/_/./-.
* 최대 20개, 각 50자 이내. trim, 중복 제거, 정규식 통과 항목만 유효.
* 입력은 배열 / JSON 배열 문자열 / 쉼표구분 문자열을 모두 허용한다.
*/
const TAG_VALID_RE = /^[가-힣a-zA-Z0-9 _.-]+$/;
const TAG_VALID_RE = /^[\p{Script=Han}가-힣a-zA-Z0-9 _.-]+$/u;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count supplementary Han as single tag characters

When a media tag contains newly accepted supplementary Han characters such as 𠀀, the existing trimmed.length > TAG_MAX_LENGTH check at line 41 counts each character as two UTF-16 code units. Consequently, a valid 26-character Extension-B tag is silently discarded as longer than the advertised 50-character limit, and the client performs the same incorrect check in src/client/common.ts; count Unicode code points consistently for the limit.

Useful? React with 👍 / 👎.

const TAG_MAX_COUNT = 20;
const TAG_MAX_LENGTH = 50;

Expand Down
Loading