From 40630a6272e8182bd093f22855194cba2173c6f5 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:36:05 +0900 Subject: [PATCH 1/4] wip --- .../src/server/api/endpoints/pages/create.ts | 19 +++++++++++++++-- .../src/server/api/endpoints/pages/update.ts | 21 +++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index fa03b0b457..79e9ddb6ac 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -39,6 +39,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c', }, + invalidName: { + message: 'Invalid name.', + code: 'INVALID_NAME', + id: '8702f702-f18f-4657-b50b-f746a3dffd3c', + }, nameAlreadyExists: { message: 'Specified name already exists.', code: 'NAME_ALREADY_EXISTS', @@ -81,6 +86,16 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + const trimmedName = ps.name.trim(); + + if (trimmedName.trim() === '') { + throw new ApiError(meta.errors.invalidName); + } + + if ([' ', '/', '\\', '.', '#', '&', '%', '?', '!', '+', '<', '>'].some(c => trimmedName.includes(c))) { + throw new ApiError(meta.errors.invalidName); + } + let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { eyeCatchingImage = await this.driveFilesRepository.findOneBy({ @@ -95,7 +110,7 @@ export default class extends Endpoint { // eslint- await this.pagesRepository.findBy({ userId: me.id, - name: ps.name, + name: trimmedName, }).then(result => { if (result.length > 0) { throw new ApiError(meta.errors.nameAlreadyExists); @@ -106,7 +121,7 @@ export default class extends Endpoint { // eslint- id: this.idService.gen(), updatedAt: new Date(), title: ps.title, - name: ps.name, + name: trimmedName, summary: ps.summary, content: ps.content, variables: ps.variables, diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index e52d9c32df..2fc01e41b0 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -31,13 +31,16 @@ export const meta = { code: 'NO_SUCH_PAGE', id: '21149b9e-3616-4778-9592-c4ce89f5a864', }, - accessDenied: { message: 'Access denied.', code: 'ACCESS_DENIED', id: '3c15cd52-3b4b-4274-967d-6456fc4f792b', }, - + invalidName: { + message: 'Invalid name.', + code: 'INVALID_NAME', + id: '75a78404-bcd1-4d98-9354-25c60f930e78', + }, noSuchFile: { message: 'No such file.', code: 'NO_SUCH_FILE', @@ -103,10 +106,20 @@ export default class extends Endpoint { // eslint- } if (ps.name != null) { + const trimmedName = ps.name.trim(); + + if (trimmedName.trim() === '') { + throw new ApiError(meta.errors.invalidName); + } + + if ([' ', '/', '\\', '.', '#', '&', '%', '?', '!', '+', '<', '>'].some(c => trimmedName.includes(c))) { + throw new ApiError(meta.errors.invalidName); + } + await this.pagesRepository.findBy({ id: Not(ps.pageId), userId: me.id, - name: ps.name, + name: trimmedName, }).then(result => { if (result.length > 0) { throw new ApiError(meta.errors.nameAlreadyExists); @@ -117,7 +130,7 @@ export default class extends Endpoint { // eslint- await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, - name: ps.name, + name: ps.name ? ps.name.trim() : undefined, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, From 89789af19c53280d477bc93b9a515c1b770efa58 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:39:06 +0900 Subject: [PATCH 2/4] =?UTF-8?q?param=E3=81=AE=E6=AD=A3=E8=A6=8F=E8=A1=A8?= =?UTF-8?q?=E7=8F=BE=E3=81=A7=E5=BC=BE=E3=81=8F=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/models/Page.ts | 2 ++ .../src/server/api/endpoints/pages/create.ts | 23 ++++--------------- .../src/server/api/endpoints/pages/update.ts | 22 ++++-------------- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts index 1695bf570e..0b59e7a92c 100644 --- a/packages/backend/src/models/Page.ts +++ b/packages/backend/src/models/Page.ts @@ -118,3 +118,5 @@ export class MiPage { } } } + +export const pageNameSchema = { type: 'string', pattern: /^[^\s:\/?#\[\]@!$&'()*+,;=\\%\x00-\x20]{1,256}$/.source } as const; diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 79e9ddb6ac..6de5fe3d44 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -7,7 +7,7 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import type { DriveFilesRepository, PagesRepository } from '@/models/_.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 { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,11 +39,6 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c', }, - invalidName: { - message: 'Invalid name.', - code: 'INVALID_NAME', - id: '8702f702-f18f-4657-b50b-f746a3dffd3c', - }, nameAlreadyExists: { message: 'Specified name already exists.', code: 'NAME_ALREADY_EXISTS', @@ -56,7 +51,7 @@ export const paramDef = { type: 'object', properties: { title: { type: 'string' }, - name: { type: 'string', minLength: 1 }, + name: { ...pageNameSchema, minLength: 1 }, summary: { type: 'string', nullable: true }, content: { type: 'array', items: { type: 'object', additionalProperties: true, @@ -86,16 +81,6 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const trimmedName = ps.name.trim(); - - if (trimmedName.trim() === '') { - throw new ApiError(meta.errors.invalidName); - } - - if ([' ', '/', '\\', '.', '#', '&', '%', '?', '!', '+', '<', '>'].some(c => trimmedName.includes(c))) { - throw new ApiError(meta.errors.invalidName); - } - let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { eyeCatchingImage = await this.driveFilesRepository.findOneBy({ @@ -110,7 +95,7 @@ export default class extends Endpoint { // eslint- await this.pagesRepository.findBy({ userId: me.id, - name: trimmedName, + name: ps.name, }).then(result => { if (result.length > 0) { throw new ApiError(meta.errors.nameAlreadyExists); @@ -121,7 +106,7 @@ export default class extends Endpoint { // eslint- id: this.idService.gen(), updatedAt: new Date(), title: ps.title, - name: trimmedName, + name: ps.name, summary: ps.summary, content: ps.content, variables: ps.variables, diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index 2fc01e41b0..a6aeb6002e 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -10,6 +10,7 @@ import type { PagesRepository, DriveFilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import { pageNameSchema } from '@/models/Page.js'; export const meta = { tags: ['pages'], @@ -36,11 +37,6 @@ export const meta = { code: 'ACCESS_DENIED', id: '3c15cd52-3b4b-4274-967d-6456fc4f792b', }, - invalidName: { - message: 'Invalid name.', - code: 'INVALID_NAME', - id: '75a78404-bcd1-4d98-9354-25c60f930e78', - }, noSuchFile: { message: 'No such file.', code: 'NO_SUCH_FILE', @@ -59,7 +55,7 @@ export const paramDef = { properties: { pageId: { type: 'string', format: 'misskey:id' }, title: { type: 'string' }, - name: { type: 'string', minLength: 1 }, + name: { ...pageNameSchema, minLength: 1 }, summary: { type: 'string', nullable: true }, content: { type: 'array', items: { type: 'object', additionalProperties: true, @@ -106,20 +102,10 @@ export default class extends Endpoint { // eslint- } if (ps.name != null) { - const trimmedName = ps.name.trim(); - - if (trimmedName.trim() === '') { - throw new ApiError(meta.errors.invalidName); - } - - if ([' ', '/', '\\', '.', '#', '&', '%', '?', '!', '+', '<', '>'].some(c => trimmedName.includes(c))) { - throw new ApiError(meta.errors.invalidName); - } - await this.pagesRepository.findBy({ id: Not(ps.pageId), userId: me.id, - name: trimmedName, + name: ps.name, }).then(result => { if (result.length > 0) { throw new ApiError(meta.errors.nameAlreadyExists); @@ -130,7 +116,7 @@ export default class extends Endpoint { // eslint- await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, - name: ps.name ? ps.name.trim() : undefined, + name: ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, From fec22a14d0f5bde448b03eea1fe5275737863370 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:55:53 +0900 Subject: [PATCH 3/4] =?UTF-8?q?apiWithDialog=E3=82=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 14 +-- locales/ja-JP.yml | 5 +- .../src/pages/page-editor/page-editor.vue | 113 ++++++++---------- 3 files changed, 53 insertions(+), 79 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index a0540fd228..4e26d5406b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4195,7 +4195,7 @@ export interface Locale extends ILocale { */ "invalidParamError": string; /** - * リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。 + * リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。 */ "invalidParamErrorDescription": string; /** @@ -9180,18 +9180,6 @@ export interface Locale extends ILocale { * ソースを表示中 */ "readPage": string; - /** - * ページを作成しました - */ - "created": string; - /** - * ページを更新しました - */ - "updated": string; - /** - * ページを削除しました - */ - "deleted": string; /** * ページ設定 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a578704434..13d8aec9b8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1044,7 +1044,7 @@ youCannotCreateAnymore: "これ以上作成することはできません。" cannotPerformTemporary: "一時的に利用できません" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" invalidParamError: "パラメータエラー" -invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。" +invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。" permissionDeniedError: "操作が拒否されました" permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。" preset: "プリセット" @@ -2422,9 +2422,6 @@ _pages: newPage: "ページの作成" editPage: "ページの編集" readPage: "ソースを表示中" - created: "ページを作成しました" - updated: "ページを更新しました" - deleted: "ページを削除しました" pageSetting: "ページ設定" nameAlreadyExists: "指定されたページURLは既に存在しています" invalidNameTitle: "不正なページURLです" diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index ddb808390c..c08cfebab3 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -96,7 +96,7 @@ const summary = ref(null); const name = ref(Date.now().toString()); const eyeCatchingImage = ref(null); const eyeCatchingImageId = ref(null); -const font = ref('sans-serif'); +const font = ref<'sans-serif' | 'serif'>('sans-serif'); const content = ref([]); const alignCenter = ref(false); const hideTitleWhenPinned = ref(false); @@ -113,7 +113,7 @@ watch(eyeCatchingImageId, async () => { } }); -function getSaveOptions() { +function getSaveOptions(): Misskey.entities.PagesCreateRequest { return { title: title.value.trim(), name: name.value.trim(), @@ -128,80 +128,69 @@ function getSaveOptions() { }; } -function save() { +async function save() { 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) { - options.pageId = pageId.value; - misskeyApi('pages/update', options) - .then(page => { - currentName.value = name.value.trim(); - os.alert({ - type: 'success', - text: i18n.ts._pages.updated, - }); - }).catch(onError); + const updateOptions: Misskey.entities.PagesUpdateRequest = { + pageId: pageId.value, + ...options, + }; + + await os.apiWithDialog('pages/update', updateOptions, undefined, { + '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab': { + title: i18n.ts.somethingHappened, + text: i18n.ts._pages.nameAlreadyExists, + }, + }); + + currentName.value = name.value.trim(); } else { - misskeyApi('pages/create', options) - .then(created => { - pageId.value = created.id; - currentName.value = name.value.trim(); - os.alert({ - type: 'success', - text: i18n.ts._pages.created, - }); - mainRouter.push(`/pages/edit/${pageId.value}`); - }).catch(onError); + const created = await os.apiWithDialog('pages/create', options, undefined, { + '4650348e-301c-499a-83c9-6aa988c66bc1': { + title: i18n.ts.somethingHappened, + text: i18n.ts._pages.nameAlreadyExists, + }, + }); + + pageId.value = created.id; + currentName.value = name.value.trim(); + mainRouter.replace(`/pages/edit/${pageId.value}`); } } -function del() { - os.confirm({ +async function del() { + if (!pageId.value) return; + + const { canceled } = await os.confirm({ type: 'warning', 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'; name.value = name.value + '-copy'; - misskeyApi('pages/create', getSaveOptions()).then(created => { - pageId.value = created.id; - currentName.value = name.value.trim(); - os.alert({ - type: 'success', - text: i18n.ts._pages.created, - }); - mainRouter.push(`/pages/edit/${pageId.value}`); + + const created = await os.apiWithDialog('pages/create', getSaveOptions(), undefined, { + '4650348e-301c-499a-83c9-6aa988c66bc1': { + title: i18n.ts.somethingHappened, + text: i18n.ts._pages.nameAlreadyExists, + }, }); + + pageId.value = created.id; + currentName.value = name.value.trim(); + + mainRouter.push(`/pages/edit/${pageId.value}`); } async function add() { @@ -216,7 +205,7 @@ async function add() { content.value.push({ id, type }); } -function setEyeCatchingImage(img) { +function setEyeCatchingImage(img: Event) { selectFile(img.currentTarget ?? img.target, null).then(file => { eyeCatchingImageId.value = file.id; }); From 5cfc445be06787671c8501a71a2a0f3201b66d24 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:26:45 +0900 Subject: [PATCH 4/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7cffbf4cd..bb45d01f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,9 @@ - ローカライゼーションの更新 ### Server +- Enhance: ページのURLに使用可能な文字を限定するように - Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正 - ## 2025.1.0 ### Note