mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-28 03:40:25 +01:00
fix
This commit is contained in:
parent
48c482f77c
commit
a46b98b617
6 changed files with 196 additions and 80 deletions
|
@ -5,7 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import _Ajv from 'ajv';
|
||||
import { type PagesRepository } from '@/models/_.js';
|
||||
import { pageBlockSchema } from '@/models/Page.js';
|
||||
|
||||
/**
|
||||
* ページ関係のService
|
||||
|
@ -18,6 +20,35 @@ export class PageService {
|
|||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ページのコンテンツを検証する.
|
||||
* @param content コンテンツ
|
||||
*/
|
||||
public validatePageContent(content: unknown[]) {
|
||||
const Ajv = _Ajv.default;
|
||||
const ajv = new Ajv({
|
||||
useDefaults: true,
|
||||
});
|
||||
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||
const validator = ajv.compile({
|
||||
type: 'array',
|
||||
items: pageBlockSchema,
|
||||
});
|
||||
const valid = validator(content);
|
||||
|
||||
if (valid) {
|
||||
return {
|
||||
valid: true,
|
||||
errors: [],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
errors: validator.errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 人気のあるページ一覧を取得する.
|
||||
*/
|
||||
|
|
|
@ -120,3 +120,105 @@ export class MiPage {
|
|||
}
|
||||
|
||||
export const pageNameSchema = { type: 'string', pattern: /^[a-zA-Z0-9_-]{1,256}$/.source } as const;
|
||||
|
||||
const blockBaseSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', nullable: false },
|
||||
type: { type: 'string', nullable: false },
|
||||
},
|
||||
required: ['id', 'type'],
|
||||
} as const;
|
||||
|
||||
const textBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
type: { type: 'string', nullable: false, enum: ['text'] },
|
||||
text: { type: 'string', nullable: false },
|
||||
},
|
||||
required: [
|
||||
...blockBaseSchema.required,
|
||||
'text',
|
||||
],
|
||||
} as const;
|
||||
|
||||
const headingBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
type: { type: 'string', nullable: false, enum: ['heading'] },
|
||||
level: { type: 'number', nullable: false },
|
||||
text: { type: 'string', nullable: false },
|
||||
},
|
||||
required: [
|
||||
...blockBaseSchema.required,
|
||||
'level',
|
||||
'text',
|
||||
],
|
||||
} as const;
|
||||
|
||||
const imageBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
type: { type: 'string', nullable: false, enum: ['image'] },
|
||||
fileId: { type: 'string', nullable: true },
|
||||
},
|
||||
required: [
|
||||
...blockBaseSchema.required,
|
||||
'fileId',
|
||||
],
|
||||
} as const;
|
||||
|
||||
const noteBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
type: { type: 'string', nullable: false, enum: ['note']},
|
||||
detailed: { type: 'boolean', nullable: false },
|
||||
note: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: [
|
||||
...blockBaseSchema.required,
|
||||
'detailed',
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** @deprecated 要素を入れ子にする必要が(一旦)なくなったので非推奨。headingBlockを使用すること */
|
||||
const sectionBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
type: { type: 'string', nullable: false, enum: ['section'] },
|
||||
title: { type: 'string', nullable: false },
|
||||
children: {
|
||||
type: 'array', nullable: false,
|
||||
items: {
|
||||
oneOf: [
|
||||
textBlockSchema,
|
||||
{ $ref: '#' },
|
||||
headingBlockSchema,
|
||||
imageBlockSchema,
|
||||
noteBlockSchema,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: [
|
||||
...blockBaseSchema.required,
|
||||
'title',
|
||||
'children',
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const pageBlockSchema = {
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
textBlockSchema,
|
||||
sectionBlockSchema,
|
||||
headingBlockSchema,
|
||||
imageBlockSchema,
|
||||
noteBlockSchema,
|
||||
],
|
||||
} as const;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const blockBaseSchema = {
|
||||
const packedBlockBaseSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
|
@ -17,10 +17,10 @@ const blockBaseSchema = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
const textBlockSchema = {
|
||||
const packedTextBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
...packedBlockBaseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -33,10 +33,10 @@ const textBlockSchema = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
const headingBlockSchema = {
|
||||
const packedHeadingBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
...packedBlockBaseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -54,10 +54,10 @@ const headingBlockSchema = {
|
|||
} as const;
|
||||
|
||||
/** @deprecated 要素を入れ子にする必要が(一旦)なくなったので非推奨。headingBlockを使用すること */
|
||||
const sectionBlockSchema = {
|
||||
const packedSectionBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
...packedBlockBaseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -80,10 +80,10 @@ const sectionBlockSchema = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
const imageBlockSchema = {
|
||||
const packedImageBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
...packedBlockBaseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -96,10 +96,10 @@ const imageBlockSchema = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
const noteBlockSchema = {
|
||||
const packedNoteBlockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...blockBaseSchema.properties,
|
||||
...packedBlockBaseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -119,11 +119,11 @@ const noteBlockSchema = {
|
|||
export const packedPageBlockSchema = {
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
textBlockSchema,
|
||||
sectionBlockSchema,
|
||||
headingBlockSchema,
|
||||
imageBlockSchema,
|
||||
noteBlockSchema,
|
||||
packedTextBlockSchema,
|
||||
packedSectionBlockSchema,
|
||||
packedHeadingBlockSchema,
|
||||
packedImageBlockSchema,
|
||||
packedNoteBlockSchema,
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@ import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { MiPage, pageNameSchema } from '@/models/Page.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { PageService } from '@/core/PageService.js';
|
||||
import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { MAX_PAGE_CONTENT_BYTES } from '@/const.js';
|
||||
import { packedPageBlockSchema } from '@/models/json-schema/page.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['pages'],
|
||||
|
@ -51,6 +51,11 @@ export const meta = {
|
|||
code: 'CONTENT_TOO_LARGE',
|
||||
id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f',
|
||||
},
|
||||
invalidParam: {
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -61,7 +66,8 @@ export const paramDef = {
|
|||
name: { ...pageNameSchema, minLength: 1 },
|
||||
summary: { type: 'string', nullable: true },
|
||||
content: { type: 'array', items: {
|
||||
...packedPageBlockSchema,
|
||||
// misskey-jsの型生成に対応していないスキーマを使用しているため別途バリデーションする
|
||||
type: 'object', additionalProperties: true,
|
||||
} },
|
||||
variables: { type: 'array', items: {
|
||||
type: 'object', additionalProperties: true,
|
||||
|
@ -84,6 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private pageService: PageService,
|
||||
private pageEntityService: PageEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
|
@ -92,6 +99,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.contentTooLarge);
|
||||
}
|
||||
|
||||
const validateResult = this.pageService.validatePageContent(ps.content);
|
||||
if (!validateResult.valid) {
|
||||
const errors = validateResult.errors!;
|
||||
throw new ApiError(meta.errors.invalidParam, {
|
||||
param: errors[0].schemaPath,
|
||||
reason: errors[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
let eyeCatchingImage = null;
|
||||
if (ps.eyeCatchingImageId != null) {
|
||||
eyeCatchingImage = await this.driveFilesRepository.findOneBy({
|
||||
|
|
|
@ -11,8 +11,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { MAX_PAGE_CONTENT_BYTES } from '@/const.js';
|
||||
import { packedPageBlockSchema } from '@/models/json-schema/page.js';
|
||||
import { pageNameSchema } from '@/models/Page.js';
|
||||
import { PageService } from '@/core/PageService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['pages'],
|
||||
|
@ -56,6 +56,11 @@ export const meta = {
|
|||
code: 'CONTENT_TOO_LARGE',
|
||||
id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f',
|
||||
},
|
||||
invalidParam: {
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -67,7 +72,8 @@ export const paramDef = {
|
|||
name: { ...pageNameSchema, minLength: 1 },
|
||||
summary: { type: 'string', nullable: true },
|
||||
content: { type: 'array', items: {
|
||||
...packedPageBlockSchema,
|
||||
// misskey-jsの型生成に対応していないスキーマを使用しているため別途バリデーションする
|
||||
type: 'object', additionalProperties: true,
|
||||
} },
|
||||
variables: { type: 'array', items: {
|
||||
type: 'object', additionalProperties: true,
|
||||
|
@ -89,6 +95,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private pageService: PageService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const page = await this.pagesRepository.findOneBy({ id: ps.pageId });
|
||||
|
@ -99,8 +107,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
if (new Blob([JSON.stringify(ps.content)]).size > MAX_PAGE_CONTENT_BYTES) {
|
||||
throw new ApiError(meta.errors.contentTooLarge);
|
||||
if (ps.content != null) {
|
||||
if (new Blob([JSON.stringify(ps.content)]).size > MAX_PAGE_CONTENT_BYTES) {
|
||||
throw new ApiError(meta.errors.contentTooLarge);
|
||||
}
|
||||
|
||||
const validateResult = this.pageService.validatePageContent(ps.content);
|
||||
if (!validateResult.valid) {
|
||||
const errors = validateResult.errors!;
|
||||
throw new ApiError(meta.errors.invalidParam, {
|
||||
param: errors[0].schemaPath,
|
||||
reason: errors[0].message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.eyeCatchingImageId != null) {
|
||||
|
|
|
@ -23432,35 +23432,9 @@ export type operations = {
|
|||
title: string;
|
||||
name: string;
|
||||
summary?: string | null;
|
||||
content: (OneOf<[{
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'text';
|
||||
text?: string;
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'section';
|
||||
title?: string;
|
||||
children?: components['schemas']['PageBlock'][];
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'heading';
|
||||
level?: number;
|
||||
text?: string;
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'image';
|
||||
fileId?: string | null;
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'note';
|
||||
detailed?: boolean;
|
||||
note?: string | null;
|
||||
}]>)[];
|
||||
content: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
variables?: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
|
@ -23807,35 +23781,9 @@ export type operations = {
|
|||
title?: string;
|
||||
name?: string;
|
||||
summary?: string | null;
|
||||
content?: (OneOf<[{
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'text';
|
||||
text?: string;
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'section';
|
||||
title?: string;
|
||||
children?: components['schemas']['PageBlock'][];
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'heading';
|
||||
level?: number;
|
||||
text?: string;
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'image';
|
||||
fileId?: string | null;
|
||||
}, {
|
||||
id?: string;
|
||||
/** @enum {string} */
|
||||
type?: 'note';
|
||||
detailed?: boolean;
|
||||
note?: string | null;
|
||||
}]>)[];
|
||||
content?: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
variables?: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
|
|
Loading…
Reference in a new issue