From da43ea66a64e48f8ebded80434cdc27c3bcc9efc Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:41:40 -0600 Subject: [PATCH] Add byte length checks to poll question and options during create Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- _locales/en/messages.json | 8 +++++++ ts/components/PollCreateModal.dom.tsx | 33 +++++++++++++++++++-------- ts/types/Polls.dom.ts | 15 +++++++++++- ts/util/longAttachment.std.ts | 2 +- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 581e28fcdb..0adeaaf804 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1534,6 +1534,14 @@ "messageformat": "Poll requires at least 2 options", "description": "Error message shown when poll has fewer than 2 non-empty options" }, + "icu:PollCreateModal__Error--QuestionTooLong": { + "messageformat": "Question is too long", + "description": "Error message shown when poll question string exceeds the byte limit" + }, + "icu:PollCreateModal__Error--OptionTooLong": { + "messageformat": "Option is too long", + "description": "Error message shown when poll option string exceeds the byte limit" + }, "icu:deleteConversation": { "messageformat": "Delete", "description": "Menu item for deleting a conversation (including messages), title case." diff --git a/ts/components/PollCreateModal.dom.tsx b/ts/components/PollCreateModal.dom.tsx index 93c28d7a52..2165411e33 100644 --- a/ts/components/PollCreateModal.dom.tsx +++ b/ts/components/PollCreateModal.dom.tsx @@ -23,6 +23,7 @@ import { POLL_OPTIONS_MAX_COUNT, } from '../types/Polls.dom.js'; import { count as countGraphemes } from '../util/grapheme.std.js'; +import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js'; type PollOption = { id: string; @@ -212,20 +213,34 @@ export function PollCreateModal({ hasQuestionError: boolean; hasOptionsError: boolean; } => { - const errors: Array = []; - const hasQuestionError = !question.trim(); - const nonEmptyOptions = options.filter(opt => opt.value.trim()); - const hasOptionsError = nonEmptyOptions.length < POLL_OPTIONS_MIN_COUNT; + const questionErrors: Array = []; + const optionErrors: Array = []; - if (hasQuestionError) { - errors.push(i18n('icu:PollCreateModal__Error--RequiresQuestion')); + const questionValue = question.trim(); + if (!questionValue) { + questionErrors.push(i18n('icu:PollCreateModal__Error--RequiresQuestion')); + } + if (Buffer.byteLength(questionValue) > MAX_MESSAGE_BODY_BYTE_LENGTH) { + questionErrors.push(i18n('icu:PollCreateModal__Error--QuestionTooLong')); } - if (hasOptionsError) { - errors.push(i18n('icu:PollCreateModal__Error--RequiresTwoOptions')); + const optionValues = options.map(opt => opt.value.trim()); + const nonEmptyOptions = optionValues.filter(value => value); + if (nonEmptyOptions.length < POLL_OPTIONS_MIN_COUNT) { + optionErrors.push(i18n('icu:PollCreateModal__Error--RequiresTwoOptions')); + } + const optionOverByteLength = optionValues.find( + value => Buffer.byteLength(value) > MAX_MESSAGE_BODY_BYTE_LENGTH + ); + if (optionOverByteLength) { + optionErrors.push(i18n('icu:PollCreateModal__Error--OptionTooLong')); } - return { errors, hasQuestionError, hasOptionsError }; + return { + errors: questionErrors.concat(optionErrors), + hasQuestionError: questionErrors.length > 0, + hasOptionsError: optionErrors.length > 0, + }; }, [question, options, i18n]); const handleSend = useCallback(() => { diff --git a/ts/types/Polls.dom.ts b/ts/types/Polls.dom.ts index c6f8af96f7..ce170d06f4 100644 --- a/ts/types/Polls.dom.ts +++ b/ts/types/Polls.dom.ts @@ -12,6 +12,7 @@ import * as RemoteConfig from '../RemoteConfig.dom.js'; import { isAlpha, isBeta, isProduction } from '../util/version.std.js'; import type { SendStateByConversationId } from '../messages/MessageSendState.std.js'; import { aciSchema } from './ServiceId.std.js'; +import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js'; export const POLL_QUESTION_MAX_LENGTH = 100; export const POLL_OPTIONS_MIN_COUNT = 2; @@ -28,7 +29,13 @@ export const PollCreateSchema = z .min(1) .refine(value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH), { message: `question must contain at most ${POLL_QUESTION_MAX_LENGTH} characters`, - }), + }) + .refine( + value => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH, + { + message: `question must contain at most ${MAX_MESSAGE_BODY_BYTE_LENGTH} bytes`, + } + ), options: z .array( z @@ -40,6 +47,12 @@ export const PollCreateSchema = z message: `option must contain at most ${POLL_QUESTION_MAX_LENGTH} characters`, } ) + .refine( + value => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH, + { + message: `option must contain at most ${MAX_MESSAGE_BODY_BYTE_LENGTH} bytes`, + } + ) ) .min(POLL_OPTIONS_MIN_COUNT) .max(POLL_OPTIONS_MAX_COUNT) diff --git a/ts/util/longAttachment.std.ts b/ts/util/longAttachment.std.ts index fa2baff9ab..164d016e46 100644 --- a/ts/util/longAttachment.std.ts +++ b/ts/util/longAttachment.std.ts @@ -4,7 +4,7 @@ import { unicodeSlice } from './unicodeSlice.std.js'; const KIBIBYTE = 1024; -const MAX_MESSAGE_BODY_BYTE_LENGTH = 2 * KIBIBYTE; +export const MAX_MESSAGE_BODY_BYTE_LENGTH = 2 * KIBIBYTE; export const MAX_BODY_ATTACHMENT_BYTE_LENGTH = 64 * KIBIBYTE;