fix
This commit is contained in:
parent
48c482f77c
commit
a46b98b617
|
@ -5,7 +5,9 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import _Ajv from 'ajv';
|
||||||
import { type PagesRepository } from '@/models/_.js';
|
import { type PagesRepository } from '@/models/_.js';
|
||||||
|
import { pageBlockSchema } from '@/models/Page.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ページ関係のService
|
* ページ関係の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;
|
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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const blockBaseSchema = {
|
const packedBlockBaseSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
|
@ -17,10 +17,10 @@ const blockBaseSchema = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const textBlockSchema = {
|
const packedTextBlockSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...blockBaseSchema.properties,
|
...packedBlockBaseSchema.properties,
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -33,10 +33,10 @@ const textBlockSchema = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const headingBlockSchema = {
|
const packedHeadingBlockSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...blockBaseSchema.properties,
|
...packedBlockBaseSchema.properties,
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -54,10 +54,10 @@ const headingBlockSchema = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** @deprecated 要素を入れ子にする必要が(一旦)なくなったので非推奨。headingBlockを使用すること */
|
/** @deprecated 要素を入れ子にする必要が(一旦)なくなったので非推奨。headingBlockを使用すること */
|
||||||
const sectionBlockSchema = {
|
const packedSectionBlockSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...blockBaseSchema.properties,
|
...packedBlockBaseSchema.properties,
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -80,10 +80,10 @@ const sectionBlockSchema = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const imageBlockSchema = {
|
const packedImageBlockSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...blockBaseSchema.properties,
|
...packedBlockBaseSchema.properties,
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -96,10 +96,10 @@ const imageBlockSchema = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const noteBlockSchema = {
|
const packedNoteBlockSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...blockBaseSchema.properties,
|
...packedBlockBaseSchema.properties,
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -119,11 +119,11 @@ const noteBlockSchema = {
|
||||||
export const packedPageBlockSchema = {
|
export const packedPageBlockSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
oneOf: [
|
oneOf: [
|
||||||
textBlockSchema,
|
packedTextBlockSchema,
|
||||||
sectionBlockSchema,
|
packedSectionBlockSchema,
|
||||||
headingBlockSchema,
|
packedHeadingBlockSchema,
|
||||||
imageBlockSchema,
|
packedImageBlockSchema,
|
||||||
noteBlockSchema,
|
packedNoteBlockSchema,
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { MiPage, pageNameSchema } 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 { PageService } from '@/core/PageService.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';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { MAX_PAGE_CONTENT_BYTES } from '@/const.js';
|
import { MAX_PAGE_CONTENT_BYTES } from '@/const.js';
|
||||||
import { packedPageBlockSchema } from '@/models/json-schema/page.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['pages'],
|
tags: ['pages'],
|
||||||
|
@ -51,6 +51,11 @@ export const meta = {
|
||||||
code: 'CONTENT_TOO_LARGE',
|
code: 'CONTENT_TOO_LARGE',
|
||||||
id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f',
|
id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f',
|
||||||
},
|
},
|
||||||
|
invalidParam: {
|
||||||
|
message: 'Invalid param.',
|
||||||
|
code: 'INVALID_PARAM',
|
||||||
|
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -61,7 +66,8 @@ export const paramDef = {
|
||||||
name: { ...pageNameSchema, minLength: 1 },
|
name: { ...pageNameSchema, minLength: 1 },
|
||||||
summary: { type: 'string', nullable: true },
|
summary: { type: 'string', nullable: true },
|
||||||
content: { type: 'array', items: {
|
content: { type: 'array', items: {
|
||||||
...packedPageBlockSchema,
|
// misskey-jsの型生成に対応していないスキーマを使用しているため別途バリデーションする
|
||||||
|
type: 'object', additionalProperties: true,
|
||||||
} },
|
} },
|
||||||
variables: { type: 'array', items: {
|
variables: { type: 'array', items: {
|
||||||
type: 'object', additionalProperties: true,
|
type: 'object', additionalProperties: true,
|
||||||
|
@ -84,6 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
|
private pageService: PageService,
|
||||||
private pageEntityService: PageEntityService,
|
private pageEntityService: PageEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
|
@ -92,6 +99,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.contentTooLarge);
|
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;
|
let eyeCatchingImage = null;
|
||||||
if (ps.eyeCatchingImageId != null) {
|
if (ps.eyeCatchingImageId != null) {
|
||||||
eyeCatchingImage = await this.driveFilesRepository.findOneBy({
|
eyeCatchingImage = await this.driveFilesRepository.findOneBy({
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { MAX_PAGE_CONTENT_BYTES } from '@/const.js';
|
import { MAX_PAGE_CONTENT_BYTES } from '@/const.js';
|
||||||
import { packedPageBlockSchema } from '@/models/json-schema/page.js';
|
|
||||||
import { pageNameSchema } from '@/models/Page.js';
|
import { pageNameSchema } from '@/models/Page.js';
|
||||||
|
import { PageService } from '@/core/PageService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['pages'],
|
tags: ['pages'],
|
||||||
|
@ -56,6 +56,11 @@ export const meta = {
|
||||||
code: 'CONTENT_TOO_LARGE',
|
code: 'CONTENT_TOO_LARGE',
|
||||||
id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f',
|
id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f',
|
||||||
},
|
},
|
||||||
|
invalidParam: {
|
||||||
|
message: 'Invalid param.',
|
||||||
|
code: 'INVALID_PARAM',
|
||||||
|
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -67,7 +72,8 @@ export const paramDef = {
|
||||||
name: { ...pageNameSchema, minLength: 1 },
|
name: { ...pageNameSchema, minLength: 1 },
|
||||||
summary: { type: 'string', nullable: true },
|
summary: { type: 'string', nullable: true },
|
||||||
content: { type: 'array', items: {
|
content: { type: 'array', items: {
|
||||||
...packedPageBlockSchema,
|
// misskey-jsの型生成に対応していないスキーマを使用しているため別途バリデーションする
|
||||||
|
type: 'object', additionalProperties: true,
|
||||||
} },
|
} },
|
||||||
variables: { type: 'array', items: {
|
variables: { type: 'array', items: {
|
||||||
type: 'object', additionalProperties: true,
|
type: 'object', additionalProperties: true,
|
||||||
|
@ -89,6 +95,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
|
private pageService: PageService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const page = await this.pagesRepository.findOneBy({ id: ps.pageId });
|
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);
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new Blob([JSON.stringify(ps.content)]).size > MAX_PAGE_CONTENT_BYTES) {
|
if (ps.content != null) {
|
||||||
throw new ApiError(meta.errors.contentTooLarge);
|
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) {
|
if (ps.eyeCatchingImageId != null) {
|
||||||
|
|
|
@ -23432,35 +23432,9 @@ export type operations = {
|
||||||
title: string;
|
title: string;
|
||||||
name: string;
|
name: string;
|
||||||
summary?: string | null;
|
summary?: string | null;
|
||||||
content: (OneOf<[{
|
content: {
|
||||||
id?: string;
|
[key: string]: unknown;
|
||||||
/** @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;
|
|
||||||
}]>)[];
|
|
||||||
variables?: {
|
variables?: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}[];
|
}[];
|
||||||
|
@ -23807,35 +23781,9 @@ export type operations = {
|
||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
summary?: string | null;
|
summary?: string | null;
|
||||||
content?: (OneOf<[{
|
content?: {
|
||||||
id?: string;
|
[key: string]: unknown;
|
||||||
/** @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;
|
|
||||||
}]>)[];
|
|
||||||
variables?: {
|
variables?: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}[];
|
}[];
|
||||||
|
|
Loading…
Reference in New Issue