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:
parent
904da7bad6
commit
fbc6d0de54
|
@ -14,9 +14,9 @@
|
||||||
- Playが実装されたため、ページ機能の「ソースを見る」は削除されました
|
- Playが実装されたため、ページ機能の「ソースを見る」は削除されました
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
- Enhance: ページのURLに使用可能な文字を限定するように
|
||||||
- Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正
|
- Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正
|
||||||
|
|
||||||
|
|
||||||
## 2025.1.0
|
## 2025.1.0
|
||||||
|
|
||||||
### Note
|
### Note
|
||||||
|
|
|
@ -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;
|
|
||||||
/**
|
/**
|
||||||
* ページ設定
|
* ページ設定
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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です"
|
||||||
|
|
|
@ -118,3 +118,5 @@ export class MiPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const pageNameSchema = { type: 'string', pattern: /^[^\s:\/?#\[\]@!$&'()*+,;=\\%\x00-\x20]{1,256}$/.source } as const;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue