Fix photo upload on mobile devices

- Add HEIC/HEIF support for iPhone camera photos
- Make frontend file type validation more permissive for mobile browsers
- Add 'capture' attribute to prefer rear camera on mobile
- Include actual file extension in error messages for debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-08 12:56:50 -08:00
parent 12a8c054f3
commit 9fc984bea3
3 changed files with 34 additions and 20 deletions

View file

@ -64,14 +64,19 @@ if (bizId LTE 0) {
</cfif>
</cfif>
<!--- Validate file type --->
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp">
<!--- Validate file type (include HEIC for iPhone) --->
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp,heic,heif">
<cfif NOT listFindNoCase(allowedExtensions, actualExt)>
<cffile action="DELETE" file="#headersDir#/#uploadResult.ServerFile#">
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })#</cfoutput>
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp, heic)" })#</cfoutput>
<cfabort>
</cfif>
<!--- Convert HEIC/HEIF extension to jpg for consistency --->
<cfif actualExt EQ "heic" OR actualExt EQ "heif">
<cfset actualExt = "jpg">
</cfif>
<!--- No resize - accept image as-is --->
<!--- Delete old header if exists --->

View file

@ -26,17 +26,23 @@ if (itemId LTE 0) {
<!--- Upload the file to temp location first --->
<cffile action="UPLOAD" filefield="photo" destination="#itemsDir#/" nameconflict="MAKEUNIQUE" mode="755" result="uploadResult">
<!--- Validate file type --->
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp">
<cfif NOT listFindNoCase(allowedExtensions, uploadResult.ClientFileExt)>
<!--- Validate file type (include HEIC/HEIF for iPhone) --->
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp,heic,heif">
<cfset actualExt = lCase(uploadResult.ClientFileExt)>
<cfif NOT listFindNoCase(allowedExtensions, actualExt)>
<cffile action="DELETE" file="#itemsDir#/#uploadResult.ServerFile#">
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })#</cfoutput>
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp, heic). Got: #actualExt#" })#</cfoutput>
<cfabort>
</cfif>
<!--- Convert HEIC/HEIF to jpg for broader compatibility --->
<cfif actualExt EQ "heic" OR actualExt EQ "heif">
<cfset actualExt = "jpg">
</cfif>
<!--- Delete old photo if exists (any extension) --->
<cfscript>
for (ext in listToArray(allowedExtensions)) {
for (ext in listToArray("jpg,jpeg,gif,png,webp,heic,heif")) {
oldFile = "#itemsDir#/#itemId#.#ext#";
if (fileExists(oldFile)) {
try { fileDelete(oldFile); } catch (any e) {}
@ -44,15 +50,15 @@ for (ext in listToArray(allowedExtensions)) {
}
</cfscript>
<!--- Rename to ItemID.ext --->
<cffile action="RENAME" source="#itemsDir#/#uploadResult.ServerFile#" destination="#itemsDir#/#itemId#.#uploadResult.ClientFileExt#" mode="755">
<!--- Rename to ItemID.ext (use actualExt which may be converted from heic) --->
<cffile action="RENAME" source="#itemsDir#/#uploadResult.ServerFile#" destination="#itemsDir#/#itemId#.#actualExt#" mode="755">
<!--- Return success with image URL --->
<cfoutput>#serializeJSON({
"OK": true,
"ERROR": "",
"MESSAGE": "Photo uploaded successfully",
"IMAGEURL": "/uploads/items/#itemId#.#uploadResult.ClientFileExt#"
"IMAGEURL": "/uploads/items/#itemId#.#actualExt#"
})#</cfoutput>
<cfcatch type="any">

View file

@ -2819,14 +2819,17 @@
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/jpeg,image/png,image/gif,image/webp';
input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.heic,.heif';
input.capture = 'environment'; // Prefer rear camera on mobile
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) {
this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error');
// Be permissive with file types - server will validate
// Mobile cameras may report inconsistent MIME types
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', ''];
if (file.type && !validTypes.includes(file.type) && !file.type.startsWith('image/')) {
this.toast('Please select a valid image file', 'error');
return;
}
if (file.size > 5 * 1024 * 1024) {
@ -3115,15 +3118,15 @@
uploadHeader() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/jpeg,image/png,image/gif,image/webp';
input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.heic,.heif';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) {
this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error');
// Validate file type (be permissive for mobile cameras)
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', ''];
if (file.type && !validTypes.includes(file.type) && !file.type.startsWith('image/')) {
this.toast('Please select a valid image file', 'error');
return;
}