diff --git a/src/routes/proposal.ts b/src/routes/proposal.ts index b5054fb30..9daa7317a 100644 --- a/src/routes/proposal.ts +++ b/src/routes/proposal.ts @@ -83,7 +83,14 @@ import Time from '../utils/date/Time' import { ErrorCategory } from '../utils/errorCategories' import { isProdEnv } from '../utils/governanceEnvs' import logger from '../utils/logger' -import { validateAddress, validateId, validateIsDaoCommittee, validateStatusUpdate } from '../utils/validations' +import { + isValidImage, + validateAddress, + validateId, + validateIsDaoCommittee, + validateObjectMarkdownImages, + validateStatusUpdate, +} from '../utils/validations' const PITCH_PROPOSAL_SUBMIT_ENABLED = false const GRANT_PROPOSAL_SUBMIT_ENABLED = false @@ -481,6 +488,10 @@ export async function createProposalBid(req: WithAuth) { /* eslint-disable @typescript-eslint/no-explicit-any */ export async function createProposal(proposalInCreation: ProposalInCreation) { + const { isValid, errors } = await validateObjectMarkdownImages(proposalInCreation) + if (!isValid) { + throw new RequestError(`One or more images are not valid: ${errors.join(', ')}`, RequestError.BadRequest) + } try { return await ProposalService.createProposal(proposalInCreation) } catch (error: any) { @@ -600,17 +611,5 @@ async function validateSubmissionThreshold(user: string, submissionThreshold?: s async function checkImage(req: Request) { const imageUrl = req.query.url as string - const allowedImageTypes = new Set(['image/bmp', 'image/jpeg', 'image/png', 'image/webp']) - - return new Promise((resolve) => { - fetch(imageUrl) - .then((response) => { - const mime = response.headers.get('content-type') - resolve(!!mime && allowedImageTypes.has(mime)) - }) - .catch((error) => { - logger.error('Fetching image error', error) - resolve(false) - }) - }) + return await isValidImage(imageUrl) } diff --git a/src/utils/validations.ts b/src/utils/validations.ts index 75fa2fad0..ada482352 100644 --- a/src/utils/validations.ts +++ b/src/utils/validations.ts @@ -16,6 +16,7 @@ import { validateUniqueAddresses } from '../entities/Transparency/utils' import { ErrorService } from '../services/ErrorService' import { ProjectService } from '../services/ProjectService' import { EventFilterSchema } from '../shared/types/events' +import logger from '../utils/logger' import { ErrorCategory } from './errorCategories' @@ -237,3 +238,89 @@ export function stringToBoolean(str: string) { throw new Error('Invalid boolean value') } } + +export async function isValidImage(imageUrl: string) { + const allowedImageTypes = new Set(['image/bmp', 'image/jpeg', 'image/png', 'image/webp']) + + return new Promise((resolve) => { + fetch(imageUrl) + .then((response) => { + const mime = response.headers.get('content-type') + resolve(!!mime && allowedImageTypes.has(mime)) + }) + .catch((error) => { + logger.error('Fetching image error', error) + resolve(false) + }) + }) +} + +export async function valdidateImagesUrls(proposalSection: string) { + const imageUrls = extractImageUrls(proposalSection) + + const errors: string[] = [] + for (const imageUrl of imageUrls) { + const isValid = await isValidImage(imageUrl) + if (!isValid) { + errors.push(imageUrl) + } + } + + return { + isValid: errors.length === 0, + errors, + } +} + +export function extractImageUrls(markdown: string): string[] { + const imageRegex = /!\[.*?\]\((.*?)\)|\[.*?\]:\s*(.*?)(?:\s|$)/g + const urls: string[] = [] + let match + + while ((match = imageRegex.exec(markdown)) !== null) { + const url = match[1] || match[2] + if (url) urls.push(url) + } + + return urls +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function validateImagesOnValue(value: any, errors: string[]) { + if (typeof value === 'string') { + const imageUrls = extractImageUrls(value) + for (const imageUrl of imageUrls) { + const isValid = await isValidImage(imageUrl) + if (!isValid) { + errors.push(imageUrl) + } + } + } else if (value && typeof value === 'object') { + await validateImagesOnObject(value, errors) + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function validateImagesOnObject(obj: any, errors: string[]) { + if (Array.isArray(obj)) { + for (const item of obj) { + await validateImagesOnValue(item, errors) + } + } else { + for (const key in obj) { + await validateImagesOnValue(obj[key], errors) + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function validateObjectMarkdownImages(obj: any): Promise<{ isValid: boolean; errors: string[] }> { + const errors: string[] = [] + + await validateImagesOnObject(obj, errors) + + return { + isValid: errors.length === 0, + errors, + } +}