From 3bf6fda2752bd70590dca259882f668930650a8f Mon Sep 17 00:00:00 2001 From: Andrii Vitiv Date: Wed, 15 Jan 2025 20:25:14 +0200 Subject: [PATCH 1/2] Improve receipt file detection --- src/libs/HttpUtils.ts | 34 +---------------------- src/libs/actions/IOU.ts | 9 +++--- src/libs/isFileUploadable/index.native.ts | 10 +++++++ src/libs/isFileUploadable/index.ts | 7 +++++ src/libs/validateFormDataParameter.ts | 29 +++++++++++++++++++ 5 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 src/libs/isFileUploadable/index.native.ts create mode 100644 src/libs/isFileUploadable/index.ts create mode 100644 src/libs/validateFormDataParameter.ts diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 1fa4c7b8b4b5a..158a667468f0a 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -10,10 +10,7 @@ import {alertUser} from './actions/UpdateRequired'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from './API/types'; import {getCommandURL} from './ApiUtils'; import HttpsError from './Errors/HttpsError'; -import getPlatform from './getPlatform'; - -const platform = getPlatform(); -const isNativePlatform = platform === CONST.PLATFORM.ANDROID || platform === CONST.PLATFORM.IOS; +import validateFormDataParameter from './validateFormDataParameter'; let shouldFailAllRequests = false; let shouldForceOffline = false; @@ -178,35 +175,6 @@ function xhr(command: string, data: Record, type: RequestType = return processHTTPRequest(url, type, formData, abortSignalController?.signal); } -/** - * Ensures no value of type `object` other than null, Blob, its subclasses, or {uri: string} (native platforms only) is passed to XMLHttpRequest. - * Otherwise, it will be incorrectly serialized as `[object Object]` and cause an error on Android. - * See https://github.com/Expensify/App/issues/45086 - */ -function validateFormDataParameter(command: string, key: string, value: unknown) { - // eslint-disable-next-line @typescript-eslint/no-shadow - const isValid = (value: unknown, isTopLevel: boolean): boolean => { - if (value === null || typeof value !== 'object') { - return true; - } - if (Array.isArray(value)) { - return value.every((element) => isValid(element, false)); - } - if (isTopLevel) { - // Native platforms only require the value to include the `uri` property. - // Optionally, it can also have a `name` and `type` props. - // On other platforms, the value must be an instance of `Blob`. - return isNativePlatform ? 'uri' in value && !!value.uri : value instanceof Blob; - } - return false; - }; - - if (!isValid(value, true)) { - // eslint-disable-next-line no-console - console.warn(`An unsupported value was passed to command '${command}' (parameter: '${key}'). Only Blob and primitive types are allowed.`); - } -} - function cancelPendingRequests(command: AbortCommand = ABORT_COMMANDS.All) { const controller = abortControllerMap.get(command); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0ac396709b078..8f6f899bd7c73 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -43,6 +43,7 @@ import { navigateToStartMoneyRequestStep, updateIOUOwnerAndTotal, } from '@libs/IOUUtils'; +import isFileUploadable from '@libs/isFileUploadable'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; @@ -3952,7 +3953,7 @@ function shareTrackedExpense( taxCode, taxAmount, billable, - receipt: receipt instanceof Blob ? receipt : undefined, + receipt: isFileUploadable(receipt) ? receipt : undefined, policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID, policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID, adminsChatReportID: createdWorkspaceParams?.adminsChatReportID, @@ -4064,7 +4065,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { createdChatReportActionID, createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction.reportActionID, - receipt: receipt instanceof Blob ? receipt : undefined, + receipt: isFileUploadable(receipt) ? receipt : undefined, receiptState: receipt?.state, category, tag, @@ -4255,7 +4256,7 @@ function trackExpense( category, tag, billable, - receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined, + receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined, }; const policyParams: CategorizeTrackedExpensePolicyParams = { policyID: chatReport?.policyID ?? '-1', @@ -4327,7 +4328,7 @@ function trackExpense( createdChatReportActionID: createdChatReportActionID ?? '-1', createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, - receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined, + receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined, receiptState: trackedReceipt?.state, category, tag, diff --git a/src/libs/isFileUploadable/index.native.ts b/src/libs/isFileUploadable/index.native.ts new file mode 100644 index 0000000000000..9b0fa9bec7955 --- /dev/null +++ b/src/libs/isFileUploadable/index.native.ts @@ -0,0 +1,10 @@ +import type {FileObject} from '@components/AttachmentModal'; + +function isFileUploadable(file: File | FileObject | undefined): boolean { + // Native platforms only require the object to include the `uri` property. + // Optionally, it can also have a `name` and `type` properties. + // On other platforms, the file must be an instance of `Blob` or one of its subclasses. + return !!file && 'uri' in file && !!file.uri && typeof file.uri === 'string'; +} + +export default isFileUploadable; diff --git a/src/libs/isFileUploadable/index.ts b/src/libs/isFileUploadable/index.ts new file mode 100644 index 0000000000000..48c156313daaa --- /dev/null +++ b/src/libs/isFileUploadable/index.ts @@ -0,0 +1,7 @@ +import type {FileObject} from '@components/AttachmentModal'; + +function isFileUploadable(file: File | FileObject | undefined): boolean { + return file instanceof Blob; +} + +export default isFileUploadable; diff --git a/src/libs/validateFormDataParameter.ts b/src/libs/validateFormDataParameter.ts new file mode 100644 index 0000000000000..dd46c97a7941c --- /dev/null +++ b/src/libs/validateFormDataParameter.ts @@ -0,0 +1,29 @@ +import isFileUploadable from './isFileUploadable'; + +/** + * Ensures no value of type `object` other than null, Blob, its subclasses, or {uri: string} (native platforms only) is passed to XMLHttpRequest. + * Otherwise, it will be incorrectly serialized as `[object Object]` and cause an error on Android. + * See https://github.com/Expensify/App/issues/45086 + */ +function validateFormDataParameter(command: string, key: string, value: unknown) { + // eslint-disable-next-line @typescript-eslint/no-shadow + const isValid = (value: unknown, isTopLevel: boolean): boolean => { + if (value === null || typeof value !== 'object') { + return true; + } + if (Array.isArray(value)) { + return value.every((element) => isValid(element, false)); + } + if (isTopLevel) { + return isFileUploadable(value); + } + return false; + }; + + if (!isValid(value, true)) { + // eslint-disable-next-line no-console + console.warn(`An unsupported value was passed to command '${command}' (parameter: '${key}'). Only Blob and primitive types are allowed.`); + } +} + +export default validateFormDataParameter; From 1e049fa76f9eb783411b5aba29aa54a0f86b7561 Mon Sep 17 00:00:00 2001 From: Andrii Vitiv Date: Thu, 23 Jan 2025 15:37:16 +0200 Subject: [PATCH 2/2] Remove unnecessary type --- src/libs/isFileUploadable/index.native.ts | 2 +- src/libs/isFileUploadable/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/isFileUploadable/index.native.ts b/src/libs/isFileUploadable/index.native.ts index 9b0fa9bec7955..602d2c319a74c 100644 --- a/src/libs/isFileUploadable/index.native.ts +++ b/src/libs/isFileUploadable/index.native.ts @@ -1,6 +1,6 @@ import type {FileObject} from '@components/AttachmentModal'; -function isFileUploadable(file: File | FileObject | undefined): boolean { +function isFileUploadable(file: FileObject | undefined): boolean { // Native platforms only require the object to include the `uri` property. // Optionally, it can also have a `name` and `type` properties. // On other platforms, the file must be an instance of `Blob` or one of its subclasses. diff --git a/src/libs/isFileUploadable/index.ts b/src/libs/isFileUploadable/index.ts index 48c156313daaa..a2f985ed109df 100644 --- a/src/libs/isFileUploadable/index.ts +++ b/src/libs/isFileUploadable/index.ts @@ -1,6 +1,6 @@ import type {FileObject} from '@components/AttachmentModal'; -function isFileUploadable(file: File | FileObject | undefined): boolean { +function isFileUploadable(file: FileObject | undefined): boolean { return file instanceof Blob; }