This commit is contained in:
kakkokari-gtyih 2024-11-07 12:08:29 +09:00
parent 48c482f77c
commit a46b98b617
6 changed files with 196 additions and 80 deletions

View File

@ -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,
};
}
}
/**
* .
*/

View File

@ -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;

View File

@ -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;

View File

@ -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({

View File

@ -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) {

View File

@ -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;
}[];