enhance: ページslugに使用可能な文字を限定 (#15395)

* wip

* paramの正規表現で弾くように

* apiWithDialogを使用するように

* Update CHANGELOG.md

---------

Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
syuilo 2025-02-05 10:39:46 +09:00 committed by GitHub
parent 904da7bad6
commit fbc6d0de54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 60 additions and 85 deletions

View File

@ -14,9 +14,9 @@
- Playが実装されたため、ページ機能の「ソースを見る」は削除されました - Playが実装されたため、ページ機能の「ソースを見る」は削除されました
### Server ### Server
- Enhance: ページのURLに使用可能な文字を限定するように
- Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正 - Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正
## 2025.1.0 ## 2025.1.0
### Note ### Note

14
locales/index.d.ts vendored
View File

@ -4195,7 +4195,7 @@ export interface Locale extends ILocale {
*/ */
"invalidParamError": string; "invalidParamError": string;
/** /**
* *
*/ */
"invalidParamErrorDescription": string; "invalidParamErrorDescription": string;
/** /**
@ -9180,18 +9180,6 @@ export interface Locale extends ILocale {
* *
*/ */
"readPage": string; "readPage": string;
/**
*
*/
"created": string;
/**
*
*/
"updated": string;
/**
*
*/
"deleted": string;
/** /**
* *
*/ */

View File

@ -1044,7 +1044,7 @@ youCannotCreateAnymore: "これ以上作成することはできません。"
cannotPerformTemporary: "一時的に利用できません" cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
invalidParamError: "パラメータエラー" invalidParamError: "パラメータエラー"
invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。" invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。"
permissionDeniedError: "操作が拒否されました" permissionDeniedError: "操作が拒否されました"
permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。" permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。"
preset: "プリセット" preset: "プリセット"
@ -2422,9 +2422,6 @@ _pages:
newPage: "ページの作成" newPage: "ページの作成"
editPage: "ページの編集" editPage: "ページの編集"
readPage: "ソースを表示中" readPage: "ソースを表示中"
created: "ページを作成しました"
updated: "ページを更新しました"
deleted: "ページを削除しました"
pageSetting: "ページ設定" pageSetting: "ページ設定"
nameAlreadyExists: "指定されたページURLは既に存在しています" nameAlreadyExists: "指定されたページURLは既に存在しています"
invalidNameTitle: "不正なページURLです" invalidNameTitle: "不正なページURLです"

View File

@ -118,3 +118,5 @@ export class MiPage {
} }
} }
} }
export const pageNameSchema = { type: 'string', pattern: /^[^\s:\/?#\[\]@!$&'()*+,;=\\%\x00-\x20]{1,256}$/.source } as const;

View File

@ -7,7 +7,7 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, PagesRepository } from '@/models/_.js'; import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MiPage } from '@/models/Page.js'; import { MiPage, pageNameSchema } from '@/models/Page.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -51,7 +51,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
title: { type: 'string' }, title: { type: 'string' },
name: { type: 'string', minLength: 1 }, name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true }, summary: { type: 'string', nullable: true },
content: { type: 'array', items: { content: { type: 'array', items: {
type: 'object', additionalProperties: true, type: 'object', additionalProperties: true,

View File

@ -10,6 +10,7 @@ import type { PagesRepository, DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { pageNameSchema } from '@/models/Page.js';
export const meta = { export const meta = {
tags: ['pages'], tags: ['pages'],
@ -31,13 +32,11 @@ export const meta = {
code: 'NO_SUCH_PAGE', code: 'NO_SUCH_PAGE',
id: '21149b9e-3616-4778-9592-c4ce89f5a864', id: '21149b9e-3616-4778-9592-c4ce89f5a864',
}, },
accessDenied: { accessDenied: {
message: 'Access denied.', message: 'Access denied.',
code: 'ACCESS_DENIED', code: 'ACCESS_DENIED',
id: '3c15cd52-3b4b-4274-967d-6456fc4f792b', id: '3c15cd52-3b4b-4274-967d-6456fc4f792b',
}, },
noSuchFile: { noSuchFile: {
message: 'No such file.', message: 'No such file.',
code: 'NO_SUCH_FILE', code: 'NO_SUCH_FILE',
@ -56,7 +55,7 @@ export const paramDef = {
properties: { properties: {
pageId: { type: 'string', format: 'misskey:id' }, pageId: { type: 'string', format: 'misskey:id' },
title: { type: 'string' }, title: { type: 'string' },
name: { type: 'string', minLength: 1 }, name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true }, summary: { type: 'string', nullable: true },
content: { type: 'array', items: { content: { type: 'array', items: {
type: 'object', additionalProperties: true, type: 'object', additionalProperties: true,

View File

@ -96,7 +96,7 @@ const summary = ref<string | null>(null);
const name = ref(Date.now().toString()); const name = ref(Date.now().toString());
const eyeCatchingImage = ref<Misskey.entities.DriveFile | null>(null); const eyeCatchingImage = ref<Misskey.entities.DriveFile | null>(null);
const eyeCatchingImageId = ref<string | null>(null); const eyeCatchingImageId = ref<string | null>(null);
const font = ref('sans-serif'); const font = ref<'sans-serif' | 'serif'>('sans-serif');
const content = ref<Misskey.entities.Page['content']>([]); const content = ref<Misskey.entities.Page['content']>([]);
const alignCenter = ref(false); const alignCenter = ref(false);
const hideTitleWhenPinned = ref(false); const hideTitleWhenPinned = ref(false);
@ -113,7 +113,7 @@ watch(eyeCatchingImageId, async () => {
} }
}); });
function getSaveOptions() { function getSaveOptions(): Misskey.entities.PagesCreateRequest {
return { return {
title: title.value.trim(), title: title.value.trim(),
name: name.value.trim(), name: name.value.trim(),
@ -128,80 +128,69 @@ function getSaveOptions() {
}; };
} }
function save() { async function save() {
const options = getSaveOptions(); const options = getSaveOptions();
const onError = err => {
if (err.id === '3d81ceae-475f-4600-b2a8-2bc116157532') {
if (err.info.param === 'name') {
os.alert({
type: 'error',
title: i18n.ts._pages.invalidNameTitle,
text: i18n.ts._pages.invalidNameText,
});
}
} else if (err.code === 'NAME_ALREADY_EXISTS') {
os.alert({
type: 'error',
text: i18n.ts._pages.nameAlreadyExists,
});
}
};
if (pageId.value) { if (pageId.value) {
options.pageId = pageId.value; const updateOptions: Misskey.entities.PagesUpdateRequest = {
misskeyApi('pages/update', options) pageId: pageId.value,
.then(page => { ...options,
currentName.value = name.value.trim(); };
os.alert({
type: 'success', await os.apiWithDialog('pages/update', updateOptions, undefined, {
text: i18n.ts._pages.updated, '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab': {
}); title: i18n.ts.somethingHappened,
}).catch(onError); text: i18n.ts._pages.nameAlreadyExists,
},
});
currentName.value = name.value.trim();
} else { } else {
misskeyApi('pages/create', options) const created = await os.apiWithDialog('pages/create', options, undefined, {
.then(created => { '4650348e-301c-499a-83c9-6aa988c66bc1': {
pageId.value = created.id; title: i18n.ts.somethingHappened,
currentName.value = name.value.trim(); text: i18n.ts._pages.nameAlreadyExists,
os.alert({ },
type: 'success', });
text: i18n.ts._pages.created,
}); pageId.value = created.id;
mainRouter.push(`/pages/edit/${pageId.value}`); currentName.value = name.value.trim();
}).catch(onError); mainRouter.replace(`/pages/edit/${pageId.value}`);
} }
} }
function del() { async function del() {
os.confirm({ if (!pageId.value) return;
const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: title.value.trim() }), text: i18n.tsx.removeAreYouSure({ x: title.value.trim() }),
}).then(({ canceled }) => {
if (canceled) return;
misskeyApi('pages/delete', {
pageId: pageId.value,
}).then(() => {
os.alert({
type: 'success',
text: i18n.ts._pages.deleted,
});
mainRouter.push('/pages');
});
}); });
if (canceled) return;
await os.apiWithDialog('pages/delete', {
pageId: pageId.value,
});
mainRouter.replace('/pages');
} }
function duplicate() { async function duplicate() {
title.value = title.value + ' - copy'; title.value = title.value + ' - copy';
name.value = name.value + '-copy'; name.value = name.value + '-copy';
misskeyApi('pages/create', getSaveOptions()).then(created => {
pageId.value = created.id; const created = await os.apiWithDialog('pages/create', getSaveOptions(), undefined, {
currentName.value = name.value.trim(); '4650348e-301c-499a-83c9-6aa988c66bc1': {
os.alert({ title: i18n.ts.somethingHappened,
type: 'success', text: i18n.ts._pages.nameAlreadyExists,
text: i18n.ts._pages.created, },
});
mainRouter.push(`/pages/edit/${pageId.value}`);
}); });
pageId.value = created.id;
currentName.value = name.value.trim();
mainRouter.push(`/pages/edit/${pageId.value}`);
} }
async function add() { async function add() {
@ -216,7 +205,7 @@ async function add() {
content.value.push({ id, type }); content.value.push({ id, type });
} }
function setEyeCatchingImage(img) { function setEyeCatchingImage(img: Event) {
selectFile(img.currentTarget ?? img.target, null).then(file => { selectFile(img.currentTarget ?? img.target, null).then(file => {
eyeCatchingImageId.value = file.id; eyeCatchingImageId.value = file.id;
}); });